feat: Begins views, login is currently not working.

This commit is contained in:
2025-01-06 17:28:43 -05:00
parent 5efed277a1
commit 35ca73e1b4
10 changed files with 109 additions and 15 deletions

4
Public/css/main.css Normal file
View File

@@ -0,0 +1,4 @@
body {
background-color: #1e1e2e;
color: #ff66ff;
}

View File

@@ -2,11 +2,17 @@
<html lang="en">
<head>
<meta charset="utf-8">
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<link rel="stylesheet" href="css/main.css">
<title>#(title)</title>
</head>
<body>
<h1>#(title)</h1>
<div id="body"
hx-get="/body"
hx-trigger="load"
hx-swap="outerHTML">
</div>
</body>
</html>
</html>

View File

@@ -0,0 +1,3 @@
<div id="body">
<p>We're in!</p>
</div>

View File

@@ -0,0 +1,15 @@
<div id="body" class="container">
<form class="login-form">
<label for="email">Email</label>
<input type="text" id="email" placeholder="Email" name="email" autocomplete="email" required autofocus>
<br>
<label for="password">Password</label>
<input type="password" id="password" placeholder="Password" name="password" autocomplete="current-password" required>
<br>
<button hx-post="/login"
hx-target="#body"
hx-trigger="click">
Sign In
</button>
</form>
</div>

View File

