feat: Cleans up some file names, adds generate admin user command.
This commit is contained in:
@@ -44,17 +44,7 @@ extension SharedModels.ViewRoute {
|
|||||||
let user = try await users.get(token.userID)!
|
let user = try await users.get(token.userID)!
|
||||||
request.session.authenticate(user)
|
request.session.authenticate(user)
|
||||||
return await request.render {
|
return await request.render {
|
||||||
MainPage(displayNav: true, route: .purchaseOrders) {
|
MainPage.loggedIn(next: login.next)
|
||||||
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"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
55
Sources/App/GenerateAdminUserCommand.swift
Normal file
55
Sources/App/GenerateAdminUserCommand.swift
Normal file
@@ -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)")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import Elementary
|
import Elementary
|
||||||
import ElementaryHTMX
|
import ElementaryHTMX
|
||||||
|
import SharedModels
|
||||||
|
|
||||||
struct MainPage<Inner: HTML>: SendableHTMLDocument where Inner: Sendable {
|
struct MainPage<Inner: HTML>: SendableHTMLDocument where Inner: Sendable {
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ struct MainPage<Inner: HTML>: SendableHTMLDocument where Inner: Sendable {
|
|||||||
|
|
||||||
init(
|
init(
|
||||||
displayNav: Bool = false,
|
displayNav: Bool = false,
|
||||||
route: ViewRoute,
|
route: RouteHeaderView.ViewRoute,
|
||||||
_ inner: () -> Inner
|
_ inner: () -> Inner
|
||||||
) {
|
) {
|
||||||
self.displayNav = displayNav
|
self.displayNav = displayNav
|
||||||
@@ -39,6 +40,29 @@ struct MainPage<Inner: HTML>: 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 {
|
struct RouteHeaderView: HTML {
|
||||||
|
|
||||||
let title: String
|
let title: String
|
||||||
@@ -61,6 +85,38 @@ struct RouteHeaderView: HTML {
|
|||||||
br()
|
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 {
|
struct Logo: HTML, Sendable {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import Elementary
|
import Elementary
|
||||||
|
|
||||||
enum Img {
|
enum Img {
|
||||||
|
@Sendable
|
||||||
static func spinner(width: Int = 30, height: Int = 30) -> some HTML<HTMLTag.img> {
|
static func spinner(width: Int = 30, height: Int = 30) -> some HTML<HTMLTag.img> {
|
||||||
img(.src("/images/spinner.svg"), .width(width), .height(height))
|
img(.src("/images/spinner.svg"), .width(width), .height(height))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Sendable
|
||||||
static func search(width: Int = 30, height: Int = 30) -> some HTML<HTMLTag.img> {
|
static func search(width: Int = 30, height: Int = 30) -> some HTML<HTMLTag.img> {
|
||||||
img(.src("/images/search.svg"), .width(width), .height(height))
|
img(.src("/images/search.svg"), .width(width), .height(height))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ struct VendorTable: HTML {
|
|||||||
.attributes(
|
.attributes(
|
||||||
.style("padding: 0px 10px;"),
|
.style("padding: 0px 10px;"),
|
||||||
.hx.get(route: .vendor(.form)),
|
.hx.get(route: .vendor(.form)),
|
||||||
.hx.target("#float"),
|
.hx.target(.float),
|
||||||
.hx.swap(.outerHTML)
|
.hx.swap(.outerHTML)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import FluentSQLiteDriver
|
|||||||
import NIOSSL
|
import NIOSSL
|
||||||
import SharedModels
|
import SharedModels
|
||||||
import Vapor
|
import Vapor
|
||||||
|
@preconcurrency import VaporRouting
|
||||||
|
|
||||||
// configures your application
|
// configures your application
|
||||||
public func configure(_ app: Application) async throws {
|
public func configure(_ app: Application) async throws {
|
||||||
@@ -18,7 +19,6 @@ public func configure(_ app: Application) async throws {
|
|||||||
let cors = CORSMiddleware(configuration: corsConfiguration)
|
let cors = CORSMiddleware(configuration: corsConfiguration)
|
||||||
app.middleware.use(cors, at: .beginning)
|
app.middleware.use(cors, at: .beginning)
|
||||||
|
|
||||||
// uncomment to serve files from /Public folder
|
|
||||||
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
|
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
|
||||||
app.middleware.use(app.sessions.middleware)
|
app.middleware.use(app.sessions.middleware)
|
||||||
app.middleware.use(DependenciesMiddleware())
|
app.middleware.use(DependenciesMiddleware())
|
||||||
@@ -38,17 +38,45 @@ public func configure(_ app: Application) async throws {
|
|||||||
let databaseClient = DatabaseClient.live(database: app.db)
|
let databaseClient = DatabaseClient.live(database: app.db)
|
||||||
try await app.migrations.add(databaseClient.migrations())
|
try await app.migrations.add(databaseClient.migrations())
|
||||||
|
|
||||||
try withDependencies {
|
app.mount(
|
||||||
$0.database = databaseClient
|
SiteRoute.router,
|
||||||
} operation: {
|
middleware: { $0.middleware() },
|
||||||
try routes(app)
|
use: siteHandler
|
||||||
}
|
)
|
||||||
|
|
||||||
// if app.environment != .production {
|
|
||||||
try await app.autoMigrate()
|
try await app.autoMigrate()
|
||||||
// }
|
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
app.asyncCommands.use(SeedCommand(), as: "seed")
|
app.asyncCommands.use(SeedCommand(), as: "seed")
|
||||||
#endif
|
#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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
4
env.example
Normal file
4
env.example
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
ADMIN_USERNAME="admin"
|
||||||
|
ADMIN_EMAIL="admin@development.com"
|
||||||
|
ADMIN_PASSWORD="super-secret"
|
||||||
|
SQLITE_FILENAME="db.sqlite"
|
||||||
Reference in New Issue
Block a user