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"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <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> <title>#(title)</title>
</head> </head>
<body> <body>
<h1>#(title)</h1> <h1>#(title)</h1>
<div id="body"
hx-get="/body"
hx-trigger="load"
hx-swap="outerHTML">
</div>
</body> </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 { struct ApiController: RouteCollection {
func boot(routes: RoutesBuilder) throws { func boot(routes: RoutesBuilder) throws {
let api = routes.grouped("api", "v1") let api = routes.grouped("api", "v1")
let passwordProtected = api.grouped(User.authenticator(), User.guardMiddleware())
// Allows basic or token authentication. // Allows basic or token authentication.
let tokenProtected = api.grouped( let protected = api.grouped(
User.authenticator(), User.authenticator(),
UserToken.authenticator(), UserToken.authenticator(),
User.guardMiddleware() User.guardMiddleware()
) )
let employees = tokenProtected.grouped("employees") let employees = protected.grouped("employees")
let purchaseOrders = tokenProtected.grouped("purchase-orders") let purchaseOrders = protected.grouped("purchase-orders")
// TODO: Need to handle the intial creation of users somehow. // TODO: Need to handle the intial creation of users somehow.
let users = tokenProtected.grouped("users") let users = protected.grouped("users")
let vendors = tokenProtected.grouped("vendors") let vendors = protected.grouped("vendors")
let vendorBranches = vendors.grouped(":vendorID", "branches") let vendorBranches = vendors.grouped(":vendorID", "branches")
employees.get(use: employeesIndex(req:)) employees.get(use: employeesIndex(req:))
@@ -32,7 +31,7 @@ struct ApiController: RouteCollection {
purchaseOrders.post(use: createPurchaseOrder(req:)) purchaseOrders.post(use: createPurchaseOrder(req:))
users.post(use: createUser(req:)) users.post(use: createUser(req:))
passwordProtected.group("users", "login") { users.group("login") {
$0.get(use: self.login(req:)) $0.get(use: self.login(req:))
} }
@@ -106,6 +105,7 @@ struct ApiController: RouteCollection {
.with(\.$vendorBranch) { .with(\.$vendorBranch) {
$0.with(\.$vendor) $0.with(\.$vendor)
} }
.sort(\.$id, .descending)
.all() .all()
.map { $0.toDTO() } .map { $0.toDTO() }
} }
@@ -115,11 +115,20 @@ struct ApiController: RouteCollection {
try PurchaseOrder.Create.validate(content: req) try PurchaseOrder.Create.validate(content: req)
let createdById = try req.auth.require(User.self).requireID() let createdById = try req.auth.require(User.self).requireID()
let create = try req.content.decode(PurchaseOrder.Create.self) 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) let purchaseOrder = create.toModel(createdByID: createdById)
try await purchaseOrder.save(on: req.db) try await purchaseOrder.save(on: req.db)
guard let loaded = try await PurchaseOrder.query(on: req.db) guard let loaded = try await PurchaseOrder.query(on: req.db)
.filter(\.$id == purchaseOrder.id!) .filter(\.$id == purchaseOrder.requireID())
.with(\.$createdBy) .with(\.$createdBy)
.with(\.$createdFor) .with(\.$createdFor)
.with(\.$vendorBranch, { branch in .with(\.$vendorBranch, { branch in
@@ -127,7 +136,8 @@ struct ApiController: RouteCollection {
}) })
.first() .first()
else { 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() return loaded.toDTO()
} }

View File

@@ -2,6 +2,13 @@ import Fluent
import struct Foundation.UUID import struct Foundation.UUID
import Vapor 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 { final class Employee: Model, @unchecked Sendable {
static let schema = "employee" static let schema = "employee"

View File

@@ -1,6 +1,11 @@
import Fluent import Fluent
import Vapor 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 { final class PurchaseOrder: Model, Content, @unchecked Sendable {
static let schema = "purchase_order" static let schema = "purchase_order"
@@ -80,6 +85,7 @@ final class PurchaseOrder: Model, Content, @unchecked Sendable {
extension PurchaseOrder { extension PurchaseOrder {
struct Create: Content { struct Create: Content {
let id: Int?
let workOrder: Int? let workOrder: Int?
let materials: String let materials: String
let customer: String let customer: String
@@ -89,7 +95,7 @@ extension PurchaseOrder {
func toModel(createdByID: User.IDValue) -> PurchaseOrder { func toModel(createdByID: User.IDValue) -> PurchaseOrder {
.init( .init(
id: nil, id: id,
workOrder: workOrder, workOrder: workOrder,
materials: materials, materials: materials,
customer: customer, customer: customer,

View File

@@ -1,6 +1,13 @@
import Fluent import Fluent
import Vapor 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 { final class User: Model, @unchecked Sendable {
static let schema = "user" static let schema = "user"
@@ -82,6 +89,7 @@ extension User: ModelAuthenticatable {
} }
extension User: ModelSessionAuthenticatable {} extension User: ModelSessionAuthenticatable {}
extension User: ModelCredentialsAuthenticatable {}
extension User.Create: Validatable { extension User.Create: Validatable {
static func validations(_ validations: inout Validations) { 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(app.sessions.middleware)
app.middleware.use(User.sessionAuthenticator()) 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(Vendor.Migrate())
app.migrations.add(VendorBranch.Migrate()) app.migrations.add(VendorBranch.Migrate())
@@ -24,4 +29,8 @@ public func configure(_ app: Application) async throws {
// register routes // register routes
try routes(app) try routes(app)
if app.environment != .production {
try await app.autoMigrate()
}
} }

View File

@@ -2,10 +2,36 @@ import Fluent
import Vapor import Vapor
func routes(_ app: Application) throws { func routes(_ app: Application) throws {
app.get { req async throws in let redirectMiddleware = User.redirectMiddleware { req in
try await req.view.render("index", ["title": "Hello Vapor!"]) "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 app.get("hello") { _ async -> String in
"Hello, world!" "Hello, world!"
} }