@@ -4,21 +4,20 @@ import Vapor
struct ApiController: RouteCollection {
func boot(routes: RoutesBuilder) throws {
let api = routes.grouped("api", "v1")
let passwordProtected = api.grouped(User.authenticator(), User.guardMiddleware())
// Allows basic or token authentication.
let tokenProtected = api.grouped(
let protected = api.grouped(
User.authenticator(),
UserToken.authenticator(),
User.guardMiddleware()
)
let employees = tokenProtected.grouped("employees")
let purchaseOrders = tokenProtected.grouped("purchase-orders")
let employees = protected.grouped("employees")
let purchaseOrders = protected.grouped("purchase-orders")
// TODO: Need to handle the intial creation of users somehow.
let users = tokenProtected.grouped("users")
let vendors = tokenProtected.grouped("vendors")
let users = protected.grouped("users")
let vendors = protected.grouped("vendors")
let vendorBranches = vendors.grouped(":vendorID", "branches")
employees.get(use: employeesIndex(req:))
@@ -32,7 +31,7 @@ struct ApiController: RouteCollection {
purchaseOrders.post(use: createPurchaseOrder(req:))
users.post(use: createUser(req:))
passwordProtected.group("users", "login") {
users.group("login") {
$0.get(use: self.login(req:))
}
@@ -106,6 +105,7 @@ struct ApiController: RouteCollection {
.with(\.$vendorBranch) {
$0.with(\.$vendor)
}
.sort(\.$id, .descending)
.all()
.map { $0.toDTO() }
}
@@ -115,11 +115,20 @@ struct ApiController: RouteCollection {
try PurchaseOrder.Create.validate(content: req)
let createdById = try req.auth.require(User.self).requireID()
let create = try req.content.decode(PurchaseOrder.Create.self)
guard let employee = try await Employee.find(create.createdForID, on: req.db) else {
throw Abort(.notFound, reason: "Employee not found.")
}
guard employee.active else {
throw Abort(.badRequest, reason: "Employee is not active, unable to generate a PO for in-active employees")
}
let purchaseOrder = create.toModel(createdByID: createdById)
try await purchaseOrder.save(on: req.db)
guard let loaded = try await PurchaseOrder.query(on: req.db)
.filter(\.$id == purchaseOrder.id!)
.filter(\.$id == purchaseOrder.requireID())
.with(\.$createdBy)
.with(\.$createdFor)
.with(\.$vendorBranch, { branch in
@@ -127,7 +136,8 @@ struct ApiController: RouteCollection {
})
.first()
else {
throw Abort(.noContent)
// This should really never happen, since we just created the purchase order.
throw Abort(.noContent, reason: "Failed to load purchase order after creation")
}
return loaded.toDTO()
}

View File

@@ -2,6 +2,13 @@ import Fluent
import struct Foundation.UUID
import Vapor
/// The employee database model.
///
/// An employee is someone that PO's can be generated for. They can be either a field
/// employee / technician, an office employee, or an administrator.
///
/// # NOTE: Only `User` types can login and generate po's for employees.
///
final class Employee: Model, @unchecked Sendable {
static let schema = "employee"

View File

@@ -1,6 +1,11 @@
import Fluent
import Vapor
/// The purchase order database model.
///
/// # NOTE: An initial purchase order should be created with an `id` higher than our current PO
/// so that subsequent PO's are generated with higher values than our current system produces.
/// once the first one is set, the rest will auto-increment from there.
final class PurchaseOrder: Model, Content, @unchecked Sendable {
static let schema = "purchase_order"
@@ -80,6 +85,7 @@ final class PurchaseOrder: Model, Content, @unchecked Sendable {
extension PurchaseOrder {
struct Create: Content {
let id: Int?
let workOrder: Int?
let materials: String
let customer: String
@@ -89,7 +95,7 @@ extension PurchaseOrder {
func toModel(createdByID: User.IDValue) -> PurchaseOrder {
.init(
id: nil,
id: id,
workOrder: workOrder,
materials: materials,
customer: customer,

View File

@@ -1,6 +1,13 @@
import Fluent
import Vapor
/// The user database model.
///
/// A user is someone who is able to login and generate PO's for employees. Generally a user should also
/// have an employee profile, but not all employees are users. Users are generally restricted to office workers
/// and administrators.
///
///
final class User: Model, @unchecked Sendable {
static let schema = "user"
@@ -82,6 +89,7 @@ extension User: ModelAuthenticatable {
}
extension User: ModelSessionAuthenticatable {}
extension User: ModelCredentialsAuthenticatable {}
extension User.Create: Validatable {
static func validations(_ validations: inout Validations) {

View File

@@ -11,7 +11,12 @@ public func configure(_ app: Application) async throws {
app.middleware.use(app.sessions.middleware)
app.middleware.use(User.sessionAuthenticator())
app.databases.use(DatabaseConfigurationFactory.sqlite(.file("db.sqlite")), as: .sqlite)
switch app.environment {
case .production, .development:
app.databases.use(DatabaseConfigurationFactory.sqlite(.file("db.sqlite")), as: .sqlite)
default:
app.databases.use(DatabaseConfigurationFactory.sqlite(.memory), as: .sqlite)
}
app.migrations.add(Vendor.Migrate())
app.migrations.add(VendorBranch.Migrate())
@@ -24,4 +29,8 @@ public func configure(_ app: Application) async throws {
// register routes
try routes(app)
if app.environment != .production {
try await app.autoMigrate()
}
}

View File

@@ -2,10 +2,36 @@ import Fluent
import Vapor
func routes(_ app: Application) throws {
app.get { req async throws in
try await req.view.render("index", ["title": "Hello Vapor!"])
let redirectMiddleware = User.redirectMiddleware { req in
"login?next=\(req.url.path)"
}
let protected = app.grouped(User.sessionAuthenticator(), redirectMiddleware, User.guardMiddleware())
let credentialsProtected = protected.grouped(User.credentialsAuthenticator())
app.get { req async throws in
try await req.view.render("index", ["title": "HHE - Purchase Orders"])
}
app.get("login") { req async throws in
req.logger.info("login")
return try await req.view.render("login")
}
credentialsProtected.post("login") { req async throws -> View in
req.logger.info("login POST")
return try await req.view.render("logged-in")
}
credentialsProtected.get("body") { req async throws in
req.logger.info("body")
return try await req.view.render("logged-in")
}
// app.get("index") { req async throws -> View in
//
// }
app.get("hello") { _ async -> String in
"Hello, world!"
}