diff --git a/api/src/controllers/user.controller.ts b/api/src/controllers/user.controller.ts index 77d81b0b2e3..608f9f90258 100644 --- a/api/src/controllers/user.controller.ts +++ b/api/src/controllers/user.controller.ts @@ -32,14 +32,11 @@ import { mapTo } from '../utilities/mapTo'; import { PaginatedUserDto } from '../dtos/users/paginated-user.dto'; import { UserQueryParams } from '../dtos/users/user-query-param.dto'; import { Request as ExpressRequest, Response } from 'express'; -import { UserUpdate } from '../dtos/users/user-update.dto'; import { SuccessDTO } from '../dtos/shared/success.dto'; -import { UserCreate } from '../dtos/users/user-create.dto'; import { UserCreateParams } from '../dtos/users/user-create-params.dto'; import { UserFavoriteListing } from '../dtos/users/user-favorite-listing.dto'; import { EmailAndAppUrl } from '../dtos/users/email-and-app-url.dto'; import { ConfirmationRequest } from '../dtos/users/confirmation-request.dto'; -import { UserInvite } from '../dtos/users/user-invite.dto'; import { JwtAuthGuard } from '../guards/jwt.guard'; import { UserProfilePermissionGuard } from '../guards/user-profile-permission-guard'; import { OptionalAuthGuard } from '../guards/optional.guard'; @@ -53,12 +50,27 @@ import { ExportLogInterceptor } from '../interceptors/export-log.interceptor'; import { RequestSingleUseCode } from '../dtos/single-use-code/request-single-use-code.dto'; import { ApiKeyGuard } from '../guards/api-key.guard'; import { UserDeleteDTO } from '../dtos/users/user-delete.dto'; +import { PublicUserCreate } from '../dtos/users/public-user-create.dto'; +import { PartnerUserCreate } from '../dtos/users/partner-user-create.dto'; +import { AdvocateUserCreate } from '../dtos/users/advocate-user-create.dto'; +import { PublicUserUpdate } from '../dtos/users/public-user-update.dto'; +import { PartnerUserUpdate } from '../dtos/users/partner-user-update.dto'; +import { AdvocateUserUpdate } from '../dtos/users/advocate-user-update.dto'; @Controller('user') @ApiTags('user') @PermissionTypeDecorator('user') @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) -@ApiExtraModels(IdDTO, EmailAndAppUrl) +@ApiExtraModels( + IdDTO, + EmailAndAppUrl, + PublicUserCreate, + PartnerUserCreate, + AdvocateUserCreate, + PublicUserUpdate, + PartnerUserUpdate, + AdvocateUserUpdate, +) @UseGuards(ApiKeyGuard) export class UserController { constructor( @@ -125,21 +137,55 @@ export class UserController { return await this.userService.favoriteListings(userId); } - @Post() + @Post('/public') @ApiOperation({ summary: 'Creates a public only user', - operationId: 'create', + operationId: 'createPublic', + }) + @UseGuards(OptionalAuthGuard, PermissionGuard) + @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) + @ApiOkResponse({ type: User }) + async createPublicUser( + @Request() req: ExpressRequest, + @Body() dto: PublicUserCreate, + @Query() queryParams: UserCreateParams, + ): Promise { + return await this.userService.createPublicUser( + dto, + queryParams.noWelcomeEmail !== true, + req, + ); + } + + @Post('/partner') + @ApiOperation({ + summary: 'Creates a partner only user', + operationId: 'createPartner', + }) + @ApiOkResponse({ type: User }) + @UseGuards(PermissionGuard, JwtAuthGuard) + @UseInterceptors(ActivityLogInterceptor) + async createPartnerUser( + @Request() req: ExpressRequest, + @Body() dto: PartnerUserCreate, + ): Promise { + return await this.userService.createPartnerUser(dto, req); + } + + @Post('/advocate') + @ApiOperation({ + summary: 'Creates a advocate only user', + operationId: 'createAdvocate', }) @ApiOkResponse({ type: User }) @UseGuards(OptionalAuthGuard, PermissionGuard) - async create( + async createAdvocateUser( @Request() req: ExpressRequest, - @Body() dto: UserCreate, + @Body() dto: AdvocateUserCreate, @Query() queryParams: UserCreateParams, ): Promise { - return await this.userService.create( + return await this.userService.createAdvocateUser( dto, - false, queryParams.noWelcomeEmail !== true, req, ); @@ -161,18 +207,6 @@ export class UserController { ); } - @Post('/invite') - @ApiOperation({ summary: 'Invite partner user', operationId: 'invite' }) - @ApiOkResponse({ type: User }) - @UseGuards(OptionalAuthGuard) - @UseInterceptors(ActivityLogInterceptor) - async invite( - @Body() dto: UserInvite, - @Request() req: ExpressRequest, - ): Promise { - return await this.userService.create(dto, true, undefined, req); - } - @Post('request-single-use-code') @ApiOperation({ summary: 'Request single use code', @@ -268,21 +302,40 @@ export class UserController { return await this.userService.deleteAfterInactivity(); } - @Put(':id') - @ApiOperation({ summary: 'Update user', operationId: 'update' }) + @Put('/public') + @ApiOperation({ summary: 'Update user', operationId: 'updatePublic' }) @ApiOkResponse({ type: User }) @UseGuards(JwtAuthGuard, PermissionGuard) @UseInterceptors(ActivityLogInterceptor) - async update( + async updatePublicUser( @Request() req: ExpressRequest, - @Body() dto: UserUpdate, + @Body() dto: PublicUserUpdate, ): Promise { - const jurisdictionName = req.headers['jurisdictionname'] || ''; - return await this.userService.update( - dto, - mapTo(User, req['user']), - jurisdictionName as string, - ); + return await this.userService.update(dto, req); + } + + @Put('/partner') + @ApiOperation({ summary: 'Update user', operationId: 'updatePartner' }) + @ApiOkResponse({ type: User }) + @UseGuards(JwtAuthGuard, PermissionGuard) + @UseInterceptors(ActivityLogInterceptor) + async updatePartnerUser( + @Request() req: ExpressRequest, + @Body() dto: PartnerUserUpdate, + ): Promise { + return await this.userService.update(dto, req); + } + + @Put('/advocate') + @ApiOperation({ summary: 'Update user', operationId: 'updateAdvocate' }) + @ApiOkResponse({ type: User }) + @UseGuards(JwtAuthGuard, PermissionGuard) + @UseInterceptors(ActivityLogInterceptor) + async updateAdvocateUser( + @Request() req: ExpressRequest, + @Body() dto: AdvocateUserUpdate, + ): Promise { + return await this.userService.update(dto, req); } @Get(`:id`) diff --git a/api/src/dtos/users/advocate-user-create.dto.ts b/api/src/dtos/users/advocate-user-create.dto.ts new file mode 100644 index 00000000000..85caa431b2f --- /dev/null +++ b/api/src/dtos/users/advocate-user-create.dto.ts @@ -0,0 +1,23 @@ +import { OmitType } from '@nestjs/swagger'; +import { AdvocateUserUpdate } from './advocate-user-update.dto'; +export class AdvocateUserCreate extends OmitType(AdvocateUserUpdate, [ + 'id', + 'newEmail', + 'currentPassword', + 'password', + 'title', + 'phoneNumber', + 'phoneType', + 'phoneExtension', + 'additionalPhoneNumber', + 'additionalPhoneNumberType', + 'additionalPhoneExtension', +] as const) { + /* Fields inherited from AdvocateUserUpdate: + * - firstName (inherited as required from AdvocateUserUpdate) + * - middleName (inherited as optional from AdvocateUserUpdate) + * - lastName (inherited as required from AdvocateUserUpdate) + * - agency (inherited as required from AdvocateUserUpdate) + * - email (inherited as required from AdvocateUserUpdate) + **/ +} diff --git a/api/src/dtos/users/advocate-user-update.dto.ts b/api/src/dtos/users/advocate-user-update.dto.ts new file mode 100644 index 00000000000..7b0ee51cc7e --- /dev/null +++ b/api/src/dtos/users/advocate-user-update.dto.ts @@ -0,0 +1,99 @@ +import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger'; +import { Expose, Type } from 'class-transformer'; +import { + IsArray, + IsEmail, + IsNotEmpty, + IsPhoneNumber, + IsString, + Matches, + MaxLength, + ValidateIf, + ValidateNested, +} from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import Agency from '../agency/agency.dto'; +import { User } from './user.dto'; +import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; +import { passwordRegex } from '../../utilities/password-regex'; +import { AddressUpdate } from '../addresses/address-update.dto'; +import { IdDTO } from '../shared/id.dto'; + +export class AdvocateUserUpdate extends OmitType(User, [ + 'createdAt', + 'updatedAt', + 'agency', + 'address', + 'phoneNumber', + 'passwordUpdatedAt', + 'passwordValidForDays', + 'passwordUpdatedAt', + 'jurisdictions', +] as const) { + /* Fields inherited from BaseUser: + * - firstName (inherited as required from BaseUser) + * - middleName (inherited as optional from BaseUser) + * - lastName (inherited as required from BaseUser) + * - email (inherited as required from BaseUser) + * - title (inherited as optional from BaseUserUpdate) + * - phoneExtension (inherited as optional from BaseUserUpdate) + * - additionalPhoneNumber (inherited as optional from BaseUserUpdate) + * - additionalPhoneNumberType (inherited as optional from BaseUserUpdate) + * - additionalPhoneExtension (inherited as optional from BaseUserUpdate) + **/ + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Agency) + @ApiProperty({ type: Agency }) + agency: Agency; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AddressUpdate) + @ApiProperty({ type: AddressUpdate }) + address: AddressUpdate; + + @Expose() + @IsPhoneNumber('US', { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + phoneNumber: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + phoneType: string; + + @Expose() + @ApiPropertyOptional() + @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() + newEmail?: string; + + @Expose() + @ApiPropertyOptional() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @Matches(passwordRegex, { + message: 'passwordTooWeak', + groups: [ValidationsGroupsEnum.default], + }) + password?: string; + + @Expose() + @ValidateIf((o) => o.password, { groups: [ValidationsGroupsEnum.default] }) + @IsNotEmpty({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() + currentPassword?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() + appUrl?: string; + + @Expose() + @Type(() => IdDTO) + @IsArray({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional({ type: IdDTO, isArray: true }) + jurisdictions?: IdDTO[]; +} diff --git a/api/src/dtos/users/user-invite.dto.ts b/api/src/dtos/users/partner-user-create.dto.ts similarity index 52% rename from api/src/dtos/users/user-invite.dto.ts rename to api/src/dtos/users/partner-user-create.dto.ts index 84f2d3803c6..c781066ebee 100644 --- a/api/src/dtos/users/user-invite.dto.ts +++ b/api/src/dtos/users/partner-user-create.dto.ts @@ -1,31 +1,23 @@ import { ApiProperty, OmitType } from '@nestjs/swagger'; +import { PartnerUserUpdate } from './partner-user-update.dto'; import { Expose, Type } from 'class-transformer'; -import { - ArrayMinSize, - IsArray, - IsEmail, - ValidateNested, -} from 'class-validator'; -import { UserUpdate } from './user-update.dto'; - -import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { IdDTO } from '../shared/id.dto'; +import { ArrayMinSize, IsArray, ValidateNested } from 'class-validator'; -export class UserInvite extends OmitType(UserUpdate, [ +export class PartnerUserCreate extends OmitType(PartnerUserUpdate, [ 'id', + 'newEmail', 'password', 'currentPassword', - 'email', - 'agreedToTermsOfService', 'jurisdictions', -]) { - @Expose() - @ApiProperty() - @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) - @EnforceLowerCase() - email: string; - +] as const) { + /* Fields inherited from PartnerUserUpdate: + * - firstName (inherited as required from PartnerUserUpdate) + * - lastName (inherited as required from PartnerUserUpdate) + * - email (inherited as required from PartnerUserUpdate) + * - userRoles (inherited as required from PartnerUserUpdate) + **/ @Expose() @Type(() => IdDTO) @IsArray({ groups: [ValidationsGroupsEnum.default] }) diff --git a/api/src/dtos/users/user-update.dto.ts b/api/src/dtos/users/partner-user-update.dto.ts similarity index 73% rename from api/src/dtos/users/user-update.dto.ts rename to api/src/dtos/users/partner-user-update.dto.ts index 5f0ed5b255b..6097448241e 100644 --- a/api/src/dtos/users/user-update.dto.ts +++ b/api/src/dtos/users/partner-user-update.dto.ts @@ -1,5 +1,8 @@ -import { ApiPropertyOptional, OmitType } from '@nestjs/swagger'; +import { OmitType, ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { User } from './user.dto'; import { Expose, Type } from 'class-transformer'; +import { UserRole } from './user-role.dto'; +import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; import { IsArray, IsEmail, @@ -9,35 +12,29 @@ import { MaxLength, ValidateIf, } from 'class-validator'; -import { User } from './user.dto'; - -import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { passwordRegex } from '../../utilities/password-regex'; import { IdDTO } from '../shared/id.dto'; -export class UserUpdate extends OmitType(User, [ +export class PartnerUserUpdate extends OmitType(User, [ 'createdAt', 'updatedAt', - 'email', - 'mfaEnabled', + 'userRoles', 'passwordUpdatedAt', 'passwordValidForDays', - 'lastLoginAt', - 'failedLoginAttemptsCount', - 'confirmedAt', - 'lastLoginAt', - 'phoneNumberVerified', - 'hitConfirmationURL', - 'activeAccessToken', - 'activeRefreshToken', + 'passwordUpdatedAt', 'jurisdictions', -]) { +] as const) { + /* Fields inherited from User: + * - firstName (inherited as required from User) + * - lastName (inherited as required from User) + * - email (inherited as required from User) + **/ + @Expose() - @ApiPropertyOptional() - @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) - @EnforceLowerCase() - email?: string; + @Type(() => UserRole) + @ApiProperty({ type: UserRole }) + userRoles: UserRole; @Expose() @ApiPropertyOptional() @@ -70,5 +67,5 @@ export class UserUpdate extends OmitType(User, [ @Type(() => IdDTO) @IsArray({ groups: [ValidationsGroupsEnum.default] }) @ApiPropertyOptional({ type: IdDTO, isArray: true }) - jurisdictions: IdDTO[]; + jurisdictions?: IdDTO[]; } diff --git a/api/src/dtos/users/user-create.dto.ts b/api/src/dtos/users/public-user-create.dto.ts similarity index 59% rename from api/src/dtos/users/user-create.dto.ts rename to api/src/dtos/users/public-user-create.dto.ts index e746e30067e..7f990800142 100644 --- a/api/src/dtos/users/user-create.dto.ts +++ b/api/src/dtos/users/public-user-create.dto.ts @@ -1,29 +1,26 @@ import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger'; -import { Expose, Type } from 'class-transformer'; -import { - IsArray, - IsEmail, - IsString, - Matches, - MaxLength, - ValidateNested, -} from 'class-validator'; -import { UserUpdate } from './user-update.dto'; - -import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; +import { PublicUserUpdate } from './public-user-update.dto'; +import { Expose } from 'class-transformer'; +import { IsEmail, IsString, Matches, MaxLength } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { passwordRegex } from '../../utilities/password-regex'; import { Match } from '../../decorators/match-decorator'; -import { IdDTO } from '../shared/id.dto'; +import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; -export class UserCreate extends OmitType(UserUpdate, [ +export class PublicUserCreate extends OmitType(PublicUserUpdate, [ 'id', - 'userRoles', + 'newEmail', 'password', 'currentPassword', - 'email', - 'jurisdictions', -]) { +] as const) { + /* Fields inherited from PublicUserUpdate: + * - firstName (inherited as required from PublicUserUpdate) + * - middleName (inherited as optional from PublicUserUpdate) + * - lastName (inherited as required from PublicUserUpdate) + * - email (inherited as required from PublicUserUpdate) + * - dob (inherited as required from PublicUserUpdate) + **/ + @Expose() @ApiProperty() @IsString({ groups: [ValidationsGroupsEnum.default] }) @@ -40,23 +37,10 @@ export class UserCreate extends OmitType(UserUpdate, [ @ApiProperty() passwordConfirmation: string; - @Expose() - @ApiProperty() - @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) - @EnforceLowerCase() - email: string; - @Expose() @ApiPropertyOptional() @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) @Match('email', { groups: [ValidationsGroupsEnum.default] }) @EnforceLowerCase() emailConfirmation?: string; - - @Expose() - @Type(() => IdDTO) - @IsArray({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @ApiPropertyOptional({ type: IdDTO, isArray: true }) - jurisdictions?: IdDTO[]; } diff --git a/api/src/dtos/users/public-user-update.dto.ts b/api/src/dtos/users/public-user-update.dto.ts new file mode 100644 index 00000000000..aa4f7ac18e1 --- /dev/null +++ b/api/src/dtos/users/public-user-update.dto.ts @@ -0,0 +1,73 @@ +import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger'; +import { Expose, Type } from 'class-transformer'; +import { + IsArray, + IsDate, + IsEmail, + IsNotEmpty, + IsString, + Matches, + MaxLength, + ValidateIf, +} from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { User } from './user.dto'; +import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; +import { passwordRegex } from '../../utilities/password-regex'; +import { IdDTO } from '../shared/id.dto'; + +export class PublicUserUpdate extends OmitType(User, [ + 'createdAt', + 'updatedAt', + 'dob', + 'passwordUpdatedAt', + 'passwordValidForDays', + 'passwordUpdatedAt', + 'jurisdictions', +] as const) { + /* Fields inherited from BaseUser: + * - firstName (inherited as required from BaseUser) + * - middleName (inherited as optional from BaseUser) + * - lastName (inherited as required from BaseUser) + * - email (inherited as required from BaseUser) + **/ + + @Expose() + @Type(() => Date) + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + dob: Date; + + @Expose() + @ApiPropertyOptional() + @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() + newEmail?: string; + + @Expose() + @ApiPropertyOptional() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @Matches(passwordRegex, { + message: 'passwordTooWeak', + groups: [ValidationsGroupsEnum.default], + }) + password?: string; + + @Expose() + @ValidateIf((o) => o.password, { groups: [ValidationsGroupsEnum.default] }) + @IsNotEmpty({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() + currentPassword?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() + appUrl?: string; + + @Expose() + @Type(() => IdDTO) + @IsArray({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional({ type: IdDTO, isArray: true }) + jurisdictions?: IdDTO[]; +} diff --git a/api/src/dtos/users/user.dto.ts b/api/src/dtos/users/user.dto.ts index 21f122a5cb1..0e50fd4f847 100644 --- a/api/src/dtos/users/user.dto.ts +++ b/api/src/dtos/users/user.dto.ts @@ -20,6 +20,8 @@ import { LanguagesEnum } from '@prisma/client'; import { IdDTO } from '../shared/id.dto'; import { UserRole } from './user-role.dto'; import { Jurisdiction } from '../jurisdictions/jurisdiction.dto'; +import Agency from '../agency/agency.dto'; +import { Address } from '../addresses/address.dto'; export class User extends AbstractDTO { @Expose() @@ -149,4 +151,46 @@ export class User extends AbstractDTO { @Type(() => IdDTO) @ApiPropertyOptional({ type: IdDTO, isArray: true }) favoriteListings?: IdDTO[]; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() + title?: string; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Agency) + @ApiPropertyOptional({ type: Agency }) + agency?: Agency; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Address) + @ApiPropertyOptional({ type: Address }) + address?: Address; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() + phoneType?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() + phoneExtension?: string; + + @Expose() + @IsPhoneNumber('US', { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() + additionalPhoneNumber?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() + additionalPhoneNumberType?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() + additionalPhoneExtension?: string; } diff --git a/api/src/services/user.service.ts b/api/src/services/user.service.ts index 9a245c62c69..56eda99b657 100644 --- a/api/src/services/user.service.ts +++ b/api/src/services/user.service.ts @@ -28,7 +28,6 @@ import { buildOrderBy } from '../utilities/build-order-by'; import { UserQueryParams } from '../dtos/users/user-query-param.dto'; import { PaginatedUserDto } from '../dtos/users/paginated-user.dto'; import { OrderByEnum } from '../enums/shared/order-by-enum'; -import { UserUpdate } from '../dtos/users/user-update.dto'; import { isPasswordOutdated, isPasswordValid, @@ -38,8 +37,6 @@ import { SuccessDTO } from '../dtos/shared/success.dto'; import { EmailAndAppUrl } from '../dtos/users/email-and-app-url.dto'; import { ConfirmationRequest } from '../dtos/users/confirmation-request.dto'; import { IdDTO } from '../dtos/shared/id.dto'; -import { UserInvite } from '../dtos/users/user-invite.dto'; -import { UserCreate } from '../dtos/users/user-create.dto'; import { EmailService } from './email.service'; import { PermissionService } from './permission.service'; import { permissionActions } from '../enums/permissions/permission-actions-enum'; @@ -53,6 +50,12 @@ import { UserFavoriteListing } from '../dtos/users/user-favorite-listing.dto'; import { ModificationEnum } from '../enums/shared/modification-enum'; import { CronJobService } from './cron-job.service'; import { ApplicationService } from './application.service'; +import { PublicUserUpdate } from '../dtos/users/public-user-update.dto'; +import { PartnerUserUpdate } from '../dtos/users/partner-user-update.dto'; +import { AdvocateUserUpdate } from '../dtos/users/advocate-user-update.dto'; +import { PublicUserCreate } from '../dtos/users/public-user-create.dto'; +import { PartnerUserCreate } from '../dtos/users/partner-user-create.dto'; +import { AdvocateUserCreate } from '../dtos/users/advocate-user-create.dto'; import { SnapshotCreateService } from './snapshot-create.service'; import { toAddHelper, toRemoveHelper } from '../utilities/snapshot-helpers'; @@ -176,10 +179,12 @@ export class UserService { this will update a user or error if no user is found with the Id */ async update( - dto: UserUpdate, - requestingUser: User, - jurisdictionName: string, + dto: PublicUserUpdate | PartnerUserUpdate | AdvocateUserUpdate, + req: Request, ): Promise { + const jurisdictionName = (req.headers['jurisdictionname'] as string) || ''; + const requestingUser = mapTo(User, req['user']); + const storedUser = await this.findUserOrError( { userId: dto.id }, UserViews.full, @@ -255,12 +260,12 @@ export class UserService { dto.newEmail, ); } - const transactions = []; + const transactions = []; await this.snapshotCreateService.createUserSnapshot(dto.id); // only update userRoles if something has changed - if (dto.userRoles && storedUser.userRoles) { + if (dto?.userRoles && storedUser.userRoles) { if ( this.isUserRoleChangeAllowed(requestingUser, dto.userRoles) && !( @@ -283,7 +288,6 @@ export class UserService { } } - // handle listings if (dto.listings?.length || storedUser.listings?.length) { // if the listing is stored in the db but not on the incoming dto, mark as to be removed const toRemove = toRemoveHelper(storedUser.listings, dto.listings); @@ -307,6 +311,34 @@ export class UserService { } } + //handle address for advocated users + let newAddressId: string | undefined; + if (dto?.address) { + if (dto.address?.id) { + transactions.push(async (transactions: PrismaClient) => { + return transactions.address.update({ + data: { + ...dto.address, + id: undefined, + }, + where: { + id: dto.address.id, + }, + }); + }); + } else { + transactions.push(async (transactions: PrismaClient) => { + const newAddress = await transactions.address.create({ + data: { + ...dto.address, + }, + }); + newAddressId = newAddress.id; + return newAddress; + }); + } + } + // handle jurisdictions if (dto.jurisdictions?.length || storedUser.jurisdictions?.length) { // if the jurisdiction is stored in the db but not on the incoming dto, mark as to be removed @@ -334,6 +366,41 @@ export class UserService { } } + // handle agency + if (!dto?.agency && storedUser.agency) { + transactions.push(async (transaction: PrismaClient) => { + return transaction.userAccounts.update({ + where: { + id: dto.id, + }, + data: { + agency: { + disconnect: { + id: storedUser.agency.id, + }, + }, + }, + }); + }); + } else { + transactions.push(async (transaction: PrismaClient) => { + return transaction.userAccounts.update({ + where: { + id: dto.id, + }, + data: { + agency: dto?.agency?.id + ? { + connect: { + id: dto.agency.id, + }, + } + : undefined, + }, + }); + }); + } + transactions.push(async (transaction: PrismaClient) => { return transaction.userAccounts.update({ include: views.full, @@ -348,7 +415,20 @@ export class UserService { lastName: dto.lastName, dob: dto.dob, phoneNumber: dto.phoneNumber, + title: dto.title, + phoneType: dto.phoneType, + phoneExtension: dto.phoneExtension, + additionalPhoneNumber: dto.additionalPhoneNumber, + additionalPhoneNumberType: dto.additionalPhoneNumberType, + additionalPhoneExtension: dto.additionalPhoneExtension, language: dto.language, + address: newAddressId + ? { + connect: { + id: newAddressId, + }, + } + : undefined, }, where: { id: dto.id, @@ -557,36 +637,11 @@ export class UserService { } /* - creates a new user - takes in either the dto for creating a public user or the dto for creating a partner user - if forPartners is true then we are creating a partner, otherwise we are creating a public user - if sendWelcomeEmail is true then we are sending a public user a welcome email + Checks if creation should update an existing user to a new role instead of creating a new entry */ - async create( - dto: UserCreate | UserInvite, - forPartners: boolean, - sendWelcomeEmail = false, - req: Request, - ): Promise { - const requestingUser = mapTo(User, req['user']); - const jurisdictionName = (req.headers['jurisdictionname'] as string) || ''; - - if ( - this.containsInvalidCharacters(dto.firstName) || - this.containsInvalidCharacters(dto.lastName) - ) { - throw new ForbiddenException( - `${dto.firstName} ${dto.lastName} was found to be invalid`, - ); - } - - if (forPartners) { - await this.authorizeAction( - requestingUser, - mapTo(User, dto), - permissionActions.confirm, - ); - } + async handleExistingUser( + dto: PublicUserCreate | PartnerUserCreate | AdvocateUserCreate, + ): Promise { const existingUser = await this.prisma.userAccounts.findUnique({ include: views.full, where: { @@ -597,8 +652,8 @@ export class UserService { if (existingUser) { // if attempting to recreate an existing user if (!existingUser.userRoles && 'userRoles' in dto) { - await this.snapshotCreateService.createUserSnapshot(existingUser.id); // existing user && public user && user will get roles -> trying to grant partner access to a public user + await this.snapshotCreateService.createUserSnapshot(existingUser.id); const res = await this.prisma.userAccounts.update({ include: views.full, data: { @@ -634,6 +689,7 @@ export class UserService { const listings = existingUser.listings .map((juris) => ({ id: juris.id })) .concat(dto.listings); + await this.snapshotCreateService.createUserSnapshot(existingUser.id); const res = await this.prisma.userAccounts.update({ include: views.full, @@ -657,39 +713,38 @@ export class UserService { } } - let passwordHash = ''; - if (forPartners) { - passwordHash = await passwordToHash( - crypto.randomBytes(8).toString('hex'), + return null; + } + + /* + creates a public user, and sends a welcome email with a confirmation link + */ + async createPublicUser( + dto: PublicUserCreate, + sendWelcomeEmail = false, + req: Request, + ): Promise { + const jurisdictionName = (req.headers['jurisdictionname'] as string) || ''; + + if ( + this.containsInvalidCharacters(dto.firstName) || + (dto.middleName && this.containsInvalidCharacters(dto.middleName)) || + this.containsInvalidCharacters(dto.lastName) + ) { + throw new ForbiddenException( + `${dto.firstName}${dto.middleName ? ` ${dto.middleName} ` : ' '}${ + dto.lastName + } was found to be invalid`, ); - } else { - passwordHash = await passwordToHash((dto as UserCreate).password); } - let jurisdictions: - | { - jurisdictions: Prisma.JurisdictionsCreateNestedManyWithoutUser_accountsInput; - } - | Record = dto.jurisdictions - ? { - jurisdictions: { - connect: dto.jurisdictions.map((juris) => ({ - id: juris.id, - })), - }, - } - : {}; - - if (!forPartners && jurisdictionName) { - jurisdictions = { - jurisdictions: { - connect: { - name: jurisdictionName, - }, - }, - }; + const recreatedUser = await this.handleExistingUser(dto); + if (recreatedUser !== null) { + return recreatedUser; } + const passwordHash = await passwordToHash(dto.password); + let newUser = await this.prisma.userAccounts.create({ data: { passwordHash: passwordHash, @@ -698,18 +753,13 @@ export class UserService { middleName: dto.middleName, lastName: dto.lastName, dob: dto.dob, - phoneNumber: dto.phoneNumber, - language: dto.language, - mfaEnabled: forPartners, - ...jurisdictions, - userRoles: - 'userRoles' in dto - ? { - create: { - ...dto.userRoles, - }, - } - : undefined, + jurisdictions: dto.jurisdictions + ? { + connect: dto.jurisdictions.map((juris) => ({ + id: juris.id, + })), + } + : undefined, listings: dto.listings ? { connect: dto.listings.map((listing) => ({ @@ -734,8 +784,7 @@ export class UserService { }, }); - // Public user that needs email - if (!forPartners && sendWelcomeEmail) { + if (sendWelcomeEmail) { const fullJurisdiction = await this.prisma.jurisdictions.findFirst({ where: { name: jurisdictionName as string, @@ -756,23 +805,201 @@ export class UserService { confirmationUrl, ); } - } else if (forPartners) { - const confirmationUrl = this.getPartnersConfirmationUrl( - this.configService.get('PARTNERS_PORTAL_URL'), - confirmationToken, + } + + await this.connectUserWithExistingApplications(newUser.email, newUser.id); + + return mapTo(User, newUser); + } + + /* + creates a partner user + */ + async createPartnerUser(dto: PartnerUserCreate, req: Request) { + const requestingUser = mapTo(User, req['user']); + + if ( + this.containsInvalidCharacters(dto.firstName) || + this.containsInvalidCharacters(dto.lastName) + ) { + throw new ForbiddenException( + `${dto.firstName} ${dto.lastName} was found to be invalid`, ); - await this.emailService.invitePartnerUser( - dto.jurisdictions, - mapTo(User, newUser), - this.configService.get('PARTNERS_PORTAL_URL'), - confirmationUrl, + } + + await this.authorizeAction( + requestingUser, + mapTo(User, dto), + permissionActions.confirm, + ); + + const recreatedUser = await this.handleExistingUser(dto); + if (recreatedUser !== null) { + return recreatedUser; + } + + const passwordHash = await passwordToHash( + crypto.randomBytes(8).toString('hex'), + ); + + let newUser = await this.prisma.userAccounts.create({ + data: { + passwordHash: passwordHash, + email: dto.email, + firstName: dto.firstName, + lastName: dto.lastName, + mfaEnabled: true, + userRoles: { + create: { + ...dto.userRoles, + }, + }, + jurisdictions: dto.jurisdictions + ? { + connect: dto.jurisdictions.map((juris) => ({ + id: juris.id, + })), + } + : undefined, + listings: dto.listings + ? { + connect: dto.listings.map((listing) => ({ + id: listing.id, + })), + } + : undefined, + }, + }); + + const confirmationToken = this.createConfirmationToken( + newUser.id, + newUser.email, + ); + newUser = await this.prisma.userAccounts.update({ + include: views.full, + data: { + confirmationToken: confirmationToken, + }, + where: { + id: newUser.id, + }, + }); + + const confirmationUrl = this.getPartnersConfirmationUrl( + this.configService.get('PARTNERS_PORTAL_URL'), + confirmationToken, + ); + + await this.emailService.invitePartnerUser( + dto.jurisdictions, + mapTo(User, newUser), + this.configService.get('PARTNERS_PORTAL_URL'), + confirmationUrl, + ); + + return mapTo(User, newUser); + } + + /* + creates an advocate user, and sends a welcome email with a confirmation link + */ + async createAdvocateUser( + dto: AdvocateUserCreate, + sendWelcomeEmail = false, + req: Request, + ) { + const jurisdictionName = (req.headers['jurisdictionname'] as string) || ''; + + if ( + this.containsInvalidCharacters(dto.firstName) || + (dto.middleName && this.containsInvalidCharacters(dto.middleName)) || + this.containsInvalidCharacters(dto.lastName) + ) { + throw new ForbiddenException( + `${dto.firstName}${dto.middleName ? ` ${dto.middleName} ` : ' '}${ + dto.lastName + } was found to be invalid`, ); } - if (!forPartners) { - await this.connectUserWithExistingApplications(newUser.email, newUser.id); + const recreatedUser = await this.handleExistingUser(dto); + if (recreatedUser !== null) { + return recreatedUser; + } + + const passwordHash = await passwordToHash( + crypto.randomBytes(8).toString('hex'), + ); + + let newUser = await this.prisma.userAccounts.create({ + data: { + passwordHash: passwordHash, + email: dto.email, + firstName: dto.firstName, + middleName: dto.middleName, + lastName: dto.lastName, + agency: { + connect: { + id: dto.agency.id, + }, + }, + isAdvocate: true, + jurisdictions: dto.jurisdictions + ? { + connect: dto.jurisdictions.map((juris) => ({ + id: juris.id, + })), + } + : undefined, + listings: dto.listings + ? { + connect: dto.listings.map((listing) => ({ + id: listing.id, + })), + } + : undefined, + }, + }); + + const confirmationToken = this.createConfirmationToken( + newUser.id, + newUser.email, + ); + newUser = await this.prisma.userAccounts.update({ + include: views.full, + data: { + confirmationToken: confirmationToken, + }, + where: { + id: newUser.id, + }, + }); + + if (sendWelcomeEmail) { + const fullJurisdiction = await this.prisma.jurisdictions.findFirst({ + where: { + name: jurisdictionName as string, + }, + }); + + if (fullJurisdiction?.allowSingleUseCodeLogin) { + this.requestSingleUseCode(dto, req); + } else { + const confirmationUrl = this.getPublicConfirmationUrl( + dto.appUrl, + confirmationToken, + ); + await this.emailService.welcome( + jurisdictionName, + mapTo(User, newUser), + dto.appUrl, + confirmationUrl, + ); + } } + await this.connectUserWithExistingApplications(newUser.email, newUser.id); + return mapTo(User, newUser); } diff --git a/api/test/integration/permission-tests/helpers.ts b/api/test/integration/permission-tests/helpers.ts index 4041bc1bd12..f1094068917 100644 --- a/api/test/integration/permission-tests/helpers.ts +++ b/api/test/integration/permission-tests/helpers.ts @@ -46,11 +46,11 @@ import { ListingCreate } from '../../../src/dtos/listings/listing-create.dto'; import { ListingUpdate } from '../../../src/dtos/listings/listing-update.dto'; import { MultiselectQuestionCreate } from '../../../src/dtos/multiselect-questions/multiselect-question-create.dto'; import { MultiselectQuestionUpdate } from '../../../src/dtos/multiselect-questions/multiselect-question-update.dto'; -import { UserCreate } from '../../../src/dtos/users/user-create.dto'; -import { UserInvite } from '../../../src/dtos/users/user-invite.dto'; import { AlternateContactRelationship } from '../../../src/enums/applications/alternate-contact-relationship-enum'; import { HouseholdMemberRelationship } from '../../../src/enums/applications/household-member-relationship-enum'; import { UnitAccessibilityPriorityTypeEnum } from '../../../src/enums/units/accessibility-priority-type-enum'; +import { PublicUserCreate } from '../../../src/dtos/users/public-user-create.dto'; +import { PartnerUserCreate } from '../../../src/dtos/users/partner-user-create.dto'; export const generateJurisdiction = async ( prisma: PrismaService, @@ -238,31 +238,33 @@ export const buildMultiselectQuestionUpdateMock = ( export const buildUserCreateMock = ( jurisId: string, email: string, -): UserCreate => { +): PublicUserCreate => { return { firstName: 'Public User firstName', lastName: 'Public User lastName', password: 'Abcdef12345!', + passwordConfirmation: 'Abcdef12345!', + dob: new Date(), + agreedToTermsOfService: true, email, jurisdictions: [{ id: jurisId }], - } as unknown as UserCreate; + }; }; export const buildUserInviteMock = ( jurisId: string, email: string, -): UserInvite => { +): PartnerUserCreate => { return { firstName: 'Partner User firstName', lastName: 'Partner User lastName', - password: 'Abcdef12345!', email, jurisdictions: [{ id: jurisId }], agreedToTermsOfService: true, userRoles: { isAdmin: true, }, - } as unknown as UserInvite; + }; }; export const buildApplicationCreateMock = ( diff --git a/api/test/integration/permission-tests/permission-as-admin.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-admin.e2e-spec.ts index 5936cd5a903..150ca9ae5e2 100644 --- a/api/test/integration/permission-tests/permission-as-admin.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-admin.e2e-spec.ts @@ -36,7 +36,6 @@ import { UnitRentTypeUpdate } from '../../../src/dtos/unit-rent-types/unit-rent- import { UnitTypeCreate } from '../../../src/dtos/unit-types/unit-type-create.dto'; import { UnitTypeUpdate } from '../../../src/dtos/unit-types/unit-type-update.dto'; import { multiselectQuestionFactory } from '../../../prisma/seed-helpers/multiselect-question-factory'; -import { UserUpdate } from '../../../src/dtos/users/user-update.dto'; import { EmailAndAppUrl } from '../../../src/dtos/users/email-and-app-url.dto'; import { ConfirmationRequest } from '../../../src/dtos/users/confirmation-request.dto'; import { UserService } from '../../../src/services/user.service'; @@ -63,6 +62,7 @@ import { createSimpleListing, } from './helpers'; import { featureFlagFactory } from '../../../prisma/seed-helpers/feature-flag-factory'; +import { PublicUserUpdate } from '../../../src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), @@ -873,13 +873,13 @@ describe('Testing Permissioning of endpoints as Admin User', () => { }); await request(app.getHttpServer()) - .put(`/user/${userA.id}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, firstName: 'New User First Name', lastName: 'New User Last Name', - } as UserUpdate) + } as PublicUserUpdate) .set('Cookie', cookies) .expect(200); @@ -1000,7 +1000,7 @@ describe('Testing Permissioning of endpoints as Admin User', () => { }); await request(app.getHttpServer()) - .post(`/user/`) + .post(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send(buildUserCreateMock(jurisdictionId, 'publicUser+admin@email.com')) .set('Cookie', cookies) @@ -1009,7 +1009,7 @@ describe('Testing Permissioning of endpoints as Admin User', () => { it('should succeed for partner create endpoint & create an activity log entry', async () => { const res = await request(app.getHttpServer()) - .post(`/user/invite`) + .post(`/user/partner`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send( buildUserInviteMock(jurisdictionId, 'partnerUser+admin@email.com'), diff --git a/api/test/integration/permission-tests/permission-as-juris-admin-correct-juris.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-juris-admin-correct-juris.e2e-spec.ts index e5aa457eada..7ce0d6ca694 100644 --- a/api/test/integration/permission-tests/permission-as-juris-admin-correct-juris.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-juris-admin-correct-juris.e2e-spec.ts @@ -35,7 +35,6 @@ import { UnitRentTypeUpdate } from '../../../src/dtos/unit-rent-types/unit-rent- import { UnitTypeCreate } from '../../../src/dtos/unit-types/unit-type-create.dto'; import { UnitTypeUpdate } from '../../../src/dtos/unit-types/unit-type-update.dto'; import { multiselectQuestionFactory } from '../../../prisma/seed-helpers/multiselect-question-factory'; -import { UserUpdate } from '../../../src/dtos/users/user-update.dto'; import { EmailAndAppUrl } from '../../../src/dtos/users/email-and-app-url.dto'; import { ConfirmationRequest } from '../../../src/dtos/users/confirmation-request.dto'; import { UserService } from '../../../src/services/user.service'; @@ -61,6 +60,7 @@ import { createSimpleApplication, createSimpleListing, } from './helpers'; +import { PublicUserUpdate } from '../../../src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), @@ -879,14 +879,14 @@ describe('Testing Permissioning of endpoints as Jurisdictional Admin in the corr }); await request(app.getHttpServer()) - .put(`/user/${userA.id}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, firstName: 'New User First Name', lastName: 'New User Last Name', jurisdictions: [{ id: jurisdictionId } as IdDTO], - } as UserUpdate) + } as PublicUserUpdate) .set('Cookie', cookies) .expect(200); }); @@ -995,7 +995,7 @@ describe('Testing Permissioning of endpoints as Jurisdictional Admin in the corr }); await request(app.getHttpServer()) - .post(`/user/`) + .post(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send(buildUserCreateMock(juris, 'publicUser+jurisCorrect@email.com')) .set('Cookie', cookies) @@ -1004,7 +1004,7 @@ describe('Testing Permissioning of endpoints as Jurisdictional Admin in the corr it('should error as forbidden for partner create endpoint', async () => { await request(app.getHttpServer()) - .post(`/user/invite`) + .post(`/user/partner`) .send( // builds an invite for an admin buildUserInviteMock( diff --git a/api/test/integration/permission-tests/permission-as-juris-admin-wrong-juris.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-juris-admin-wrong-juris.e2e-spec.ts index 6af16fbdb5f..3b0a68b3022 100644 --- a/api/test/integration/permission-tests/permission-as-juris-admin-wrong-juris.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-juris-admin-wrong-juris.e2e-spec.ts @@ -35,7 +35,6 @@ import { UnitRentTypeUpdate } from '../../../src/dtos/unit-rent-types/unit-rent- import { UnitTypeCreate } from '../../../src/dtos/unit-types/unit-type-create.dto'; import { UnitTypeUpdate } from '../../../src/dtos/unit-types/unit-type-update.dto'; import { multiselectQuestionFactory } from '../../../prisma/seed-helpers/multiselect-question-factory'; -import { UserUpdate } from '../../../src/dtos/users/user-update.dto'; import { EmailAndAppUrl } from '../../../src/dtos/users/email-and-app-url.dto'; import { ConfirmationRequest } from '../../../src/dtos/users/confirmation-request.dto'; import { UserService } from '../../../src/services/user.service'; @@ -60,6 +59,7 @@ import { createSimpleApplication, createSimpleListing, } from './helpers'; +import { PublicUserUpdate } from '../../../src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), @@ -840,13 +840,13 @@ describe('Testing Permissioning of endpoints as Jurisdictional Admin in the wron }); await request(app.getHttpServer()) - .put(`/user/${userA.id}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, firstName: 'New User First Name', lastName: 'New User Last Name', - } as UserUpdate) + } as PublicUserUpdate) .set('Cookie', cookies) .expect(403); }); @@ -952,7 +952,7 @@ describe('Testing Permissioning of endpoints as Jurisdictional Admin in the wron }); await request(app.getHttpServer()) - .post(`/user/`) + .post(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send(buildUserCreateMock(juris, 'publicUser+jurisWrong@email.com')) .set('Cookie', cookies) @@ -966,7 +966,7 @@ describe('Testing Permissioning of endpoints as Jurisdictional Admin in the wron ); await request(app.getHttpServer()) - .post(`/user/invite`) + .post(`/user/partner`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send(buildUserInviteMock(juris, 'partnerUser+jurisWrong@email.com')) .set('Cookie', cookies) diff --git a/api/test/integration/permission-tests/permission-as-limited-juris-admin-correct-juris.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-limited-juris-admin-correct-juris.e2e-spec.ts index bda1cc85144..811f2970fd1 100644 --- a/api/test/integration/permission-tests/permission-as-limited-juris-admin-correct-juris.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-limited-juris-admin-correct-juris.e2e-spec.ts @@ -35,7 +35,6 @@ import { UnitRentTypeUpdate } from '../../../src/dtos/unit-rent-types/unit-rent- import { UnitTypeCreate } from '../../../src/dtos/unit-types/unit-type-create.dto'; import { UnitTypeUpdate } from '../../../src/dtos/unit-types/unit-type-update.dto'; import { multiselectQuestionFactory } from '../../../prisma/seed-helpers/multiselect-question-factory'; -import { UserUpdate } from '../../../src/dtos/users/user-update.dto'; import { EmailAndAppUrl } from '../../../src/dtos/users/email-and-app-url.dto'; import { ConfirmationRequest } from '../../../src/dtos/users/confirmation-request.dto'; import { UserService } from '../../../src/services/user.service'; @@ -61,6 +60,7 @@ import { createSimpleApplication, createSimpleListing, } from './helpers'; +import { PublicUserUpdate } from '../../../src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), @@ -824,14 +824,14 @@ describe('Testing Permissioning of endpoints as Limited Jurisdictional Admin in }); await request(app.getHttpServer()) - .put(`/user/${userA.id}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, firstName: 'New User First Name', lastName: 'New User Last Name', jurisdictions: [{ id: jurisdictionId } as IdDTO], - } as UserUpdate) + } as PublicUserUpdate) .set('Cookie', cookies) .expect(403); }); @@ -934,7 +934,7 @@ describe('Testing Permissioning of endpoints as Limited Jurisdictional Admin in }); await request(app.getHttpServer()) - .post(`/user/`) + .post(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send(buildUserCreateMock(juris, 'publicUser2+jurisCorrect@email.com')) .set('Cookie', cookies) @@ -943,7 +943,7 @@ describe('Testing Permissioning of endpoints as Limited Jurisdictional Admin in it('should error as forbidden for partner create endpoint', async () => { await request(app.getHttpServer()) - .post(`/user/invite`) + .post(`/user/partner`) .send( // builds an invite for an admin buildUserInviteMock( diff --git a/api/test/integration/permission-tests/permission-as-limited-juris-admin-wrong-juris.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-limited-juris-admin-wrong-juris.e2e-spec.ts index 40651e2c7dc..30781be37b0 100644 --- a/api/test/integration/permission-tests/permission-as-limited-juris-admin-wrong-juris.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-limited-juris-admin-wrong-juris.e2e-spec.ts @@ -35,7 +35,6 @@ import { UnitRentTypeUpdate } from '../../../src/dtos/unit-rent-types/unit-rent- import { UnitTypeCreate } from '../../../src/dtos/unit-types/unit-type-create.dto'; import { UnitTypeUpdate } from '../../../src/dtos/unit-types/unit-type-update.dto'; import { multiselectQuestionFactory } from '../../../prisma/seed-helpers/multiselect-question-factory'; -import { UserUpdate } from '../../../src/dtos/users/user-update.dto'; import { EmailAndAppUrl } from '../../../src/dtos/users/email-and-app-url.dto'; import { ConfirmationRequest } from '../../../src/dtos/users/confirmation-request.dto'; import { UserService } from '../../../src/services/user.service'; @@ -60,6 +59,7 @@ import { createSimpleApplication, createSimpleListing, } from './helpers'; +import { PublicUserUpdate } from '../../../src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), @@ -840,13 +840,13 @@ describe('Testing Permissioning of endpoints as Limited Jurisdictional Admin in }); await request(app.getHttpServer()) - .put(`/user/${userA.id}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, firstName: 'New User First Name', lastName: 'New User Last Name', - } as UserUpdate) + } as PublicUserUpdate) .set('Cookie', cookies) .expect(403); }); @@ -948,7 +948,7 @@ describe('Testing Permissioning of endpoints as Limited Jurisdictional Admin in }); await request(app.getHttpServer()) - .post(`/user/`) + .post(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send(buildUserCreateMock(juris, 'publicUser+jurisIncorrect@email.com')) .set('Cookie', cookies) @@ -962,7 +962,7 @@ describe('Testing Permissioning of endpoints as Limited Jurisdictional Admin in ); await request(app.getHttpServer()) - .post(`/user/invite`) + .post(`/user/partner`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send( buildUserInviteMock(jurisdiction, 'partnerUser+jurisWrong@email.com'), diff --git a/api/test/integration/permission-tests/permission-as-no-user.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-no-user.e2e-spec.ts index 8ce1808e7d0..7d7fe9eec83 100644 --- a/api/test/integration/permission-tests/permission-as-no-user.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-no-user.e2e-spec.ts @@ -32,7 +32,6 @@ import { import { UnitTypeCreate } from '../../../src/dtos/unit-types/unit-type-create.dto'; import { UnitTypeUpdate } from '../../../src/dtos/unit-types/unit-type-update.dto'; import { multiselectQuestionFactory } from '../../../prisma/seed-helpers/multiselect-question-factory'; -import { UserUpdate } from '../../../src/dtos/users/user-update.dto'; import { EmailAndAppUrl } from '../../../src/dtos/users/email-and-app-url.dto'; import { ConfirmationRequest } from '../../../src/dtos/users/confirmation-request.dto'; import { UserService } from '../../../src/services/user.service'; @@ -62,6 +61,7 @@ import { createListing, createComplexApplication, } from './helpers'; +import { PublicUserUpdate } from '../../../src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), @@ -782,13 +782,13 @@ describe('Testing Permissioning of endpoints as logged out user', () => { }); await request(app.getHttpServer()) - .put(`/user/${userA.id}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, firstName: 'New User First Name', lastName: 'New User Last Name', - } as UserUpdate) + } as PublicUserUpdate) .set('Cookie', cookies) .expect(401); }); @@ -889,7 +889,7 @@ describe('Testing Permissioning of endpoints as logged out user', () => { }); await request(app.getHttpServer()) - .post(`/user/`) + .post(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send( buildUserCreateMock(jurisdictionId, 'publicUser+noUser@email.com'), @@ -900,7 +900,7 @@ describe('Testing Permissioning of endpoints as logged out user', () => { it('should error as unauthorized for partner create endpoint', async () => { await request(app.getHttpServer()) - .post(`/user/invite`) + .post(`/user/partner`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send( buildUserInviteMock(jurisdictionId, 'partnerUser+noUser@email.com'), diff --git a/api/test/integration/permission-tests/permission-as-partner-correct-listing.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-partner-correct-listing.e2e-spec.ts index 55c7e36e9c9..3933ee0ab26 100644 --- a/api/test/integration/permission-tests/permission-as-partner-correct-listing.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-partner-correct-listing.e2e-spec.ts @@ -36,7 +36,6 @@ import { UnitRentTypeUpdate } from '../../../src/dtos/unit-rent-types/unit-rent- import { UnitTypeCreate } from '../../../src/dtos/unit-types/unit-type-create.dto'; import { UnitTypeUpdate } from '../../../src/dtos/unit-types/unit-type-update.dto'; import { multiselectQuestionFactory } from '../../../prisma/seed-helpers/multiselect-question-factory'; -import { UserUpdate } from '../../../src/dtos/users/user-update.dto'; import { EmailAndAppUrl } from '../../../src/dtos/users/email-and-app-url.dto'; import { ConfirmationRequest } from '../../../src/dtos/users/confirmation-request.dto'; import { UserService } from '../../../src/services/user.service'; @@ -62,6 +61,7 @@ import { createSimpleApplication, } from './helpers'; import { ApplicationFlaggedSetService } from '../../../src/services/application-flagged-set.service'; +import { PublicUserUpdate } from '../../../src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), @@ -883,13 +883,13 @@ describe('Testing Permissioning of endpoints as partner with correct listing', ( }); await request(app.getHttpServer()) - .put(`/user/${userA.id}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, firstName: 'New User First Name', lastName: 'New User Last Name', - } as UserUpdate) + } as PublicUserUpdate) .set('Cookie', cookies) .expect(403); }); @@ -995,7 +995,7 @@ describe('Testing Permissioning of endpoints as partner with correct listing', ( }); await request(app.getHttpServer()) - .post(`/user/`) + .post(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send(buildUserCreateMock(juris, 'publicUser+partnerCorrect@email.com')) .set('Cookie', cookies) @@ -1009,7 +1009,7 @@ describe('Testing Permissioning of endpoints as partner with correct listing', ( ); await request(app.getHttpServer()) - .post(`/user/invite`) + .post(`/user/partner`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send( buildUserInviteMock(juris, 'partnerUser+partnerCorrect@email.com'), diff --git a/api/test/integration/permission-tests/permission-as-partner-wrong-listing.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-partner-wrong-listing.e2e-spec.ts index a614e3ab986..4ef05a939ed 100644 --- a/api/test/integration/permission-tests/permission-as-partner-wrong-listing.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-partner-wrong-listing.e2e-spec.ts @@ -36,7 +36,6 @@ import { UnitRentTypeUpdate } from '../../../src/dtos/unit-rent-types/unit-rent- import { UnitTypeCreate } from '../../../src/dtos/unit-types/unit-type-create.dto'; import { UnitTypeUpdate } from '../../../src/dtos/unit-types/unit-type-update.dto'; import { multiselectQuestionFactory } from '../../../prisma/seed-helpers/multiselect-question-factory'; -import { UserUpdate } from '../../../src/dtos/users/user-update.dto'; import { EmailAndAppUrl } from '../../../src/dtos/users/email-and-app-url.dto'; import { ConfirmationRequest } from '../../../src/dtos/users/confirmation-request.dto'; import { UserService } from '../../../src/services/user.service'; @@ -60,6 +59,7 @@ import { constructFullListingData, createSimpleApplication, } from './helpers'; +import { PublicUserUpdate } from '../../../src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), @@ -849,13 +849,13 @@ describe('Testing Permissioning of endpoints as partner with wrong listing', () }); await request(app.getHttpServer()) - .put(`/user/${userA.id}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, firstName: 'New User First Name', lastName: 'New User Last Name', - } as UserUpdate) + } as PublicUserUpdate) .set('Cookie', cookies) .expect(403); }); @@ -961,7 +961,7 @@ describe('Testing Permissioning of endpoints as partner with wrong listing', () }); await request(app.getHttpServer()) - .post(`/user/`) + .post(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send(buildUserCreateMock(juris, 'publicUser+partnerWrong@email.com')) .set('Cookie', cookies) @@ -975,7 +975,7 @@ describe('Testing Permissioning of endpoints as partner with wrong listing', () ); await request(app.getHttpServer()) - .post(`/user/invite`) + .post(`/user/partner`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send(buildUserInviteMock(juris, 'partnerUser+partnerWrong@email.com')) .set('Cookie', cookies) diff --git a/api/test/integration/permission-tests/permission-as-public.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-public.e2e-spec.ts index e519646d93f..0d51df3bab0 100644 --- a/api/test/integration/permission-tests/permission-as-public.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-public.e2e-spec.ts @@ -36,7 +36,6 @@ import { UnitRentTypeUpdate } from '../../../src/dtos/unit-rent-types/unit-rent- import { UnitTypeCreate } from '../../../src/dtos/unit-types/unit-type-create.dto'; import { UnitTypeUpdate } from '../../../src/dtos/unit-types/unit-type-update.dto'; import { multiselectQuestionFactory } from '../../../prisma/seed-helpers/multiselect-question-factory'; -import { UserUpdate } from '../../../src/dtos/users/user-update.dto'; import { EmailAndAppUrl } from '../../../src/dtos/users/email-and-app-url.dto'; import { ConfirmationRequest } from '../../../src/dtos/users/confirmation-request.dto'; import { UserService } from '../../../src/services/user.service'; @@ -63,6 +62,7 @@ import { createListing, createComplexApplication, } from './helpers'; +import { PublicUserUpdate } from '../../../src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), @@ -834,26 +834,26 @@ describe('Testing Permissioning of endpoints as public user', () => { }); await request(app.getHttpServer()) - .put(`/user/${userA.id}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, firstName: 'New User First Name', lastName: 'New User Last Name', - } as UserUpdate) + } as PublicUserUpdate) .set('Cookie', cookies) .expect(403); }); it('should succeed for update endpoint targeting self', async () => { await request(app.getHttpServer()) - .put(`/user/${storedUserId}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: storedUserId, firstName: 'New User First Name', lastName: 'New User Last Name', - } as UserUpdate) + } as PublicUserUpdate) .set('Cookie', cookies) .expect(200); }); @@ -959,7 +959,7 @@ describe('Testing Permissioning of endpoints as public user', () => { }); await request(app.getHttpServer()) - .post(`/user/`) + .post(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send(buildUserCreateMock(juris, 'publicUser+public@email.com')) .set('Cookie', cookies) @@ -973,7 +973,7 @@ describe('Testing Permissioning of endpoints as public user', () => { ); await request(app.getHttpServer()) - .post(`/user/invite`) + .post(`/user/partner`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send(buildUserInviteMock(juris, 'partnerUser+public@email.com')) .set('Cookie', cookies) diff --git a/api/test/integration/permission-tests/permission-as-support-admin.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-support-admin.e2e-spec.ts index b9a98d02e27..64bd5553841 100644 --- a/api/test/integration/permission-tests/permission-as-support-admin.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-support-admin.e2e-spec.ts @@ -36,7 +36,6 @@ import { UnitRentTypeUpdate } from '../../../src/dtos/unit-rent-types/unit-rent- import { UnitTypeCreate } from '../../../src/dtos/unit-types/unit-type-create.dto'; import { UnitTypeUpdate } from '../../../src/dtos/unit-types/unit-type-update.dto'; import { multiselectQuestionFactory } from '../../../prisma/seed-helpers/multiselect-question-factory'; -import { UserUpdate } from '../../../src/dtos/users/user-update.dto'; import { EmailAndAppUrl } from '../../../src/dtos/users/email-and-app-url.dto'; import { ConfirmationRequest } from '../../../src/dtos/users/confirmation-request.dto'; import { UserService } from '../../../src/services/user.service'; @@ -65,6 +64,7 @@ import { buildJurisdictionUpdateMock, } from './helpers'; import { featureFlagFactory } from '../../../prisma/seed-helpers/feature-flag-factory'; +import { PublicUserUpdate } from '../../../src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), @@ -871,14 +871,14 @@ describe('Testing Permissioning of endpoints as Support Admin User', () => { }); await request(app.getHttpServer()) - .put(`/user/${userA.id}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, firstName: 'New User First Name', lastName: 'New User Last Name', jurisdictions: [{ id: jurisdictionId } as IdDTO], - } as UserUpdate) + } as PublicUserUpdate) .set('Cookie', cookies) .expect(403); }); @@ -973,7 +973,7 @@ describe('Testing Permissioning of endpoints as Support Admin User', () => { it('should error as forbidden for partner create endpoint', async () => { await request(app.getHttpServer()) - .post(`/user/invite`) + .post(`/user/partner`) .send( // builds an invite for an admin buildUserInviteMock( diff --git a/api/test/integration/user.e2e-spec.ts b/api/test/integration/user.e2e-spec.ts index 274ee26e3b7..b6d8697099a 100644 --- a/api/test/integration/user.e2e-spec.ts +++ b/api/test/integration/user.e2e-spec.ts @@ -9,22 +9,25 @@ import { PrismaService } from '../../src/services/prisma.service'; import { userFactory } from '../../prisma/seed-helpers/user-factory'; import cookieParser from 'cookie-parser'; import { UserQueryParams } from '../../src/dtos/users/user-query-param.dto'; -import { UserUpdate } from '../../src/dtos/users/user-update.dto'; import { IdDTO } from '../../src/dtos/shared/id.dto'; import { EmailAndAppUrl } from '../../src/dtos/users/email-and-app-url.dto'; import { ConfirmationRequest } from '../../src/dtos/users/confirmation-request.dto'; import { UserService } from '../../src/services/user.service'; -import { UserCreate } from '../../src/dtos/users/user-create.dto'; import { jurisdictionFactory } from '../../prisma/seed-helpers/jurisdiction-factory'; import { listingFactory } from '../../prisma/seed-helpers/listing-factory'; import { applicationFactory } from '../../prisma/seed-helpers/application-factory'; import { randomName } from '../../prisma/seed-helpers/word-generator'; -import { UserInvite } from '../../src/dtos/users/user-invite.dto'; import { EmailService } from '../../src/services/email.service'; import { Login } from '../../src/dtos/auth/login.dto'; import { RequestMfaCode } from '../../src/dtos/mfa/request-mfa-code.dto'; import { ModificationEnum } from '../../src/enums/shared/modification-enum'; import dayjs from 'dayjs'; +import { PublicUserUpdate } from '../../src/dtos/users/public-user-update.dto'; +import { PublicUserCreate } from '../../src/dtos/users/public-user-create.dto'; +import { PartnerUserCreate } from '../../src/dtos/users/partner-user-create.dto'; +import { AdvocateUserCreate } from '../../src/dtos/users/advocate-user-create.dto'; +import { addressFactory } from '../../prisma/seed-helpers/address-factory'; +import { AddressUpdate } from '../../src/dtos/addresses/address-update.dto'; describe('User Controller Tests', () => { let app: INestApplication; @@ -197,13 +200,13 @@ describe('User Controller Tests', () => { }); const res = await request(app.getHttpServer()) - .put(`/user/${userA.id}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, firstName: 'New User First Name', lastName: 'New User Last Name', - } as UserUpdate) + } as PublicUserUpdate) .set('Cookie', cookies) .expect(200); @@ -226,13 +229,13 @@ describe('User Controller Tests', () => { }); const randomId = randomUUID(); const res = await request(app.getHttpServer()) - .put(`/user/${randomId}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: randomId, firstName: 'New User First Name', lastName: 'New User Last Name', - } as UserUpdate) + } as PublicUserUpdate) .set('Cookie', cookies) .expect(404); @@ -249,7 +252,7 @@ describe('User Controller Tests', () => { }); const res = await request(app.getHttpServer()) - .delete(`/user/`) + .delete(`/user`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, @@ -266,7 +269,7 @@ describe('User Controller Tests', () => { }); const res = await request(app.getHttpServer()) - .delete(`/user/`) + .delete(`/user`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, @@ -280,7 +283,7 @@ describe('User Controller Tests', () => { it("should error when deleting user that doesn't exist", async () => { const randomId = randomUUID(); const res = await request(app.getHttpServer()) - .delete(`/user/`) + .delete(`/user`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: randomId, @@ -743,19 +746,22 @@ describe('User Controller Tests', () => { }); const res = await request(app.getHttpServer()) - .post(`/user/`) + .post(`/user/public/`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ - firstName: 'Public User firstName', - lastName: 'Public User lastName', + firstName: 'Public First Name', + lastName: 'Public Last Name', password: 'Abcdef12345!', + passwordConfirmation: 'Abcdef12345!', email: 'publicUser@email.com', + emailConfirmation: 'publicUser@email.com', + dob: new Date(), jurisdictions: [{ id: juris.id }], - } as UserCreate) + } as PublicUserCreate) .set('Cookie', cookies) .expect(201); - expect(res.body.firstName).toEqual('Public User firstName'); + expect(res.body.firstName).toEqual('Public First Name'); expect(res.body.jurisdictions).toEqual([ expect.objectContaining({ id: juris.id, name: juris.name }), ]); @@ -773,6 +779,69 @@ describe('User Controller Tests', () => { application.id, ); }); + + it('should create an advocate user', async () => { + const juris = await prisma.jurisdictions.create({ + data: jurisdictionFactory(), + }); + + const agencyData = await prisma.agency.create({ + data: { + name: 'Test Agency', + jurisdictions: { + connect: { + id: juris.id, + }, + }, + }, + }); + + const data = await applicationFactory(); + data.applicant.create.emailAddress = 'advocateuser@email.com'; + const application = await prisma.applications.create({ + data, + }); + + const advocateUserData: AdvocateUserCreate = { + firstName: 'Advocate First Name', + lastName: 'Advocate Last Name', + email: 'advocateUser@email.com', + agreedToTermsOfService: true, + agency: { + ...agencyData, + jurisdictions: { + id: agencyData.jurisdictionsId, + }, + }, + address: addressFactory() as AddressUpdate, + jurisdictions: [{ id: juris.id }], + }; + + const res = await request(app.getHttpServer()) + .post(`/user/advocate`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .send(advocateUserData) + .set('Cookie', cookies) + .expect(201); + + expect(res.body.firstName).toEqual('Advocate First Name'); + expect(res.body.jurisdictions).toEqual([ + expect.objectContaining({ id: juris.id, name: juris.name }), + ]); + expect(res.body.email).toEqual('advocateuser@email.com'); + + const applicationsOnUser = await prisma.userAccounts.findUnique({ + include: { + applications: true, + }, + where: { + id: res.body.id, + }, + }); + expect(applicationsOnUser.applications.map((app) => app.id)).toContain( + application.id, + ); + }); }); describe('invite partner endpoint', () => { @@ -782,7 +851,7 @@ describe('User Controller Tests', () => { }); const res = await request(app.getHttpServer()) - .post(`/user/invite`) + .post(`/user/partner`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ firstName: 'Partner User firstName', @@ -794,7 +863,7 @@ describe('User Controller Tests', () => { userRoles: { isAdmin: true, }, - } as UserInvite) + } as PartnerUserCreate) .set('Cookie', cookies) .expect(201); diff --git a/api/test/unit/services/user.service.spec.ts b/api/test/unit/services/user.service.spec.ts index 75f6695a1d6..01ed162ff7a 100644 --- a/api/test/unit/services/user.service.spec.ts +++ b/api/test/unit/services/user.service.spec.ts @@ -25,6 +25,10 @@ import { TranslationService } from '../../../src/services/translation.service'; import { UserService } from '../../../src/services/user.service'; import { passwordToHash } from '../../../src/utilities/password-helpers'; import { SnapshotCreateService } from '../../../src/services/snapshot-create.service'; +import { PublicUserUpdate } from '../../../src/dtos/users/public-user-update.dto'; +import { addressFactory } from '../../../prisma/seed-helpers/address-factory'; +import { AddressUpdate } from '../../../src/dtos/addresses/address-update.dto'; +import { AdvocateUserUpdate } from '../../../src/dtos/users/advocate-user-update.dto'; describe('Testing user service', () => { let service: UserService; @@ -1329,20 +1333,24 @@ describe('Testing user service', () => { .fn() .mockImplementation((callBack) => callBack(prisma)); - await service.update( - { - id, - firstName: 'first name', - lastName: 'last name', - jurisdictions: [{ id: jurisId }], - agreedToTermsOfService: true, - }, - { + const mockUserUpdate: PublicUserUpdate = { + id, + firstName: 'first name', + middleName: 'middle name', + lastName: 'last name', + dob: new Date(), + email: 'test@email.com', + jurisdictions: [{ id: jurisId }], + agreedToTermsOfService: true, + }; + + await service.update(mockUserUpdate, { + headers: { jurisdictionname: 'juris 1' }, + user: { id: 'requestingUser id', userRoles: { isAdmin: true }, } as unknown as User, - 'jurisdictionName', - ); + } as unknown as Request); expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ include: { jurisdictions: true, @@ -1359,22 +1367,16 @@ describe('Testing user service', () => { id, }, }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ - data: { - jurisdictions: { - connect: [{ id: jurisId }], - }, - }, - where: { - id, - }, - }); - expect(prisma.userAccounts.update).toHaveBeenCalledWith({ - data: { - firstName: 'first name', - lastName: 'last name', + data: expect.objectContaining({ + firstName: mockUserUpdate.firstName, + middleName: mockUserUpdate.middleName, + lastName: mockUserUpdate.lastName, + email: mockUserUpdate.email, + dob: mockUserUpdate.dob, agreedToTermsOfService: true, - }, + }), include: { jurisdictions: true, listings: true, @@ -1426,22 +1428,26 @@ describe('Testing user service', () => { .fn() .mockImplementation((callBack) => callBack(prisma)); - await service.update( - { - id, - firstName: 'first name', - lastName: 'last name', - jurisdictions: [{ id: jurisId }], - password: 'new password', - currentPassword: 'current password', - agreedToTermsOfService: true, - }, - { + const mockPublicUserUpdate: PublicUserUpdate = { + id, + firstName: 'first name', + lastName: 'last name', + dob: new Date(), + email: 'updated@email.com', + jurisdictions: [{ id: jurisId } as any], + password: 'new password', + currentPassword: 'current password', + agreedToTermsOfService: true, + }; + + await service.update(mockPublicUserUpdate, { + headers: { jurisdictionname: 'juris 1' }, + user: { id: 'requestingUser id', userRoles: { isAdmin: true }, } as unknown as User, - 'jurisdictionName', - ); + } as unknown as Request); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ include: { jurisdictions: true, @@ -1458,24 +1464,15 @@ describe('Testing user service', () => { id, }, }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ - data: { - jurisdictions: { - connect: [{ id: jurisId }], - }, - }, - where: { - id, - }, - }); - expect(prisma.userAccounts.update).toHaveBeenCalledWith({ - data: { - firstName: 'first name', - lastName: 'last name', + data: expect.objectContaining({ + firstName: mockPublicUserUpdate.firstName, + lastName: mockPublicUserUpdate.lastName, passwordHash: expect.anything(), passwordUpdatedAt: expect.anything(), agreedToTermsOfService: true, - }, + }), include: { jurisdictions: true, listings: true, @@ -1531,15 +1528,17 @@ describe('Testing user service', () => { id, firstName: 'first name', lastName: 'last name', - jurisdictions: [{ id: jurisId }], + jurisdictions: [{ id: jurisId } as any], password: 'new password', agreedToTermsOfService: true, - }, + } as PublicUserUpdate, { - id: 'requestingUser id', - userRoles: { isAdmin: true }, - } as unknown as User, - 'jurisdictionName', + headers: { jurisdictionname: 'juris 1' }, + user: { + id: 'requestingUser id', + userRoles: { isAdmin: true }, + } as unknown as User, + } as unknown as Request, ), ).rejects.toThrowError(`userID ${id}: request missing currentPassword`); expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ @@ -1593,16 +1592,18 @@ describe('Testing user service', () => { id, firstName: 'first name', lastName: 'last name', - jurisdictions: [{ id: jurisId }], + jurisdictions: [{ id: jurisId } as any], password: 'new password', currentPassword: 'new password', agreedToTermsOfService: true, - }, + } as PublicUserUpdate, { - id: 'requestingUser id', - userRoles: { isAdmin: true }, - } as unknown as User, - 'jurisdictionName', + headers: { jurisdictionname: 'juris 1' }, + user: { + id: 'requestingUser id', + userRoles: { isAdmin: true }, + } as unknown as User, + } as unknown as Request, ), ).rejects.toThrowError( `userID ${id}: incoming password doesn't match stored password`, @@ -1655,22 +1656,25 @@ describe('Testing user service', () => { .fn() .mockImplementation((callBack) => callBack(prisma)); - await service.update( - { - id, - firstName: 'first name', - lastName: 'last name', - jurisdictions: [{ id: jurisId }], - newEmail: 'new@email.com', - appUrl: 'https://www.example.com', - agreedToTermsOfService: true, - }, - { + const mockUserUpdate: PublicUserUpdate = { + id, + firstName: 'first name', + lastName: 'last name', + dob: new Date(), + email: 'updated@email.com', + jurisdictions: [{ id: jurisId } as any], + newEmail: 'new@email.com', + appUrl: 'https://www.example.com', + agreedToTermsOfService: true, + }; + + await service.update(mockUserUpdate, { + headers: { jurisdictionname: 'juris 1' }, + user: { id: 'requestingUser id', userRoles: { isAdmin: true }, } as unknown as User, - 'jurisdictionName', - ); + } as unknown as Request); expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ include: { jurisdictions: true, @@ -1687,23 +1691,14 @@ describe('Testing user service', () => { id, }, }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ - data: { - jurisdictions: { - connect: [{ id: jurisId }], - }, - }, - where: { - id, - }, - }); - expect(prisma.userAccounts.update).toHaveBeenCalledWith({ - data: { - firstName: 'first name', - lastName: 'last name', + data: expect.objectContaining({ + firstName: mockUserUpdate.firstName, + lastName: mockUserUpdate.lastName, confirmationToken: expect.anything(), agreedToTermsOfService: true, - }, + }), include: { jurisdictions: true, listings: true, @@ -1764,15 +1759,17 @@ describe('Testing user service', () => { id, firstName: 'first name', lastName: 'last name', - jurisdictions: [{ id: jurisId }], + jurisdictions: [{ id: jurisId } as any], agreedToTermsOfService: true, listings: [{ id: listingA }, { id: listingC }], - }, + } as PublicUserUpdate, { - id: 'requestingUser id', - userRoles: { isAdmin: true }, - } as unknown as User, - 'jurisdictionName', + headers: { jurisdictionname: 'juris 1' }, + user: { + id: 'requestingUser id', + userRoles: { isAdmin: true }, + } as unknown as User, + } as unknown as Request, ); expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ include: { @@ -1790,7 +1787,7 @@ describe('Testing user service', () => { id, }, }); - expect(prisma.userAccounts.update).toHaveBeenCalledTimes(3); + expect(prisma.userAccounts.update).toHaveBeenCalledTimes(4); expect(prisma.userAccounts.update).toHaveBeenCalledWith({ data: { listings: { @@ -1826,10 +1823,141 @@ describe('Testing user service', () => { }); expect(prisma.userAccounts.update).toHaveBeenCalledWith({ data: { + agency: undefined, + }, + where: { + id, + }, + }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: expect.objectContaining({ + firstName: 'first name', + lastName: 'last name', + agreedToTermsOfService: true, + }), + include: { + jurisdictions: true, + listings: true, + userRoles: true, + favoriteListings: { + select: { + id: true, + name: true, + }, + }, + }, + where: { + id, + }, + }); + expect(canOrThrowMock).toHaveBeenCalledWith( + { + id: 'requestingUser id', + userRoles: { isAdmin: true }, + } as unknown as User, + 'user', + permissionActions.update, + { + id, + jurisdictionId: jurisId, + }, + ); + expect(prisma.userAccountSnapshot.create).toHaveBeenCalledWith({ + data: { + originalId: id, + listing: { + connect: [{ id: listingA }, { id: listingB }], + }, + }, + }); + }); + + it('should connect agency to a user', async () => { + const id = randomUUID(); + const jurisId = randomUUID(); + const listingA = randomUUID(); + const agencyId = randomUUID(); + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id, + listings: [{ id: listingA }], + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + }); + prisma.userAccountSnapshot.create = jest.fn().mockResolvedValue({ id }); + + prisma.$transaction = jest + .fn() + .mockImplementation((callBack) => callBack(prisma)); + + await service.update( + { + id, firstName: 'first name', lastName: 'last name', + jurisdictions: [{ id: jurisId } as any], agreedToTermsOfService: true, + listings: [{ id: listingA }], + agency: { id: agencyId }, + } as AdvocateUserUpdate, + { + headers: { jurisdictionname: 'juris 1' }, + user: { + id: 'requestingUser id', + userRoles: { isAdmin: true }, + } as unknown as User, + } as unknown as Request, + ); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + listings: true, + userRoles: true, + favoriteListings: { + select: { + id: true, + name: true, + }, + }, + }, + where: { + id, }, + }); + expect(prisma.userAccounts.update).toHaveBeenCalledTimes(3); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + jurisdictions: { + connect: [ + { + id: jurisId, + }, + ], + }, + }, + where: { + id, + }, + }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + agency: { + connect: { + id: agencyId, + }, + }, + }, + where: { + id, + }, + }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: expect.objectContaining({ + firstName: 'first name', + lastName: 'last name', + agreedToTermsOfService: true, + }), include: { jurisdictions: true, listings: true, @@ -1861,12 +1989,127 @@ describe('Testing user service', () => { data: { originalId: id, listing: { - connect: [{ id: listingA }, { id: listingB }], + connect: [{ id: listingA }], }, }, }); }); + it('should disconnect agency from and user', async () => { + const id = randomUUID(); + const jurisId = randomUUID(); + const listingA = randomUUID(); + const agencyId = randomUUID(); + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id, + listings: [{ id: listingA }], + agency: { id: agencyId }, + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + }); + prisma.userAccountSnapshot.create = jest.fn().mockResolvedValue({ id }); + + prisma.$transaction = jest + .fn() + .mockImplementation((callBack) => callBack(prisma)); + + await service.update( + { + id, + firstName: 'first name', + lastName: 'last name', + jurisdictions: [{ id: jurisId } as any], + agreedToTermsOfService: true, + listings: [{ id: listingA }], + } as AdvocateUserUpdate, + { + headers: { jurisdictionname: 'juris 1' }, + user: { + id: 'requestingUser id', + userRoles: { isAdmin: true }, + } as unknown as User, + } as unknown as Request, + ); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + listings: true, + userRoles: true, + favoriteListings: { + select: { + id: true, + name: true, + }, + }, + }, + where: { + id, + }, + }); + expect(prisma.userAccounts.update).toHaveBeenCalledTimes(3); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + jurisdictions: { + connect: [ + { + id: jurisId, + }, + ], + }, + }, + where: { + id, + }, + }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + agency: { + disconnect: { + id: agencyId, + }, + }, + }, + where: { + id, + }, + }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: expect.objectContaining({ + firstName: 'first name', + lastName: 'last name', + agreedToTermsOfService: true, + }), + include: { + jurisdictions: true, + listings: true, + userRoles: true, + favoriteListings: { + select: { + id: true, + name: true, + }, + }, + }, + where: { + id, + }, + }); + expect(canOrThrowMock).toHaveBeenCalledWith( + { + id: 'requestingUser id', + userRoles: { isAdmin: true }, + } as unknown as User, + 'user', + permissionActions.update, + { + id, + jurisdictionId: jurisId, + }, + ); + }); + it('should error when trying to update nonexistent user', async () => { const id = randomUUID(); @@ -1880,14 +2123,16 @@ describe('Testing user service', () => { id, firstName: 'first name', lastName: 'last name', - jurisdictions: [{ id: randomUUID() }], + jurisdictions: [{ id: randomUUID() } as any], agreedToTermsOfService: true, - }, + } as PublicUserUpdate, { - id: 'requestingUser id', - userRoles: { isAdmin: true }, - } as unknown as User, - 'jurisdictionName', + headers: { jurisdictionname: 'juris 1' }, + user: { + id: 'requestingUser id', + userRoles: { isAdmin: true }, + } as unknown as User, + } as unknown as Request, ), ).rejects.toThrowError(`user id: ${id} was requested but not found`); expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ @@ -1925,20 +2170,17 @@ describe('Testing user service', () => { id, }); emailService.invitePartnerUser = jest.fn(); - await service.create( + await service.createPartnerUser( { firstName: 'Partner User firstName', lastName: 'Partner User lastName', - password: 'Abcdef12345!', + agreedToTermsOfService: true, email: 'partnerUser@email.com', jurisdictions: [{ id: jurisId }], userRoles: { isAdmin: true, }, }, - true, - undefined, - { headers: { jurisdictionname: 'juris 1' }, user: { @@ -2007,11 +2249,11 @@ describe('Testing user service', () => { }); prisma.userAccountSnapshot.create = jest.fn().mockResolvedValue({ id }); - await service.create( + await service.createPartnerUser( { firstName: 'Partner User firstName', lastName: 'Partner User lastName', - password: 'Abcdef12345!', + agreedToTermsOfService: true, email: 'partnerUser@email.com', jurisdictions: [{ id: jurisId }], userRoles: { @@ -2019,8 +2261,6 @@ describe('Testing user service', () => { }, listings: [{ id: 'listing id' }], }, - true, - undefined, { headers: { jurisdictionname: 'juris 1' }, user: { @@ -2108,11 +2348,11 @@ describe('Testing user service', () => { prisma.userAccounts.create = jest.fn().mockResolvedValue(null); await expect( async () => - await service.create( + await service.createPartnerUser( { firstName: 'Partner User firstName', lastName: 'Partner User lastName', - password: 'Abcdef12345!', + agreedToTermsOfService: true, email: 'partnerUser@email.com', jurisdictions: [{ id: jurisId }], userRoles: { @@ -2120,8 +2360,6 @@ describe('Testing user service', () => { }, listings: [{ id: 'listing id' }], }, - true, - undefined, { headers: { jurisdictionname: 'juris 1' }, user: { @@ -2182,16 +2420,18 @@ describe('Testing user service', () => { id, email: 'publicUser@email.com', }); - await service.create( + await service.createPublicUser( { firstName: 'public User firstName', lastName: 'public User lastName', password: 'Abcdef12345!', + passwordConfirmation: 'Abcdef12345!', + agreedToTermsOfService: true, + dob: new Date('2000-01-01'), email: 'publicUser@email.com', - jurisdictions: [{ id: jurisId }], + jurisdictions: [{ id: jurisId } as any], }, false, - undefined, { headers: { jurisdictionname: 'juris 1' }, user: { @@ -2218,19 +2458,15 @@ describe('Testing user service', () => { }); expect(prisma.userAccounts.create).toHaveBeenCalledWith({ data: { - dob: undefined, + dob: expect.anything(), passwordHash: expect.anything(), - phoneNumber: undefined, - userRoles: undefined, email: 'publicUser@email.com', firstName: 'public User firstName', lastName: 'public User lastName', - language: undefined, listings: undefined, middleName: undefined, - mfaEnabled: false, jurisdictions: { - connect: { name: 'juris 1' }, + connect: [{ id: expect.anything() }], }, }, }); @@ -2287,6 +2523,146 @@ describe('Testing user service', () => { }); expect(canOrThrowMock).not.toHaveBeenCalled(); }); + + it('should create an advocate user', async () => { + const jurisId = randomUUID(); + const id = randomUUID(); + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue(null); + prisma.applications.findMany = jest + .fn() + .mockResolvedValue([ + { id: 'application id 1' }, + { id: 'application id 2' }, + ]); + prisma.applications.update = jest.fn().mockResolvedValue(null); + prisma.userAccounts.create = jest.fn().mockResolvedValue({ + id, + email: 'advocateUser@email.com', + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + email: 'advocateUser@email.com', + }); + + const mockAdress = addressFactory(); + + await service.createAdvocateUser( + { + firstName: 'advocate User firstName', + lastName: 'advocate User lastName', + agreedToTermsOfService: true, + dob: new Date('2000-01-01'), + email: 'advocateUser@email.com', + jurisdictions: [{ id: jurisId } as any], + address: mockAdress as AddressUpdate, + agency: { + id: 'test_agency_id', + createdAt: new Date(), + updatedAt: new Date(), + name: 'Test Agency', + jurisdictions: { + id: jurisId, + }, + }, + }, + false, + { + headers: { jurisdictionname: 'juris 1' }, + user: { + id: 'requestingUser id', + userRoles: { isAdmin: true }, + } as unknown as User, + } as unknown as Request, + ); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + listings: true, + userRoles: true, + favoriteListings: { + select: { + id: true, + name: true, + }, + }, + }, + where: { + email: 'advocateUser@email.com', + }, + }); + expect(prisma.userAccounts.create).toHaveBeenCalledWith({ + data: { + passwordHash: expect.anything(), + email: 'advocateUser@email.com', + firstName: 'advocate User firstName', + lastName: 'advocate User lastName', + listings: undefined, + middleName: undefined, + isAdvocate: true, + agency: { + connect: { + id: 'test_agency_id', + }, + }, + jurisdictions: { + connect: [{ id: expect.anything() }], + }, + }, + }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + listings: true, + userRoles: true, + favoriteListings: { + select: { + id: true, + name: true, + }, + }, + }, + data: { + confirmationToken: expect.anything(), + }, + where: { + id: id, + }, + }); + expect(prisma.applications.findMany).toHaveBeenCalledWith({ + where: { + applicant: { + emailAddress: 'advocateUser@email.com', + }, + userAccounts: null, + }, + }); + expect(prisma.applications.update).toHaveBeenNthCalledWith(1, { + data: { + userAccounts: { + connect: { + id, + }, + }, + }, + where: { + id: 'application id 1', + }, + }); + expect(prisma.applications.update).toHaveBeenNthCalledWith(2, { + data: { + userAccounts: { + connect: { + id, + }, + }, + }, + where: { + id: 'application id 2', + }, + }); + expect(canOrThrowMock).not.toHaveBeenCalled(); + }); }); describe('isUserRoleChangeAllowed', () => { diff --git a/shared-helpers/src/auth/AuthContext.ts b/shared-helpers/src/auth/AuthContext.ts index 18f1466f2e3..91b89929eed 100644 --- a/shared-helpers/src/auth/AuthContext.ts +++ b/shared-helpers/src/auth/AuthContext.ts @@ -27,7 +27,6 @@ import { ReservedCommunityTypesService, UnitTypesService, User, - UserCreate, UserService, serviceOptions, SuccessDTO, @@ -35,6 +34,9 @@ import { LanguagesEnum, FeatureFlagsService, PropertiesService, + PublicUserCreate, + AdvocateUserCreate, + PartnerUserCreate, } from "../types/backend-swagger" import { getListingRedirectUrl } from "../utilities/getListingRedirectUrl" import { useRouter } from "next/router" @@ -71,7 +73,15 @@ type ContextProps = { signOut: () => Promise confirmAccount: (token: string) => Promise forgotPassword: (email: string, listingIdRedirect?: string) => Promise - createUser: (user: UserCreate, listingIdRedirect?: string) => Promise + createPublicUser: ( + user: PublicUserCreate, + listingIdRedirect?: string + ) => Promise + createAdvocateUser: ( + user: AdvocateUserCreate, + listingIdRedirect?: string + ) => Promise + createPartnerUser: (user: PartnerUserCreate) => Promise resendConfirmation: (email: string, listingIdRedirect?: string) => Promise initialStateLoaded?: boolean loading?: boolean @@ -335,11 +345,34 @@ export const AuthProvider: FunctionComponent = ({ child dispatch(stopLoading()) } }, - createUser: async (user: UserCreate, listingIdRedirect) => { + createPublicUser: async (user: PublicUserCreate, listingIdRedirect) => { + dispatch(startLoading()) + const appUrl = getListingRedirectUrl(listingIdRedirect) + try { + const response = await userService?.createPublic({ + body: { ...user, appUrl }, + }) + return response + } finally { + dispatch(stopLoading()) + } + }, + createPartnerUser: async (user: PartnerUserCreate) => { + dispatch(startLoading()) + try { + const response = await userService?.createPartner({ + body: user, + }) + return response + } finally { + dispatch(stopLoading()) + } + }, + createAdvocateUser: async (user: AdvocateUserCreate, listingIdRedirect) => { dispatch(startLoading()) const appUrl = getListingRedirectUrl(listingIdRedirect) try { - const response = await userService?.create({ + const response = await userService?.createAdvocate({ body: { ...user, appUrl }, }) return response diff --git a/shared-helpers/src/types/backend-swagger.ts b/shared-helpers/src/types/backend-swagger.ts index 7c58f785fd1..df04fcda094 100644 --- a/shared-helpers/src/types/backend-swagger.ts +++ b/shared-helpers/src/types/backend-swagger.ts @@ -1815,31 +1815,6 @@ export class UserService { axios(configs, resolve, reject) }) } - /** - * Creates a public only user - */ - create( - params: { - /** */ - noWelcomeEmail?: boolean - /** requestBody */ - body?: UserCreate - } = {} as any, - options: IRequestOptions = {} - ): Promise { - return new Promise((resolve, reject) => { - let url = basePath + "/user" - - const configs: IRequestConfig = getConfigs("post", "application/json", url, options) - configs.params = { noWelcomeEmail: params["noWelcomeEmail"] } - - let data = params.body - - configs.data = data - - axios(configs, resolve, reject) - }) - } /** * Delete user by id */ @@ -1930,19 +1905,135 @@ export class UserService { }) } /** - * Invite partner user + * Creates a public only user + */ + createPublic( + params: { + /** */ + noWelcomeEmail?: boolean + /** requestBody */ + body?: PublicUserCreate + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/user/public" + + const configs: IRequestConfig = getConfigs("post", "application/json", url, options) + configs.params = { noWelcomeEmail: params["noWelcomeEmail"] } + + let data = params.body + + configs.data = data + + axios(configs, resolve, reject) + }) + } + /** + * Update user + */ + updatePublic( + params: { + /** requestBody */ + body?: PublicUserUpdate + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/user/public" + + const configs: IRequestConfig = getConfigs("put", "application/json", url, options) + + let data = params.body + + configs.data = data + + axios(configs, resolve, reject) + }) + } + /** + * Creates a partner only user + */ + createPartner( + params: { + /** requestBody */ + body?: PartnerUserCreate + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/user/partner" + + const configs: IRequestConfig = getConfigs("post", "application/json", url, options) + + let data = params.body + + configs.data = data + + axios(configs, resolve, reject) + }) + } + /** + * Update user + */ + updatePartner( + params: { + /** requestBody */ + body?: PartnerUserUpdate + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/user/partner" + + const configs: IRequestConfig = getConfigs("put", "application/json", url, options) + + let data = params.body + + configs.data = data + + axios(configs, resolve, reject) + }) + } + /** + * Creates a advocate only user */ - invite( + createAdvocate( params: { + /** */ + noWelcomeEmail?: boolean /** requestBody */ - body?: UserInvite + body?: AdvocateUserCreate } = {} as any, options: IRequestOptions = {} ): Promise { return new Promise((resolve, reject) => { - let url = basePath + "/user/invite" + let url = basePath + "/user/advocate" const configs: IRequestConfig = getConfigs("post", "application/json", url, options) + configs.params = { noWelcomeEmail: params["noWelcomeEmail"] } + + let data = params.body + + configs.data = data + + axios(configs, resolve, reject) + }) + } + /** + * Update user + */ + updateAdvocate( + params: { + /** requestBody */ + body?: AdvocateUserUpdate + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/user/advocate" + + const configs: IRequestConfig = getConfigs("put", "application/json", url, options) let data = params.body @@ -2115,28 +2206,6 @@ export class UserService { axios(configs, resolve, reject) }) } - /** - * Update user - */ - update( - params: { - /** requestBody */ - body?: UserUpdate - } = {} as any, - options: IRequestOptions = {} - ): Promise { - return new Promise((resolve, reject) => { - let url = basePath + "/user/{id}" - - const configs: IRequestConfig = getConfigs("put", "application/json", url, options) - - let data = params.body - - configs.data = data - - axios(configs, resolve, reject) - }) - } /** * Get user by id */ @@ -8650,7 +8719,7 @@ export interface UserRole { isSupportAdmin?: boolean } -export interface User { +export interface Agency { /** */ id: string @@ -8661,11 +8730,13 @@ export interface User { updatedAt: Date /** */ - passwordUpdatedAt: Date + name: string /** */ - passwordValidForDays: number + jurisdictions: IdDTO +} +export interface PublicUserCreate { /** */ confirmedAt?: Date @@ -8681,9 +8752,6 @@ export interface User { /** */ lastName: string - /** */ - dob?: Date - /** */ phoneNumber?: string @@ -8696,9 +8764,6 @@ export interface User { /** */ language?: LanguagesEnum - /** */ - jurisdictions: Jurisdiction[] - /** */ mfaEnabled?: boolean @@ -8725,54 +8790,39 @@ export interface User { /** */ favoriteListings?: IdDTO[] -} - -export interface UserFilterParams { - /** */ - isPortalUser?: boolean -} - -export interface PaginatedUser { - /** */ - items: User[] /** */ - meta: PaginationMeta -} - -export interface UserCreate { - /** */ - firstName: string + title?: string /** */ - middleName?: string + agency?: Agency /** */ - lastName: string + address?: Address /** */ - dob?: Date + phoneType?: string /** */ - phoneNumber?: string + phoneExtension?: string /** */ - listings: IdDTO[] + additionalPhoneNumber?: string /** */ - language?: LanguagesEnum + additionalPhoneNumberType?: string /** */ - agreedToTermsOfService: boolean + additionalPhoneExtension?: string /** */ - favoriteListings?: IdDTO[] + dob: Date /** */ - newEmail?: string + appUrl?: string /** */ - appUrl?: string + jurisdictions?: IdDTO[] /** */ password: string @@ -8780,25 +8830,17 @@ export interface UserCreate { /** */ passwordConfirmation: string - /** */ - email: string - /** */ emailConfirmation?: string - - /** */ - jurisdictions?: IdDTO[] } -export interface UserDeleteDTO { +export interface PartnerUserCreate { /** */ - id: string + confirmedAt?: Date /** */ - shouldRemoveApplication?: boolean -} + email: string -export interface UserInvite { /** */ firstName: string @@ -8817,98 +8859,580 @@ export interface UserInvite { /** */ listings: IdDTO[] - /** */ - userRoles?: UserRole - /** */ language?: LanguagesEnum /** */ - favoriteListings?: IdDTO[] + mfaEnabled?: boolean /** */ - newEmail?: string + lastLoginAt?: Date /** */ - appUrl?: string + failedLoginAttemptsCount?: number /** */ - email: string + phoneNumberVerified?: boolean /** */ - jurisdictions: IdDTO[] -} + agreedToTermsOfService: boolean -export interface RequestSingleUseCode { /** */ - email: string -} + hitConfirmationURL?: Date -export interface ConfirmationRequest { /** */ - token: string -} + activeAccessToken?: string -export interface UserFavoriteListing { /** */ - id: string + activeRefreshToken?: string /** */ - action: ModificationEnum -} + favoriteListings?: IdDTO[] -export interface UserUpdate { /** */ - id: string + title?: string /** */ - firstName: string + agency?: Agency /** */ - middleName?: string + address?: Address /** */ - lastName: string + phoneType?: string /** */ - dob?: Date + phoneExtension?: string /** */ - phoneNumber?: string + additionalPhoneNumber?: string /** */ - listings: IdDTO[] + additionalPhoneNumberType?: string /** */ - userRoles?: UserRole + additionalPhoneExtension?: string /** */ - language?: LanguagesEnum + userRoles: UserRole /** */ - agreedToTermsOfService: boolean + appUrl?: string /** */ - favoriteListings?: IdDTO[] + jurisdictions: IdDTO[] +} +export interface AdvocateUserCreate { /** */ - email?: string + confirmedAt?: Date /** */ - newEmail?: string + email: string /** */ - password?: string + firstName: string /** */ - currentPassword?: string + middleName?: string /** */ - appUrl?: string + lastName: string /** */ - jurisdictions?: IdDTO[] -} + dob?: Date + + /** */ + listings: IdDTO[] + + /** */ + userRoles?: UserRole + + /** */ + language?: LanguagesEnum + + /** */ + mfaEnabled?: boolean + + /** */ + lastLoginAt?: Date + + /** */ + failedLoginAttemptsCount?: number + + /** */ + phoneNumberVerified?: boolean + + /** */ + agreedToTermsOfService: boolean + + /** */ + hitConfirmationURL?: Date + + /** */ + activeAccessToken?: string + + /** */ + activeRefreshToken?: string + + /** */ + favoriteListings?: IdDTO[] + + /** */ + agency: Agency + + /** */ + address: AddressUpdate + + /** */ + appUrl?: string + + /** */ + jurisdictions?: IdDTO[] +} + +export interface PublicUserUpdate { + /** */ + id: string + + /** */ + confirmedAt?: Date + + /** */ + email: string + + /** */ + firstName: string + + /** */ + middleName?: string + + /** */ + lastName: string + + /** */ + phoneNumber?: string + + /** */ + listings: IdDTO[] + + /** */ + userRoles?: UserRole + + /** */ + language?: LanguagesEnum + + /** */ + mfaEnabled?: boolean + + /** */ + lastLoginAt?: Date + + /** */ + failedLoginAttemptsCount?: number + + /** */ + phoneNumberVerified?: boolean + + /** */ + agreedToTermsOfService: boolean + + /** */ + hitConfirmationURL?: Date + + /** */ + activeAccessToken?: string + + /** */ + activeRefreshToken?: string + + /** */ + favoriteListings?: IdDTO[] + + /** */ + title?: string + + /** */ + agency?: Agency + + /** */ + address?: Address + + /** */ + phoneType?: string + + /** */ + phoneExtension?: string + + /** */ + additionalPhoneNumber?: string + + /** */ + additionalPhoneNumberType?: string + + /** */ + additionalPhoneExtension?: string + + /** */ + dob: Date + + /** */ + newEmail?: string + + /** */ + password?: string + + /** */ + currentPassword?: string + + /** */ + appUrl?: string + + /** */ + jurisdictions?: IdDTO[] +} + +export interface PartnerUserUpdate { + /** */ + id: string + + /** */ + confirmedAt?: Date + + /** */ + email: string + + /** */ + firstName: string + + /** */ + middleName?: string + + /** */ + lastName: string + + /** */ + dob?: Date + + /** */ + phoneNumber?: string + + /** */ + listings: IdDTO[] + + /** */ + language?: LanguagesEnum + + /** */ + mfaEnabled?: boolean + + /** */ + lastLoginAt?: Date + + /** */ + failedLoginAttemptsCount?: number + + /** */ + phoneNumberVerified?: boolean + + /** */ + agreedToTermsOfService: boolean + + /** */ + hitConfirmationURL?: Date + + /** */ + activeAccessToken?: string + + /** */ + activeRefreshToken?: string + + /** */ + favoriteListings?: IdDTO[] + + /** */ + title?: string + + /** */ + agency?: Agency + + /** */ + address?: Address + + /** */ + phoneType?: string + + /** */ + phoneExtension?: string + + /** */ + additionalPhoneNumber?: string + + /** */ + additionalPhoneNumberType?: string + + /** */ + additionalPhoneExtension?: string + + /** */ + userRoles: UserRole + + /** */ + newEmail?: string + + /** */ + password?: string + + /** */ + currentPassword?: string + + /** */ + appUrl?: string + + /** */ + jurisdictions?: IdDTO[] +} + +export interface AdvocateUserUpdate { + /** */ + id: string + + /** */ + confirmedAt?: Date + + /** */ + email: string + + /** */ + firstName: string + + /** */ + middleName?: string + + /** */ + lastName: string + + /** */ + dob?: Date + + /** */ + listings: IdDTO[] + + /** */ + userRoles?: UserRole + + /** */ + language?: LanguagesEnum + + /** */ + mfaEnabled?: boolean + + /** */ + lastLoginAt?: Date + + /** */ + failedLoginAttemptsCount?: number + + /** */ + phoneNumberVerified?: boolean + + /** */ + agreedToTermsOfService: boolean + + /** */ + hitConfirmationURL?: Date + + /** */ + activeAccessToken?: string + + /** */ + activeRefreshToken?: string + + /** */ + favoriteListings?: IdDTO[] + + /** */ + title?: string + + /** */ + phoneType?: string + + /** */ + phoneExtension?: string + + /** */ + additionalPhoneNumber?: string + + /** */ + additionalPhoneNumberType?: string + + /** */ + additionalPhoneExtension?: string + + /** */ + agency: Agency + + /** */ + address: AddressUpdate + + /** */ + phoneNumber: string + + /** */ + newEmail?: string + + /** */ + password?: string + + /** */ + currentPassword?: string + + /** */ + appUrl?: string + + /** */ + jurisdictions?: IdDTO[] +} + +export interface User { + /** */ + id: string + + /** */ + createdAt: Date + + /** */ + updatedAt: Date + + /** */ + passwordUpdatedAt: Date + + /** */ + passwordValidForDays: number + + /** */ + confirmedAt?: Date + + /** */ + email: string + + /** */ + firstName: string + + /** */ + middleName?: string + + /** */ + lastName: string + + /** */ + dob?: Date + + /** */ + phoneNumber?: string + + /** */ + listings: IdDTO[] + + /** */ + userRoles?: UserRole + + /** */ + language?: LanguagesEnum + + /** */ + jurisdictions: Jurisdiction[] + + /** */ + mfaEnabled?: boolean + + /** */ + lastLoginAt?: Date + + /** */ + failedLoginAttemptsCount?: number + + /** */ + phoneNumberVerified?: boolean + + /** */ + agreedToTermsOfService: boolean + + /** */ + hitConfirmationURL?: Date + + /** */ + activeAccessToken?: string + + /** */ + activeRefreshToken?: string + + /** */ + favoriteListings?: IdDTO[] + + /** */ + title?: string + + /** */ + agency?: Agency + + /** */ + address?: Address + + /** */ + phoneType?: string + + /** */ + phoneExtension?: string + + /** */ + additionalPhoneNumber?: string + + /** */ + additionalPhoneNumberType?: string + + /** */ + additionalPhoneExtension?: string +} + +export interface UserFilterParams { + /** */ + isPortalUser?: boolean +} + +export interface PaginatedUser { + /** */ + items: User[] + + /** */ + meta: PaginationMeta +} + +export interface UserDeleteDTO { + /** */ + id: string + + /** */ + shouldRemoveApplication?: boolean +} + +export interface RequestSingleUseCode { + /** */ + email: string +} + +export interface ConfirmationRequest { + /** */ + token: string +} + +export interface UserFavoriteListing { + /** */ + id: string + + /** */ + action: ModificationEnum +} export interface Login { /** */ @@ -9187,23 +9711,6 @@ export interface AgencyCreate { jurisdictions: IdDTO } -export interface Agency { - /** */ - id: string - - /** */ - createdAt: Date - - /** */ - updatedAt: Date - - /** */ - name: string - - /** */ - jurisdictions: IdDTO -} - export interface AgencyUpdate { /** */ id: string diff --git a/sites/partners/__tests__/components/users/FormUserManage.test.tsx b/sites/partners/__tests__/components/users/FormUserManage.test.tsx index 082ca7a20cb..30afde780f8 100644 --- a/sites/partners/__tests__/components/users/FormUserManage.test.tsx +++ b/sites/partners/__tests__/components/users/FormUserManage.test.tsx @@ -62,7 +62,7 @@ describe("", () => { // Watch the invite call to make sure it's called const requestSpy = jest.fn() server.events.on("request:start", (request) => { - if (request.method === "POST" && request.url.href.includes("invite")) { + if (request.method === "POST" && request.url.href.includes("partner")) { requestSpy(request.body) } }) @@ -70,7 +70,7 @@ describe("", () => { rest.get("http://localhost/api/adapter/user", (_req, res, ctx) => { return res(ctx.json(adminUserWithJurisdictions)) }), - rest.post("http://localhost/api/adapter/user/invite", (_req, res, ctx) => { + rest.post("http://localhost/api/adapter/user/partner", (_req, res, ctx) => { return res(ctx.json({ success: true })) }) ) @@ -134,7 +134,7 @@ describe("", () => { // Watch the invite call to make sure it's called const requestSpy = jest.fn() server.events.on("request:start", (request) => { - if (request.method === "POST" && request.url.href.includes("invite")) { + if (request.method === "POST" && request.url.href.includes("partner")) { requestSpy(request.body) } }) @@ -142,7 +142,7 @@ describe("", () => { rest.get("http://localhost/api/adapter/user", (_req, res, ctx) => { return res(ctx.json(adminUserWithJurisdictions)) }), - rest.post("http://localhost/api/adapter/user/invite", (_req, res, ctx) => { + rest.post("http://localhost/api/adapter/user/partner", (_req, res, ctx) => { return res(ctx.json({ success: true })) }) ) @@ -212,7 +212,7 @@ describe("", () => { // Watch the invite call to make sure it's called const requestSpy = jest.fn() server.events.on("request:start", (request) => { - if (request.method === "POST" && request.url.href.includes("invite")) { + if (request.method === "POST" && request.url.href.includes("partner")) { requestSpy(request.body) } }) @@ -220,7 +220,7 @@ describe("", () => { rest.get("http://localhost/api/adapter/user", (_req, res, ctx) => { return res(ctx.json(adminUserWithJurisdictions)) }), - rest.post("http://localhost/api/adapter/user/invite", (_req, res, ctx) => { + rest.post("http://localhost/api/adapter/user/partner", (_req, res, ctx) => { return res(ctx.json({ success: true })) }) ) @@ -305,7 +305,7 @@ describe("", () => { rest.get("http://localhost/api/adapter/user", (_req, res, ctx) => { return res(ctx.json(jurisAdminUserWithJurisdictions)) }), - rest.post("http://localhost/api/adapter/user/invite", (_req, res, ctx) => { + rest.post("http://localhost/api/adapter/user/partner", (_req, res, ctx) => { return res(ctx.json({ success: true })) }) ) @@ -348,7 +348,7 @@ describe("", () => { rest.get("http://localhost/api/adapter/user", (_req, res, ctx) => { return res(ctx.json(adminUserWithJurisdictionsAndOneDisabled)) }), - rest.post("http://localhost/api/adapter/user/invite", (_req, res, ctx) => { + rest.post("http://localhost/api/adapter/user/partner", (_req, res, ctx) => { return res(ctx.json({ success: true })) }) ) @@ -408,7 +408,7 @@ describe("", () => { rest.get("http://localhost/api/adapter/user", (_req, res, ctx) => { return res(ctx.json(adminUserWithJurisdictionsAndAllDisabled)) }), - rest.post("http://localhost/api/adapter/user/invite", (_req, res, ctx) => { + rest.post("http://localhost/api/adapter/user/partner", (_req, res, ctx) => { return res(ctx.json({ success: true })) }) ) @@ -459,7 +459,7 @@ describe("", () => { rest.get("http://localhost/api/adapter/user", (_req, res, ctx) => { return res(ctx.json(adminUserWithJurisdictionsAndAllDisabled)) }), - rest.post("http://localhost/api/adapter/user/invite", (_req, res, ctx) => { + rest.post("http://localhost/api/adapter/user/partner", (_req, res, ctx) => { return res(ctx.json({ success: true })) }) ) @@ -507,7 +507,7 @@ describe("", () => { rest.get("http://localhost/api/adapter/user", (_req, res, ctx) => { return res(ctx.json(adminUserWithJurisdictionsAndOneEnabled)) }), - rest.post("http://localhost/api/adapter/user/invite", (_req, res, ctx) => { + rest.post("http://localhost/api/adapter/user/partner", (_req, res, ctx) => { return res(ctx.json({ success: true })) }) ) @@ -550,7 +550,7 @@ describe("", () => { // Watch the update call to make sure it's called const requestSpy = jest.fn() server.events.on("request:start", (request) => { - if (request.method === "PUT" && request.url.href.includes("user/%7Bid%7D")) { + if (request.method === "PUT" && request.url.href.includes("user")) { requestSpy(request.body) } }) @@ -558,7 +558,7 @@ describe("", () => { rest.get("http://localhost/api/adapter/user", (_req, res, ctx) => { return res(ctx.json(adminUserWithJurisdictions)) }), - rest.put("http://localhost/api/adapter/user/%7Bid%7D", (_req, res, ctx) => { + rest.put("http://localhost/api/adapter/user/partner", (_req, res, ctx) => { return res(ctx.json({ success: true })) }) ) diff --git a/sites/partners/__tests__/pages/users/terms.test.tsx b/sites/partners/__tests__/pages/users/terms.test.tsx index 84ac5effec2..e515506afb1 100644 --- a/sites/partners/__tests__/pages/users/terms.test.tsx +++ b/sites/partners/__tests__/pages/users/terms.test.tsx @@ -26,7 +26,7 @@ beforeEach(() => { rest.post("http://localhost:3100/auth/token", (_req, res, ctx) => { return res(ctx.json("")) }), - rest.put("http://localhost/api/adapter/user/%7Bid%7D", (_req, res, ctx) => { + rest.put("http://localhost/api/adapter/user/public", (_req, res, ctx) => { return res(ctx.json("")) }) ) diff --git a/sites/partners/src/components/users/FormUserManage.tsx b/sites/partners/src/components/users/FormUserManage.tsx index e085f4941e7..fd893102533 100644 --- a/sites/partners/src/components/users/FormUserManage.tsx +++ b/sites/partners/src/components/users/FormUserManage.tsx @@ -246,7 +246,7 @@ const FormUserManage = ({ void sendInvite(() => userService - .invite({ + .createPartner({ body: body, }) .then(() => { @@ -296,7 +296,7 @@ const FormUserManage = ({ void updateUser(() => userService - .update({ + .updatePartner({ body: body, }) .then(() => { diff --git a/sites/partners/src/pages/users/terms.tsx b/sites/partners/src/pages/users/terms.tsx index baa009339cd..1433d2fb545 100644 --- a/sites/partners/src/pages/users/terms.tsx +++ b/sites/partners/src/pages/users/terms.tsx @@ -10,8 +10,8 @@ const TermsPage = () => { const onSubmit = useCallback(async () => { if (!profile) return - await userService?.update({ - body: { ...profile, agreedToTermsOfService: true }, + await userService?.updatePublic({ + body: { ...profile, dob: profile.dob, agreedToTermsOfService: true }, }) loadProfile?.("/") diff --git a/sites/public/__tests__/pages/account/edit.test.tsx b/sites/public/__tests__/pages/account/edit.test.tsx index cfcc1669ae2..38af4633d62 100644 --- a/sites/public/__tests__/pages/account/edit.test.tsx +++ b/sites/public/__tests__/pages/account/edit.test.tsx @@ -7,9 +7,13 @@ import { AuthContext } from "@bloom-housing/shared-helpers" const mockUserService = { retrieve: jest.fn(), - update: jest.fn(), + updatePublic: jest.fn(), + updatePartner: jest.fn(), + updateAdvocate: jest.fn(), delete: jest.fn(), - create: jest.fn(), + createPublic: jest.fn(), + createPartner: jest.fn(), + createAdvocate: jest.fn(), list: jest.fn(), listAsCsv: jest.fn(), forgotPassword: jest.fn(), @@ -63,7 +67,7 @@ describe("", () => { listings: [], jurisdictions: [], } - mockUserService.update.mockResolvedValue(updatedUser) + mockUserService.updatePublic.mockResolvedValue(updatedUser) renderEditPage() @@ -82,14 +86,14 @@ describe("", () => { await userEvent.click(updateButtons[0]) await waitFor(() => { - expect(mockUserService.update).toHaveBeenCalledWith({ body: updatedUser }) + expect(mockUserService.updatePublic).toHaveBeenCalledWith({ body: updatedUser }) }) }) it("should handle name update errors", async () => { // Hide the console.warn statement to not flood the testing logs jest.spyOn(console, "warn").mockImplementation() - mockUserService.update.mockRejectedValue(new Error("Server error")) + mockUserService.updatePublic.mockRejectedValue(new Error("Server error")) renderEditPage() @@ -117,7 +121,7 @@ describe("", () => { listings: [], jurisdictions: [], } - mockUserService.update.mockResolvedValue(updatedUser) + mockUserService.updatePublic.mockResolvedValue(updatedUser) renderEditPage() @@ -140,7 +144,7 @@ describe("", () => { await userEvent.click(updateButtons[1]) await waitFor(() => { - expect(mockUserService.update).toHaveBeenCalledWith({ + expect(mockUserService.updatePublic).toHaveBeenCalledWith({ body: expect.objectContaining({ dob: new Date("1990-05-15"), }), @@ -149,7 +153,7 @@ describe("", () => { }) it("should handle date of birth update errors", async () => { - mockUserService.update.mockRejectedValue(new Error("Server error")) + mockUserService.updatePublic.mockRejectedValue(new Error("Server error")) renderEditPage() @@ -220,7 +224,7 @@ describe("", () => { listings: [], jurisdictions: [], } - mockUserService.update.mockResolvedValue(updatedUser) + mockUserService.updatePublic.mockResolvedValue(updatedUser) renderEditPage() @@ -237,7 +241,7 @@ describe("", () => { await userEvent.click(updateButtons[2]) await waitFor(() => { - expect(mockUserService.update).toHaveBeenCalledWith({ body: updatedUser }) + expect(mockUserService.updatePublic).toHaveBeenCalledWith({ body: updatedUser }) }) }) @@ -265,7 +269,7 @@ describe("", () => { describe("Password form", () => { it("should update password successfully", async () => { const updatedUser = { ...user, listings: [], jurisdictions: [] } - mockUserService.update.mockResolvedValue(updatedUser) + mockUserService.updatePublic.mockResolvedValue(updatedUser) renderEditPage() @@ -287,7 +291,7 @@ describe("", () => { await userEvent.type(confirmPasswordField, "newPassword123!") await userEvent.click(updateButtons[3]) - expect(mockUserService.update).toHaveBeenCalled() + expect(mockUserService.updatePublic).toHaveBeenCalled() }) it("should show error when passwords don't match", async () => { @@ -335,7 +339,7 @@ describe("", () => { }) it("should show error when current password is incorrect", async () => { - mockUserService.update.mockRejectedValue({ + mockUserService.updatePublic.mockRejectedValue({ response: { status: 401 }, }) diff --git a/sites/public/__tests__/pages/create-account.test.tsx b/sites/public/__tests__/pages/create-account.test.tsx index 91fb4701d4c..60d0ac5327d 100644 --- a/sites/public/__tests__/pages/create-account.test.tsx +++ b/sites/public/__tests__/pages/create-account.test.tsx @@ -368,7 +368,7 @@ describe("Create Account Page", () => { jest.spyOn(console, "error").mockImplementation() const { message, value, status } = response server.use( - rest.post("http://localhost/api/adapter/user", (_req, res, ctx) => { + rest.post("http://localhost/api/adapter/user/public", (_req, res, ctx) => { if (message) { return res( ctx.status(status), @@ -418,7 +418,7 @@ describe("Create Account Page", () => { process.env.showPwdless = "TRUE" const { pushMock } = mockNextRouter() server.use( - rest.post("http://localhost/api/adapter/user", (_req, res, ctx) => { + rest.post("http://localhost/api/adapter/user/public", (_req, res, ctx) => { return res(ctx.json(user)) }) ) diff --git a/sites/public/src/pages/account/edit.tsx b/sites/public/src/pages/account/edit.tsx index cb07d28673c..f44bbfe1d01 100644 --- a/sites/public/src/pages/account/edit.tsx +++ b/sites/public/src/pages/account/edit.tsx @@ -110,7 +110,7 @@ const Edit = () => { const { firstName, middleName, lastName } = data setNameAlert(null) try { - const newUser = await userService.update({ + const newUser = await userService.updatePublic({ body: { ...user, firstName, middleName, lastName }, }) setUser(newUser) @@ -128,7 +128,7 @@ const Edit = () => { const { dateOfBirth } = data setDobAlert(null) try { - const newUser = await userService.update({ + const newUser = await userService.updatePublic({ body: { ...user, dob: dayjs( @@ -151,7 +151,7 @@ const Edit = () => { const { email } = data setEmailAlert(null) try { - const newUser = await userService.update({ + const newUser = await userService.updatePublic({ body: { ...user, appUrl: window.location.origin, @@ -188,7 +188,7 @@ const Edit = () => { return } try { - const newUser = await userService.update({ + const newUser = await userService.updatePublic({ body: { ...user, password, currentPassword }, }) setUser(newUser) diff --git a/sites/public/src/pages/create-account.tsx b/sites/public/src/pages/create-account.tsx index 674eac71e4e..df993a6c2d4 100644 --- a/sites/public/src/pages/create-account.tsx +++ b/sites/public/src/pages/create-account.tsx @@ -24,7 +24,7 @@ import SignUpBenefits from "../components/account/SignUpBenefits" import SignUpBenefitsHeadingGroup from "../components/account/SignUpBenefitsHeadingGroup" const CreateAccount = () => { - const { createUser, resendConfirmation } = useContext(AuthContext) + const { createPublicUser, resendConfirmation } = useContext(AuthContext) const [confirmationResent, setConfirmationResent] = useState(false) const signUpCopy = process.env.showMandatedAccounts /* Form Handler */ @@ -55,7 +55,7 @@ const CreateAccount = () => { const { dob, ...rest } = data const listingIdRedirect = process.env.showMandatedAccounts && listingId ? listingId : undefined - await createUser( + await createPublicUser( { ...rest, dob: dayjs(`${dob.birthYear}-${dob.birthMonth}-${dob.birthDay}`),