import { Injectable, OnModuleInit } from '@nestjs/common' import { auth } from 'firebase-admin' import { getAuth, sendPasswordResetEmail, signInWithEmailAndPassword, UserCredential } from 'firebase/auth' import { isEmpty, isNil } from 'lodash' import { Roles } from '../../models/enums/roles.enum' import { CountryHelper, LanguageCountryCode } from '../../utils/countries' import { isNotBlank } from '../../utils/string/is_not_blank' import { Firebender } from '../firebender' export type CreateParams = { id?: string email?: string name?: string password?: string country_code?: string } export const USER_NOT_FOUND_ERROR = 'auth/user-not-found' @Injectable() export class AuthenticationService implements OnModuleInit { get userImportOptions(): auth.UserImportOptions { return { hash: { algorithm: 'SCRYPT', key: Buffer.from(process.env.FIREBASE_AUTH_MIGRATION_HASH, 'base64'), saltSeparator: Buffer.from('Bw==', 'base64'), rounds: 8, memoryCost: 14, }, } } private firebender: Firebender onModuleInit(): void { this.firebender = new Firebender() } async signIn(email: string, password: string, tenant?: string): Promise { const auth = getAuth(this.firebender.firebaseClient) if (isNotBlank(tenant)) auth.tenantId = await this.getTenantId(tenant) return signInWithEmailAndPassword(auth, email, password) } async getUser(uid: string, tenant?: string): Promise { let user: auth.UserRecord try { user = await (await this.getTenantAuth(tenant)).getUser(uid) } catch (error) { if (error.code !== USER_NOT_FOUND_ERROR) throw error } return user } async getUserByEmail(email: string, tenant?: string): Promise { let user: auth.UserRecord try { user = await (await this.getTenantAuth(tenant)).getUserByEmail(email) } catch (error) { if (error.code !== USER_NOT_FOUND_ERROR) throw error } return user } async revokePatientToken(uid: string): Promise { await auth(this.firebender.firebaseAdmin).revokeRefreshTokens(uid) } async createPatient(params: CreateParams): Promise { const properties: auth.CreateRequest = { uid: params.id, displayName: params.name, } if (params.email) properties.email = params.email const user = await auth(this.firebender.firebaseAdmin).createUser(params) await auth(this.firebender.firebaseAdmin).setCustomUserClaims( user.uid, this.claims(Roles.PATIENT, user.uid, params.country_code as LanguageCountryCode), ) return user } async createUser(params: CreateParams, tenant?: string): Promise { const auth = await this.getTenantAuth(tenant) const user = await auth.createUser({ uid: params.id, email: params.email, displayName: params.name, password: params.password, }) await auth.setCustomUserClaims( user.uid, this.claims(Roles.USER, user.uid, params.country_code as LanguageCountryCode), ) return user } async loadAllUsers(tenant?: string): Promise> { const auth = await this.getTenantAuth(tenant) let users: Array = [] let nextPageToken: string do { const listUsersResult = await auth.listUsers(1000, nextPageToken) nextPageToken = listUsersResult.pageToken users = [...users, ...listUsersResult.users] } while (!isNil(nextPageToken)) return users } async delete(uid: string, tenant?: string): Promise { await (await this.getTenantAuth(tenant)).deleteUser(uid) } async deleteUsers(users: Array): Promise { const auths = {} users.forEach(user => (auths[user.tenantId || ''] = [...(auths[user.tenantId || ''] || []), user.uid])) await Promise.all( Object.keys(auths).map(async tenant => (await this.getTenantAuth(tenant)).deleteUsers(auths[tenant])), ) } async getAccessToken( uid: string, tenant?: string, roleClaims: Roles = Roles.PATIENT, languageCountryCode?: LanguageCountryCode, ): Promise { return (await this.getTenantAuth(tenant)).createCustomToken(uid, this.claims(roleClaims, uid, languageCountryCode)) } async generateResetPasswordUrl(uid: string, tenant?: string): Promise { const auth = await this.getTenantAuth(tenant) const user = await auth.getUser(uid) return auth.generatePasswordResetLink(user.email) } async sendResetPasswordEmail(email: string, tenant?: string): Promise { const auth = getAuth(this.firebender.firebaseClient) if (isNotBlank(tenant)) auth.tenantId = await this.getTenantId(tenant) await sendPasswordResetEmail(auth, email) } private async getTenantAuth(tenant?: string): Promise { if (isNil(tenant) || isEmpty(tenant)) return auth(this.firebender.firebaseAdmin) const tenantId = await this.getTenantId(tenant) if (isNil(tenantId)) throw new Error(`Tenant id '${tenant}' non exists`) return auth(this.firebender.firebaseAdmin) .tenantManager() .authForTenant(tenantId) } private async getTenantId(tenant: string): Promise { const tenants = ( await auth(this.firebender.firebaseAdmin) .tenantManager() .listTenants() ).tenants for (const value of tenants) { if (value.tenantId === tenant || value.tenantId.indexOf(`${tenant}-`) >= 0) return value.tenantId } return null } private modificateUsers = (users: Array): Array => users.map(user => { const modifiedUser: any = user.toJSON() if (modifiedUser.passwordHash) { modifiedUser.passwordHash = Buffer.from(user.passwordHash, 'base64') modifiedUser.passwordSalt = Buffer.from(user.passwordSalt, 'base64') } modifiedUser.tenant = modifiedUser.tenantId delete modifiedUser.tenantId return modifiedUser }) private claims(role: Roles, uid: string, languageCountryCode: LanguageCountryCode = LanguageCountryCode.PT_BR) { return { 'https://hasura.io/jwt/claims': { 'x-hasura-default-role': role, 'x-hasura-role': role, 'x-hasura-allowed-roles': [role], 'x-hasura-user-id': uid, 'x-hasura-language': CountryHelper.extractOnlyLanguage(languageCountryCode), 'x-hasura-locale': languageCountryCode.toLocaleLowerCase(), }, } } }