Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
f668a83
chore: add new Advocate User related fields to user DTO
matzduniuk Feb 11, 2026
686424e
chore: remove generic user update and create DTOs
matzduniuk Feb 11, 2026
d689c52
chore: add a new public user update and create DTOs
matzduniuk Feb 11, 2026
87c1b9f
chore: add a new partner user update and create DTOs
matzduniuk Feb 11, 2026
a1297c5
chore: add a new advocate user update and create DTOs
matzduniuk Feb 11, 2026
338bf34
chore: update user invite DTO to be partner user specific
matzduniuk Feb 11, 2026
5f08240
chore: upadte the user controller and service methods to work with ne…
matzduniuk Feb 11, 2026
0b6993d
chore: remove unused imports
matzduniuk Feb 11, 2026
29752a8
fix: uupdate import path to relative format
matzduniuk Feb 11, 2026
3353d85
fix: update the partner creation pipeline to use the userRoles field
matzduniuk Feb 11, 2026
0169139
chore: add missing update DTOs as additional models to user controller
matzduniuk Feb 11, 2026
4b6350c
fix: update the advocate user update dto to use AdressUpdate type
matzduniuk Feb 12, 2026
7e60d6f
chore: add missing advocate user related fields to user update pipeline
matzduniuk Feb 12, 2026
1f84ab3
fix: fix imports to relative paths
matzduniuk Feb 12, 2026
b21c324
fix: generate new swagger types
matzduniuk Feb 16, 2026
1cc0967
fix: update api layer tests to use new endpoints and DTO
matzduniuk Feb 16, 2026
da61df5
Merge branch 'main' into 5719/housing-advocate-user-updates
matzduniuk Feb 16, 2026
f0003f2
fix: remove unusued service
matzduniuk Feb 16, 2026
b01047d
chore: update the jurisdiction field for user creation and updates
matzduniuk Feb 17, 2026
b0e13ca
fix: fix failing user controller integration tests
matzduniuk Feb 17, 2026
a75e3e9
chore: gupdate update sergice function to accept entire request objec…
matzduniuk Feb 17, 2026
b3382ca
chore: update and re-generate swagger API types
matzduniuk Feb 17, 2026
ef59f4f
chore: update userService usage to new creation and update methods
matzduniuk Feb 17, 2026
c334f53
chore: remove the user from the update service function arguments
matzduniuk Feb 17, 2026
443909b
fix: update failing user service tests
matzduniuk Feb 17, 2026
6a9b836
fix: fix failing tests
matzduniuk Feb 17, 2026
e3dc8de
Merge branch 'main' into 5719/housing-advocate-user-updates
matzduniuk Feb 17, 2026
6cf0364
fix: fix public page integration tests
matzduniuk Feb 17, 2026
2531b3d
fix: fix partner sites integration tests
matzduniuk Feb 17, 2026
1be9591
chore: update partner creation endpoints guards
matzduniuk Feb 17, 2026
50f1d4f
chore: remove the partner user invite DTO and endpoint
matzduniuk Feb 17, 2026
0c99054
Merge branch 'main' into 5719/housing-advocate-user-updates
matzduniuk Feb 17, 2026
39667ff
Merge branch 'main' into 5719/housing-advocate-user-updates
matzduniuk Feb 18, 2026
5c488ef
fix: fix main branch merge resolve errors
matzduniuk Feb 18, 2026
6c4382d
fix: fix failing user service integration tests
matzduniuk Feb 18, 2026
a5cf16b
fix: fix partners site integration tests
matzduniuk Feb 18, 2026
2741b7d
fix: add a activity log interceptor to the partner creation endpoint
matzduniuk Feb 18, 2026
31d26ab
chore: add a user service tests for creating an advocate user
matzduniuk Feb 18, 2026
e3ad934
chore: add user controller advocate user creation test case
matzduniuk Feb 18, 2026
f37de70
fix: fix user controller failing test
matzduniuk Feb 18, 2026
d9554fd
Merge branch 'main' into 5719/housing-advocate-user-updates
matzduniuk Feb 18, 2026
4a1562f
fix: remove listing redirect appURL from partner creation
matzduniuk Feb 19, 2026
9afc2ec
fix: fix import paths
matzduniuk Feb 19, 2026
5850f14
chore: update creation address handling to use the transactions array
matzduniuk Feb 19, 2026
d1486ef
fix: skip id when updating and exiting users address
matzduniuk Feb 19, 2026
aa03f30
fix: remove redundant listings and jurisdictions handling from final …
matzduniuk Feb 19, 2026
b5a1695
chore: move agency handling to a separated call for more graniular ha…
matzduniuk Feb 19, 2026
85ef057
fix: simplify jurisdictions creation for user creation pipeline
matzduniuk Feb 19, 2026
e2095a7
fix: fix wrong agency id usage for diconnect
matzduniuk Feb 19, 2026
86717b2
chore: fix failing tests
matzduniuk Feb 19, 2026
97186cc
chore: add integration tests for agency connecting and disconnecting
matzduniuk Feb 19, 2026
14f3dc1
Merge branch 'main' into 5719/housing-advocate-user-updates
matzduniuk Feb 19, 2026
f71ee2e
fix: remaining relative path
emilyjablonski Feb 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 85 additions & 32 deletions api/src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -53,12 +50,27 @@ import { ExportLogInterceptor } from '../interceptors/export-log.interceptor';
import { RequestSingleUseCode } from '../dtos/single-use-code/request-single-use-code.dto';
import { ApiKeyGuard } from '../guards/api-key.guard';
import { UserDeleteDTO } from '../dtos/users/user-delete.dto';
import { PublicUserCreate } from '../dtos/users/public-user-create.dto';
import { PartnerUserCreate } from '../dtos/users/partner-user-create.dto';
import { AdvocateUserCreate } from '../dtos/users/advocate-user-create.dto';
import { PublicUserUpdate } from '../dtos/users/public-user-update.dto';
import { PartnerUserUpdate } from '../dtos/users/partner-user-update.dto';
import { AdvocateUserUpdate } from '../dtos/users/advocate-user-update.dto';

