From 35ca73e1b4558ae73028d5bd4d19b2ae2ce68c19 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Mon, 6 Jan 2025 17:28:43 -0500 Subject: [PATCH] feat: Begins views, login is currently not working. --- Public/css/main.css | 4 +++ Resources/Views/index.leaf | 10 +++++-- Resources/Views/logged-in.leaf | 3 +++ Resources/Views/login.leaf | 15 +++++++++++ Sources/App/Controllers/ApiController.swift | 28 ++++++++++++------- Sources/App/Models/Employee.swift | 7 +++++ Sources/App/Models/PurchaseOrder.swift | 8 +++++- Sources/App/Models/User.swift | 8 ++++++ Sources/App/configure.swift | 11 +++++++- Sources/App/routes.swift | 30 +++++++++++++++++++-- 10 files changed, 109 insertions(+), 15 deletions(-) create mode 100644 Public/css/main.css create mode 100644 Resources/Views/logged-in.leaf create mode 100644 Resources/Views/login.leaf diff --git a/Public/css/main.css b/Public/css/main.css new file mode 100644 index 0000000..bc25d42 --- /dev/null +++ b/Public/css/main.css @@ -0,0 +1,4 @@ +body { + background-color: #1e1e2e; + color: #ff66ff; +} diff --git a/Resources/Views/index.leaf b/Resources/Views/index.leaf index 10cb4f7..8a2c7b0 100644 --- a/Resources/Views/index.leaf +++ b/Resources/Views/index.leaf @@ -2,11 +2,17 @@ - + + #(title)

#(title)

+
+
- \ No newline at end of file + diff --git a/Resources/Views/logged-in.leaf b/Resources/Views/logged-in.leaf new file mode 100644 index 0000000..2785db0 --- /dev/null +++ b/Resources/Views/logged-in.leaf @@ -0,0 +1,3 @@ +
+

We're in!

+
diff --git a/Resources/Views/login.leaf b/Resources/Views/login.leaf new file mode 100644 index 0000000..4b7e7ab --- /dev/null +++ b/Resources/Views/login.leaf @@ -0,0 +1,15 @@ +
+ +
diff --git a/Sources/App/Controllers/ApiController.swift b/Sources/App/Controllers/ApiController.swift index aa8c90b..1caf7b3 100644 --- a/Sources/App/Controllers/ApiController.swift +++ b/Sources/App/Controllers/ApiController.swift @@ -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() } diff --git a/Sources/App/Models/Employee.swift b/Sources/App/Models/Employee.swift index 0d82dd8..b1065e6 100644 --- a/Sources/App/Models/Employee.swift +++ b/Sources/App/Models/Employee.swift @@ -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" diff --git a/Sources/App/Models/PurchaseOrder.swift b/Sources/App/Models/PurchaseOrder.swift index bbe8626..2a1462d 100644 --- a/Sources/App/Models/PurchaseOrder.swift +++ b/Sources/App/Models/PurchaseOrder.swift @@ -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, diff --git a/Sources/App/Models/User.swift b/Sources/App/Models/User.swift index 7a831d3..cd87ef3 100644 --- a/Sources/App/Models/User.swift +++ b/Sources/App/Models/User.swift @@ -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) { diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index f7ea191..31b0434 100644 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -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() + } } diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index 27cf8f6..d1880e1 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -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!" }