import { HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { PortalDeterminationService } from '@core/services/portal-determination.service'; import { Applicant } from '@core/typings/applicant.typing'; import { AddEditUser, User, UserFromApi } from '@core/typings/client-user.typing'; import { UsersImport, UsersValidationPayload } from '@core/typings/user.typing'; import { createValidator, CSVBoolean, IsArrayOfType, IsEmail, IsString, PaginationOptions, Required, Transform } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { LogService } from '@yourcause/common/logging'; import { NotifierService } from '@yourcause/common/notifier'; import { AttachYCState, BaseYCService } from '@yourcause/common/state'; import { Subscription } from 'rxjs'; import { UserResources } from './user.resources'; import { UserState } from './user.state'; @AttachYCState(UserState) @Injectable({ providedIn: 'root' }) export class UserService extends BaseYCService { sub = new Subscription(); constructor ( private logger: LogService, private i18n: I18nService, private portal: PortalDeterminationService, private notifier: NotifierService, private userResources: UserResources ) { super(); this.sub.add( this.changesTo$(this.userKey).subscribe(() => { const user = this.currentUser; const firstName = user ? user.firstName : ''; const lastName = user ? user.lastName : ''; const jobTitle = user ? (user as User).jobTitle : ''; const name = `${ firstName.slice(0, 10) + (firstName.length > 10 ? '...' : '') } ${ lastName.slice(0, 15) + (lastName.length > 15 ? '...' : '') }`; this.set('userName', name); this.set('userJobTitle', jobTitle); }) ); } get userKey (): 'user'|'applicant'|'admin' { return this.portal.isManager ? 'user' : this.portal.isApply ? 'applicant' : 'admin'; } get user () { return this.get('user'); } get allUsers () { return this.get('allUsers'); } get applicant () { return this.get('applicant'); } get userEmail () { return this.user ? this.user.email : ''; } get currentUser () { return this.get(this.userKey); } get adminPermissions () { return this.get('adminPermissions'); } get lastSelectedCurrency () { return this.get('lastSelectedCurrency'); } setApplicant (applicant: Applicant) { this.set('applicant', applicant); } setLastSelectedCurrency (currency: string) { this.set('lastSelectedCurrency', currency); } setAdmin (admin: User) { this.set('admin', admin); } async setAdminPermissions () { const adminPermissions = await this.userResources.getAdminPermissions(); this.set('adminPermissions', adminPermissions); } setUser (user: User) { this.set('user', user); } getCurrentUserCulture () { return this.currentUser ? this.currentUser.culture || 'en-US' : 'en-US'; } resetAllUsers () { this.set('allUsers', undefined); return this.setAllUsers(); } async setAllUsers () { if (!this.allUsers) { const allUsers = await this.userResources.getAllUsers(); this.set('allUsers', allUsers); } } getUsersPaginated (options: PaginationOptions) { return this.userResources.getUsersPaginated(options); } async addEditUser (user: AddEditUser) { try { user.firstName = user.firstName.trim(); user.lastName = user.lastName.trim(); await this.userResources.addEditUser(user); this.notifier.success(this.i18n.translate( user.id ? 'USERS:textSuccessfullyUpdatedUser' : 'USERS:textSuccessfullyAddedUser', {}, user.id ? 'Successfully updated the user' : 'Successfully added the user' )); return true; } catch (err) { const e = err as HttpErrorResponse; this.logger.error(e); if (e.error && e.error.message === 'Email already exists.') { this.notifier.error(this.i18n.translate( 'common:textEmailAlreadyInUse', {}, 'Email address already in use' )); } else { this.notifier.error(this.i18n.translate( user.id ? 'USERS:textErrorUpdatingUser' : 'USERS:textErrorAddingUser', {}, user.id ? 'There was an error updating the user' : 'There was an error adding the user' )); throw e; } return false; } } async activateUser (id: number) { try { await this.userResources.activateUser(id); this.notifier.success(this.i18n.translate( 'USERS:textSuccessfullyActivatedUser', {}, 'Successfully activated the user' )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'USERS:textErrorActivatingUser', {}, 'There was an error activating the user' )); throw e; } } async handleUsersImport (users: UsersImport[]) { try { await this.userResources.importUsers(users); this.notifier.success( this.i18n.translate( 'MANAGE:textSuccessfullyImportedUsers', {}, 'Successfully imported users' ) ); } catch (e) { this.logger.error(e); this.notifier.error( this.i18n.translate( 'MANAGE:textErrorImportingUsers', {}, 'There was an error importing users' ) ); } } async deactivateUser (ids: number[]) { try { await this.userResources.deactivateUser(ids); if (ids.length === 1) { this.notifier.success(this.i18n.translate( 'USERS:textSuccessfullyDeactivatedUser', {}, 'Successfully deactivated the user' )); } else { this.notifier.success(this.i18n.translate( 'USERS:textSuccessfullyDeactivatedUsers', {}, 'Successfully deactivated the users' )); } } catch (e) { this.logger.error(e); if (ids.length === 1) { this.notifier.error(this.i18n.translate( 'USERS:textErrorDeactivatingUser', {}, 'There was an error deactivating the user' )); } else { this.notifier.error(this.i18n.translate( 'USERS:textErrorDeactivatingUsers', {}, 'There was an error deactivating the users' )); } throw e; } } async validateUsersImport (context: Record<'call', ReturnType>, group: UsersImportValidationModel[]) { if (!context.call) { const params = this.getUserImportParams(group); context.call = this.validateUsers(params); } const validatorResponse = await context.call; return validatorResponse; } getUserImportParams (group: UsersImportValidationModel[]) { return { emails: group.map((member) => member['Email']), roleIds: group.reduce((acc, item) => { return [...acc].concat(item['Roles']); }, []), workflowLevelIds: group.reduce((acc, item) => { return [...acc].concat(item['Workflow Levels']); }, []) }; } async validateUsers ( payload: UsersValidationPayload ) { const response = await this.userResources.validateUsers(payload); return { Email: response.emails, Roles: response.roleIds, 'Workflow Levels': response.workflowLevelIds }; } } export const UsersValidator = createValidator(() => async ( prop, { ent, attr, group, injector, context } ) => { const service: UserService = injector.get(UserService); const validatorResponse = await service.validateUsersImport(context, group); const valid = attr === 'Email' ? !validatorResponse[attr].includes(ent[attr]) : !validatorResponse[attr].some(a => ent[attr].includes(a)); // break up functions, write comments and test (payment import for example) if (!valid) { switch (attr) { case 'Workflow Levels': return { i18nKey: 'common:textWorkflowLevelsMustExist', defaultValue: 'Workflow Level must exist in the system' }; case 'Roles': return { i18nKey: 'common:textRoleMustExist', defaultValue: 'Roles must exist in the system' }; case 'Email': return { i18nKey: 'common:textEmailAlreadyInUse', defaultValue: 'Email address already in use' }; default: return null; } } return []; }); export class UsersImportValidationModel { @IsString() @Required() 'First Name': string; @IsString() @Required() 'Last Name': string; @IsString() @Required() 'Job Title': string; @IsEmail() @UsersValidator() @Required() 'Email': string; @IsArrayOfType('number') @Transform((val: string) => { if (!val) { return []; } const data = val .split(',') .map(x => parseInt(x, 10)); return data; }) @UsersValidator() 'Roles': number[]; @IsArrayOfType('number') @Transform((val: string) => { if (!val) { return []; } const data = val .split(',') .map(x => parseInt(x, 10)); return data; }) @UsersValidator() 'Workflow Levels': number[]; @CSVBoolean() @Required() 'Is SSO': boolean; }