import Vapor import FluentPostgresDriver import SwiftSMTP public struct AuthenticationController: Sendable where User.SessionID == ExpiringUserId { public init() {} public func routes (_ router: any RoutesBuilder, api apiRouter: any RoutesBuilder) { let auth = router.grouped ("auth") let api = apiRouter .grouped ("auth") auth.get ("login", use: loginForm) api.post ("login", use: login) auth.get ("password", ":token", use: passwordForm) auth.grouped (LogoutMiddleware()).get ("logout", use: logout) api.get ("token", ":token", use: token) api.post ("forgot", use: forgotPassword) api.post ("password", ":token", use: savePassword) } func loginForm (request: Request) async throws -> View { return try await request.view.render ("login", ["section": "login"]) } private struct LoginInput: Decodable { let email: String let password: String } func login (request: Request) async throws -> some Response { let loginInput = try request.content.decode (LoginInput.self) return try await request.db.withSQLConnection { connection in if let user = try await User.find (email: loginInput.email, password: loginInput.password, on: connection) { request.auth.login (user) request.logger.info ("Authenticated user \(user)") return Response (status: .ok) } else { return Response (status: .unauthorized) } } } func passwordForm (request: Request) async throws -> View { let token = try request.parameters.require ("token") return try await request.view.render ("password", ["token": token, "section": "login"]) } func logout (request: Request) async throws -> some Response { return request.redirect (to: try Environment.baseURL.absoluteString) } func token (request: Request) async throws -> UserToken.Token { let token = try request.parameters.require ("token") return try await request.db.withSQLConnection { connection in guard let token = try await UserToken.fetch (token: token, connection: connection) else { throw Abort (.notFound) } return token } } private struct NewPassword: Decodable { let password: String } func savePassword (request: Request) async throws -> Response { let token = try request.parameters.require ("token") let newPassword = try request.content.decode (NewPassword.self) return try await request.db.withConnection { connection in try await connection.transaction { connection in guard let userToken = try await UserToken.fetch (token: token, connection: connection) else { throw Abort (.notFound) } try await User.update (userId: userToken.userId, password: newPassword.password, on: connection) try await UserToken.delete (token: token, connection: connection) return Response(status: .ok) } } } private struct Input: Decodable { let email: String } struct TokenEmailContext: Encodable { let token: String let host: String let expiration: Date } func forgotPassword (request: Request) async throws -> Response { let input = try request.content.decode (Input.self) return try await request.db.withConnection { connection in try await connection.transaction { connection in if let user = try await User.find (email: input.email, on: connection) { let token = try await UserToken.create (connection: connection).token try await User.store (token: token, userId: user.id, on: connection) let host = try Environment.baseURL.host() ?? "" let body = try await request.view.render ("email/reset", TokenEmailContext (token: token, host: host, expiration: Calendar.current.date (byAdding: .hour, value: 1, to: Date()) ?? Date())) .data let message = Email (sender: Email.Contact (emailAddress: try Environment.emailSender), recipients: [Email.Contact(emailAddress: input.email)], subject: "Aktivera ditt konto på \(host)", body: .plain (String (buffer: body))) try await request.swiftSMTP.mailer.send (message) } return Response(status: .ok) } } } } struct LogoutMiddleware: AsyncMiddleware { func respond (to request: Vapor.Request, chainingTo next: any AsyncResponder) async throws -> Vapor.Response { let response = try await next.respond (to: request) request.auth.logout (User.self) request.session.destroy() return response } }