diff --git a/Sources/App/Controllers/ViewController.swift b/Sources/App/Controllers/ViewController.swift index b1b507c..45b58f1 100644 --- a/Sources/App/Controllers/ViewController.swift +++ b/Sources/App/Controllers/ViewController.swift @@ -44,17 +44,7 @@ extension SharedModels.ViewRoute { let user = try await users.get(token.userID)! request.session.authenticate(user) return await request.render { - MainPage(displayNav: true, route: .purchaseOrders) { - div( - .hx.get(login.next ?? "/purchase-orders"), - .hx.pushURL(true), - .hx.target("body"), - .hx.trigger(.event(.revealed)), - .hx.indicator(".hx-indicator") - ) { - Img.spinner().attributes(.class("hx-indicator")) - } - } + MainPage.loggedIn(next: login.next) } } diff --git a/Sources/App/GenerateAdminUserCommand.swift b/Sources/App/GenerateAdminUserCommand.swift new file mode 100644 index 0000000..25fa128 --- /dev/null +++ b/Sources/App/GenerateAdminUserCommand.swift @@ -0,0 +1,55 @@ +import DatabaseClientLive +import Dependencies +import FluentSQLiteDriver +import SharedModels +import Vapor + +struct GenerateAdminUserCommand: AsyncCommand { + + struct Signature: CommandSignature { + @Option(name: "username", short: "u") + var userame: String? + + @Option(name: "email", short: "e") + var email: String? + + @Option(name: "password", short: "p") + var password: String? + + } + + var help: String { + "Generate admin user in the database." + } + + func run(using context: CommandContext, signature: Signature) async throws { + let database = DatabaseClient.live(database: context.application.db(.sqlite)) + + let username = signature.userame ?? Environment.get("ADMIN_USERNAME") + guard let username else { + throw Abort(.badRequest, reason: "Username not provided or found in the environment.") + } + + let email = signature.email ?? Environment.get("ADMIN_EMAIL") + guard let email else { + throw Abort(.badRequest, reason: "Email not provided or found in the environment.") + } + + let password = signature.password ?? Environment.get("ADMIN_PASSWORD") + guard let password else { + throw Abort(.badRequest, reason: "Password not provided or found in the environment.") + } + + let adminUser = User.Create( + username: username, + email: email, + password: password, + confirmPassword: password + ) + + _ = try await database.users.create(adminUser) + + context.console.print("Generated admin user: \(adminUser.username)") + } + +} diff --git a/Sources/App/Views/ViewRoute.swift b/Sources/App/Views/HTMXExtensions.swift similarity index 78% rename from Sources/App/Views/ViewRoute.swift rename to Sources/App/Views/HTMXExtensions.swift index d4af274..268a5b4 100644 --- a/Sources/App/Views/ViewRoute.swift +++ b/Sources/App/Views/HTMXExtensions.swift @@ -111,36 +111,3 @@ enum HXTarget { } } } - -// TODO: Move to MainPage -enum ViewRoute: String { - - case employees - case login - case purchaseOrders - case users - case vendors - - var title: String { - switch self { - case .purchaseOrders: - return "Purchase Orders" - default: - return rawValue.capitalized - } - } - - var description: String { - switch self { - case .employees: - return "Employees are who purchase orders can be issued to." - case .purchaseOrders, .login: - return "" - case .users: - return "Users are who can login and issue purchase orders for employees." - case .vendors: - return "Vendors are where purchase orders can be issued to." - } - } - -} diff --git a/Sources/App/Views/Main.swift b/Sources/App/Views/Main.swift index f7b4739..75d12f5 100644 --- a/Sources/App/Views/Main.swift +++ b/Sources/App/Views/Main.swift @@ -1,5 +1,6 @@ import Elementary import ElementaryHTMX +import SharedModels struct MainPage: SendableHTMLDocument where Inner: Sendable { @@ -12,7 +13,7 @@ struct MainPage: SendableHTMLDocument where Inner: Sendable { init( displayNav: Bool = false, - route: ViewRoute, + route: RouteHeaderView.ViewRoute, _ inner: () -> Inner ) { self.displayNav = displayNav @@ -39,6 +40,29 @@ struct MainPage: SendableHTMLDocument where Inner: Sendable { } } +extension MainPage where Inner == LoggedIn { + static func loggedIn(next: String?) -> Self { + MainPage(displayNav: true, route: .purchaseOrders) { + LoggedIn(next: next) + } + } +} + +struct LoggedIn: HTML { + let next: String? + var content: some HTML { + div( + .hx.get(next ?? ViewRoute.router.path(for: .purchaseOrder(.index))), + .hx.pushURL(true), + .hx.target("body"), + .hx.trigger(.event(.revealed)), + .hx.indicator(".hx-indicator") + ) { + Img.spinner().attributes(.class("hx-indicator")) + } + } +} + struct RouteHeaderView: HTML { let title: String @@ -61,6 +85,38 @@ struct RouteHeaderView: HTML { br() } } + + enum ViewRoute: String { + + case employees + case login + case purchaseOrders + case users + case vendors + + var title: String { + switch self { + case .purchaseOrders: + return "Purchase Orders" + default: + return rawValue.capitalized + } + } + + var description: String { + switch self { + case .employees: + return "Employees are who purchase orders can be issued to." + case .purchaseOrders, .login: + return "" + case .users: + return "Users are who can login and issue purchase orders for employees." + case .vendors: + return "Vendors are where purchase orders can be issued to." + } + } + + } } struct Logo: HTML, Sendable { diff --git a/Sources/App/Views/Utils/Img.swift b/Sources/App/Views/Utils/Img.swift index 4044bd6..00f63c1 100644 --- a/Sources/App/Views/Utils/Img.swift +++ b/Sources/App/Views/Utils/Img.swift @@ -1,10 +1,12 @@ import Elementary enum Img { + @Sendable static func spinner(width: Int = 30, height: Int = 30) -> some HTML { img(.src("/images/spinner.svg"), .width(width), .height(height)) } + @Sendable static func search(width: Int = 30, height: Int = 30) -> some HTML { img(.src("/images/search.svg"), .width(width), .height(height)) } diff --git a/Sources/App/Views/Vendors/VendorTable.swift b/Sources/App/Views/Vendors/VendorTable.swift index 989e4d9..bf82507 100644 --- a/Sources/App/Views/Vendors/VendorTable.swift +++ b/Sources/App/Views/Vendors/VendorTable.swift @@ -16,7 +16,7 @@ struct VendorTable: HTML { .attributes( .style("padding: 0px 10px;"), .hx.get(route: .vendor(.form)), - .hx.target("#float"), + .hx.target(.float), .hx.swap(.outerHTML) ) } diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index 690f281..f3c743f 100644 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -5,6 +5,7 @@ import FluentSQLiteDriver import NIOSSL import SharedModels import Vapor +@preconcurrency import VaporRouting // configures your application public func configure(_ app: Application) async throws { @@ -18,7 +19,6 @@ public func configure(_ app: Application) async throws { let cors = CORSMiddleware(configuration: corsConfiguration) app.middleware.use(cors, at: .beginning) - // uncomment to serve files from /Public folder app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)) app.middleware.use(app.sessions.middleware) app.middleware.use(DependenciesMiddleware()) @@ -38,17 +38,45 @@ public func configure(_ app: Application) async throws { let databaseClient = DatabaseClient.live(database: app.db) try await app.migrations.add(databaseClient.migrations()) - try withDependencies { - $0.database = databaseClient - } operation: { - try routes(app) - } + app.mount( + SiteRoute.router, + middleware: { $0.middleware() }, + use: siteHandler + ) - // if app.environment != .production { try await app.autoMigrate() - // } #if DEBUG app.asyncCommands.use(SeedCommand(), as: "seed") #endif + app.asyncCommands.use(GenerateAdminUserCommand(), as: "generate-admin") +} + +extension SiteRoute { + + func middleware() -> [any Middleware]? { + switch self { + case let .api(route): + return route.middleware + case .health: + return nil + case let .view(route): + return route.middleware + } + } +} + +@Sendable +func siteHandler( + request: Request, + route: SiteRoute +) async throws -> any AsyncResponseEncodable { + switch route { + case let .api(route): + return try await route.handle(request: request) + case .health: + return HTTPStatus.ok + case let .view(route): + return try await route.handle(request: request) + } } diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift deleted file mode 100644 index 89ee37f..0000000 --- a/Sources/App/routes.swift +++ /dev/null @@ -1,56 +0,0 @@ -import CasePathsCore -import DatabaseClientLive -import Dependencies -import Elementary -import ElementaryHTMX -import Fluent -import SharedModels -import Vapor -import VaporElementary -@preconcurrency import VaporRouting - -func routes(_ app: Application) throws { - app.mount( - SiteRoute.router, - middleware: { route in - switch route { - case let .api(route): - return route.middleware - case .health: - return nil - case let .view(route): - return route.middleware - } - }, - use: siteHandler - ) - - app.get { _ in - HTMLResponse { - MainPage(displayNav: false, route: .purchaseOrders) { - div(.class("container")) { - h1 { "iT WORKS" } - } - } - } - } -} - -private struct LoginContext: Content { - let next: String? -} - -@Sendable -func siteHandler( - request: Request, - route: SiteRoute -) async throws -> any AsyncResponseEncodable { - switch route { - case let .api(route): - return try await route.handle(request: request) - case .health: - return HTTPStatus.ok - case let .view(route): - return try await route.handle(request: request) - } -} diff --git a/env.example b/env.example new file mode 100644 index 0000000..adfc4fb --- /dev/null +++ b/env.example @@ -0,0 +1,4 @@ +ADMIN_USERNAME="admin" +ADMIN_EMAIL="admin@development.com" +ADMIN_PASSWORD="super-secret" +SQLITE_FILENAME="db.sqlite" diff --git a/justfile b/justfile index 88356f2..e8d3bf0 100644 --- a/justfile +++ b/justfile @@ -15,3 +15,6 @@ run: clean: rm -rf .build + +bootstrap: + cp ./env.example .env