@Controller('user')
@ApiTags('user')
@PermissionTypeDecorator('user')
@UsePipes(new ValidationPipe(defaultValidationPipeOptions))
@ApiExtraModels(IdDTO, EmailAndAppUrl)
@ApiExtraModels(
IdDTO,
EmailAndAppUrl,
PublicUserCreate,
PartnerUserCreate,
AdvocateUserCreate,
PublicUserUpdate,
PartnerUserUpdate,
AdvocateUserUpdate,
)
@UseGuards(ApiKeyGuard)
export class UserController {
constructor(
Expand Down Expand Up @@ -125,21 +137,55 @@ export class UserController {
return await this.userService.favoriteListings(userId);
}

@Post()
@Post('/public')
@ApiOperation({
summary: 'Creates a public only user',
operationId: 'create',
operationId: 'createPublic',
})
@UseGuards(OptionalAuthGuard, PermissionGuard)
@UsePipes(new ValidationPipe(defaultValidationPipeOptions))
@ApiOkResponse({ type: User })
async createPublicUser(
@Request() req: ExpressRequest,
@Body() dto: PublicUserCreate,
@Query() queryParams: UserCreateParams,
): Promise<User> {
return await this.userService.createPublicUser(
dto,
queryParams.noWelcomeEmail !== true,
req,
);
}

@Post('/partner')
@ApiOperation({
summary: 'Creates a partner only user',
operationId: 'createPartner',
})
@ApiOkResponse({ type: User })
@UseGuards(PermissionGuard, JwtAuthGuard)
@UseInterceptors(ActivityLogInterceptor)
async createPartnerUser(
@Request() req: ExpressRequest,
@Body() dto: PartnerUserCreate,
): Promise<User> {
return await this.userService.createPartnerUser(dto, req);
}

@Post('/advocate')
@ApiOperation({
summary: 'Creates a advocate only user',
operationId: 'createAdvocate',
})
@ApiOkResponse({ type: User })
@UseGuards(OptionalAuthGuard, PermissionGuard)
async create(
async createAdvocateUser(
@Request() req: ExpressRequest,
@Body() dto: UserCreate,
@Body() dto: AdvocateUserCreate,
@Query() queryParams: UserCreateParams,
): Promise<User> {
return await this.userService.create(
return await this.userService.createAdvocateUser(
dto,
false,
queryParams.noWelcomeEmail !== true,
req,
);
Expand All @@ -161,18 +207,6 @@ export class UserController {
);
}

@Post('/invite')
@ApiOperation({ summary: 'Invite partner user', operationId: 'invite' })
@ApiOkResponse({ type: User })
@UseGuards(OptionalAuthGuard)
@UseInterceptors(ActivityLogInterceptor)
async invite(
@Body() dto: UserInvite,
@Request() req: ExpressRequest,
): Promise<User> {
return await this.userService.create(dto, true, undefined, req);
}

@Post('request-single-use-code')
@ApiOperation({
summary: 'Request single use code',
Expand Down Expand Up @@ -268,21 +302,40 @@ export class UserController {
return await this.userService.deleteAfterInactivity();
}

@Put(':id')
@ApiOperation({ summary: 'Update user', operationId: 'update' })
@Put('/public')
@ApiOperation({ summary: 'Update user', operationId: 'updatePublic' })
@ApiOkResponse({ type: User })
@UseGuards(JwtAuthGuard, PermissionGuard)
@UseInterceptors(ActivityLogInterceptor)
async update(
async updatePublicUser(
@Request() req: ExpressRequest,
@Body() dto: UserUpdate,
@Body() dto: PublicUserUpdate,
): Promise<User> {
const jurisdictionName = req.headers['jurisdictionname'] || '';
return await this.userService.update(
dto,
mapTo(User, req['user']),
jurisdictionName as string,
);
return await this.userService.update(dto, req);
}

@Put('/partner')
@ApiOperation({ summary: 'Update user', operationId: 'updatePartner' })
@ApiOkResponse({ type: User })
@UseGuards(JwtAuthGuard, PermissionGuard)
@UseInterceptors(ActivityLogInterceptor)
async updatePartnerUser(
@Request() req: ExpressRequest,
@Body() dto: PartnerUserUpdate,
): Promise<User> {
return await this.userService.update(dto, req);
}

@Put('/advocate')
@ApiOperation({ summary: 'Update user', operationId: 'updateAdvocate' })
@ApiOkResponse({ type: User })
@UseGuards(JwtAuthGuard, PermissionGuard)
@UseInterceptors(ActivityLogInterceptor)
async updateAdvocateUser(
@Request() req: ExpressRequest,
@Body() dto: AdvocateUserUpdate,
): Promise<User> {
return await this.userService.update(dto, req);
}

@Get(`:id`)
Expand Down
23 changes: 23 additions & 0 deletions api/src/dtos/users/advocate-user-create.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { OmitType } from '@nestjs/swagger';
import { AdvocateUserUpdate } from './advocate-user-update.dto';
export class AdvocateUserCreate extends OmitType(AdvocateUserUpdate, [
'id',
'newEmail',
'currentPassword',
'password',
'title',
'phoneNumber',
'phoneType',
'phoneExtension',
'additionalPhoneNumber',
'additionalPhoneNumberType',
'additionalPhoneExtension',
] as const) {
/* Fields inherited from AdvocateUserUpdate:
* - firstName (inherited as required from AdvocateUserUpdate)
* - middleName (inherited as optional from AdvocateUserUpdate)
* - lastName (inherited as required from AdvocateUserUpdate)
* - agency (inherited as required from AdvocateUserUpdate)
* - email (inherited as required from AdvocateUserUpdate)
**/
}
99 changes: 99 additions & 0 deletions api/src/dtos/users/advocate-user-update.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger';
import { Expose, Type } from 'class-transformer';
import {
IsArray,
IsEmail,
IsNotEmpty,
IsPhoneNumber,
IsString,
Matches,
MaxLength,
ValidateIf,
ValidateNested,
} from 'class-validator';
import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum';
import Agency from '../agency/agency.dto';
import { User } from './user.dto';
import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator';
import { passwordRegex } from '../../utilities/password-regex';
import { AddressUpdate } from '../addresses/address-update.dto';
import { IdDTO } from '../shared/id.dto';

export class AdvocateUserUpdate extends OmitType(User, [
'createdAt',
'updatedAt',
'agency',
'address',
'phoneNumber',
'passwordUpdatedAt',
'passwordValidForDays',
'passwordUpdatedAt',
'jurisdictions',
] as const) {
/* Fields inherited from BaseUser:
* - firstName (inherited as required from BaseUser)
* - middleName (inherited as optional from BaseUser)
* - lastName (inherited as required from BaseUser)
* - email (inherited as required from BaseUser)
* - title (inherited as optional from BaseUserUpdate)
* - phoneExtension (inherited as optional from BaseUserUpdate)
* - additionalPhoneNumber (inherited as optional from BaseUserUpdate)
* - additionalPhoneNumberType (inherited as optional from BaseUserUpdate)
* - additionalPhoneExtension (inherited as optional from BaseUserUpdate)
**/

@Expose()
@ValidateNested({ groups: [ValidationsGroupsEnum.default] })
@Type(() => Agency)
@ApiProperty({ type: Agency })
agency: Agency;

@Expose()
@ValidateNested({ groups: [ValidationsGroupsEnum.default] })
@Type(() => AddressUpdate)
@ApiProperty({ type: AddressUpdate })
address: AddressUpdate;

@Expose()
@IsPhoneNumber('US', { groups: [ValidationsGroupsEnum.default] })
@ApiProperty()
phoneNumber: string;

@Expose()
@IsString({ groups: [ValidationsGroupsEnum.default] })
@ApiProperty()
phoneType: string;

@Expose()
@ApiPropertyOptional()
@IsEmail({}, { groups: [ValidationsGroupsEnum.default] })
@EnforceLowerCase()
newEmail?: string;

@Expose()
@ApiPropertyOptional()
@IsString({ groups: [ValidationsGroupsEnum.default] })
@Matches(passwordRegex, {
message: 'passwordTooWeak',
groups: [ValidationsGroupsEnum.default],
})
password?: string;

@Expose()
@ValidateIf((o) => o.password, { groups: [ValidationsGroupsEnum.default] })
@IsNotEmpty({ groups: [ValidationsGroupsEnum.default] })
@ApiPropertyOptional()
currentPassword?: string;

@Expose()
@IsString({ groups: [ValidationsGroupsEnum.default] })
@MaxLength(256, { groups: [ValidationsGroupsEnum.default] })
@ApiPropertyOptional()
appUrl?: string;

@Expose()
@Type(() => IdDTO)
@IsArray({ groups: [ValidationsGroupsEnum.default] })
@ApiPropertyOptional({ type: IdDTO, isArray: true })
jurisdictions?: IdDTO[];
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,23 @@
import { ApiProperty, OmitType } from '@nestjs/swagger';
import { PartnerUserUpdate } from './partner-user-update.dto';
import { Expose, Type } from 'class-transformer';
import {
ArrayMinSize,
IsArray,
IsEmail,
ValidateNested,
} from 'class-validator';
import { UserUpdate } from './user-update.dto';

import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator';
import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum';
import { IdDTO } from '../shared/id.dto';
import { ArrayMinSize, IsArray, ValidateNested } from 'class-validator';

export class UserInvite extends OmitType(UserUpdate, [
export class PartnerUserCreate extends OmitType(PartnerUserUpdate, [
'id',
'newEmail',
'password',
'currentPassword',
'email',
'agreedToTermsOfService',
'jurisdictions',
]) {
@Expose()
@ApiProperty()
@IsEmail({}, { groups: [ValidationsGroupsEnum.default] })
@EnforceLowerCase()
email: string;

] as const) {
/* Fields inherited from PartnerUserUpdate:
* - firstName (inherited as required from PartnerUserUpdate)
* - lastName (inherited as required from PartnerUserUpdate)
* - email (inherited as required from PartnerUserUpdate)
* - userRoles (inherited as required from PartnerUserUpdate)
**/
@Expose()
@Type(() => IdDTO)
@IsArray({ groups: [ValidationsGroupsEnum.default] })
Expand Down
Loading
Loading