From f668a8348838ee6aeb85f475608eb16b781946e4 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Wed, 11 Feb 2026 14:41:30 +0100 Subject: [PATCH 01/47] chore: add new Advocate User related fields to user DTO --- api/src/dtos/users/user.dto.ts | 48 ++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/api/src/dtos/users/user.dto.ts b/api/src/dtos/users/user.dto.ts index 21f122a5cb1..eedb539eb9c 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() @@ -97,8 +99,8 @@ export class User extends AbstractDTO { @IsArray({ groups: [ValidationsGroupsEnum.default] }) @ArrayMinSize(1, { groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @ApiProperty({ type: Jurisdiction, isArray: true }) - jurisdictions: Jurisdiction[]; + @ApiPropertyOptional({ type: Jurisdiction, isArray: true }) + jurisdictions?: Jurisdiction[]; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) @@ -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; } From 686424e311d70d08219a505f51f835af7649726c Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Wed, 11 Feb 2026 14:44:59 +0100 Subject: [PATCH 02/47] chore: remove generic user update and create DTOs --- api/src/dtos/users/user-create.dto.ts | 62 ---------------------- api/src/dtos/users/user-update.dto.ts | 74 --------------------------- 2 files changed, 136 deletions(-) delete mode 100644 api/src/dtos/users/user-create.dto.ts delete mode 100644 api/src/dtos/users/user-update.dto.ts diff --git a/api/src/dtos/users/user-create.dto.ts b/api/src/dtos/users/user-create.dto.ts deleted file mode 100644 index e746e30067e..00000000000 --- a/api/src/dtos/users/user-create.dto.ts +++ /dev/null @@ -1,62 +0,0 @@ -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 { 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'; - -export class UserCreate extends OmitType(UserUpdate, [ - 'id', - 'userRoles', - 'password', - 'currentPassword', - 'email', - 'jurisdictions', -]) { - @Expose() - @ApiProperty() - @IsString({ groups: [ValidationsGroupsEnum.default] }) - @Matches(passwordRegex, { - message: 'passwordTooWeak', - groups: [ValidationsGroupsEnum.default], - }) - password: string; - - @Expose() - @IsString({ groups: [ValidationsGroupsEnum.default] }) - @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) - @Match('password', { groups: [ValidationsGroupsEnum.default] }) - @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/user-update.dto.ts b/api/src/dtos/users/user-update.dto.ts deleted file mode 100644 index 5f0ed5b255b..00000000000 --- a/api/src/dtos/users/user-update.dto.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { ApiPropertyOptional, OmitType } from '@nestjs/swagger'; -import { Expose, Type } from 'class-transformer'; -import { - IsArray, - IsEmail, - IsNotEmpty, - IsString, - Matches, - 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, [ - 'createdAt', - 'updatedAt', - 'email', - 'mfaEnabled', - 'passwordUpdatedAt', - 'passwordValidForDays', - 'lastLoginAt', - 'failedLoginAttemptsCount', - 'confirmedAt', - 'lastLoginAt', - 'phoneNumberVerified', - 'hitConfirmationURL', - 'activeAccessToken', - 'activeRefreshToken', - 'jurisdictions', -]) { - @Expose() - @ApiPropertyOptional() - @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) - @EnforceLowerCase() - email?: 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[]; -} From d689c523d17060a7a44d45c8b5d797d99b38c223 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Wed, 11 Feb 2026 14:45:23 +0100 Subject: [PATCH 03/47] chore: add a new public user update and create DTOs --- api/src/dtos/users/public-user-create.dto.ts | 46 ++++++++++++++ api/src/dtos/users/public-user-update.dto.ts | 65 ++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 api/src/dtos/users/public-user-create.dto.ts create mode 100644 api/src/dtos/users/public-user-update.dto.ts diff --git a/api/src/dtos/users/public-user-create.dto.ts b/api/src/dtos/users/public-user-create.dto.ts new file mode 100644 index 00000000000..7f990800142 --- /dev/null +++ b/api/src/dtos/users/public-user-create.dto.ts @@ -0,0 +1,46 @@ +import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger'; +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 { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; + +export class PublicUserCreate extends OmitType(PublicUserUpdate, [ + 'id', + 'newEmail', + 'password', + 'currentPassword', +] 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] }) + @Matches(passwordRegex, { + message: 'passwordTooWeak', + groups: [ValidationsGroupsEnum.default], + }) + password: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @Match('password', { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + passwordConfirmation: string; + + @Expose() + @ApiPropertyOptional() + @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @Match('email', { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() + emailConfirmation?: string; +} 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..ba7c271de28 --- /dev/null +++ b/api/src/dtos/users/public-user-update.dto.ts @@ -0,0 +1,65 @@ +import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger'; +import { Expose, Type } from 'class-transformer'; +import { + 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'; + +export class PublicUserUpdate extends OmitType(User, [ + 'createdAt', + 'updatedAt', + 'dob', + 'passwordUpdatedAt', + 'passwordValidForDays', + 'passwordUpdatedAt', + , +] 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; +} From 87c1b9f6ba87dd0bc0a3464329d7dad817d5a362 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Wed, 11 Feb 2026 14:45:44 +0100 Subject: [PATCH 04/47] chore: add a new partner user update and create DTOs --- api/src/dtos/users/partner-user-create.dto.ts | 45 ++++++++++++++ api/src/dtos/users/partner-user-update.dto.ts | 62 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 api/src/dtos/users/partner-user-create.dto.ts create mode 100644 api/src/dtos/users/partner-user-update.dto.ts diff --git a/api/src/dtos/users/partner-user-create.dto.ts b/api/src/dtos/users/partner-user-create.dto.ts new file mode 100644 index 00000000000..8375eac8e46 --- /dev/null +++ b/api/src/dtos/users/partner-user-create.dto.ts @@ -0,0 +1,45 @@ +import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger'; +import { PartnerUserUpdate } from './partner-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 'src/utilities/password-regex'; +import { Match } from '../../decorators/match-decorator'; +import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; + +export class PartnerUserCreate extends OmitType(PartnerUserUpdate, [ + 'id', + 'newEmail', + 'password', + 'currentPassword', +] 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() + @ApiProperty() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @Matches(passwordRegex, { + message: 'passwordTooWeak', + groups: [ValidationsGroupsEnum.default], + }) + password: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @Match('password', { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + passwordConfirmation: string; + + @Expose() + @ApiPropertyOptional() + @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @Match('email', { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() + emailConfirmation?: string; +} diff --git a/api/src/dtos/users/partner-user-update.dto.ts b/api/src/dtos/users/partner-user-update.dto.ts new file mode 100644 index 00000000000..c7aa96dae44 --- /dev/null +++ b/api/src/dtos/users/partner-user-update.dto.ts @@ -0,0 +1,62 @@ +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 { + IsEmail, + IsNotEmpty, + IsString, + Matches, + MaxLength, + ValidateIf, +} from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { passwordRegex } from '../../utilities/password-regex'; + +export class PartnerUserUpdate extends OmitType(User, [ + 'createdAt', + 'updatedAt', + 'userRoles', + 'passwordUpdatedAt', + 'passwordValidForDays', + 'passwordUpdatedAt', +] 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() + @Type(() => UserRole) + @ApiProperty({ type: UserRole }) + userRoles: UserRole; + + @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; +} From a1297c569099af94f26da786360679bd4f026aae Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Wed, 11 Feb 2026 14:46:01 +0100 Subject: [PATCH 05/47] chore: add a new advocate user update and create DTOs --- .../dtos/users/advocate-user-create.dto.ts | 30 +++++++ .../dtos/users/advocate-user-update.dto.ts | 90 +++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 api/src/dtos/users/advocate-user-create.dto.ts create mode 100644 api/src/dtos/users/advocate-user-update.dto.ts 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..65f815dd1f1 --- /dev/null +++ b/api/src/dtos/users/advocate-user-create.dto.ts @@ -0,0 +1,30 @@ +import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger'; +import { AdvocateUserUpdate } from './advocate-user-update.dto'; +import { IsEmail, IsString, Matches, MaxLength } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { passwordRegex } from 'src/utilities/password-regex'; +import { Match } from '../../decorators/match-decorator'; +import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; +import { Expose } from 'class-transformer'; + +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..52fa6406cc2 --- /dev/null +++ b/api/src/dtos/users/advocate-user-update.dto.ts @@ -0,0 +1,90 @@ +import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger'; +import { Expose, Type } from 'class-transformer'; +import { + 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 { Address } from '../addresses/address.dto'; +import { User } from './user.dto'; +import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; +import { passwordRegex } from '../../utilities/password-regex'; + +export class AdvocateUserUpdate extends OmitType(User, [ + 'createdAt', + 'updatedAt', + 'agency', + 'address', + 'phoneNumber', + 'passwordUpdatedAt', + 'passwordValidForDays', + 'passwordUpdatedAt', +] 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(() => Address) + @ApiProperty({ type: Address }) + address: Address; + + @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; +} From 338bf3481a98787db7b022aae50695c63a96d76e Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Wed, 11 Feb 2026 14:47:51 +0100 Subject: [PATCH 06/47] chore: update user invite DTO to be partner user specific --- ...vite.dto.ts => partner-user-invite.dto.ts} | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) rename api/src/dtos/users/{user-invite.dto.ts => partner-user-invite.dto.ts} (60%) diff --git a/api/src/dtos/users/user-invite.dto.ts b/api/src/dtos/users/partner-user-invite.dto.ts similarity index 60% rename from api/src/dtos/users/user-invite.dto.ts rename to api/src/dtos/users/partner-user-invite.dto.ts index 84f2d3803c6..844c49562fe 100644 --- a/api/src/dtos/users/user-invite.dto.ts +++ b/api/src/dtos/users/partner-user-invite.dto.ts @@ -1,30 +1,21 @@ import { ApiProperty, OmitType } from '@nestjs/swagger'; import { Expose, Type } from 'class-transformer'; -import { - ArrayMinSize, - IsArray, - IsEmail, - ValidateNested, -} from 'class-validator'; -import { UserUpdate } from './user-update.dto'; +import { ArrayMinSize, IsArray, ValidateNested } from 'class-validator'; -import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { IdDTO } from '../shared/id.dto'; +import { PartnerUserUpdate } from './partner-user-update.dto'; -export class UserInvite extends OmitType(UserUpdate, [ +export class PartnerUserInvite extends OmitType(PartnerUserUpdate, [ 'id', 'password', 'currentPassword', - 'email', 'agreedToTermsOfService', 'jurisdictions', ]) { - @Expose() - @ApiProperty() - @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) - @EnforceLowerCase() - email: string; + /* Fields inherited from User: + * - email (inherited as required from User) + **/ @Expose() @Type(() => IdDTO) From 5f08240e93722d0fbbb37ea63cdf26409d188a39 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Wed, 11 Feb 2026 14:52:50 +0100 Subject: [PATCH 07/47] chore: upadte the user controller and service methods to work with new user specific DTOs --- api/src/controllers/user.controller.ts | 106 ++++++-- api/src/services/user.service.ts | 333 +++++++++++++++++++------ 2 files changed, 349 insertions(+), 90 deletions(-) diff --git a/api/src/controllers/user.controller.ts b/api/src/controllers/user.controller.ts index 77d81b0b2e3..edab45ec8ba 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,26 @@ 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'; +import { PartnerUserInvite } from '../dtos/users/partner-user-invite.dto'; @Controller('user') @ApiTags('user') @PermissionTypeDecorator('user') @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) -@ApiExtraModels(IdDTO, EmailAndAppUrl) +@ApiExtraModels( + IdDTO, + EmailAndAppUrl, + PublicUserCreate, + PartnerUserCreate, + AdvocateUserCreate, + PartnerUserInvite, +) @UseGuards(ApiKeyGuard) export class UserController { constructor( @@ -125,21 +136,54 @@ export class UserController { return await this.userService.favoriteListings(userId); } - @Post() + @Post('/public') @ApiOperation({ summary: 'Creates a public only user', operationId: 'create', }) + @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: 'create', + }) @ApiOkResponse({ type: User }) @UseGuards(OptionalAuthGuard, PermissionGuard) - async create( + async createPartnerUser( @Request() req: ExpressRequest, - @Body() dto: UserCreate, + @Body() dto: PartnerUserCreate, + ): Promise { + return await this.userService.createPartnerUser(dto, req); + } + + @Post('/advocate') + @ApiOperation({ + summary: 'Creates a advocate only user', + operationId: 'create', + }) + @ApiOkResponse({ type: User }) + @UseGuards(OptionalAuthGuard, PermissionGuard) + async createAdvocateUser( + @Request() req: ExpressRequest, + @Body() dto: AdvocateUserCreate, @Query() queryParams: UserCreateParams, ): Promise { - return await this.userService.create( + return await this.userService.createAdvocateUser( dto, - false, queryParams.noWelcomeEmail !== true, req, ); @@ -167,10 +211,10 @@ export class UserController { @UseGuards(OptionalAuthGuard) @UseInterceptors(ActivityLogInterceptor) async invite( - @Body() dto: UserInvite, + @Body() dto: PartnerUserInvite, @Request() req: ExpressRequest, ): Promise { - return await this.userService.create(dto, true, undefined, req); + return await this.userService.createPartnerUser(dto, req); } @Post('request-single-use-code') @@ -268,14 +312,48 @@ export class UserController { return await this.userService.deleteAfterInactivity(); } - @Put(':id') + @Put('/public/:id') + @ApiOperation({ summary: 'Update user', operationId: 'update' }) + @ApiOkResponse({ type: User }) + @UseGuards(JwtAuthGuard, PermissionGuard) + @UseInterceptors(ActivityLogInterceptor) + async updatePublicUser( + @Request() req: ExpressRequest, + @Body() dto: PublicUserUpdate, + ): Promise { + const jurisdictionName = req.headers['jurisdictionname'] || ''; + return await this.userService.update( + dto, + mapTo(User, req['user']), + jurisdictionName as string, + ); + } + + @Put('/partner/:id') + @ApiOperation({ summary: 'Update user', operationId: 'update' }) + @ApiOkResponse({ type: User }) + @UseGuards(JwtAuthGuard, PermissionGuard) + @UseInterceptors(ActivityLogInterceptor) + async updatePartnerUser( + @Request() req: ExpressRequest, + @Body() dto: PartnerUserUpdate, + ): Promise { + const jurisdictionName = req.headers['jurisdictionname'] || ''; + return await this.userService.update( + dto, + mapTo(User, req['user']), + jurisdictionName as string, + ); + } + + @Put('/advocate/:id') @ApiOperation({ summary: 'Update user', operationId: 'update' }) @ApiOkResponse({ type: User }) @UseGuards(JwtAuthGuard, PermissionGuard) @UseInterceptors(ActivityLogInterceptor) - async update( + async updateAdvocateUser( @Request() req: ExpressRequest, - @Body() dto: UserUpdate, + @Body() dto: AdvocateUserUpdate, ): Promise { const jurisdictionName = req.headers['jurisdictionname'] || ''; return await this.userService.update( diff --git a/api/src/services/user.service.ts b/api/src/services/user.service.ts index ce2ca28597a..54aa0245d4c 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,7 @@ 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 { PartnerUserInvite } from '../dtos/users/partner-user-invite.dto'; import { EmailService } from './email.service'; import { PermissionService } from './permission.service'; import { permissionActions } from '../enums/permissions/permission-actions-enum'; @@ -53,6 +51,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 'src/dtos/users/public-user-update.dto'; +import { PartnerUserUpdate } from 'src/dtos/users/partner-user-update.dto'; +import { AdvocateUserUpdate } from 'src/dtos/users/advocate-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'; /* this is the service for users @@ -173,7 +177,7 @@ export class UserService { this will update a user or error if no user is found with the Id */ async update( - dto: UserUpdate, + dto: PublicUserUpdate | PartnerUserUpdate | AdvocateUserUpdate, requestingUser: User, jurisdictionName: string, ): Promise { @@ -254,7 +258,7 @@ export class UserService { } // only update userRoles if something has changed - if (dto.userRoles && storedUser.userRoles) { + if (dto?.userRoles && storedUser.userRoles) { if ( this.isUserRoleChangeAllowed(requestingUser, dto.userRoles) && !( @@ -314,7 +318,7 @@ export class UserService { passwordUpdatedAt: passwordUpdatedAt ?? undefined, confirmationToken: confirmationToken ?? undefined, firstName: dto.firstName, - middleName: dto.middleName, + middleName: dto?.middleName ?? storedUser.middleName, lastName: dto.lastName, dob: dto.dob, phoneNumber: dto.phoneNumber, @@ -529,36 +533,15 @@ 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 + | PartnerUserInvite, + ): Promise { const existingUser = await this.prisma.userAccounts.findUnique({ include: views.full, where: { @@ -628,16 +611,39 @@ 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: + const recreatedUser = await this.handleExistingUser(dto); + if (recreatedUser !== null) { + return recreatedUser; + } + + const passwordHash = await passwordToHash(dto.password); + + const jurisdictions: | { jurisdictions: Prisma.JurisdictionsCreateNestedManyWithoutUser_accountsInput; } @@ -651,16 +657,6 @@ export class UserService { } : {}; - if (!forPartners && jurisdictionName) { - jurisdictions = { - jurisdictions: { - connect: { - name: jurisdictionName, - }, - }, - }; - } - let newUser = await this.prisma.userAccounts.create({ data: { passwordHash: passwordHash, @@ -669,18 +665,7 @@ 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, listings: dto.listings ? { connect: dto.listings.map((listing) => ({ @@ -705,8 +690,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, @@ -727,23 +711,220 @@ 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 | PartnerUserInvite, + 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'), + ); + + const jurisdictions: + | { + jurisdictions: Prisma.JurisdictionsCreateNestedManyWithoutUser_accountsInput; + } + | Record = dto.jurisdictions + ? { + jurisdictions: { + connect: dto.jurisdictions.map((juris) => ({ + id: juris.id, + })), + }, + } + : {}; + + let newUser = await this.prisma.userAccounts.create({ + data: { + passwordHash: passwordHash, + email: dto.email, + firstName: dto.firstName, + lastName: dto.lastName, + mfaEnabled: true, + ...jurisdictions, + userRoles: { + create: { + isPartner: true, + }, + }, + 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'), + ); + + const jurisdictions: + | { + jurisdictions: Prisma.JurisdictionsCreateNestedManyWithoutUser_accountsInput; + } + | Record = dto.jurisdictions + ? { + jurisdictions: { + connect: dto.jurisdictions.map((juris) => ({ + id: juris.id, + })), + }, + } + : {}; + + 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, + 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); } From 0b6993d535ea5efe169c538a0f247039105bd3b1 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Wed, 11 Feb 2026 16:38:26 +0100 Subject: [PATCH 08/47] chore: remove unused imports --- api/src/dtos/users/advocate-user-create.dto.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/api/src/dtos/users/advocate-user-create.dto.ts b/api/src/dtos/users/advocate-user-create.dto.ts index 65f815dd1f1..85caa431b2f 100644 --- a/api/src/dtos/users/advocate-user-create.dto.ts +++ b/api/src/dtos/users/advocate-user-create.dto.ts @@ -1,12 +1,5 @@ -import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger'; +import { OmitType } from '@nestjs/swagger'; import { AdvocateUserUpdate } from './advocate-user-update.dto'; -import { IsEmail, IsString, Matches, MaxLength } from 'class-validator'; -import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -import { passwordRegex } from 'src/utilities/password-regex'; -import { Match } from '../../decorators/match-decorator'; -import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; -import { Expose } from 'class-transformer'; - export class AdvocateUserCreate extends OmitType(AdvocateUserUpdate, [ 'id', 'newEmail', From 29752a80dda2ed73b6f6849d328ab853f9f16811 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Wed, 11 Feb 2026 16:38:43 +0100 Subject: [PATCH 09/47] fix: uupdate import path to relative format --- api/src/dtos/users/partner-user-create.dto.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/dtos/users/partner-user-create.dto.ts b/api/src/dtos/users/partner-user-create.dto.ts index 8375eac8e46..15fa06d478e 100644 --- a/api/src/dtos/users/partner-user-create.dto.ts +++ b/api/src/dtos/users/partner-user-create.dto.ts @@ -3,7 +3,7 @@ import { PartnerUserUpdate } from './partner-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 'src/utilities/password-regex'; +import { passwordRegex } from '../../utilities/password-regex'; import { Match } from '../../decorators/match-decorator'; import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; From 3353d85616ae1697ea5cc2ce7a9048fc438b973d Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Wed, 11 Feb 2026 16:56:59 +0100 Subject: [PATCH 10/47] fix: update the partner creation pipeline to use the userRoles field --- api/src/services/user.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/services/user.service.ts b/api/src/services/user.service.ts index 54aa0245d4c..56008bcab90 100644 --- a/api/src/services/user.service.ts +++ b/api/src/services/user.service.ts @@ -775,7 +775,7 @@ export class UserService { ...jurisdictions, userRoles: { create: { - isPartner: true, + ...dto.userRoles, }, }, listings: dto.listings From 01691399ae1bf1fa6774689550e122dfe6b8d012 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Wed, 11 Feb 2026 16:57:30 +0100 Subject: [PATCH 11/47] chore: add missing update DTOs as additional models to user controller --- api/src/controllers/user.controller.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/src/controllers/user.controller.ts b/api/src/controllers/user.controller.ts index edab45ec8ba..a42f7f2f8f7 100644 --- a/api/src/controllers/user.controller.ts +++ b/api/src/controllers/user.controller.ts @@ -68,6 +68,9 @@ import { PartnerUserInvite } from '../dtos/users/partner-user-invite.dto'; PublicUserCreate, PartnerUserCreate, AdvocateUserCreate, + PublicUserUpdate, + PartnerUserUpdate, + AdvocateUserUpdate, PartnerUserInvite, ) @UseGuards(ApiKeyGuard) From 4b6350c5df9ff2c84b999506043c0d5b2a3da964 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Thu, 12 Feb 2026 14:39:32 +0100 Subject: [PATCH 12/47] fix: update the advocate user update dto to use AdressUpdate type --- api/src/dtos/users/advocate-user-update.dto.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/src/dtos/users/advocate-user-update.dto.ts b/api/src/dtos/users/advocate-user-update.dto.ts index 52fa6406cc2..c41fa1e4e1a 100644 --- a/api/src/dtos/users/advocate-user-update.dto.ts +++ b/api/src/dtos/users/advocate-user-update.dto.ts @@ -12,10 +12,10 @@ import { } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import Agency from '../agency/agency.dto'; -import { Address } from '../addresses/address.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'; export class AdvocateUserUpdate extends OmitType(User, [ 'createdAt', @@ -47,9 +47,9 @@ export class AdvocateUserUpdate extends OmitType(User, [ @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => Address) - @ApiProperty({ type: Address }) - address: Address; + @Type(() => AddressUpdate) + @ApiProperty({ type: AddressUpdate }) + address: AddressUpdate; @Expose() @IsPhoneNumber('US', { groups: [ValidationsGroupsEnum.default] }) From 7e60d6f4e15d0243295555f92dbd42dceed33d5c Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Thu, 12 Feb 2026 15:08:09 +0100 Subject: [PATCH 13/47] chore: add missing advocate user related fields to user update pipeline --- api/src/services/user.service.ts | 46 +++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/api/src/services/user.service.ts b/api/src/services/user.service.ts index 56008bcab90..e1323392866 100644 --- a/api/src/services/user.service.ts +++ b/api/src/services/user.service.ts @@ -57,6 +57,7 @@ import { AdvocateUserUpdate } from 'src/dtos/users/advocate-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 { AgencyService } from './agency.service'; /* this is the service for users @@ -102,6 +103,7 @@ export class UserService { private readonly configService: ConfigService, private permissionService: PermissionService, private applicationService: ApplicationService, + private agencyService: AgencyService, @Inject(Logger) private logger = new Logger(UserService.name), private cronJobService: CronJobService, @@ -279,6 +281,28 @@ export class UserService { } } + //handle address for advocated users + let newAddressId: string | undefined; + if (dto?.address) { + if (dto.address?.id) { + await this.prisma.address.update({ + data: { + ...dto.address, + }, + where: { + id: dto.address.id, + }, + }); + } else { + const newAddress = await this.prisma.address.create({ + data: { + ...dto.address, + }, + }); + newAddressId = newAddress.id; + } + } + // disconnect existing connected listings/jurisdictions if (storedUser.listings?.length) { await this.prisma.userAccounts.update({ @@ -318,11 +342,31 @@ export class UserService { passwordUpdatedAt: passwordUpdatedAt ?? undefined, confirmationToken: confirmationToken ?? undefined, firstName: dto.firstName, - middleName: dto?.middleName ?? storedUser.middleName, + middleName: dto.middleName, 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, + agency: dto?.agency?.id + ? { + connect: { + id: dto.agency.id, + }, + } + : undefined, + address: newAddressId + ? { + connect: { + id: newAddressId, + }, + } + : undefined, listings: dto.listings ? { connect: dto.listings.map((listing) => ({ id: listing.id })), From 1f84ab3f8efb37936ef292a73ce9ed829748d115 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Thu, 12 Feb 2026 16:02:24 +0100 Subject: [PATCH 14/47] fix: fix imports to relative paths --- api/src/services/user.service.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/api/src/services/user.service.ts b/api/src/services/user.service.ts index e1323392866..043c44c0370 100644 --- a/api/src/services/user.service.ts +++ b/api/src/services/user.service.ts @@ -51,12 +51,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 'src/dtos/users/public-user-update.dto'; -import { PartnerUserUpdate } from 'src/dtos/users/partner-user-update.dto'; -import { AdvocateUserUpdate } from 'src/dtos/users/advocate-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 { 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 { AgencyService } from './agency.service'; /* From b21c3249407e0584a07a6e557f24ae4cf31ed9c3 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Mon, 16 Feb 2026 16:50:04 +0100 Subject: [PATCH 15/47] fix: generate new swagger types --- shared-helpers/src/types/backend-swagger.ts | 917 ++++++++++++++++---- 1 file changed, 772 insertions(+), 145 deletions(-) diff --git a/shared-helpers/src/types/backend-swagger.ts b/shared-helpers/src/types/backend-swagger.ts index 7abc0ac4c6c..9ac82a9c672 100644 --- a/shared-helpers/src/types/backend-swagger.ts +++ b/shared-helpers/src/types/backend-swagger.ts @@ -1913,31 +1913,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 */ @@ -2027,13 +2002,85 @@ export class UserService { axios(configs, resolve, reject) }) } + /** + * Creates a public only user + */ + create( + 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) + }) + } + /** + * Creates a partner only user + */ + create1( + 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) + }) + } + /** + * Creates a advocate only user + */ + create2( + params: { + /** */ + noWelcomeEmail?: boolean + /** requestBody */ + body?: AdvocateUserCreate + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + 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) + }) + } /** * Invite partner user */ invite( params: { /** requestBody */ - body?: UserInvite + body?: PartnerUserInvite } = {} as any, options: IRequestOptions = {} ): Promise { @@ -2219,12 +2266,56 @@ export class UserService { update( params: { /** requestBody */ - body?: UserUpdate + body?: PublicUserUpdate } = {} as any, options: IRequestOptions = {} ): Promise { return new Promise((resolve, reject) => { - let url = basePath + "/user/{id}" + let url = basePath + "/user/public/{id}" + + const configs: IRequestConfig = getConfigs("put", "application/json", url, options) + + let data = params.body + + configs.data = data + + axios(configs, resolve, reject) + }) + } + /** + * Update user + */ + update1( + params: { + /** requestBody */ + body?: PartnerUserUpdate + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/user/partner/{id}" + + const configs: IRequestConfig = getConfigs("put", "application/json", url, options) + + let data = params.body + + configs.data = data + + axios(configs, resolve, reject) + }) + } + /** + * Update user + */ + update2( + params: { + /** requestBody */ + body?: AdvocateUserUpdate + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/user/advocate/{id}" const configs: IRequestConfig = getConfigs("put", "application/json", url, options) @@ -8592,7 +8683,7 @@ export interface UserRole { isSupportAdmin?: boolean } -export interface User { +export interface Agency { /** */ id: string @@ -8603,11 +8694,13 @@ export interface User { updatedAt: Date /** */ - passwordUpdatedAt: Date + name: string /** */ - passwordValidForDays: number + jurisdictions: IdDTO +} +export interface PublicUserCreate { /** */ confirmedAt?: Date @@ -8623,9 +8716,6 @@ export interface User { /** */ lastName: string - /** */ - dob?: Date - /** */ phoneNumber?: string @@ -8639,7 +8729,7 @@ export interface User { language?: LanguagesEnum /** */ - jurisdictions: Jurisdiction[] + jurisdictions?: Jurisdiction[] /** */ mfaEnabled?: boolean @@ -8667,51 +8757,33 @@ export interface User { /** */ favoriteListings?: IdDTO[] -} - -export interface UserFilterParams { - /** */ - isPortalUser?: boolean -} - -export interface PaginatedUser { - /** */ - items: User[] - - /** */ - meta: PaginationMeta -} - -export interface UserCreate { - /** */ - firstName: string /** */ - middleName?: string + title?: string /** */ - lastName: string + agency?: Agency /** */ - dob?: Date + address?: Address /** */ - phoneNumber?: string + phoneType?: string /** */ - listings: IdDTO[] + phoneExtension?: string /** */ - language?: LanguagesEnum + additionalPhoneNumber?: string /** */ - agreedToTermsOfService: boolean + additionalPhoneNumberType?: string /** */ - favoriteListings?: IdDTO[] + additionalPhoneExtension?: string /** */ - newEmail?: string + dob: Date /** */ appUrl?: string @@ -8722,25 +8794,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 @@ -8759,137 +8823,717 @@ export interface UserInvite { /** */ listings: IdDTO[] - /** */ - userRoles?: UserRole - /** */ language?: LanguagesEnum /** */ - favoriteListings?: IdDTO[] + jurisdictions?: Jurisdiction[] /** */ - newEmail?: string + mfaEnabled?: boolean /** */ - appUrl?: string + lastLoginAt?: Date /** */ - email: string + failedLoginAttemptsCount?: number /** */ - jurisdictions: IdDTO[] -} + phoneNumberVerified?: boolean -export interface RequestSingleUseCode { /** */ - email: string -} + agreedToTermsOfService: boolean -export interface ConfirmationRequest { /** */ - token: string -} + hitConfirmationURL?: Date -export interface UserFavoriteListing { /** */ - id: string + activeAccessToken?: string /** */ - action: ModificationEnum -} + activeRefreshToken?: string -export interface UserUpdate { /** */ - id: string + favoriteListings?: IdDTO[] /** */ - firstName: string + title?: string /** */ - middleName?: string + agency?: Agency /** */ - lastName: string + address?: Address /** */ - dob?: Date + phoneType?: string /** */ - phoneNumber?: string + phoneExtension?: string /** */ - listings: IdDTO[] + additionalPhoneNumber?: string /** */ - userRoles?: UserRole + additionalPhoneNumberType?: string /** */ - language?: LanguagesEnum + additionalPhoneExtension?: string /** */ - agreedToTermsOfService: boolean + userRoles: UserRole /** */ - favoriteListings?: IdDTO[] + appUrl?: string /** */ - email?: string + password: string /** */ - newEmail?: string + passwordConfirmation: string /** */ - password?: string + emailConfirmation?: string +} +export interface AdvocateUserCreate { /** */ - currentPassword?: string + confirmedAt?: Date /** */ - appUrl?: string + email: string /** */ - jurisdictions?: IdDTO[] -} + firstName: string -export interface Login { /** */ - email: string + middleName?: string /** */ - password: string + lastName: string /** */ - mfaCode?: string + dob?: Date /** */ - mfaType?: MfaType + listings: IdDTO[] /** */ - reCaptchaToken?: string -} + userRoles?: UserRole -export interface LoginViaSingleUseCode { /** */ - email: string + language?: LanguagesEnum /** */ - singleUseCode: string -} + jurisdictions?: Jurisdiction[] -export interface RequestMfaCode { /** */ - email: string + mfaEnabled?: boolean /** */ - password: string + lastLoginAt?: Date /** */ - mfaType: MfaType + failedLoginAttemptsCount?: number /** */ - phoneNumber?: string -} + phoneNumberVerified?: boolean + + /** */ + agreedToTermsOfService: boolean + + /** */ + hitConfirmationURL?: Date + + /** */ + activeAccessToken?: string + + /** */ + activeRefreshToken?: string + + /** */ + favoriteListings?: IdDTO[] + + /** */ + agency: Agency + + /** */ + address: AddressUpdate + + /** */ + appUrl?: string +} + +export interface PublicUserUpdate { + /** */ + id: string + + /** */ + confirmedAt?: Date + + /** */ + email: string + + /** */ + firstName: string + + /** */ + middleName?: string + + /** */ + lastName: string + + /** */ + 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 + + /** */ + dob: Date + + /** */ + newEmail?: string + + /** */ + password?: string + + /** */ + currentPassword?: string + + /** */ + appUrl?: string +} + +export interface PartnerUserUpdate { + /** */ + id: string + + /** */ + confirmedAt?: Date + + /** */ + email: string + + /** */ + firstName: string + + /** */ + middleName?: string + + /** */ + lastName: string + + /** */ + dob?: Date + + /** */ + phoneNumber?: string + + /** */ + listings: IdDTO[] + + /** */ + 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 + + /** */ + userRoles: UserRole + + /** */ + newEmail?: string + + /** */ + password?: string + + /** */ + currentPassword?: string + + /** */ + appUrl?: string +} + +export interface AdvocateUserUpdate { + /** */ + id: string + + /** */ + confirmedAt?: Date + + /** */ + email: string + + /** */ + firstName: string + + /** */ + middleName?: string + + /** */ + lastName: string + + /** */ + dob?: Date + + /** */ + 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 + + /** */ + phoneType?: string + + /** */ + phoneExtension?: string + + /** */ + additionalPhoneNumber?: string + + /** */ + additionalPhoneNumberType?: string + + /** */ + additionalPhoneExtension?: string + + /** */ + agency: Agency + + /** */ + address: AddressUpdate + + /** */ + phoneNumber: string + + /** */ + newEmail?: string + + /** */ + password?: string + + /** */ + currentPassword?: string + + /** */ + appUrl?: string +} + +export interface PartnerUserInvite { + /** */ + 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 + + /** */ + 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 + + /** */ + 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 { + /** */ + email: string + + /** */ + password: string + + /** */ + mfaCode?: string + + /** */ + mfaType?: MfaType + + /** */ + reCaptchaToken?: string +} + +export interface LoginViaSingleUseCode { + /** */ + email: string + + /** */ + singleUseCode: string +} + +export interface RequestMfaCode { + /** */ + email: string + + /** */ + password: string + + /** */ + mfaType: MfaType + + /** */ + phoneNumber?: string +} export interface RequestMfaCodeResponse { /** */ @@ -9155,23 +9799,6 @@ export interface AgencyCreate { jurisdictions: IdDTO } -export interface Agency { - /** */ - id: string - - /** */ - createdAt: Date - - /** */ - updatedAt: Date - - /** */ - name: string - - /** */ - jurisdictions: IdDTO -} - export interface AgencyUpdate { /** */ id: string From 1cc096702effb42b5dab5bdf9fc795c0b914b919 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Mon, 16 Feb 2026 16:51:08 +0100 Subject: [PATCH 16/47] fix: update api layer tests to use new endpoints and DTO --- .../integration/permission-tests/helpers.ts | 8 +-- .../permission-as-admin.e2e-spec.ts | 8 +-- ...n-as-juris-admin-correct-juris.e2e-spec.ts | 8 +-- ...ion-as-juris-admin-wrong-juris.e2e-spec.ts | 8 +-- ...ited-juris-admin-correct-juris.e2e-spec.ts | 8 +-- ...imited-juris-admin-wrong-juris.e2e-spec.ts | 8 +-- .../permission-as-no-user.e2e-spec.ts | 8 +-- ...ion-as-partner-correct-listing.e2e-spec.ts | 8 +-- ...ssion-as-partner-wrong-listing.e2e-spec.ts | 8 +-- .../permission-as-public.e2e-spec.ts | 12 ++-- .../permission-as-support-admin.e2e-spec.ts | 6 +- api/test/integration/user.e2e-spec.ts | 22 +++---- api/test/unit/services/user.service.spec.ts | 58 ++++++++----------- 13 files changed, 81 insertions(+), 89 deletions(-) diff --git a/api/test/integration/permission-tests/helpers.ts b/api/test/integration/permission-tests/helpers.ts index 557bd9c8da2..42bcab74f16 100644 --- a/api/test/integration/permission-tests/helpers.ts +++ b/api/test/integration/permission-tests/helpers.ts @@ -47,10 +47,10 @@ 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 { UserInvite } from '../../../src/dtos/users/partner-user-invite.dto'; import { AlternateContactRelationship } from '../../../src/enums/applications/alternate-contact-relationship-enum'; import { HouseholdMemberRelationship } from '../../../src/enums/applications/household-member-relationship-enum'; +import { PublicUserCreate } from 'src/dtos/users/public-user-create.dto'; export const generateJurisdiction = async ( prisma: PrismaService, @@ -234,14 +234,14 @@ export const buildMultiselectQuestionUpdateMock = ( export const buildUserCreateMock = ( jurisId: string, email: string, -): UserCreate => { +): PublicUserCreate => { return { firstName: 'Public User firstName', lastName: 'Public User lastName', password: 'Abcdef12345!', email, jurisdictions: [{ id: jurisId }], - } as unknown as UserCreate; + } as PublicUserCreate; }; export const buildUserInviteMock = ( 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 c07150effa9..60b894d61ba 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 @@ -42,7 +42,6 @@ import { UnitAccessibilityPriorityTypeUpdate } from '../../../src/dtos/unit-acce 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'; @@ -70,6 +69,7 @@ import { } from './helpers'; import { ApplicationFlaggedSetService } from '../../../src/services/application-flagged-set.service'; import { featureFlagFactory } from '../../../prisma/seed-helpers/feature-flag-factory'; +import { PublicUserUpdate } from 'src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), @@ -952,13 +952,13 @@ describe('Testing Permissioning of endpoints as Admin User', () => { }); await request(app.getHttpServer()) - .put(`/user/${userA.id}`) + .put(`/user/public/${userA.id}`) .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); @@ -1079,7 +1079,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) 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 73e313d4278..742eea58fb9 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 @@ -41,7 +41,6 @@ import { UnitAccessibilityPriorityTypeUpdate } from '../../../src/dtos/unit-acce 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'; @@ -68,6 +67,7 @@ import { createSimpleListing, } 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(), @@ -955,14 +955,14 @@ describe('Testing Permissioning of endpoints as Jurisdictional Admin in the corr }); await request(app.getHttpServer()) - .put(`/user/${userA.id}`) + .put(`/user/public/${userA.id}`) .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); }); @@ -1071,7 +1071,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) 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 902603a1b7e..c19f725f429 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 @@ -41,7 +41,6 @@ import { UnitAccessibilityPriorityTypeUpdate } from '../../../src/dtos/unit-acce 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'; @@ -67,6 +66,7 @@ import { createSimpleListing, } 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(), @@ -916,13 +916,13 @@ describe('Testing Permissioning of endpoints as Jurisdictional Admin in the wron }); await request(app.getHttpServer()) - .put(`/user/${userA.id}`) + .put(`/user/public/${userA.id}`) .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); }); @@ -1028,7 +1028,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) 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 7223e0d6053..df3363d530a 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 @@ -41,7 +41,6 @@ import { UnitAccessibilityPriorityTypeUpdate } from '../../../src/dtos/unit-acce 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'; @@ -68,6 +67,7 @@ import { createSimpleListing, } 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(), @@ -900,14 +900,14 @@ describe('Testing Permissioning of endpoints as Limited Jurisdictional Admin in }); await request(app.getHttpServer()) - .put(`/user/${userA.id}`) + .put(`/user/public/${userA.id}`) .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); }); @@ -1010,7 +1010,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) 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 7388ce5774b..ff6fc8a96c2 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 @@ -41,7 +41,6 @@ import { UnitAccessibilityPriorityTypeUpdate } from '../../../src/dtos/unit-acce 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'; @@ -67,6 +66,7 @@ import { createSimpleListing, } 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(), @@ -916,13 +916,13 @@ describe('Testing Permissioning of endpoints as Limited Jurisdictional Admin in }); await request(app.getHttpServer()) - .put(`/user/${userA.id}`) + .put(`/user/public/${userA.id}`) .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); }); @@ -1024,7 +1024,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) 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 a636aade076..1d97ca8e93c 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 @@ -41,7 +41,6 @@ import { UnitAccessibilityPriorityTypeUpdate } from '../../../src/dtos/unit-acce 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'; @@ -68,6 +67,7 @@ import { createListing, createComplexApplication, } from './helpers'; +import { PublicUserUpdate } from 'src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), @@ -852,13 +852,13 @@ describe('Testing Permissioning of endpoints as logged out user', () => { }); await request(app.getHttpServer()) - .put(`/user/${userA.id}`) + .put(`/user/public/${userA.id}`) .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); }); @@ -959,7 +959,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'), 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 0d1f58dbc30..db8fae691a6 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 @@ -42,7 +42,6 @@ import { UnitAccessibilityPriorityTypeUpdate } from '../../../src/dtos/unit-acce 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'; @@ -68,6 +67,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(), @@ -953,13 +953,13 @@ describe('Testing Permissioning of endpoints as partner with correct listing', ( }); await request(app.getHttpServer()) - .put(`/user/${userA.id}`) + .put(`/user/public/${userA.id}`) .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); }); @@ -1065,7 +1065,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) 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 a6c2b6ca90a..60af6c2216b 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 @@ -42,7 +42,6 @@ import { UnitAccessibilityPriorityTypeUpdate } from '../../../src/dtos/unit-acce 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'; @@ -67,6 +66,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(), @@ -925,13 +925,13 @@ describe('Testing Permissioning of endpoints as partner with wrong listing', () }); await request(app.getHttpServer()) - .put(`/user/${userA.id}`) + .put(`/user/public/${userA.id}`) .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); }); @@ -1037,7 +1037,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) 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 bf76b6d997a..f24fe8e9cf3 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 @@ -42,7 +42,6 @@ import { UnitAccessibilityPriorityTypeUpdate } from '../../../src/dtos/unit-acce 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'; @@ -69,6 +68,7 @@ import { createListing, createComplexApplication, } from './helpers'; +import { PublicUserUpdate } from 'src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), @@ -904,26 +904,26 @@ describe('Testing Permissioning of endpoints as public user', () => { }); await request(app.getHttpServer()) - .put(`/user/${userA.id}`) + .put(`/user/public/${userA.id}`) .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/${storedUserId}`) .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); }); @@ -1029,7 +1029,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) 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 2e10dbf2de9..5b4bc578e73 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 @@ -42,7 +42,6 @@ import { UnitAccessibilityPriorityTypeUpdate } from '../../../src/dtos/unit-acce 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'; @@ -72,6 +71,7 @@ import { } from './helpers'; import { ApplicationFlaggedSetService } from '../../../src/services/application-flagged-set.service'; import { featureFlagFactory } from '../../../prisma/seed-helpers/feature-flag-factory'; +import { PublicUserUpdate } from 'src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), @@ -949,14 +949,14 @@ describe('Testing Permissioning of endpoints as Support Admin User', () => { }); await request(app.getHttpServer()) - .put(`/user/${userA.id}`) + .put(`/user/public/${userA.id}`) .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); }); diff --git a/api/test/integration/user.e2e-spec.ts b/api/test/integration/user.e2e-spec.ts index f82c7f39735..a2d3006a18d 100644 --- a/api/test/integration/user.e2e-spec.ts +++ b/api/test/integration/user.e2e-spec.ts @@ -9,22 +9,22 @@ 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 { UserInvite } from '../../src/dtos/users/partner-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'; describe('User Controller Tests', () => { let app: INestApplication; @@ -197,13 +197,13 @@ describe('User Controller Tests', () => { }); const res = await request(app.getHttpServer()) - .put(`/user/${userA.id}`) + .put(`/user/public/${userA.id}`) .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); @@ -218,13 +218,13 @@ describe('User Controller Tests', () => { }); const randomId = randomUUID(); const res = await request(app.getHttpServer()) - .put(`/user/${randomId}`) + .put(`/user/public/${randomId}`) .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); @@ -735,15 +735,15 @@ 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!', email: 'publicUser@email.com', jurisdictions: [{ id: juris.id }], - } as UserCreate) + } as PublicUserCreate) .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 ce1cd28f1b7..b0e1fac1033 100644 --- a/api/test/unit/services/user.service.spec.ts +++ b/api/test/unit/services/user.service.spec.ts @@ -24,6 +24,7 @@ import { SendGridService } from '../../../src/services/sendgrid.service'; import { TranslationService } from '../../../src/services/translation.service'; import { UserService } from '../../../src/services/user.service'; import { passwordToHash } from '../../../src/utilities/password-helpers'; +import { PublicUserUpdate } from 'src/dtos/users/public-user-update.dto'; describe('Testing user service', () => { let service: UserService; @@ -1328,9 +1329,9 @@ describe('Testing user service', () => { id, firstName: 'first name', lastName: 'last name', - jurisdictions: [{ id: jurisId }], + jurisdictions: [{ id: jurisId } as any], agreedToTermsOfService: true, - }, + } as PublicUserUpdate, { id: 'requestingUser id', userRoles: { isAdmin: true }, @@ -1409,11 +1410,11 @@ describe('Testing user service', () => { id, firstName: 'first name', lastName: 'last name', - jurisdictions: [{ id: jurisId }], + jurisdictions: [{ id: jurisId } as any], password: 'new password', currentPassword: 'current password', agreedToTermsOfService: true, - }, + } as PublicUserUpdate, { id: 'requestingUser id', userRoles: { isAdmin: true }, @@ -1496,10 +1497,10 @@ 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 }, @@ -1558,11 +1559,11 @@ 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 }, @@ -1620,11 +1621,11 @@ describe('Testing user service', () => { id, firstName: 'first name', lastName: 'last name', - jurisdictions: [{ id: jurisId }], + jurisdictions: [{ id: jurisId } as any], newEmail: 'new@email.com', appUrl: 'https://www.example.com', agreedToTermsOfService: true, - }, + } as PublicUserUpdate, { id: 'requestingUser id', userRoles: { isAdmin: true }, @@ -1707,10 +1708,10 @@ 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 }, @@ -1812,9 +1813,9 @@ 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 }, @@ -1857,7 +1858,7 @@ describe('Testing user service', () => { id, }); emailService.invitePartnerUser = jest.fn(); - await service.create( + await service.createPartnerUser( { firstName: 'Partner User firstName', lastName: 'Partner User lastName', @@ -1868,9 +1869,6 @@ describe('Testing user service', () => { isAdmin: true, }, }, - true, - undefined, - { headers: { jurisdictionname: 'juris 1' }, user: { @@ -1937,7 +1935,7 @@ describe('Testing user service', () => { prisma.userAccounts.update = jest.fn().mockResolvedValue({ id, }); - await service.create( + await service.createPartnerUser( { firstName: 'Partner User firstName', lastName: 'Partner User lastName', @@ -1949,8 +1947,6 @@ describe('Testing user service', () => { }, listings: [{ id: 'listing id' }], }, - true, - undefined, { headers: { jurisdictionname: 'juris 1' }, user: { @@ -2032,7 +2028,7 @@ 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', @@ -2044,8 +2040,6 @@ describe('Testing user service', () => { }, listings: [{ id: 'listing id' }], }, - true, - undefined, { headers: { jurisdictionname: 'juris 1' }, user: { @@ -2106,16 +2100,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: { @@ -2142,19 +2138,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() }], }, }, }); From f0003f26642bde5165a4748c5b9d32723943a10f Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Mon, 16 Feb 2026 17:00:48 +0100 Subject: [PATCH 17/47] fix: remove unusued service --- api/src/services/user.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/api/src/services/user.service.ts b/api/src/services/user.service.ts index 043c44c0370..6901918c1b1 100644 --- a/api/src/services/user.service.ts +++ b/api/src/services/user.service.ts @@ -103,7 +103,6 @@ export class UserService { private readonly configService: ConfigService, private permissionService: PermissionService, private applicationService: ApplicationService, - private agencyService: AgencyService, @Inject(Logger) private logger = new Logger(UserService.name), private cronJobService: CronJobService, From b01047d0ee8f4c85ef42341f124836a9c42620a5 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Tue, 17 Feb 2026 12:10:26 +0100 Subject: [PATCH 18/47] chore: update the jurisdiction field for user creation and updates --- api/src/dtos/users/advocate-user-update.dto.ts | 9 +++++++++ api/src/dtos/users/partner-user-update.dto.ts | 9 +++++++++ api/src/dtos/users/public-user-update.dto.ts | 10 +++++++++- api/src/dtos/users/user.dto.ts | 4 ++-- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/api/src/dtos/users/advocate-user-update.dto.ts b/api/src/dtos/users/advocate-user-update.dto.ts index c41fa1e4e1a..7b0ee51cc7e 100644 --- a/api/src/dtos/users/advocate-user-update.dto.ts +++ b/api/src/dtos/users/advocate-user-update.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger'; import { Expose, Type } from 'class-transformer'; import { + IsArray, IsEmail, IsNotEmpty, IsPhoneNumber, @@ -16,6 +17,7 @@ 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', @@ -26,6 +28,7 @@ export class AdvocateUserUpdate extends OmitType(User, [ 'passwordUpdatedAt', 'passwordValidForDays', 'passwordUpdatedAt', + 'jurisdictions', ] as const) { /* Fields inherited from BaseUser: * - firstName (inherited as required from BaseUser) @@ -87,4 +90,10 @@ export class AdvocateUserUpdate extends OmitType(User, [ @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/partner-user-update.dto.ts b/api/src/dtos/users/partner-user-update.dto.ts index c7aa96dae44..6097448241e 100644 --- a/api/src/dtos/users/partner-user-update.dto.ts +++ b/api/src/dtos/users/partner-user-update.dto.ts @@ -4,6 +4,7 @@ import { Expose, Type } from 'class-transformer'; import { UserRole } from './user-role.dto'; import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; import { + IsArray, IsEmail, IsNotEmpty, IsString, @@ -13,6 +14,7 @@ import { } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { passwordRegex } from '../../utilities/password-regex'; +import { IdDTO } from '../shared/id.dto'; export class PartnerUserUpdate extends OmitType(User, [ 'createdAt', @@ -21,6 +23,7 @@ export class PartnerUserUpdate extends OmitType(User, [ 'passwordUpdatedAt', 'passwordValidForDays', 'passwordUpdatedAt', + 'jurisdictions', ] as const) { /* Fields inherited from User: * - firstName (inherited as required from User) @@ -59,4 +62,10 @@ export class PartnerUserUpdate extends OmitType(User, [ @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/public-user-update.dto.ts b/api/src/dtos/users/public-user-update.dto.ts index ba7c271de28..aa4f7ac18e1 100644 --- a/api/src/dtos/users/public-user-update.dto.ts +++ b/api/src/dtos/users/public-user-update.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger'; import { Expose, Type } from 'class-transformer'; import { + IsArray, IsDate, IsEmail, IsNotEmpty, @@ -13,6 +14,7 @@ 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', @@ -21,7 +23,7 @@ export class PublicUserUpdate extends OmitType(User, [ 'passwordUpdatedAt', 'passwordValidForDays', 'passwordUpdatedAt', - , + 'jurisdictions', ] as const) { /* Fields inherited from BaseUser: * - firstName (inherited as required from BaseUser) @@ -62,4 +64,10 @@ export class PublicUserUpdate extends OmitType(User, [ @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 eedb539eb9c..0e50fd4f847 100644 --- a/api/src/dtos/users/user.dto.ts +++ b/api/src/dtos/users/user.dto.ts @@ -99,8 +99,8 @@ export class User extends AbstractDTO { @IsArray({ groups: [ValidationsGroupsEnum.default] }) @ArrayMinSize(1, { groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @ApiPropertyOptional({ type: Jurisdiction, isArray: true }) - jurisdictions?: Jurisdiction[]; + @ApiProperty({ type: Jurisdiction, isArray: true }) + jurisdictions: Jurisdiction[]; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) From b0e13ca9abedd90c0c42c28297ec8b4bbcd0264f Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Tue, 17 Feb 2026 12:21:36 +0100 Subject: [PATCH 19/47] fix: fix failing user controller integration tests --- api/test/integration/user.e2e-spec.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/api/test/integration/user.e2e-spec.ts b/api/test/integration/user.e2e-spec.ts index a2d3006a18d..16236b6533b 100644 --- a/api/test/integration/user.e2e-spec.ts +++ b/api/test/integration/user.e2e-spec.ts @@ -17,7 +17,6 @@ import { jurisdictionFactory } from '../../prisma/seed-helpers/jurisdiction-fact 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/partner-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'; @@ -25,6 +24,7 @@ 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 { PartnerUserInvite } from 'src/dtos/users/partner-user-invite.dto'; describe('User Controller Tests', () => { let app: INestApplication; @@ -741,13 +741,16 @@ describe('User Controller Tests', () => { 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 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 }), ]); @@ -786,7 +789,7 @@ describe('User Controller Tests', () => { userRoles: { isAdmin: true, }, - } as UserInvite) + } as PartnerUserInvite) .set('Cookie', cookies) .expect(201); From a75e3e91b70f8eda406607e7ee4d453f59d46c8e Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Tue, 17 Feb 2026 14:06:02 +0100 Subject: [PATCH 20/47] chore: gupdate update sergice function to accept entire request object as argument --- api/src/controllers/user.controller.ts | 21 +++------------------ api/src/services/user.service.ts | 5 +++-- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/api/src/controllers/user.controller.ts b/api/src/controllers/user.controller.ts index a42f7f2f8f7..acd11646cc8 100644 --- a/api/src/controllers/user.controller.ts +++ b/api/src/controllers/user.controller.ts @@ -324,12 +324,7 @@ export class UserController { @Request() req: ExpressRequest, @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, mapTo(User, req['user']), req); } @Put('/partner/:id') @@ -341,12 +336,7 @@ export class UserController { @Request() req: ExpressRequest, @Body() dto: PartnerUserUpdate, ): 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, mapTo(User, req['user']), req); } @Put('/advocate/:id') @@ -358,12 +348,7 @@ export class UserController { @Request() req: ExpressRequest, @Body() dto: AdvocateUserUpdate, ): 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, mapTo(User, req['user']), req); } @Get(`:id`) diff --git a/api/src/services/user.service.ts b/api/src/services/user.service.ts index 6901918c1b1..cd9ecb81117 100644 --- a/api/src/services/user.service.ts +++ b/api/src/services/user.service.ts @@ -57,7 +57,6 @@ 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 { AgencyService } from './agency.service'; /* this is the service for users @@ -180,8 +179,10 @@ export class UserService { async update( dto: PublicUserUpdate | PartnerUserUpdate | AdvocateUserUpdate, requestingUser: User, - jurisdictionName: string, + req: Request, ): Promise { + const jurisdictionName = (req.headers['jurisdictionname'] as string) || ''; + const storedUser = await this.findUserOrError( { userId: dto.id }, UserViews.full, From b3382caef57dd2fbe89fd7e7e0f2d9647192b3cf Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Tue, 17 Feb 2026 14:07:06 +0100 Subject: [PATCH 21/47] chore: update and re-generate swagger API types --- api/src/controllers/user.controller.ts | 18 +- shared-helpers/src/types/backend-swagger.ts | 176 ++++++++++---------- 2 files changed, 97 insertions(+), 97 deletions(-) diff --git a/api/src/controllers/user.controller.ts b/api/src/controllers/user.controller.ts index acd11646cc8..7fc4a844b1b 100644 --- a/api/src/controllers/user.controller.ts +++ b/api/src/controllers/user.controller.ts @@ -142,7 +142,7 @@ export class UserController { @Post('/public') @ApiOperation({ summary: 'Creates a public only user', - operationId: 'create', + operationId: 'createPublic', }) @UseGuards(OptionalAuthGuard, PermissionGuard) @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) @@ -162,7 +162,7 @@ export class UserController { @Post('/partner') @ApiOperation({ summary: 'Creates a partner only user', - operationId: 'create', + operationId: 'createPartner', }) @ApiOkResponse({ type: User }) @UseGuards(OptionalAuthGuard, PermissionGuard) @@ -176,7 +176,7 @@ export class UserController { @Post('/advocate') @ApiOperation({ summary: 'Creates a advocate only user', - operationId: 'create', + operationId: 'createAdvocate', }) @ApiOkResponse({ type: User }) @UseGuards(OptionalAuthGuard, PermissionGuard) @@ -315,8 +315,8 @@ export class UserController { return await this.userService.deleteAfterInactivity(); } - @Put('/public/:id') - @ApiOperation({ summary: 'Update user', operationId: 'update' }) + @Put('/public') + @ApiOperation({ summary: 'Update user', operationId: 'updatePublic' }) @ApiOkResponse({ type: User }) @UseGuards(JwtAuthGuard, PermissionGuard) @UseInterceptors(ActivityLogInterceptor) @@ -327,8 +327,8 @@ export class UserController { return await this.userService.update(dto, mapTo(User, req['user']), req); } - @Put('/partner/:id') - @ApiOperation({ summary: 'Update user', operationId: 'update' }) + @Put('/partner') + @ApiOperation({ summary: 'Update user', operationId: 'updatePartner' }) @ApiOkResponse({ type: User }) @UseGuards(JwtAuthGuard, PermissionGuard) @UseInterceptors(ActivityLogInterceptor) @@ -339,8 +339,8 @@ export class UserController { return await this.userService.update(dto, mapTo(User, req['user']), req); } - @Put('/advocate/:id') - @ApiOperation({ summary: 'Update user', operationId: 'update' }) + @Put('/advocate') + @ApiOperation({ summary: 'Update user', operationId: 'updateAdvocate' }) @ApiOkResponse({ type: User }) @UseGuards(JwtAuthGuard, PermissionGuard) @UseInterceptors(ActivityLogInterceptor) diff --git a/shared-helpers/src/types/backend-swagger.ts b/shared-helpers/src/types/backend-swagger.ts index 42cf70ef6d0..5eb63afbcac 100644 --- a/shared-helpers/src/types/backend-swagger.ts +++ b/shared-helpers/src/types/backend-swagger.ts @@ -1882,7 +1882,7 @@ export class UserService { /** * Creates a public only user */ - create( + createPublic( params: { /** */ noWelcomeEmail?: boolean @@ -1904,10 +1904,32 @@ export class UserService { 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 */ - create1( + createPartner( params: { /** requestBody */ body?: PartnerUserCreate @@ -1926,10 +1948,32 @@ export class UserService { 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 */ - create2( + createAdvocate( params: { /** */ noWelcomeEmail?: boolean @@ -1951,6 +1995,28 @@ export class UserService { 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 + + configs.data = data + + axios(configs, resolve, reject) + }) + } /** * Invite partner user */ @@ -2137,72 +2203,6 @@ export class UserService { axios(configs, resolve, reject) }) } - /** - * Update user - */ - update( - params: { - /** requestBody */ - body?: PublicUserUpdate - } = {} as any, - options: IRequestOptions = {} - ): Promise { - return new Promise((resolve, reject) => { - let url = basePath + "/user/public/{id}" - - const configs: IRequestConfig = getConfigs("put", "application/json", url, options) - - let data = params.body - - configs.data = data - - axios(configs, resolve, reject) - }) - } - /** - * Update user - */ - update1( - params: { - /** requestBody */ - body?: PartnerUserUpdate - } = {} as any, - options: IRequestOptions = {} - ): Promise { - return new Promise((resolve, reject) => { - let url = basePath + "/user/partner/{id}" - - const configs: IRequestConfig = getConfigs("put", "application/json", url, options) - - let data = params.body - - configs.data = data - - axios(configs, resolve, reject) - }) - } - /** - * Update user - */ - update2( - params: { - /** requestBody */ - body?: AdvocateUserUpdate - } = {} as any, - options: IRequestOptions = {} - ): Promise { - return new Promise((resolve, reject) => { - let url = basePath + "/user/advocate/{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 */ @@ -8704,9 +8704,6 @@ export interface PublicUserCreate { /** */ language?: LanguagesEnum - /** */ - jurisdictions?: Jurisdiction[] - /** */ mfaEnabled?: boolean @@ -8764,6 +8761,9 @@ export interface PublicUserCreate { /** */ appUrl?: string + /** */ + jurisdictions?: IdDTO[] + /** */ password: string @@ -8802,9 +8802,6 @@ export interface PartnerUserCreate { /** */ language?: LanguagesEnum - /** */ - jurisdictions?: Jurisdiction[] - /** */ mfaEnabled?: boolean @@ -8862,6 +8859,9 @@ export interface PartnerUserCreate { /** */ appUrl?: string + /** */ + jurisdictions?: IdDTO[] + /** */ password: string @@ -8900,9 +8900,6 @@ export interface AdvocateUserCreate { /** */ language?: LanguagesEnum - /** */ - jurisdictions?: Jurisdiction[] - /** */ mfaEnabled?: boolean @@ -8938,6 +8935,9 @@ export interface AdvocateUserCreate { /** */ appUrl?: string + + /** */ + jurisdictions?: IdDTO[] } export interface PublicUserUpdate { @@ -8971,9 +8971,6 @@ export interface PublicUserUpdate { /** */ language?: LanguagesEnum - /** */ - jurisdictions?: Jurisdiction[] - /** */ mfaEnabled?: boolean @@ -9039,6 +9036,9 @@ export interface PublicUserUpdate { /** */ appUrl?: string + + /** */ + jurisdictions?: IdDTO[] } export interface PartnerUserUpdate { @@ -9072,9 +9072,6 @@ export interface PartnerUserUpdate { /** */ language?: LanguagesEnum - /** */ - jurisdictions?: Jurisdiction[] - /** */ mfaEnabled?: boolean @@ -9140,6 +9137,9 @@ export interface PartnerUserUpdate { /** */ appUrl?: string + + /** */ + jurisdictions?: IdDTO[] } export interface AdvocateUserUpdate { @@ -9173,9 +9173,6 @@ export interface AdvocateUserUpdate { /** */ language?: LanguagesEnum - /** */ - jurisdictions?: Jurisdiction[] - /** */ mfaEnabled?: boolean @@ -9241,6 +9238,9 @@ export interface AdvocateUserUpdate { /** */ appUrl?: string + + /** */ + jurisdictions?: IdDTO[] } export interface PartnerUserInvite { @@ -9379,7 +9379,7 @@ export interface User { language?: LanguagesEnum /** */ - jurisdictions?: Jurisdiction[] + jurisdictions: Jurisdiction[] /** */ mfaEnabled?: boolean From ef59f4f235dd301b99e794d23824bd9fde0b6754 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Tue, 17 Feb 2026 14:07:35 +0100 Subject: [PATCH 22/47] chore: update userService usage to new creation and update methods --- shared-helpers/src/auth/AuthContext.ts | 45 +++++++++++++++++-- .../src/components/users/FormUserManage.tsx | 2 +- sites/public/src/pages/account/edit.tsx | 8 ++-- sites/public/src/pages/create-account.tsx | 4 +- 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/shared-helpers/src/auth/AuthContext.ts b/shared-helpers/src/auth/AuthContext.ts index b8ac11ee797..11aa18c3dc4 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,18 @@ 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, + listingIdRedirect?: string + ) => Promise resendConfirmation: (email: string, listingIdRedirect?: string) => Promise initialStateLoaded?: boolean loading?: boolean @@ -335,11 +348,35 @@ 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, listingIdRedirect) => { + dispatch(startLoading()) + const appUrl = getListingRedirectUrl(listingIdRedirect) + try { + const response = await userService?.createPartner({ + body: { ...user, appUrl }, + }) + 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/sites/partners/src/components/users/FormUserManage.tsx b/sites/partners/src/components/users/FormUserManage.tsx index e085f4941e7..cef8598d2ba 100644 --- a/sites/partners/src/components/users/FormUserManage.tsx +++ b/sites/partners/src/components/users/FormUserManage.tsx @@ -296,7 +296,7 @@ const FormUserManage = ({ void updateUser(() => userService - .update({ + .updatePartner({ body: body, }) .then(() => { 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}`), From c334f53d0c3812b6843ffffdaf09aa1e93b29aad Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Tue, 17 Feb 2026 14:17:40 +0100 Subject: [PATCH 23/47] chore: remove the user from the update service function arguments --- api/src/controllers/user.controller.ts | 6 +++--- api/src/services/user.service.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/src/controllers/user.controller.ts b/api/src/controllers/user.controller.ts index 7fc4a844b1b..d446c4ae6fd 100644 --- a/api/src/controllers/user.controller.ts +++ b/api/src/controllers/user.controller.ts @@ -324,7 +324,7 @@ export class UserController { @Request() req: ExpressRequest, @Body() dto: PublicUserUpdate, ): Promise { - return await this.userService.update(dto, mapTo(User, req['user']), req); + return await this.userService.update(dto, req); } @Put('/partner') @@ -336,7 +336,7 @@ export class UserController { @Request() req: ExpressRequest, @Body() dto: PartnerUserUpdate, ): Promise { - return await this.userService.update(dto, mapTo(User, req['user']), req); + return await this.userService.update(dto, req); } @Put('/advocate') @@ -348,7 +348,7 @@ export class UserController { @Request() req: ExpressRequest, @Body() dto: AdvocateUserUpdate, ): Promise { - return await this.userService.update(dto, mapTo(User, req['user']), req); + return await this.userService.update(dto, req); } @Get(`:id`) diff --git a/api/src/services/user.service.ts b/api/src/services/user.service.ts index cd9ecb81117..60332c98dcf 100644 --- a/api/src/services/user.service.ts +++ b/api/src/services/user.service.ts @@ -178,10 +178,10 @@ export class UserService { */ async update( dto: PublicUserUpdate | PartnerUserUpdate | AdvocateUserUpdate, - requestingUser: User, req: Request, ): Promise { const jurisdictionName = (req.headers['jurisdictionname'] as string) || ''; + const requestingUser = mapTo(User, req['user']); const storedUser = await this.findUserOrError( { userId: dto.id }, From 443909b2b9034be90b69c982d474ff296d86d758 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Tue, 17 Feb 2026 14:17:53 +0100 Subject: [PATCH 24/47] fix: update failing user service tests --- api/test/unit/services/user.service.spec.ts | 70 ++++++++++++--------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/api/test/unit/services/user.service.spec.ts b/api/test/unit/services/user.service.spec.ts index b0e1fac1033..884fff096b7 100644 --- a/api/test/unit/services/user.service.spec.ts +++ b/api/test/unit/services/user.service.spec.ts @@ -1333,10 +1333,12 @@ describe('Testing user service', () => { 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, ); expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ include: { @@ -1416,10 +1418,12 @@ describe('Testing user service', () => { 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, ); expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ include: { @@ -1502,10 +1506,12 @@ describe('Testing user service', () => { 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({ @@ -1565,10 +1571,12 @@ describe('Testing user service', () => { 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`, @@ -1627,10 +1635,12 @@ describe('Testing user service', () => { 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, ); expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ include: { @@ -1713,10 +1723,12 @@ describe('Testing user service', () => { 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: { @@ -1817,10 +1829,12 @@ describe('Testing user service', () => { 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({ From 6a9b83664ae1a1edc6d408dd791c2719a96097fa Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Tue, 17 Feb 2026 16:05:52 +0100 Subject: [PATCH 25/47] fix: fix failing tests --- .../permission-tests/permission-as-admin.e2e-spec.ts | 2 +- ...permission-as-juris-admin-correct-juris.e2e-spec.ts | 2 +- .../permission-as-juris-admin-wrong-juris.e2e-spec.ts | 2 +- ...on-as-limited-juris-admin-correct-juris.e2e-spec.ts | 2 +- ...sion-as-limited-juris-admin-wrong-juris.e2e-spec.ts | 2 +- .../permission-tests/permission-as-no-user.e2e-spec.ts | 2 +- .../permission-as-partner-correct-listing.e2e-spec.ts | 2 +- .../permission-as-partner-wrong-listing.e2e-spec.ts | 2 +- .../permission-tests/permission-as-public.e2e-spec.ts | 4 ++-- .../permission-as-support-admin.e2e-spec.ts | 2 +- api/test/integration/user.e2e-spec.ts | 10 +++++----- .../__tests__/components/users/FormUserManage.test.tsx | 2 +- sites/partners/__tests__/pages/users/terms.test.tsx | 2 +- sites/partners/src/pages/users/terms.tsx | 4 ++-- sites/public/__tests__/pages/account/edit.test.tsx | 4 +++- 15 files changed, 23 insertions(+), 21 deletions(-) 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 200db99d0ce..0926dbaadd5 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 @@ -873,7 +873,7 @@ describe('Testing Permissioning of endpoints as Admin User', () => { }); await request(app.getHttpServer()) - .put(`/user/public/${userA.id}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, 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 b372ad0453c..9b19c8be63e 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 @@ -879,7 +879,7 @@ describe('Testing Permissioning of endpoints as Jurisdictional Admin in the corr }); await request(app.getHttpServer()) - .put(`/user/public/${userA.id}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, 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 d68f5aa5a1c..799a41844dc 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 @@ -840,7 +840,7 @@ describe('Testing Permissioning of endpoints as Jurisdictional Admin in the wron }); await request(app.getHttpServer()) - .put(`/user/public/${userA.id}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, 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 305ad0c152e..225d3ab0719 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 @@ -824,7 +824,7 @@ describe('Testing Permissioning of endpoints as Limited Jurisdictional Admin in }); await request(app.getHttpServer()) - .put(`/user/public/${userA.id}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, 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 7c8f5c8f81d..694769f03cd 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 @@ -840,7 +840,7 @@ describe('Testing Permissioning of endpoints as Limited Jurisdictional Admin in }); await request(app.getHttpServer()) - .put(`/user/public/${userA.id}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, 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 63eb21407ee..f50f720c4f2 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 @@ -782,7 +782,7 @@ describe('Testing Permissioning of endpoints as logged out user', () => { }); await request(app.getHttpServer()) - .put(`/user/public/${userA.id}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, 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 601d3d04caa..44533412570 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 @@ -883,7 +883,7 @@ describe('Testing Permissioning of endpoints as partner with correct listing', ( }); await request(app.getHttpServer()) - .put(`/user/public/${userA.id}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, 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 62621ea6720..9c7d453acf9 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 @@ -849,7 +849,7 @@ describe('Testing Permissioning of endpoints as partner with wrong listing', () }); await request(app.getHttpServer()) - .put(`/user/public/${userA.id}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, 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 0d0956d88be..d01dce43261 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 @@ -834,7 +834,7 @@ describe('Testing Permissioning of endpoints as public user', () => { }); await request(app.getHttpServer()) - .put(`/user/public/${userA.id}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, @@ -847,7 +847,7 @@ describe('Testing Permissioning of endpoints as public user', () => { it('should succeed for update endpoint targeting self', async () => { await request(app.getHttpServer()) - .put(`/user/public/${storedUserId}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: storedUserId, 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 29dc894b58a..ab5f2f9e868 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 @@ -871,7 +871,7 @@ describe('Testing Permissioning of endpoints as Support Admin User', () => { }); await request(app.getHttpServer()) - .put(`/user/public/${userA.id}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, diff --git a/api/test/integration/user.e2e-spec.ts b/api/test/integration/user.e2e-spec.ts index 16236b6533b..0005215e525 100644 --- a/api/test/integration/user.e2e-spec.ts +++ b/api/test/integration/user.e2e-spec.ts @@ -197,7 +197,7 @@ describe('User Controller Tests', () => { }); const res = await request(app.getHttpServer()) - .put(`/user/public/${userA.id}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: userA.id, @@ -218,7 +218,7 @@ describe('User Controller Tests', () => { }); const randomId = randomUUID(); const res = await request(app.getHttpServer()) - .put(`/user/public/${randomId}`) + .put(`/user/public`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ id: randomId, @@ -241,7 +241,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, @@ -258,7 +258,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, @@ -272,7 +272,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, diff --git a/sites/partners/__tests__/components/users/FormUserManage.test.tsx b/sites/partners/__tests__/components/users/FormUserManage.test.tsx index 082ca7a20cb..43f3a00ca8c 100644 --- a/sites/partners/__tests__/components/users/FormUserManage.test.tsx +++ b/sites/partners/__tests__/components/users/FormUserManage.test.tsx @@ -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", (_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..9711a8e67f2 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", (_req, res, ctx) => { return res(ctx.json("")) }) ) 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..b3142dc5b4e 100644 --- a/sites/public/__tests__/pages/account/edit.test.tsx +++ b/sites/public/__tests__/pages/account/edit.test.tsx @@ -9,7 +9,9 @@ const mockUserService = { retrieve: jest.fn(), update: 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(), From 6cf036404ce0f0dfc3fab46e3e18f4fa1564b8fd Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Tue, 17 Feb 2026 16:31:23 +0100 Subject: [PATCH 26/47] fix: fix public page integration tests --- .../__tests__/pages/account/edit.test.tsx | 26 ++++++++++--------- .../__tests__/pages/create-account.test.tsx | 4 +-- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/sites/public/__tests__/pages/account/edit.test.tsx b/sites/public/__tests__/pages/account/edit.test.tsx index b3142dc5b4e..38af4633d62 100644 --- a/sites/public/__tests__/pages/account/edit.test.tsx +++ b/sites/public/__tests__/pages/account/edit.test.tsx @@ -7,7 +7,9 @@ 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(), createPublic: jest.fn(), createPartner: jest.fn(), @@ -65,7 +67,7 @@ describe("", () => { listings: [], jurisdictions: [], } - mockUserService.update.mockResolvedValue(updatedUser) + mockUserService.updatePublic.mockResolvedValue(updatedUser) renderEditPage() @@ -84,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() @@ -119,7 +121,7 @@ describe("", () => { listings: [], jurisdictions: [], } - mockUserService.update.mockResolvedValue(updatedUser) + mockUserService.updatePublic.mockResolvedValue(updatedUser) renderEditPage() @@ -142,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"), }), @@ -151,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() @@ -222,7 +224,7 @@ describe("", () => { listings: [], jurisdictions: [], } - mockUserService.update.mockResolvedValue(updatedUser) + mockUserService.updatePublic.mockResolvedValue(updatedUser) renderEditPage() @@ -239,7 +241,7 @@ describe("", () => { await userEvent.click(updateButtons[2]) await waitFor(() => { - expect(mockUserService.update).toHaveBeenCalledWith({ body: updatedUser }) + expect(mockUserService.updatePublic).toHaveBeenCalledWith({ body: updatedUser }) }) }) @@ -267,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() @@ -289,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 () => { @@ -337,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)) }) ) From 2531b3d85fff4925d773c18ece5a9ad43f1348b0 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Tue, 17 Feb 2026 16:50:46 +0100 Subject: [PATCH 27/47] fix: fix partner sites integration tests --- .../__tests__/components/users/FormUserManage.test.tsx | 4 ++-- sites/partners/__tests__/pages/users/terms.test.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sites/partners/__tests__/components/users/FormUserManage.test.tsx b/sites/partners/__tests__/components/users/FormUserManage.test.tsx index 43f3a00ca8c..90d5ede5273 100644 --- a/sites/partners/__tests__/components/users/FormUserManage.test.tsx +++ b/sites/partners/__tests__/components/users/FormUserManage.test.tsx @@ -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", (_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 9711a8e67f2..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", (_req, res, ctx) => { + rest.put("http://localhost/api/adapter/user/public", (_req, res, ctx) => { return res(ctx.json("")) }) ) From 1be95919d64efae0e6d337ff5cda5eb0a3acf0d2 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Tue, 17 Feb 2026 17:35:22 +0100 Subject: [PATCH 28/47] chore: update partner creation endpoints guards --- api/src/controllers/user.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/controllers/user.controller.ts b/api/src/controllers/user.controller.ts index d446c4ae6fd..cae8fc1690f 100644 --- a/api/src/controllers/user.controller.ts +++ b/api/src/controllers/user.controller.ts @@ -165,7 +165,7 @@ export class UserController { operationId: 'createPartner', }) @ApiOkResponse({ type: User }) - @UseGuards(OptionalAuthGuard, PermissionGuard) + @UseGuards(PermissionGuard, JwtAuthGuard) async createPartnerUser( @Request() req: ExpressRequest, @Body() dto: PartnerUserCreate, @@ -211,7 +211,7 @@ export class UserController { @Post('/invite') @ApiOperation({ summary: 'Invite partner user', operationId: 'invite' }) @ApiOkResponse({ type: User }) - @UseGuards(OptionalAuthGuard) + @UseGuards(PermissionGuard, JwtAuthGuard) @UseInterceptors(ActivityLogInterceptor) async invite( @Body() dto: PartnerUserInvite, From 50f1d4f621224ff052d984f592e6c69d2511a500 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Tue, 17 Feb 2026 18:07:26 +0100 Subject: [PATCH 29/47] chore: remove the partner user invite DTO and endpoint --- api/src/controllers/user.controller.ts | 14 -- api/src/dtos/users/partner-user-create.dto.ts | 39 ++---- api/src/dtos/users/partner-user-invite.dto.ts | 27 ---- api/src/services/user.service.ts | 12 +- .../integration/permission-tests/helpers.ts | 12 +- .../permission-as-admin.e2e-spec.ts | 2 +- ...n-as-juris-admin-correct-juris.e2e-spec.ts | 2 +- ...ion-as-juris-admin-wrong-juris.e2e-spec.ts | 2 +- ...ited-juris-admin-correct-juris.e2e-spec.ts | 2 +- ...imited-juris-admin-wrong-juris.e2e-spec.ts | 2 +- .../permission-as-no-user.e2e-spec.ts | 2 +- ...ion-as-partner-correct-listing.e2e-spec.ts | 2 +- ...ssion-as-partner-wrong-listing.e2e-spec.ts | 2 +- .../permission-as-public.e2e-spec.ts | 2 +- .../permission-as-support-admin.e2e-spec.ts | 2 +- api/test/integration/user.e2e-spec.ts | 10 +- shared-helpers/src/types/backend-swagger.ts | 122 +----------------- .../components/users/FormUserManage.test.tsx | 16 +-- .../src/components/users/FormUserManage.tsx | 2 +- 19 files changed, 45 insertions(+), 229 deletions(-) delete mode 100644 api/src/dtos/users/partner-user-invite.dto.ts diff --git a/api/src/controllers/user.controller.ts b/api/src/controllers/user.controller.ts index cae8fc1690f..f0abcf5de82 100644 --- a/api/src/controllers/user.controller.ts +++ b/api/src/controllers/user.controller.ts @@ -56,7 +56,6 @@ 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'; -import { PartnerUserInvite } from '../dtos/users/partner-user-invite.dto'; @Controller('user') @ApiTags('user') @@ -71,7 +70,6 @@ import { PartnerUserInvite } from '../dtos/users/partner-user-invite.dto'; PublicUserUpdate, PartnerUserUpdate, AdvocateUserUpdate, - PartnerUserInvite, ) @UseGuards(ApiKeyGuard) export class UserController { @@ -208,18 +206,6 @@ export class UserController { ); } - @Post('/invite') - @ApiOperation({ summary: 'Invite partner user', operationId: 'invite' }) - @ApiOkResponse({ type: User }) - @UseGuards(PermissionGuard, JwtAuthGuard) - @UseInterceptors(ActivityLogInterceptor) - async invite( - @Body() dto: PartnerUserInvite, - @Request() req: ExpressRequest, - ): Promise { - return await this.userService.createPartnerUser(dto, req); - } - @Post('request-single-use-code') @ApiOperation({ summary: 'Request single use code', diff --git a/api/src/dtos/users/partner-user-create.dto.ts b/api/src/dtos/users/partner-user-create.dto.ts index 15fa06d478e..c781066ebee 100644 --- a/api/src/dtos/users/partner-user-create.dto.ts +++ b/api/src/dtos/users/partner-user-create.dto.ts @@ -1,17 +1,16 @@ -import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger'; +import { ApiProperty, OmitType } from '@nestjs/swagger'; import { PartnerUserUpdate } from './partner-user-update.dto'; -import { Expose } from 'class-transformer'; -import { IsEmail, IsString, Matches, MaxLength } from 'class-validator'; +import { Expose, Type } from 'class-transformer'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -import { passwordRegex } from '../../utilities/password-regex'; -import { Match } from '../../decorators/match-decorator'; -import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; +import { IdDTO } from '../shared/id.dto'; +import { ArrayMinSize, IsArray, ValidateNested } from 'class-validator'; export class PartnerUserCreate extends OmitType(PartnerUserUpdate, [ 'id', 'newEmail', 'password', 'currentPassword', + 'jurisdictions', ] as const) { /* Fields inherited from PartnerUserUpdate: * - firstName (inherited as required from PartnerUserUpdate) @@ -19,27 +18,11 @@ export class PartnerUserCreate extends OmitType(PartnerUserUpdate, [ * - email (inherited as required from PartnerUserUpdate) * - userRoles (inherited as required from PartnerUserUpdate) **/ - - @Expose() - @ApiProperty() - @IsString({ groups: [ValidationsGroupsEnum.default] }) - @Matches(passwordRegex, { - message: 'passwordTooWeak', - groups: [ValidationsGroupsEnum.default], - }) - password: string; - - @Expose() - @IsString({ groups: [ValidationsGroupsEnum.default] }) - @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) - @Match('password', { groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() - passwordConfirmation: string; - @Expose() - @ApiPropertyOptional() - @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) - @Match('email', { groups: [ValidationsGroupsEnum.default] }) - @EnforceLowerCase() - emailConfirmation?: string; + @Type(() => IdDTO) + @IsArray({ groups: [ValidationsGroupsEnum.default] }) + @ArrayMinSize(1, { groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @ApiProperty({ type: IdDTO, isArray: true }) + jurisdictions: IdDTO[]; } diff --git a/api/src/dtos/users/partner-user-invite.dto.ts b/api/src/dtos/users/partner-user-invite.dto.ts deleted file mode 100644 index 844c49562fe..00000000000 --- a/api/src/dtos/users/partner-user-invite.dto.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ApiProperty, OmitType } from '@nestjs/swagger'; -import { Expose, Type } from 'class-transformer'; -import { ArrayMinSize, IsArray, ValidateNested } from 'class-validator'; - -import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -import { IdDTO } from '../shared/id.dto'; -import { PartnerUserUpdate } from './partner-user-update.dto'; - -export class PartnerUserInvite extends OmitType(PartnerUserUpdate, [ - 'id', - 'password', - 'currentPassword', - 'agreedToTermsOfService', - 'jurisdictions', -]) { - /* Fields inherited from User: - * - email (inherited as required from User) - **/ - - @Expose() - @Type(() => IdDTO) - @IsArray({ groups: [ValidationsGroupsEnum.default] }) - @ArrayMinSize(1, { groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @ApiProperty({ type: IdDTO, isArray: true }) - jurisdictions: IdDTO[]; -} diff --git a/api/src/services/user.service.ts b/api/src/services/user.service.ts index 60332c98dcf..65bb6bbe488 100644 --- a/api/src/services/user.service.ts +++ b/api/src/services/user.service.ts @@ -37,7 +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 { PartnerUserInvite } from '../dtos/users/partner-user-invite.dto'; import { EmailService } from './email.service'; import { PermissionService } from './permission.service'; import { permissionActions } from '../enums/permissions/permission-actions-enum'; @@ -580,11 +579,7 @@ export class UserService { Checks if creation should update an existing user to a new role instead of creating a new entry */ async handleExistingUser( - dto: - | PublicUserCreate - | PartnerUserCreate - | AdvocateUserCreate - | PartnerUserInvite, + dto: PublicUserCreate | PartnerUserCreate | AdvocateUserCreate, ): Promise { const existingUser = await this.prisma.userAccounts.findUnique({ include: views.full, @@ -765,10 +760,7 @@ export class UserService { /* creates a partner user */ - async createPartnerUser( - dto: PartnerUserCreate | PartnerUserInvite, - req: Request, - ) { + async createPartnerUser(dto: PartnerUserCreate, req: Request) { const requestingUser = mapTo(User, req['user']); if ( diff --git a/api/test/integration/permission-tests/helpers.ts b/api/test/integration/permission-tests/helpers.ts index aa1cb2a5ccb..5bf3a837b2d 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 { UserInvite } from '../../../src/dtos/users/partner-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, @@ -241,26 +241,28 @@ export const buildUserCreateMock = ( firstName: 'Public User firstName', lastName: 'Public User lastName', password: 'Abcdef12345!', + passwordConfirmation: 'Abcdef12345!', + dob: new Date(), + agreedToTermsOfService: true, email, jurisdictions: [{ id: jurisId }], - } as PublicUserCreate; + }; }; 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 0926dbaadd5..0348d7b2549 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 @@ -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 9b19c8be63e..8da08d42b8d 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 @@ -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 799a41844dc..117414495d8 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 @@ -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 225d3ab0719..ffdf0280ffc 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 @@ -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 694769f03cd..98ab389b846 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 @@ -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 f50f720c4f2..562c6396cef 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 @@ -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 44533412570..10842a3c953 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 @@ -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 9c7d453acf9..78ad5931b8d 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 @@ -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 d01dce43261..50b35a3b383 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 @@ -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 ab5f2f9e868..255867204de 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 @@ -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 0005215e525..0f95c122db2 100644 --- a/api/test/integration/user.e2e-spec.ts +++ b/api/test/integration/user.e2e-spec.ts @@ -22,9 +22,9 @@ 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 { PartnerUserInvite } from 'src/dtos/users/partner-user-invite.dto'; +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'; describe('User Controller Tests', () => { let app: INestApplication; @@ -777,7 +777,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', @@ -789,7 +789,7 @@ describe('User Controller Tests', () => { userRoles: { isAdmin: true, }, - } as PartnerUserInvite) + } as PartnerUserCreate) .set('Cookie', cookies) .expect(201); diff --git a/shared-helpers/src/types/backend-swagger.ts b/shared-helpers/src/types/backend-swagger.ts index 5eb63afbcac..eb9f2ecb2f8 100644 --- a/shared-helpers/src/types/backend-swagger.ts +++ b/shared-helpers/src/types/backend-swagger.ts @@ -2017,28 +2017,6 @@ export class UserService { axios(configs, resolve, reject) }) } - /** - * Invite partner user - */ - invite( - params: { - /** requestBody */ - body?: PartnerUserInvite - } = {} as any, - options: IRequestOptions = {} - ): Promise { - return new Promise((resolve, reject) => { - let url = basePath + "/user/invite" - - const configs: IRequestConfig = getConfigs("post", "application/json", url, options) - - let data = params.body - - configs.data = data - - axios(configs, resolve, reject) - }) - } /** * Request single use code */ @@ -8860,16 +8838,7 @@ export interface PartnerUserCreate { appUrl?: string /** */ - jurisdictions?: IdDTO[] - - /** */ - password: string - - /** */ - passwordConfirmation: string - - /** */ - emailConfirmation?: string + jurisdictions: IdDTO[] } export interface AdvocateUserCreate { @@ -9243,95 +9212,6 @@ export interface AdvocateUserUpdate { jurisdictions?: IdDTO[] } -export interface PartnerUserInvite { - /** */ - 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 - - /** */ - 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 - - /** */ - appUrl?: string - - /** */ - jurisdictions: IdDTO[] -} - export interface User { /** */ id: string diff --git a/sites/partners/__tests__/components/users/FormUserManage.test.tsx b/sites/partners/__tests__/components/users/FormUserManage.test.tsx index 90d5ede5273..f375e99ffa2 100644 --- a/sites/partners/__tests__/components/users/FormUserManage.test.tsx +++ b/sites/partners/__tests__/components/users/FormUserManage.test.tsx @@ -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 })) }) ) @@ -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 })) }) ) @@ -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 })) }) ) diff --git a/sites/partners/src/components/users/FormUserManage.tsx b/sites/partners/src/components/users/FormUserManage.tsx index cef8598d2ba..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(() => { From 5c488efaf3b81f7332db6622b4d9150559235a94 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Wed, 18 Feb 2026 13:26:01 +0100 Subject: [PATCH 30/47] fix: fix main branch merge resolve errors --- api/src/services/user.service.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/api/src/services/user.service.ts b/api/src/services/user.service.ts index b1d65a7c844..b704addc4ba 100644 --- a/api/src/services/user.service.ts +++ b/api/src/services/user.service.ts @@ -288,6 +288,29 @@ export class UserService { } } + 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); + // if listing is on dto but not in db, we need to store it + const toAdd = toAddHelper(storedUser.listings, dto.listings); + + if (toAdd?.length || toRemove?.length) { + transactions.push(async (transaction: PrismaClient) => { + return transaction.userAccounts.update({ + where: { + id: dto.id, + }, + data: { + listings: { + disconnect: toRemove?.length ? toRemove : undefined, + connect: toAdd?.length ? toAdd : undefined, + }, + }, + }); + }); + } + } + //handle address for advocated users let newAddressId: string | undefined; if (dto?.address) { @@ -608,6 +631,7 @@ export class UserService { // if attempting to recreate an existing user if (!existingUser.userRoles && 'userRoles' in dto) { // 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: { @@ -644,6 +668,7 @@ export class UserService { .map((juris) => ({ id: juris.id })) .concat(dto.listings); + await this.snapshotCreateService.createUserSnapshot(existingUser.id); const res = await this.prisma.userAccounts.update({ include: views.full, data: { @@ -1408,6 +1433,7 @@ export class UserService { for (const user of users) { try { await this.emailService.warnOfAccountRemoval(mapTo(User, user)); + await this.snapshotCreateService.createUserSnapshot(user.id); await this.prisma.userAccounts.update({ data: { wasWarnedOfDeletion: true }, where: { id: user.id }, From 6c4382d7bfdb0910542c84e007ad56f1d84ea7fc Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Wed, 18 Feb 2026 13:26:15 +0100 Subject: [PATCH 31/47] fix: fix failing user service integration tests --- api/test/unit/services/user.service.spec.ts | 182 ++++++++++---------- 1 file changed, 87 insertions(+), 95 deletions(-) diff --git a/api/test/unit/services/user.service.spec.ts b/api/test/unit/services/user.service.spec.ts index 265e2dd94ae..152e2a02c60 100644 --- a/api/test/unit/services/user.service.spec.ts +++ b/api/test/unit/services/user.service.spec.ts @@ -1330,22 +1330,24 @@ describe('Testing user service', () => { .fn() .mockImplementation((callBack) => callBack(prisma)); - await service.update( - { - id, - firstName: 'first name', - lastName: 'last name', - jurisdictions: [{ id: jurisId } as any], - agreedToTermsOfService: true, - } as PublicUserUpdate, - { - headers: { jurisdictionname: 'juris 1' }, - user: { - id: 'requestingUser id', - userRoles: { isAdmin: true }, - } as unknown as User, - } as unknown as Request, - ); + 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, + } as unknown as Request); expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ include: { jurisdictions: true, @@ -1362,22 +1364,17 @@ 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, - }, + jurisdictions: { connect: [{ id: jurisId }] }, + }), include: { jurisdictions: true, listings: true, @@ -1429,24 +1426,26 @@ describe('Testing user service', () => { .fn() .mockImplementation((callBack) => callBack(prisma)); - await service.update( - { - id, - firstName: 'first name', - lastName: 'last name', - jurisdictions: [{ id: jurisId } as any], - password: 'new password', - currentPassword: 'current password', - agreedToTermsOfService: true, - } as PublicUserUpdate, - { - headers: { jurisdictionname: 'juris 1' }, - user: { - id: 'requestingUser id', - userRoles: { isAdmin: true }, - } as unknown as User, - } as unknown as Request, - ); + 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, + } as unknown as Request); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ include: { jurisdictions: true, @@ -1463,24 +1462,22 @@ 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, - }, + jurisdictions: { + connect: [ + { + id: jurisId, + }, + ], + }, + }), include: { jurisdictions: true, listings: true, @@ -1664,24 +1661,25 @@ describe('Testing user service', () => { .fn() .mockImplementation((callBack) => callBack(prisma)); - await service.update( - { - id, - firstName: 'first name', - lastName: 'last name', - jurisdictions: [{ id: jurisId } as any], - newEmail: 'new@email.com', - appUrl: 'https://www.example.com', - agreedToTermsOfService: true, - } as PublicUserUpdate, - { - headers: { jurisdictionname: 'juris 1' }, - user: { - id: 'requestingUser id', - userRoles: { isAdmin: true }, - } as unknown as User, - } as unknown as Request, - ); + 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, + } as unknown as Request); expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ include: { jurisdictions: true, @@ -1698,23 +1696,17 @@ describe('Testing user service', () => { id, }, }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ - data: { + data: expect.objectContaining({ + firstName: mockUserUpdate.firstName, + lastName: mockUserUpdate.lastName, + confirmationToken: expect.anything(), + agreedToTermsOfService: true, jurisdictions: { connect: [{ id: jurisId }], }, - }, - where: { - id, - }, - }); - expect(prisma.userAccounts.update).toHaveBeenCalledWith({ - data: { - firstName: 'first name', - lastName: 'last name', - confirmationToken: expect.anything(), - agreedToTermsOfService: true, - }, + }), include: { jurisdictions: true, listings: true, @@ -1838,11 +1830,11 @@ describe('Testing user service', () => { }, }); expect(prisma.userAccounts.update).toHaveBeenCalledWith({ - data: { + data: expect.objectContaining({ firstName: 'first name', lastName: 'last name', agreedToTermsOfService: true, - }, + }), include: { jurisdictions: true, listings: true, From a5cf16b68a7e280a838b5baabe4b525decca2841 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Wed, 18 Feb 2026 13:36:31 +0100 Subject: [PATCH 32/47] fix: fix partners site integration tests --- .../__tests__/components/users/FormUserManage.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sites/partners/__tests__/components/users/FormUserManage.test.tsx b/sites/partners/__tests__/components/users/FormUserManage.test.tsx index f375e99ffa2..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) } }) @@ -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) } }) @@ -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) } }) From 2741b7df4eb86a28e163b1cd4c528c141fe490bd Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Wed, 18 Feb 2026 15:45:45 +0100 Subject: [PATCH 33/47] fix: add a activity log interceptor to the partner creation endpoint --- api/src/controllers/user.controller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/controllers/user.controller.ts b/api/src/controllers/user.controller.ts index f0abcf5de82..608f9f90258 100644 --- a/api/src/controllers/user.controller.ts +++ b/api/src/controllers/user.controller.ts @@ -164,6 +164,7 @@ export class UserController { }) @ApiOkResponse({ type: User }) @UseGuards(PermissionGuard, JwtAuthGuard) + @UseInterceptors(ActivityLogInterceptor) async createPartnerUser( @Request() req: ExpressRequest, @Body() dto: PartnerUserCreate, From 31d26abc28900f41d65ceb7b0e22eec7c1179a21 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Wed, 18 Feb 2026 16:26:35 +0100 Subject: [PATCH 34/47] chore: add a user service tests for creating an advocate user --- api/test/unit/services/user.service.spec.ts | 144 +++++++++++++++++++- 1 file changed, 143 insertions(+), 1 deletion(-) diff --git a/api/test/unit/services/user.service.spec.ts b/api/test/unit/services/user.service.spec.ts index 152e2a02c60..6cb26ae7b74 100644 --- a/api/test/unit/services/user.service.spec.ts +++ b/api/test/unit/services/user.service.spec.ts @@ -25,7 +25,9 @@ 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 { 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'; describe('Testing user service', () => { let service: UserService; @@ -2285,6 +2287,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', () => { From e3ad934b9f2ebd83d07e556d63791de06aa9af61 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Wed, 18 Feb 2026 16:38:29 +0100 Subject: [PATCH 35/47] chore: add user controller advocate user creation test case --- api/test/integration/user.e2e-spec.ts | 59 +++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/api/test/integration/user.e2e-spec.ts b/api/test/integration/user.e2e-spec.ts index 3d6d13c2cbd..832a95b2483 100644 --- a/api/test/integration/user.e2e-spec.ts +++ b/api/test/integration/user.e2e-spec.ts @@ -25,6 +25,9 @@ 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; @@ -776,6 +779,62 @@ describe('User Controller Tests', () => { application.id, ); }); + + it('should create an advocate user', async () => { + const juris = await prisma.jurisdictions.create({ + data: jurisdictionFactory(), + }); + + const data = await applicationFactory(); + data.applicant.create.emailAddress = 'publicuser@email.com'; + const application = await prisma.applications.create({ + data, + }); + + const advocateUserData: AdvocateUserCreate = { + firstName: 'Advocate First Name', + lastName: 'Advocate Last Name', + email: 'advocateUser@email.com', + dob: new Date(), + agreedToTermsOfService: true, + agency: { + id: 'test_agency_id', + createdAt: new Date(), + updatedAt: new Date(), + name: 'Test Agency', + jurisdictions: { + id: juris.id, + }, + }, + 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', () => { From f37de70c717021d982e6295eeb90a9a1b3c1fb7c Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Wed, 18 Feb 2026 17:05:37 +0100 Subject: [PATCH 36/47] fix: fix user controller failing test --- api/test/integration/user.e2e-spec.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/api/test/integration/user.e2e-spec.ts b/api/test/integration/user.e2e-spec.ts index 832a95b2483..b6d8697099a 100644 --- a/api/test/integration/user.e2e-spec.ts +++ b/api/test/integration/user.e2e-spec.ts @@ -785,8 +785,19 @@ describe('User Controller Tests', () => { 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 = 'publicuser@email.com'; + data.applicant.create.emailAddress = 'advocateuser@email.com'; const application = await prisma.applications.create({ data, }); @@ -795,15 +806,11 @@ describe('User Controller Tests', () => { firstName: 'Advocate First Name', lastName: 'Advocate Last Name', email: 'advocateUser@email.com', - dob: new Date(), agreedToTermsOfService: true, agency: { - id: 'test_agency_id', - createdAt: new Date(), - updatedAt: new Date(), - name: 'Test Agency', + ...agencyData, jurisdictions: { - id: juris.id, + id: agencyData.jurisdictionsId, }, }, address: addressFactory() as AddressUpdate, @@ -811,7 +818,7 @@ describe('User Controller Tests', () => { }; const res = await request(app.getHttpServer()) - .post(`/user/advocate/`) + .post(`/user/advocate`) .set({ passkey: process.env.API_PASS_KEY || '' }) .send(advocateUserData) .set('Cookie', cookies) @@ -821,7 +828,7 @@ describe('User Controller Tests', () => { expect(res.body.jurisdictions).toEqual([ expect.objectContaining({ id: juris.id, name: juris.name }), ]); - expect(res.body.email).toEqual('advocateUser@email.com'); + expect(res.body.email).toEqual('advocateuser@email.com'); const applicationsOnUser = await prisma.userAccounts.findUnique({ include: { From 4a1562f82e2a2ec43103c30d06c582270f5637be Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Thu, 19 Feb 2026 21:05:20 +0100 Subject: [PATCH 37/47] fix: remove listing redirect appURL from partner creation --- shared-helpers/src/auth/AuthContext.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/shared-helpers/src/auth/AuthContext.ts b/shared-helpers/src/auth/AuthContext.ts index e734f45bf4e..91b89929eed 100644 --- a/shared-helpers/src/auth/AuthContext.ts +++ b/shared-helpers/src/auth/AuthContext.ts @@ -81,10 +81,7 @@ type ContextProps = { user: AdvocateUserCreate, listingIdRedirect?: string ) => Promise - createPartnerUser: ( - user: PartnerUserCreate, - listingIdRedirect?: string - ) => Promise + createPartnerUser: (user: PartnerUserCreate) => Promise resendConfirmation: (email: string, listingIdRedirect?: string) => Promise initialStateLoaded?: boolean loading?: boolean @@ -360,12 +357,11 @@ export const AuthProvider: FunctionComponent = ({ child dispatch(stopLoading()) } }, - createPartnerUser: async (user: PartnerUserCreate, listingIdRedirect) => { + createPartnerUser: async (user: PartnerUserCreate) => { dispatch(startLoading()) - const appUrl = getListingRedirectUrl(listingIdRedirect) try { const response = await userService?.createPartner({ - body: { ...user, appUrl }, + body: user, }) return response } finally { From 9afc2ecf93acb5ad0edefe4117d770deb8d79943 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Thu, 19 Feb 2026 21:12:51 +0100 Subject: [PATCH 38/47] fix: fix import paths --- api/test/integration/permission-tests/helpers.ts | 4 ++-- .../permission-tests/permission-as-admin.e2e-spec.ts | 2 +- .../permission-as-juris-admin-correct-juris.e2e-spec.ts | 2 +- .../permission-as-juris-admin-wrong-juris.e2e-spec.ts | 2 +- ...ermission-as-limited-juris-admin-correct-juris.e2e-spec.ts | 2 +- .../permission-as-limited-juris-admin-wrong-juris.e2e-spec.ts | 2 +- .../permission-tests/permission-as-no-user.e2e-spec.ts | 2 +- .../permission-as-partner-correct-listing.e2e-spec.ts | 2 +- .../permission-as-partner-wrong-listing.e2e-spec.ts | 2 +- .../permission-tests/permission-as-public.e2e-spec.ts | 2 +- .../permission-tests/permission-as-support-admin.e2e-spec.ts | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/api/test/integration/permission-tests/helpers.ts b/api/test/integration/permission-tests/helpers.ts index 5bf3a837b2d..20b2c8484fe 100644 --- a/api/test/integration/permission-tests/helpers.ts +++ b/api/test/integration/permission-tests/helpers.ts @@ -49,8 +49,8 @@ import { MultiselectQuestionUpdate } from '../../../src/dtos/multiselect-questio 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'; +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, 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 0348d7b2549..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 @@ -62,7 +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'; +import { PublicUserUpdate } from '../../../src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), 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 8da08d42b8d..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 @@ -60,7 +60,7 @@ import { createSimpleApplication, createSimpleListing, } from './helpers'; -import { PublicUserUpdate } from 'src/dtos/users/public-user-update.dto'; +import { PublicUserUpdate } from '../../../src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), 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 117414495d8..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 @@ -59,7 +59,7 @@ import { createSimpleApplication, createSimpleListing, } from './helpers'; -import { PublicUserUpdate } from 'src/dtos/users/public-user-update.dto'; +import { PublicUserUpdate } from '../../../src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), 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 ffdf0280ffc..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 @@ -60,7 +60,7 @@ import { createSimpleApplication, createSimpleListing, } from './helpers'; -import { PublicUserUpdate } from 'src/dtos/users/public-user-update.dto'; +import { PublicUserUpdate } from '../../../src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), 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 98ab389b846..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 @@ -59,7 +59,7 @@ import { createSimpleApplication, createSimpleListing, } from './helpers'; -import { PublicUserUpdate } from 'src/dtos/users/public-user-update.dto'; +import { PublicUserUpdate } from '../../../src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), 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 562c6396cef..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 @@ -61,7 +61,7 @@ import { createListing, createComplexApplication, } from './helpers'; -import { PublicUserUpdate } from 'src/dtos/users/public-user-update.dto'; +import { PublicUserUpdate } from '../../../src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), 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 10842a3c953..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 @@ -61,7 +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'; +import { PublicUserUpdate } from '../../../src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), 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 78ad5931b8d..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 @@ -59,7 +59,7 @@ import { constructFullListingData, createSimpleApplication, } from './helpers'; -import { PublicUserUpdate } from 'src/dtos/users/public-user-update.dto'; +import { PublicUserUpdate } from '../../../src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), 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 50b35a3b383..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 @@ -62,7 +62,7 @@ import { createListing, createComplexApplication, } from './helpers'; -import { PublicUserUpdate } from 'src/dtos/users/public-user-update.dto'; +import { PublicUserUpdate } from '../../../src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), 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 255867204de..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 @@ -64,7 +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'; +import { PublicUserUpdate } from '../../../src/dtos/users/public-user-update.dto'; const testEmailService = { confirmation: jest.fn(), From 5850f14b247be706d0f90b3bdddc1523cb736266 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Thu, 19 Feb 2026 21:19:25 +0100 Subject: [PATCH 39/47] chore: update creation address handling to use the transactions array --- api/src/services/user.service.ts | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/api/src/services/user.service.ts b/api/src/services/user.service.ts index b704addc4ba..604d02fb67a 100644 --- a/api/src/services/user.service.ts +++ b/api/src/services/user.service.ts @@ -315,21 +315,26 @@ export class UserService { let newAddressId: string | undefined; if (dto?.address) { if (dto.address?.id) { - await this.prisma.address.update({ - data: { - ...dto.address, - }, - where: { - id: dto.address.id, - }, + transactions.push(async (transactions: PrismaClient) => { + return transactions.address.update({ + data: { + ...dto.address, + }, + where: { + id: dto.address.id, + }, + }); }); } else { - const newAddress = await this.prisma.address.create({ - data: { - ...dto.address, - }, + transactions.push(async (transactions: PrismaClient) => { + const newAddress = await transactions.address.create({ + data: { + ...dto.address, + }, + }); + newAddressId = newAddress.id; + return newAddress; }); - newAddressId = newAddress.id; } } From d1486ef12c68376e157f708b4a277d54408deefe Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Thu, 19 Feb 2026 21:21:45 +0100 Subject: [PATCH 40/47] fix: skip id when updating and exiting users address --- api/src/services/user.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/services/user.service.ts b/api/src/services/user.service.ts index 604d02fb67a..57de7618500 100644 --- a/api/src/services/user.service.ts +++ b/api/src/services/user.service.ts @@ -319,6 +319,7 @@ export class UserService { return transactions.address.update({ data: { ...dto.address, + id: undefined, }, where: { id: dto.address.id, From aa03f30fff5fe4d95372361ca4e63d64eb87e5fc Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Thu, 19 Feb 2026 21:42:30 +0100 Subject: [PATCH 41/47] fix: remove redundant listings and jurisdictions handling from final user account update call --- api/src/services/user.service.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/api/src/services/user.service.ts b/api/src/services/user.service.ts index 57de7618500..8de50d4159e 100644 --- a/api/src/services/user.service.ts +++ b/api/src/services/user.service.ts @@ -401,18 +401,6 @@ export class UserService { }, } : undefined, - listings: dto.listings - ? { - connect: dto.listings.map((listing) => ({ id: listing.id })), - } - : undefined, - jurisdictions: dto.jurisdictions - ? { - connect: dto.jurisdictions.map((jurisdiction) => ({ - id: jurisdiction.id, - })), - } - : undefined, }, where: { id: dto.id, From b5a16957b7253acbd8b4c88184191be6b92c9212 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Thu, 19 Feb 2026 21:44:07 +0100 Subject: [PATCH 42/47] chore: move agency handling to a separated call for more graniular handling --- api/src/services/user.service.ts | 42 ++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/api/src/services/user.service.ts b/api/src/services/user.service.ts index 8de50d4159e..dfae4644100 100644 --- a/api/src/services/user.service.ts +++ b/api/src/services/user.service.ts @@ -366,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.agencyId, + }, + }, + }, + }); + }); + } 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, @@ -387,13 +422,6 @@ export class UserService { additionalPhoneNumberType: dto.additionalPhoneNumberType, additionalPhoneExtension: dto.additionalPhoneExtension, language: dto.language, - agency: dto?.agency?.id - ? { - connect: { - id: dto.agency.id, - }, - } - : undefined, address: newAddressId ? { connect: { From 85ef05790d590ef1e3492e17e4a457be9594cd76 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Thu, 19 Feb 2026 21:47:03 +0100 Subject: [PATCH 43/47] fix: simplify jurisdictions creation for user creation pipeline --- api/src/services/user.service.ts | 66 ++++++++++---------------------- 1 file changed, 21 insertions(+), 45 deletions(-) diff --git a/api/src/services/user.service.ts b/api/src/services/user.service.ts index dfae4644100..b356d74c73f 100644 --- a/api/src/services/user.service.ts +++ b/api/src/services/user.service.ts @@ -745,20 +745,6 @@ export class UserService { const passwordHash = await passwordToHash(dto.password); - const jurisdictions: - | { - jurisdictions: Prisma.JurisdictionsCreateNestedManyWithoutUser_accountsInput; - } - | Record = dto.jurisdictions - ? { - jurisdictions: { - connect: dto.jurisdictions.map((juris) => ({ - id: juris.id, - })), - }, - } - : {}; - let newUser = await this.prisma.userAccounts.create({ data: { passwordHash: passwordHash, @@ -767,7 +753,13 @@ export class UserService { middleName: dto.middleName, lastName: dto.lastName, dob: dto.dob, - ...jurisdictions, + jurisdictions: dto.jurisdictions + ? { + connect: dto.jurisdictions.map((juris) => ({ + id: juris.id, + })), + } + : undefined, listings: dto.listings ? { connect: dto.listings.map((listing) => ({ @@ -850,20 +842,6 @@ export class UserService { crypto.randomBytes(8).toString('hex'), ); - const jurisdictions: - | { - jurisdictions: Prisma.JurisdictionsCreateNestedManyWithoutUser_accountsInput; - } - | Record = dto.jurisdictions - ? { - jurisdictions: { - connect: dto.jurisdictions.map((juris) => ({ - id: juris.id, - })), - }, - } - : {}; - let newUser = await this.prisma.userAccounts.create({ data: { passwordHash: passwordHash, @@ -871,12 +849,18 @@ export class UserService { firstName: dto.firstName, lastName: dto.lastName, mfaEnabled: true, - ...jurisdictions, userRoles: { create: { ...dto.userRoles, }, }, + jurisdictions: dto.jurisdictions + ? { + connect: dto.jurisdictions.map((juris) => ({ + id: juris.id, + })), + } + : undefined, listings: dto.listings ? { connect: dto.listings.map((listing) => ({ @@ -947,20 +931,6 @@ export class UserService { crypto.randomBytes(8).toString('hex'), ); - const jurisdictions: - | { - jurisdictions: Prisma.JurisdictionsCreateNestedManyWithoutUser_accountsInput; - } - | Record = dto.jurisdictions - ? { - jurisdictions: { - connect: dto.jurisdictions.map((juris) => ({ - id: juris.id, - })), - }, - } - : {}; - let newUser = await this.prisma.userAccounts.create({ data: { passwordHash: passwordHash, @@ -974,7 +944,13 @@ export class UserService { }, }, isAdvocate: true, - ...jurisdictions, + jurisdictions: dto.jurisdictions + ? { + connect: dto.jurisdictions.map((juris) => ({ + id: juris.id, + })), + } + : undefined, listings: dto.listings ? { connect: dto.listings.map((listing) => ({ From e2095a71f534d3bf21493638029f91145aec84f8 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Thu, 19 Feb 2026 22:45:41 +0100 Subject: [PATCH 44/47] fix: fix wrong agency id usage for diconnect --- api/src/services/user.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/services/user.service.ts b/api/src/services/user.service.ts index b356d74c73f..56eda99b657 100644 --- a/api/src/services/user.service.ts +++ b/api/src/services/user.service.ts @@ -376,7 +376,7 @@ export class UserService { data: { agency: { disconnect: { - id: storedUser.agencyId, + id: storedUser.agency.id, }, }, }, From 86717b2670f60d32b121737c9f9dcb49683fb28e Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Thu, 19 Feb 2026 22:46:19 +0100 Subject: [PATCH 45/47] chore: fix failing tests --- api/test/unit/services/user.service.spec.ts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/api/test/unit/services/user.service.spec.ts b/api/test/unit/services/user.service.spec.ts index 6cb26ae7b74..3bea381dfe6 100644 --- a/api/test/unit/services/user.service.spec.ts +++ b/api/test/unit/services/user.service.spec.ts @@ -1375,7 +1375,6 @@ describe('Testing user service', () => { email: mockUserUpdate.email, dob: mockUserUpdate.dob, agreedToTermsOfService: true, - jurisdictions: { connect: [{ id: jurisId }] }, }), include: { jurisdictions: true, @@ -1472,13 +1471,6 @@ describe('Testing user service', () => { passwordHash: expect.anything(), passwordUpdatedAt: expect.anything(), agreedToTermsOfService: true, - jurisdictions: { - connect: [ - { - id: jurisId, - }, - ], - }, }), include: { jurisdictions: true, @@ -1705,9 +1697,6 @@ describe('Testing user service', () => { lastName: mockUserUpdate.lastName, confirmationToken: expect.anything(), agreedToTermsOfService: true, - jurisdictions: { - connect: [{ id: jurisId }], - }, }), include: { jurisdictions: true, @@ -1797,7 +1786,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: { @@ -1831,6 +1820,14 @@ describe('Testing user service', () => { id, }, }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + agency: undefined, + }, + where: { + id, + }, + }); expect(prisma.userAccounts.update).toHaveBeenCalledWith({ data: expect.objectContaining({ firstName: 'first name', From 97186cce728dfc6f11dab85ad61d431843d440b1 Mon Sep 17 00:00:00 2001 From: Mateusz Zduniuk Date: Thu, 19 Feb 2026 22:47:22 +0100 Subject: [PATCH 46/47] chore: add integration tests for agency connecting and disconnecting --- api/test/unit/services/user.service.spec.ts | 239 ++++++++++++++++++++ 1 file changed, 239 insertions(+) diff --git a/api/test/unit/services/user.service.spec.ts b/api/test/unit/services/user.service.spec.ts index 3bea381dfe6..882e2a8fbd3 100644 --- a/api/test/unit/services/user.service.spec.ts +++ b/api/test/unit/services/user.service.spec.ts @@ -28,6 +28,7 @@ import { SnapshotCreateService } from '../../../src/services/snapshot-create.ser 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; @@ -1871,6 +1872,244 @@ describe('Testing user service', () => { }); }); + 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, + 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 }], + }, + }, + }); + }); + + 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(); From f71ee2eea44fe4a91f890ab21efb6347263e8094 Mon Sep 17 00:00:00 2001 From: Emily Jablonski Date: Thu, 19 Feb 2026 17:54:19 -0700 Subject: [PATCH 47/47] fix: remaining relative path --- api/test/unit/services/user.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/test/unit/services/user.service.spec.ts b/api/test/unit/services/user.service.spec.ts index e64ed4db3ef..01ed162ff7a 100644 --- a/api/test/unit/services/user.service.spec.ts +++ b/api/test/unit/services/user.service.spec.ts @@ -28,7 +28,7 @@ import { SnapshotCreateService } from '../../../src/services/snapshot-create.ser 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'; +import { AdvocateUserUpdate } from '../../../src/dtos/users/advocate-user-update.dto'; describe('Testing user service', () => { let service: UserService;