diff --git a/Sources/ApiCore/ApiCoreBase.swift b/Sources/ApiCore/ApiCoreBase.swift index dd2e23f..d038cc8 100644 --- a/Sources/ApiCore/ApiCoreBase.swift +++ b/Sources/ApiCore/ApiCoreBase.swift @@ -40,6 +40,12 @@ public class ApiCoreBase { /// Databse config public static var databaseConfig: DatabasesConfig? + /// Blocks of code executed when new user registers + public static var userDidRegister: [(User) -> ()] = [] + + /// Blocks of code executed when new user tries to register + public static var userShouldRegister: [(User) -> (Bool)] = [] + /// Add / register model public static func add(model: Model.Type, database: DatabaseIdentifier) where Model: Fluent.Migration, Model: Fluent.Model, Model.Database: SchemaSupporting & QuerySupporting { models.append(model) diff --git a/Sources/ApiCore/Controllers/UsersController.swift b/Sources/ApiCore/Controllers/UsersController.swift index 757e952..e24f99d 100644 --- a/Sources/ApiCore/Controllers/UsersController.swift +++ b/Sources/ApiCore/Controllers/UsersController.swift @@ -153,7 +153,7 @@ public class UsersController: Controller { guard ApiCoreBase.configuration.auth.allowRegistrations == true else { throw Error.registrationsNotPermitted } - return try register(req) + return try UsersManager.register(req) } // Me @@ -193,7 +193,7 @@ public class UsersController: Controller { guard ApiCoreBase.configuration.auth.allowInvitations == true else { throw Error.invitationsNotPermitted } - return try invite(req) + return try UsersManager.invite(req) } // Input invitation data @@ -299,144 +299,3 @@ public class UsersController: Controller { } } - -// MARK: Common registration helpers - -extension UsersController { - - private static func checkDomain(email: String, for allowedDomains: [String]) throws { - if !allowedDomains.isEmpty { - guard let domain = email.domainFromEmail(), !domain.isEmpty else { - throw Error.domainNotAllowedForRegistration - } - guard allowedDomains.contains(domain) else { - throw Error.domainNotAllowedForRegistration - } - } - } - - private static func checkExistingUser(email: String, on req: Request) -> Future { - return User.query(on: req).filter(\User.email == email).first().map(to: User?.self) { existingUser in - guard let existingUser = existingUser else { - return nil - } - return existingUser - } - } - - private static func save(_ user: User, targetUri: String?, isInvite: Bool, on req: Request) throws -> Future { - return user.save(on: req).flatMap(to: User.self) { user in - let jwtService = try req.make(JWTService.self) - let jwtToken = try jwtService.signEmailConfirmation( - user: user, - type: (isInvite ? .invitation : .registration), - redirectUri: targetUri, - on: req - ) - - // TODO: Add base64 encoded server image to the template!!! - let templateModel = try User.EmailTemplate( - verification: jwtToken, - link: req.serverURL().absoluteString.finished(with: "/") + "users/\(isInvite ? "input-invite" : "verify")?token=" + jwtToken, - user: user, - sender: isInvite ? req.me.user() : nil - ) - - let templateType: EmailTemplate.Type = isInvite ? InvitationEmailTemplate.self : RegistrationEmailTemplate.self - - return try templateType.parsed( - model: templateModel, - on: req - ).flatMap(to: User.self) { double in - let from = ApiCoreBase.configuration.mail.email - let subject = isInvite ? "Invitation" : "Registration" // TODO: Localize!!!!!! - let mail = Mailer.Message(from: from, to: user.email, subject: subject, text: double.string, html: double.html) - return try req.mail.send(mail).map(to: User.self) { mailResult in - switch mailResult { - case .success: - return user - default: - throw AuthError.emailFailedToSend - } - } - } - } - } - - private static func invite(_ req: Request) throws -> Future { - return try User.Auth.EmailConfirmation.fill(post: req).flatMap(to: Response.self) { emailConfirmation in - if !ApiCoreBase.configuration.auth.allowedDomainsForInvitations.isEmpty { - guard ApiCoreBase.configuration.auth.allowInvitations == true else { - throw Error.invitationsNotPermitted - } - } - try checkDomain( - email: emailConfirmation.email, - for: ApiCoreBase.configuration.auth.allowedDomainsForInvitations - ) // Check if domain is allowed in the system - - return try User.Invitation.fill(post: req).flatMap(to: Response.self) { data in - return checkExistingUser(email: data.email, on: req).flatMap(to: Response.self) { existingUser in - let user: User - if let existingUser = existingUser { - if existingUser.verified == true { - // QUESTION: Do we want a more specific error? In this case no need to re-send invite as user is already registered - throw AuthError.emailExists - } else { - user = existingUser - } - } else { - user = try data.newUser(on: req) - } - - if ApiCoreBase.configuration.general.singleTeam == true { // Single team scenario - return Team.adminTeam(on: req).flatMap(to: Response.self) { singleTeam in - return try save(user, targetUri: emailConfirmation.targetUri, isInvite: true, on: req).flatMap(to: Response.self) { newUser in - return singleTeam.users.attach(newUser, on: req).flatMap(to: Response.self) { _ in - return try newUser.asDisplay().asResponse(.created, to: req) - } - } - } - } else { - return try save(user, targetUri: emailConfirmation.targetUri, isInvite: true, on: req).flatMap(to: Response.self) { user in - return try user.asDisplay().asResponse(.created, to: req) - } - } - } - } - } - } - - private static func register(_ req: Request) throws -> Future { - return try User.Auth.EmailConfirmation.fill(post: req).flatMap(to: Response.self) { emailConfirmation in - try checkDomain( - email: emailConfirmation.email, - for: ApiCoreBase.configuration.auth.allowedDomainsForRegistration - ) // Check if domain is allowed in the system - - return try User.Registration.fill(post: req).flatMap(to: Response.self) { data in - return checkExistingUser(email: data.email, on: req).flatMap(to: Response.self) { user in - guard user == nil else { - throw AuthError.emailExists - } - let user = try data.newUser(on: req) - - if ApiCoreBase.configuration.general.singleTeam == true { // Single team scenario - return Team.adminTeam(on: req).flatMap(to: Response.self) { singleTeam in - return try save(user, targetUri: emailConfirmation.targetUri, isInvite: false, on: req).flatMap(to: Response.self) { newUser in - return singleTeam.users.attach(newUser, on: req).flatMap(to: Response.self) { _ in - return try newUser.asDisplay().asResponse(.created, to: req) - } - } - } - } else { - return try save(user, targetUri: emailConfirmation.targetUri, isInvite: false, on: req).flatMap(to: Response.self) { user in - return try user.asDisplay().asResponse(.created, to: req) - } - } - } - } - } - } - -} diff --git a/Sources/ApiCore/Managers/UsersManager.swift b/Sources/ApiCore/Managers/UsersManager.swift index df15a33..52d2d2f 100644 --- a/Sources/ApiCore/Managers/UsersManager.swift +++ b/Sources/ApiCore/Managers/UsersManager.swift @@ -8,6 +8,8 @@ import Foundation import Vapor import FluentPostgreSQL +import ErrorsCore +import MailCore public class UsersManager { @@ -21,4 +23,139 @@ public class UsersManager { } } + public static func checkDomain(email: String, for allowedDomains: [String]) throws { + if !allowedDomains.isEmpty { + guard let domain = email.domainFromEmail(), !domain.isEmpty else { + throw UsersController.Error.domainNotAllowedForRegistration + } + guard allowedDomains.contains(domain) else { + throw UsersController.Error.domainNotAllowedForRegistration + } + } + } + + public static func checkExistingUser(email: String, on req: Request) -> Future { + return User.query(on: req).filter(\User.email == email).first().map(to: User?.self) { existingUser in + guard let existingUser = existingUser else { + return nil + } + return existingUser + } + } + + public static func save(_ user: User, targetUri: String?, isInvite: Bool, on req: Request) throws -> Future { + return user.save(on: req).flatMap(to: User.self) { user in + let jwtService = try req.make(JWTService.self) + let jwtToken = try jwtService.signEmailConfirmation( + user: user, + type: (isInvite ? .invitation : .registration), + redirectUri: targetUri, + on: req + ) + + // TODO: Add base64 encoded server image to the template!!! + let templateModel = try User.EmailTemplate( + verification: jwtToken, + link: req.serverURL().absoluteString.finished(with: "/") + "users/\(isInvite ? "input-invite" : "verify")?token=" + jwtToken, + user: user, + sender: isInvite ? req.me.user() : nil + ) + + let templateType: EmailTemplate.Type = isInvite ? InvitationEmailTemplate.self : RegistrationEmailTemplate.self + + return try templateType.parsed( + model: templateModel, + on: req + ).flatMap(to: User.self) { double in + let from = ApiCoreBase.configuration.mail.email + let subject = isInvite ? "Invitation" : "Registration" // TODO: Localize!!!!!! + let mail = Mailer.Message(from: from, to: user.email, subject: subject, text: double.string, html: double.html) + return try req.mail.send(mail).map(to: User.self) { mailResult in + switch mailResult { + case .success: + return user + default: + throw AuthError.emailFailedToSend + } + } + } + } + } + + public static func invite(_ req: Request) throws -> Future { + return try User.Auth.EmailConfirmation.fill(post: req).flatMap(to: Response.self) { emailConfirmation in + if !ApiCoreBase.configuration.auth.allowedDomainsForInvitations.isEmpty { + guard ApiCoreBase.configuration.auth.allowInvitations == true else { + throw UsersController.Error.invitationsNotPermitted + } + } + try checkDomain( + email: emailConfirmation.email, + for: ApiCoreBase.configuration.auth.allowedDomainsForInvitations + ) // Check if domain is allowed in the system + + return try User.Invitation.fill(post: req).flatMap(to: Response.self) { data in + return checkExistingUser(email: data.email, on: req).flatMap(to: Response.self) { existingUser in + let user: User + if let existingUser = existingUser { + if existingUser.verified == true { + // QUESTION: Do we want a more specific error? In this case no need to re-send invite as user is already registered + throw AuthError.emailExists + } else { + user = existingUser + } + } else { + user = try data.newUser(on: req) + } + + if ApiCoreBase.configuration.general.singleTeam == true { // Single team scenario + return Team.adminTeam(on: req).flatMap(to: Response.self) { singleTeam in + return try save(user, targetUri: emailConfirmation.targetUri, isInvite: true, on: req).flatMap(to: Response.self) { newUser in + return singleTeam.users.attach(newUser, on: req).flatMap(to: Response.self) { _ in + return try newUser.asDisplay().asResponse(.created, to: req) + } + } + } + } else { + return try save(user, targetUri: emailConfirmation.targetUri, isInvite: true, on: req).flatMap(to: Response.self) { user in + return try user.asDisplay().asResponse(.created, to: req) + } + } + } + } + } + } + + public static func register(_ req: Request) throws -> Future { + return try User.Auth.EmailConfirmation.fill(post: req).flatMap(to: Response.self) { emailConfirmation in + try checkDomain( + email: emailConfirmation.email, + for: ApiCoreBase.configuration.auth.allowedDomainsForRegistration + ) // Check if domain is allowed in the system + + return try User.Registration.fill(post: req).flatMap(to: Response.self) { data in + return checkExistingUser(email: data.email, on: req).flatMap(to: Response.self) { user in + guard user == nil else { + throw AuthError.emailExists + } + let user = try data.newUser(on: req) + + if ApiCoreBase.configuration.general.singleTeam == true { // Single team scenario + return Team.adminTeam(on: req).flatMap(to: Response.self) { singleTeam in + return try save(user, targetUri: emailConfirmation.targetUri, isInvite: false, on: req).flatMap(to: Response.self) { newUser in + return singleTeam.users.attach(newUser, on: req).flatMap(to: Response.self) { _ in + return try newUser.asDisplay().asResponse(.created, to: req) + } + } + } + } else { + return try save(user, targetUri: emailConfirmation.targetUri, isInvite: false, on: req).flatMap(to: Response.self) { user in + return try user.asDisplay().asResponse(.created, to: req) + } + } + } + } + } + } + }