import Vapor import Leaf import PostgresKit import SwiftSMTPVapor public struct AdminController: Sendable where User.SessionID == ExpiringUserId { public init() {} public func routes (_ router: any RoutesBuilder, api apiRouter: any RoutesBuilder) { let admin = router .grouped ("admin") .grouped (RoleMiddleware (role: "admin")) let api = apiRouter .grouped ("admin") .grouped (RoleAPIMiddleware (role: "admin")) admin.get (use: adminPage (request:)) api.get (use: list (request:)) api.get (":id", use: fetch (request:)) api.put (use: invite (request:)) api.post (":id", use: save(request:)) } struct AdminContext: Encodable { let roles: [String] let section: String } @Sendable private func adminPage (request: Request) async throws -> View { let roles = try await request.db.withSQLConnection { connection in return try await UserRole.all (connection: connection) } return try await request.view.render ("admin", AdminContext (roles: roles.map (\.name), section: "useradmin")) } struct UserItem: Content { let id: UUID let email: String let fullName: String let isActive: Bool let roles: [String] init(user: User) { self.id = user.id self.email = user.email self.fullName = user.fullName self.isActive = user.isActive self.roles = user.roles } } @Sendable private func list (request: Request) async throws -> [UserItem] { return try await request.db.withSQLConnection { connection in let users = try await User.all(on: connection) return users.map { UserItem (user: $0) } } } @Sendable private func fetch (request: Request) async throws -> UserItem { guard let idString = request.parameters.get ("id"), let id = UUID (uuidString: idString) else { throw Abort (.badRequest) } return try await request.db.withSQLConnection { connection in guard let user = try await User.fetch (id, on: connection) else { throw Abort (.notFound) } return UserItem (user: user) } } struct Invitation: Decodable { let email: String let fullname: String let roles: [String] } @Sendable private func invite (request: Request) async throws -> Response { let invitation = try request.content.decode(Invitation.self) return try await request.db.withConnection { connection in try await connection.transaction { connection in let token = try await UserToken.create (connection: connection).token try await User.create (email: invitation.email, fullname: invitation.fullname, roles: invitation.roles, token: token, on: connection) let host = try Environment.baseURL.host() ?? "" let body = try await request.view.render ("email/invite", AuthenticationController.TokenEmailContext (token: token, host: host, expiration: Calendar.current.date (byAdding: .day, value: 1, to: Date()) ?? Date())) .data let message = Email (sender: Email.Contact (emailAddress: try Environment.emailSender), recipients: [Email.Contact(emailAddress: invitation.email)], subject: "Aktivera ditt konto på \(host)", body: .plain (String (buffer: body))) try await request.swiftSMTP.mailer.send(message) return Response (status: .created) } } } struct Save: Decodable { let email: String let fullname: String let roles: [String] let isActive: Bool } @Sendable private func save (request: Request) async throws -> Response { guard let userId = request.parameters.get("id", as: UUID.self) else { throw Abort (.badRequest) } let save = try request.content.decode(Save.self) return try await request.db.withSQLConnection (user: try request.auth.require (BasicUser.self)) { connection in guard (try await User.fetch (userId, on: connection)) != nil else { throw Abort (.notFound) } guard !save.email.isEmpty && !save.fullname.isEmpty else { throw Abort (.badRequest) } try await User.save (id: userId, email: save.email, fullname: save.fullname, roles: save.roles, isActive: save.isActive, on: connection) return Response (status: .ok) } } }