UNPKG

vibez-core

Version:

Utilities, types and common dependencies.

774 lines (671 loc) 25.7 kB
import { DomainError } from "../common/DomainError"; import { IndexableCollection } from "../common/Common"; import { IdentityManager } from "./IdentityManagement"; import { SecurityContext } from "./SecurityContext"; import { CredentialProvider, DEFAULT_EXPIRATION_SECONDS, Credential, User, Role, UserKind, MIN_PASSWORD_LENGTH } from "./Security"; import { randomString } from "../util"; import { ContactProvider } from "../business"; import { Indexable, DataProvider } from "../common"; import { Action } from "../auditory"; import * as admin from "firebase-admin"; import { firestore } from "firebase-admin"; import { Domain, create } from "domain"; import { FieldValue } from "@google-cloud/firestore"; export class AccountManager<T extends UserKind> extends DataProvider { context: SecurityContext<T>; private static readonly COMPONENT = "ACCOUNT_MANAGER"; constructor(context: SecurityContext<T>, dataSource: firestore.Firestore) { super(dataSource); this.context = context; } private async getDefaultRoleForAccount<T extends UserKind>( userKind: T ): Promise<IndexableCollection<"Roles">> { let result = this.context.performTask<SecurityContext<T>, Role>( "Read", AccountManager.COMPONENT, async c => { let rolesCollection = this.dataSource.collection("Roles"); let roleDocumentsQuery = rolesCollection.where( "name", "==", `DEFAULT_${userKind}` ); let { docs, empty } = await roleDocumentsQuery.get(); if (empty) { return Promise.reject( new DomainError( "NOT_FOUND", `There is no default role defined to account of type ${userKind}`, AccountManager.COMPONENT, "Please validate with the platform administrator", undefined, "MEDIUM", `When retrieving default role for account of type ${userKind}, there were no matching records` ) ); } let [snapshot] = [...docs]; let role: Role = Object.assign({ id: snapshot.id } as Role, snapshot.data()); return role; } ); return result; } private async validateActivation( actionCode: string, continueUrl: string ): Promise<boolean> { let { operation, data } = await firebase.auth().checkActionCode(actionCode); if (operation !== "VERIFY_EMAIL") { return Promise.reject( new DomainError( "INVALID_ACTION", "Activation Action is not a valid one", AccountManager.COMPONENT, "Please validate the submitted information and try again", undefined, "LOW", "When validating activation for activation intent, the submitted action was invalid)" ) ); } let { email, fromEmail } = data; let result = await this.context.performTask<SecurityContext<T>, boolean>( "Write", AccountManager.COMPONENT, async context => { let activatingUser: User<"CORPORATE" | "END_USER">; let usersCollection = this.dataSource.collection("Users"); let { empty, docs } = await usersCollection.get(); if (empty) { return Promise.reject( new DomainError( "NOT_FOUND", `There were no matches for the submitted credentials`, AccountManager.COMPONENT, "Please validate the submitted information and try again", undefined, "LOW", "When validating account's activation there were not records matching credentials for the linked email" ) ); } let match = docs.map((snapshot)=> ({ ...snapshot.data(), id: snapshot.id } as User<"CORPORATE"|"END_USER">)).find(({credentials})=>credentials!.some(c=>c.issuer=="Local" && c.id == email)); if (!match) { return Promise.reject( new DomainError( "NOT_FOUND", `There were no matches for the submitted credentials`, AccountManager.COMPONENT, "Please validate the submitted information and try again", undefined, "LOW", "When validating account's activation there were not records matching credentials for the linked email" ) ); } activatingUser = match; let credential = activatingUser.credentials!.find( c => c.issuer == "Local" && c.id == email ); let credentialIndex = activatingUser.credentials!.indexOf(credential!); activatingUser.credentials![credentialIndex].validatedOn = new Date( Date.now() ); activatingUser.validatedOn = new Date(Date.now()); let action: Action = { actor: context.getActorInformation(), date: new Date(Date.now()), description: "Activate Account" }; activatingUser.actions.push(action); await usersCollection.doc(activatingUser.id).update(activatingUser); return true; } ); return true; } public async authenticateAccount( credentialID: string, password?: string, token?: string ): Promise<User<UserKind>> { let result = await this.context.performTask< SecurityContext<T>, User<UserKind> >("Read", AccountManager.COMPONENT, async c => { let authenticatedUser: User<UserKind>; let userCollection = this.dataSource.collection("Users"); // let userDocumentsQuery = userCollection.where( // "credentials.id", // "==", // credentialID // ); // if (password) { // userDocumentsQuery = userDocumentsQuery // .where("credential.issuer", "==", "Local") // .where("credentials.password", "==", password); // } // if (token) { // userDocumentsQuery = userDocumentsQuery.where( // "credentials.token", // "==", // token // ); // } let { docs, empty } = await userCollection.get(); if (empty) { return Promise.reject( new DomainError( "NOT_FOUND", `There were no matches for the submitted credentials`, AccountManager.COMPONENT, "Please validate the submitted information or if there is a pending transaction on your account/credentials and try again", undefined, "LOW", "When authenticating the account for the submitted credentials there were not records matching either the credential ID, password or token and in a valid state (Successfully Validated)" ) ); } let match = docs.map((snapshot)=>({...snapshot.data(), id: snapshot.id} as User<UserKind>)).find(({credentials})=>{ return credentials!.some(c=> c.id == credentialID && password ? c.issuer == "Local" && c.password == password : token ? c.token == token : false); }); if (!match) { return Promise.reject( new DomainError( "NOT_FOUND", `There were no matches for the submitted credentials`, AccountManager.COMPONENT, "Please validate the submitted information or if there is a pending transaction on your account/credentials and try again", undefined, "LOW", "When authenticating the account for the submitted credentials there were not records matching either the credential ID, password or token and in a valid state (Successfully Validated)" ) ); } //let [{ id, data }] = [...docs]; authenticatedUser = match; if (!authenticatedUser.validatedOn) { return Promise.reject( new DomainError( "INVALID_ACCOUNT", `Account hasn't been validated`, AccountManager.COMPONENT, "Please complete the registration process by checking your e-mails for an e-mail from [@vibez.io] with instructions.", undefined, "LOW", "When authenticating the account for the submitted credentials the account hasn't been validated" ) ); } let { issuedOn, expiration, validatedOn } = authenticatedUser.credentials!.find( c => c.id == credentialID && (password ? c.password == password : false || token ? c.token == token : false) )!; if (!validatedOn) { return Promise.reject( new DomainError( "INVALID_CREDENTIAL", `Credentials haven't been validated`, AccountManager.COMPONENT, "Please complete the registration process by checking your e-mails for an e-mail from [@vibez.io] with instructions.", undefined, "LOW", "When authenticating the account's credential haven't been validated" ) ); } let expiringAt = issuedOn; expiringAt.setSeconds(expiringAt.getSeconds() + expiration); if (expiringAt < new Date(Date.now())) { return Promise.reject( new DomainError( "INVALID_CREDENTIAL", `Expired credentials`, AccountManager.COMPONENT, "Please renew your credentials to continue", undefined, "LOW", "When authenticating the account for the submitted credentials the retrieved credentials were expired" ) ); } return authenticatedUser; }); return result; } public async createAccount<U extends UserKind>( identity: IndexableCollection<"Identities">, credential: Credential, userKind: U ): Promise<User<U>> { if (!credential.password) { return Promise.reject( new DomainError( "INVALID_CREDENTIAL", `Password required`, AccountManager.COMPONENT, "Please validate your information and try again", undefined, "LOW", "When signing up the submitted account's information. There provided credentials had missing required information" ) ); } credential.issuer = "Local"; credential.issuedOn = new Date(Date.now()); credential.expiration = DEFAULT_EXPIRATION_SECONDS; let identityManager = new IdentityManager(this.context, this.dataSource); let contactProvider = new ContactProvider(this.context, this.dataSource); let fetchedIdentity = await identityManager.getIdentity(identity); let contact = await contactProvider.getContact(fetchedIdentity.contact); let role = await this.getDefaultRoleForAccount<U>(userKind); let result = await this.context.performTask<SecurityContext<T>, User<U>>( "Write", AccountManager.COMPONENT, async c => { let action: Action = { actor: c.getActorInformation(), date: new Date(Date.now()), description: `Register account for identity with id [${identity.id}]` }; let account: User<U> = { id: "", identity, credentials: [credential], kind: userKind, role, actions: [action], collection: "Users" }; let usersCollection = this.dataSource.collection("Users"); let userDocument = await usersCollection.add(account); let { id } = await userDocument.get(); account.id = id; let user: firebase.User = await firebase .auth() .createUserWithEmailAndPassword(contact.email, credential.password!); await user.sendEmailVerification(); return account; } ); return result; } public async activateAccount<U extends UserKind>( user: IndexableCollection<"Users">, userKind: U ): Promise<User<U>> { let result = await this.context.performTask<SecurityContext<T>, User<U>>( "Write", AccountManager.COMPONENT, async c => { let action: Action = { actor: c.getActorInformation(), date: new Date(Date.now()), description: `Account with id [${ user.id }] has been activated successfully` }; let updatedAccount: User<U>; let userCollection = this.dataSource.collection("Users"); let userDocument = userCollection.doc(user.id); let snapshot = await userDocument.get(); if (!snapshot.exists) { return Promise.reject( new DomainError( "NOT_FOUND", "User cannot be activated", AccountManager.COMPONENT, "Please validate the submitted information", undefined, "LOW", "When activating account, the submitted account information has no matches" ) ); } updatedAccount = Object.assign({ id: snapshot.id } as User<U>, snapshot.data()); if (userKind != updatedAccount.kind) { return Promise.reject( new DomainError( "INVALID_ACCOUNT", "User cannot be activated", AccountManager.COMPONENT, "Please validate the submitted information", undefined, "LOW", "When activating account, the submitted account information doesn't match available records" ) ); } updatedAccount.actions.push(action); updatedAccount.validatedOn = new Date(Date.now()); let { actions, validatedOn } = updatedAccount; await userDocument.update({ actions, validatedOn }); return updatedAccount; } ); return result; } public async assignAccountRole<U extends UserKind>( user: IndexableCollection<"Users">, role: IndexableCollection<"Roles">, userKind: U ): Promise<User<U>> { let result = await this.context.performTask<SecurityContext<T>, User<U>>( "Write", AccountManager.COMPONENT, async c => { let action: Action = { actor: c.getActorInformation(), date: new Date(Date.now()), description: `The role of the account with id [${ user.id }] has been changed to [${role.id}]` }; let updatedAccount: User<U>; let rolesCollection = this.dataSource.collection("Roles"); let roleDocument = rolesCollection.doc(role.id); { let { id, exists, data } = await roleDocument.get(); if (!exists) { return Promise.reject( new DomainError( "NOT_FOUND", "New role couldn't be assigned", AccountManager.COMPONENT, "Please validate the submitted information", undefined, "LOW", "When assigning account's role, the submitted role information has no matches" ) ); } } let userCollection = this.dataSource.collection("Users"); let userDocument = userCollection.doc(user.id); { let snapshot = await userDocument.get(); updatedAccount = Object.assign({ id: snapshot.id } as User<U>, snapshot.data()); if (!snapshot.exists) { return Promise.reject( new DomainError( "NOT_FOUND", "New role couldn't be assigned", AccountManager.COMPONENT, "Please validate the submitted information", undefined, "LOW", "When assigning account's role, the submitted account information has no matches" ) ); } let { kind, actions } = updatedAccount; if (userKind != kind) { return Promise.reject( new DomainError( "INVALID_ACCOUNT", "User cannot be activated", AccountManager.COMPONENT, "Please validate the submitted information", undefined, "LOW", "When assigning account's role, the submitted account information doesn't match available records" ) ); } actions.push(action); updatedAccount.role = role; await userDocument.update(updatedAccount); } return updatedAccount; } ); return result; } public async updateCredential<U extends UserKind>( user: IndexableCollection<"Users">, credential: Indexable, issuer: CredentialProvider, data: Credential, userKind: U ): Promise<User<U>> { let result = await this.context.performTask<SecurityContext<T>, User<U>>( "Write", AccountManager.COMPONENT, async c => { let action: Action = { actor: c.getActorInformation(), date: new Date(Date.now()), description: `The credentials of the account with id [${ user.id }] and provider [${issuer}] has been successfully updated` }; let updatedAccount: User<U>; let userCollection = this.dataSource.collection("Users"); let userDocument = userCollection.doc(user.id); { let snapshot = await userDocument.get(); if (!snapshot.exists) { return Promise.reject( new DomainError( "NOT_FOUND", "Cannot update credentials", AccountManager.COMPONENT, "Please validate the submitted information", undefined, "LOW", "When updating account's credentials, the submitted account information has no matches" ) ); } updatedAccount = Object.assign({ id: snapshot.id } as User<U>, snapshot.data()); if (userKind != updatedAccount.kind) { return Promise.reject( new DomainError( "INVALID_ACCOUNT", "User cannot be activated", AccountManager.COMPONENT, "Please validate the submitted information", undefined, "LOW", "When activating account, the submitted account information doesn't match available records" ) ); } } if (!updatedAccount.credentials) { return Promise.reject( new DomainError( "UNASSIGNED_CREDENTIALS", `Account has no credentials registered`, AccountManager.COMPONENT, "Please validate the account information", undefined, "LOW", "When fetching account credentials for updating credentials, there were no credentials assigned to the corresponding account" ) ); } let updatedCredentialIndex = updatedAccount.credentials.findIndex( c => c.id == credential.id && c.issuer == issuer ); if (updatedCredentialIndex < 0) { return Promise.reject( new DomainError( "CREDENTIAL_NOT_FOUND", `Account has no credentials matching the submitted criteria`, AccountManager.COMPONENT, "Please validate the submitted information", undefined, "LOW", "When fetching account credentials for updating credentials, there were no credentials matching the submitted criteria" ) ); } updatedAccount.credentials[ updatedCredentialIndex ].validatedOn = new Date(Date.now()); updatedAccount.credentials[updatedCredentialIndex].issuedOn = new Date( Date.now() ); updatedAccount.credentials[updatedCredentialIndex].token = data.token; updatedAccount.credentials[updatedCredentialIndex].password = data.password; updatedAccount.actions.push(action); let { credentials, actions } = updatedAccount; await userDocument.update({ credentials, actions }); return updatedAccount; } ); return result; } public async resetCredential<U extends UserKind>( identity: Indexable, request?: Indexable ): Promise<User<U>> { let result = await this.context.performTask<SecurityContext<T>, User<U>>( "Write", AccountManager.COMPONENT, async c => { let usersCollection = this.dataSource.collection("Users"); let userDocumentsQuery = usersCollection.where("identity.id","==", identity.id); let { empty, docs } = await userDocumentsQuery.get(); if (empty) { return Promise.reject( new DomainError( "ACCOUNT_NOT_FOUND", `The account for the submitted identity wasn't found`, AccountManager.COMPONENT, "Please validate the identity information", undefined, "LOW", "When fetching account information for recovering credentials, there were no accounts matching the submitted criteria" ) ); } let [snapshot] = [...docs]; if (!snapshot.exists) { return Promise.reject( new DomainError( "ACCOUNT_NOT_FOUND", `The account for the submitted identity wasn't found`, AccountManager.COMPONENT, "Please validate the identity information", undefined, "LOW", "When fetching account information for recovering credentials, there were no accounts matching the submitted criteria" ) ); } let updatedAccount: User<U>; updatedAccount = Object.assign({ id: snapshot.id } as User<U>, snapshot.data()); if (!updatedAccount.credentials) { return Promise.reject( new DomainError( "UNASSIGNED_CREDENTIALS", `Account has no credentials registered`, AccountManager.COMPONENT, "Please validate the account information", undefined, "LOW", "When fetching account credentials for updating credentials, there were no credentials assigned to the corresponding account" ) ); } let updatedCredentialIndex = updatedAccount.credentials.findIndex( c => c.issuer == "Local" ); if (updatedCredentialIndex < 0) { return Promise.reject( new DomainError( "CREDENTIAL_NOT_FOUND", `Account has no credentials matching the submitted criteria`, AccountManager.COMPONENT, "Please validate the submitted information", undefined, "LOW", "When fetching account credentials for updating credentials, there were no credentials matching the submitted criteria" ) ); } let yesterday = new Date(Date.now()); yesterday.setDate(yesterday.getDate() - 1); if (!request) { let resetRequest = { date: new Date(Date.now()), ipAddress: c.clientAddress, id: randomString(12) }; let action: Action = { actor: c.getActorInformation(), date: new Date(Date.now()), description: `Scheduled request [${ resetRequest.id }] to reset password of credentials with id [${ updatedAccount.credentials[updatedCredentialIndex].id }]` }; updatedAccount.resetRequest = resetRequest; updatedAccount.actions.push(action); return updatedAccount; } else if ( request && updatedAccount.resetRequest && updatedAccount.resetRequest.id == request.id && updatedAccount.resetRequest.date > yesterday ) { let action: Action = { actor: c.getActorInformation(), date: new Date(Date.now()), description: `Reset password of credentials with id [${ updatedAccount.credentials[updatedCredentialIndex].id }` }; updatedAccount.credentials[ updatedCredentialIndex ].issuedOn = new Date(Date.now()); updatedAccount.credentials[ updatedCredentialIndex ].password = this.generateRandomPassword(); updatedAccount.actions.push(action); let { credentials, actions, resetRequest } = updatedAccount; await usersCollection.doc(snapshot.id).update({ credentials, actions, resetRequest: FieldValue.delete() }) return updatedAccount; } else { return Promise.reject( new DomainError( "INVALID_OPERATION", `The account's credentials reset request is invalid`, AccountManager.COMPONENT, "Please validate the submitted information", undefined, "LOW", "When proceeding to reset credentials for the account marching the criteria, the submitted information couldn't be validated" ) ); } } ); return result; } generateRandomPassword(): string { let passwordLength = MIN_PASSWORD_LENGTH; return randomString(passwordLength); } }