import DatabaseClient import FluentKit import Foundation import SharedModels import Vapor public extension DatabaseClient.Users { static func live(database: any Database) -> Self { .init { try await UserModel.query(on: database).count() } create: { create in let model = try create.toModel() try await model.save(on: database) return try model.toDTO() } delete: { id in guard let model = try await UserModel.find(id, on: database) else { throw NotFoundError() } try await model.delete(on: database) } fetchAll: { try await UserModel.query(on: database).all().map { try $0.toDTO() } } get: { id in try await UserModel.find(id, on: database).map { try $0.toDTO() } } login: { login in try login.validate() var query = UserModel.query(on: database) if let username = login.username { query = query.filter(\UserModel.$username == username) } else { query = query.filter(\UserModel.$email == login.email!) } guard let user = try await query.first() else { throw NotFoundError() } let token = try user.generateToken() try await token.save(on: database) return try User.Token( id: token.requireID(), userID: user.requireID(), value: token.value ) } logout: { id in guard let token = try await UserTokenModel.find(id, on: database) else { return } try await token.delete(on: database) } token: { _ in guard let user = try await UserModel.query(on: database) .with(\.$token) .first() else { throw Abort(.notFound) } guard let token = user.token else { let token = try user.generateToken() try await token.save(on: database) return try token.toDTO() } return try token.toDTO() } update: { id, updates in guard let user = try await UserModel.find(id, on: database) else { throw Abort(.notFound) } var hasChanges = false if let username = updates.username { hasChanges = true user.username = username } if let email = updates.email { hasChanges = true user.email = email } guard hasChanges else { return try user.toDTO() } try await user.save(on: database) return try user.toDTO() } } } extension User { struct Migrate: AsyncMigration { let name = "CreateUser" func prepare(on database: any Database) async throws { try await database.schema(UserModel.schema) .id() .field("username", .string, .required) .field("email", .string, .required) .field("password_hash", .string, .required) .field("created_at", .datetime) .field("updated_at", .datetime) .unique(on: "email", "username") .create() } func revert(on database: any Database) async throws { try await database.schema(UserModel.schema).delete() } } } extension User.Token { struct Migrate: AsyncMigration { let name = "CreateUserToken" func prepare(on database: any Database) async throws { try await database.schema(UserTokenModel.schema) .id() .field("value", .string, .required) .field("user_id", .uuid, .required, .references(UserModel.schema, "id")) .unique(on: "value") .create() } func revert(on database: any Database) async throws { try await database.schema(UserTokenModel.schema).delete() } } } extension User.Create { func toModel() throws -> UserModel { try validate() return try .init(username: username, email: email, passwordHash: Bcrypt.hash(password, cost: 12)) } func validate() throws { guard !username.isEmpty else { throw ValidationError(message: "Username should not be empty.") } guard !email.isEmpty else { throw ValidationError(message: "Email should not be empty") } guard password.count > 8 else { throw ValidationError(message: "Password should be more than 8 characters long.") } guard password == confirmPassword else { throw ValidationError(message: "Passwords do not match.") } } } extension User.Login { func validate() throws { guard username != nil || email != nil else { throw ValidationError(message: "Either username or email must be provided to login.") } } } /// 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 UserModel: Model, @unchecked Sendable { static let schema = "user" @ID(key: .id) var id: UUID? @Field(key: "username") var username: String @Field(key: "email") var email: String @Field(key: "password_hash") var passwordHash: String @Timestamp(key: "created_at", on: .create, format: .iso8601) var createdAt: Date? @Timestamp(key: "updated_at", on: .update, format: .iso8601) var updatedAt: Date? @OptionalChild(for: \.$user) var token: UserTokenModel? init() {} init( id: UUID? = nil, username: String, email: String, passwordHash: String ) { self.id = id self.username = username self.email = email self.passwordHash = passwordHash } func toDTO() throws -> User { try .init( id: requireID(), email: email, username: username, createdAt: createdAt, updatedAt: updatedAt ) } func generateToken() throws -> UserTokenModel { try .init( value: [UInt8].random(count: 16).base64, userID: requireID() ) } } final class UserTokenModel: Model, Codable, @unchecked Sendable { static let schema = "user_token" @ID(key: .id) var id: UUID? @Field(key: "value") var value: String @Parent(key: "user_id") var user: UserModel init() {} init(id: UUID? = nil, value: String, userID: UserModel.IDValue) { self.id = id self.value = value $user.id = userID } func toDTO() throws -> User.Token { try .init(id: requireID(), userID: $user.id, value: value) } } // MARK: - Authentication extension User: Authenticatable {} extension User: SessionAuthenticatable { public var sessionID: String { username } } public struct UserPasswordAuthenticator: AsyncBasicAuthenticator { public typealias User = SharedModels.User public init() {} public func authenticate(basic: BasicAuthorization, for request: Request) async throws { guard let user = try await UserModel.query(on: request.db) .filter(\UserModel.$username == basic.username) .first(), try Bcrypt.verify(basic.password, created: user.passwordHash) else { throw Abort(.unauthorized) } try request.auth.login(user.toDTO()) } } public struct UserTokenAuthenticator: AsyncBearerAuthenticator { public typealias User = SharedModels.User public init() {} public func authenticate(bearer: BearerAuthorization, for request: Request) async throws { guard let token = try await UserTokenModel.query(on: request.db) .filter(\UserTokenModel.$value == bearer.token) .with(\UserTokenModel.$user) .first() else { throw Abort(.unauthorized) } try request.auth.login(token.user.toDTO()) } } public struct UserSessionAuthenticator: AsyncSessionAuthenticator { public typealias User = SharedModels.User public init() {} public func authenticate(sessionID: User.SessionID, for request: Request) async throws { guard let user = try await UserModel.query(on: request.db) .filter(\UserModel.$username == sessionID) .first() else { throw Abort(.unauthorized) } try request.auth.login(user.toDTO()) } }