feat: Adds reset password api route, needs associated view route.
This commit is contained in:
@@ -110,6 +110,9 @@ private extension SiteRoute.Api.UserRoute {
|
||||
return user
|
||||
// case let .login(user):
|
||||
// return try await users.login(user)
|
||||
case let .resetPassword(id: id, request: request):
|
||||
try await users.resetPassword(id, request)
|
||||
return nil
|
||||
case let .update(id: id, updates: updates):
|
||||
return try await users.update(id, updates)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ public extension DatabaseClient {
|
||||
public var get: @Sendable (User.ID) async throws -> User?
|
||||
public var login: @Sendable (User.Login) async throws -> User.Token
|
||||
public var logout: @Sendable (User.Token.ID) async throws -> Void
|
||||
public var resetPassword: @Sendable (User.ID, User.ResetPassword) async throws -> Void
|
||||
public var token: @Sendable (User.ID) async throws -> User.Token
|
||||
public var update: @Sendable (User.ID, User.Update) async throws -> User
|
||||
}
|
||||
|
||||
@@ -57,6 +57,18 @@ public extension DatabaseClient.Users {
|
||||
guard let token = try await UserTokenModel.find(id, on: database)
|
||||
else { return }
|
||||
try await token.delete(on: database)
|
||||
} resetPassword: { id, request in
|
||||
database.logger.debug("Reset password: \(id)")
|
||||
|
||||
try request.validate()
|
||||
|
||||
guard let user = try await UserModel.find(id, on: database) else {
|
||||
throw Abort(.badRequest, reason: "User not found.")
|
||||
}
|
||||
|
||||
user.passwordHash = try User.hashPassword(request.password)
|
||||
try await user.save(on: database)
|
||||
|
||||
} token: { _ in
|
||||
guard let user = try await UserModel.query(on: database)
|
||||
.with(\.$token)
|
||||
@@ -134,11 +146,19 @@ extension User.Token {
|
||||
}
|
||||
}
|
||||
|
||||
extension User {
|
||||
|
||||
static func hashPassword(_ password: String) throws -> String {
|
||||
try Bcrypt.hash(password, cost: 12)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension User.Create {
|
||||
|
||||
func toModel() throws -> UserModel {
|
||||
try validate()
|
||||
return try .init(username: username, email: email, passwordHash: Bcrypt.hash(password, cost: 12))
|
||||
return try .init(username: username, email: email, passwordHash: User.hashPassword(password))
|
||||
}
|
||||
|
||||
func validate() throws {
|
||||
@@ -166,6 +186,18 @@ extension User.Login {
|
||||
}
|
||||
}
|
||||
|
||||
extension User.ResetPassword {
|
||||
|
||||
func validate() throws {
|
||||
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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The user database model.
|
||||
///
|
||||
/// A user is someone who is able to login and generate PO's for employees. Generally a user should also
|
||||
@@ -228,6 +260,9 @@ final class UserModel: Model, @unchecked Sendable {
|
||||
)
|
||||
}
|
||||
|
||||
func verifyPassword(_ password: String) throws -> Bool {
|
||||
try Bcrypt.verify(password, created: passwordHash)
|
||||
}
|
||||
}
|
||||
|
||||
final class UserTokenModel: Model, Codable, @unchecked Sendable {
|
||||
@@ -273,7 +308,7 @@ public struct UserPasswordAuthenticator: AsyncBasicAuthenticator {
|
||||
guard let user = try await UserModel.query(on: request.db)
|
||||
.filter(\UserModel.$username == basic.username)
|
||||
.first(),
|
||||
try Bcrypt.verify(basic.password, created: user.passwordHash)
|
||||
try user.verifyPassword(basic.password)
|
||||
else {
|
||||
throw Abort(.unauthorized)
|
||||
}
|
||||
|
||||
@@ -121,6 +121,7 @@ public extension SiteRoute {
|
||||
case create(User.Create)
|
||||
case get(id: User.ID)
|
||||
case index
|
||||
case resetPassword(id: User.ID, request: User.ResetPassword)
|
||||
case update(id: User.ID, updates: User.Update)
|
||||
|
||||
static let rootPath = "users"
|
||||
@@ -143,6 +144,11 @@ public extension SiteRoute {
|
||||
Path { rootPath }
|
||||
Method.get
|
||||
}
|
||||
Route(.case(Self.resetPassword(id:request:))) {
|
||||
Path { rootPath; User.ID.parser(); "reset-password" }
|
||||
Method.patch
|
||||
Body(.json(User.ResetPassword.self))
|
||||
}
|
||||
Route(.case(Self.update(id:updates:))) {
|
||||
Path { rootPath; User.ID.parser() }
|
||||
Method.patch
|
||||
|
||||
@@ -61,6 +61,19 @@ public extension User {
|
||||
}
|
||||
}
|
||||
|
||||
struct ResetPassword: Codable, Equatable, Sendable {
|
||||
public let password: String
|
||||
public let confirmPassword: String
|
||||
|
||||
public init(
|
||||
password: String,
|
||||
confirmPassword: String
|
||||
) {
|
||||
self.password = password
|
||||
self.confirmPassword = confirmPassword
|
||||
}
|
||||
}
|
||||
|
||||
struct Token: Codable, Equatable, Identifiable, Sendable {
|
||||
public let id: UUID
|
||||
public let userID: User.ID
|
||||
|
||||
Reference in New Issue
Block a user