Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bf739c1
[TM-3071] add users index
pachonjcl Mar 12, 2026
27c35e1
[TM-3071] add missing fields on response
pachonjcl Mar 13, 2026
67c0607
[TM-3071] add conditional login create user
pachonjcl Mar 16, 2026
7c2af91
[TM-3071] add missing fields
pachonjcl Mar 16, 2026
6d1e827
[TM-3071] Merge branch 'staging' into feat/TM-3071_users_crud_be
pachonjcl Mar 17, 2026
7e7a457
[TM-3071] Merge branch 'staging' into feat/TM-3071_users_crud_be
pachonjcl Mar 20, 2026
83ff0c1
[TM-3071] update unit tests
pachonjcl Mar 20, 2026
1a12cb7
[TM-3071] add delete user method
pachonjcl Mar 22, 2026
14b162a
[TM-3071] update role and organisation update
pachonjcl Mar 22, 2026
2026a57
[TM-3071] add missing update of direct frameworks
pachonjcl Mar 23, 2026
7d1476e
[TM-3071] remove unused variable
pachonjcl Mar 23, 2026
f6feae2
[TM-3071] add user and guard tests
pachonjcl Mar 23, 2026
445395d
[TM-3071] update unit test
pachonjcl Mar 24, 2026
b464fa0
[TM-3071] remove console log
pachonjcl Mar 24, 2026
f4d7be3
[TM-3071] address comments
pachonjcl Mar 27, 2026
27f6413
[TM-3071] find first before destroying role
pachonjcl Mar 27, 2026
ee6135b
[TM-3071] loading users frameworks
pachonjcl Mar 27, 2026
da085ed
[TM-3071] fix lint
pachonjcl Mar 27, 2026
ca48c17
[TM-3071] update unit test
pachonjcl Mar 27, 2026
1ff5e4d
[TM-3071] update users creation and tests coverage
pachonjcl Mar 27, 2026
9522dc8
[TM-3071] change not found exceptions
pachonjcl Mar 27, 2026
56c220f
[TM-3071] remove md files
pachonjcl Mar 27, 2026
87f15ed
[TM-3071] Merge branch 'staging' into feat/TM-3071_users_crud_be
pachonjcl Mar 27, 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
2 changes: 2 additions & 0 deletions apps/user-service/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ResetPasswordService } from "./auth/reset-password.service";
import { VerificationUserController } from "./auth/verification-user.controller";
import { VerificationUserService } from "./auth/verification-user.service";
import { UserCreationService } from "./users/user-creation.service";
import { UsersService } from "./users/users.service";
import { HealthModule } from "@terramatch-microservices/common/health/health.module";
import { OrganisationsController } from "./organisations/organisations.controller";
import { OrganisationsService } from "./organisations/organisations.service";
Expand Down Expand Up @@ -44,6 +45,7 @@ import { UserAssociationService } from "./user-association/user-association.serv
ResetPasswordService,
VerificationUserService,
UserCreationService,
UsersService,
OrganisationsService,
OrganisationCreationService,
ActionsService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,10 @@ export class OrganisationsController {
const document = buildJsonApi(OrganisationLightDto);
const orgResource = document.addData(organisation.uuid, new OrganisationLightDto(organisation));
if (user != null) {
const userResource = document.addData(user.uuid ?? "no-uuid", new UserDto(user, await user.myFrameworks()));
const userResource = document.addData(
user.uuid ?? "no-uuid",
new UserDto(user, user.frameworks, await user.myFrameworks())
);
userResource.relateTo("org", orgResource, { meta: { userStatus: "na" } });
}
return document;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,56 @@ describe("UserAssociationController", () => {
});
});

describe("inviteOrganisationUser", () => {
it("should authorize and invite an organisation user", async () => {
const organisation = await OrganisationFactory.create();
const invite = {
uuid: "invite-uuid",
emailAddress: "invitee@example.com",
callbackUrl: "https://example.com/invite",
organisationUuid: organisation.uuid
};
(stubProcessor.getEntity as jest.Mock).mockResolvedValue(organisation);
userAssociationService.inviteOrganisationUser.mockResolvedValue(invite as never);

const result = serialize(
await controller.inviteOrganisationUser(
{ uuid: organisation.uuid as string, model: "organisations" },
{
emailAddress: "invitee@example.com",
callbackUrl: "https://example.com/invite"
}
)
);

expect(userAssociationService.createProcessor).toHaveBeenCalledWith("organisations", organisation.uuid);
expect(policyService.authorize).toHaveBeenCalledWith("update", organisation);
expect(userAssociationService.inviteOrganisationUser).toHaveBeenCalledWith(
organisation,
"invitee@example.com",
"https://example.com/invite"
);
expect(result.data).toBeDefined();
expect((result.data as Resource).id).toBe("invite-uuid");
});

it("should propagate UnauthorizedException when policy denies", async () => {
const organisation = await OrganisationFactory.create();
(stubProcessor.getEntity as jest.Mock).mockResolvedValue(organisation);
policyService.authorize.mockRejectedValue(new UnauthorizedException());

await expect(
controller.inviteOrganisationUser(
{ uuid: organisation.uuid as string, model: "organisations" },
{
emailAddress: "invitee@example.com",
callbackUrl: "https://example.com/invite"
}
)
).rejects.toThrow(UnauthorizedException);
});
});

describe("deleteUserAssociations", () => {
it("should call handleDelete and return deleted response", async () => {
const project = await ProjectFactory.create();
Expand Down
23 changes: 23 additions & 0 deletions apps/user-service/src/users/dto/admin-user-create.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { CreateDataDto, JsonApiBodyDto } from "@terramatch-microservices/common/util/json-api-update-dto";
import { IsArray, IsNotEmpty, IsString } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
import { UserCreateBaseAttributes } from "./user-create.dto";

export class AdminUserCreateAttributes extends UserCreateBaseAttributes {
@IsNotEmpty()
@ApiProperty()
role: string;

@IsNotEmpty()
@ApiProperty()
organisationUuid: string;

@ApiProperty({ isArray: true, type: String })
@IsArray()
@IsString({ each: true })
directFrameworks: string[];
}

export class AdminUserCreateBody extends JsonApiBodyDto(
class AdminUserCreateData extends CreateDataDto("users", AdminUserCreateAttributes) {}
) {}
26 changes: 16 additions & 10 deletions apps/user-service/src/users/dto/user-create.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { IsEmail, IsIn, IsNotEmpty, IsOptional } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
import { CreateDataDto, JsonApiBodyDto } from "@terramatch-microservices/common/util/json-api-update-dto";

export class UserCreateAttributes {
export class UserCreateBaseAttributes {
@IsNotEmpty()
@ApiProperty()
firstName: string;
Expand All @@ -11,10 +11,6 @@ export class UserCreateAttributes {
@ApiProperty()
lastName: string;

@IsNotEmpty()
@ApiProperty()
password: string;

@IsEmail()
@ApiProperty()
emailAddress: string;
Expand All @@ -27,18 +23,28 @@ export class UserCreateAttributes {
@ApiProperty()
jobRole: string;

@IsNotEmpty()
@IsIn(["project-developer", "funder", "government"])
@ApiProperty()
role: string;

@IsOptional()
@ApiProperty()
country: string;

@IsOptional()
@ApiProperty()
program: string;
}

export class UserCreateBaseBody extends JsonApiBodyDto(
class UserCreateBaseData extends CreateDataDto("users", UserCreateBaseAttributes) {}
) {}

export class UserCreateAttributes extends UserCreateBaseAttributes {
@IsNotEmpty()
@ApiProperty()
password: string;

@IsNotEmpty()
@IsIn(["project-developer", "funder", "government"])
@ApiProperty()
role: string;

@IsNotEmpty()
@ApiProperty()
Expand Down
15 changes: 15 additions & 0 deletions apps/user-service/src/users/dto/user-query.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { IsOptional } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
import { IndexQueryDto } from "@terramatch-microservices/common/dto/index-query.dto";
import { TransformBooleanString } from "@terramatch-microservices/common/decorators/transform-boolean-string.decorator";

export class UserQueryDto extends IndexQueryDto {
@ApiProperty({ required: false })
@IsOptional()
search?: string;

@ApiProperty({ required: false, description: "Filter users by email address verification status" })
@IsOptional()
@TransformBooleanString()
isVerified?: boolean;
}
36 changes: 34 additions & 2 deletions apps/user-service/src/users/dto/user-update.dto.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,44 @@
import { IsEnum } from "class-validator";
import { IsArray, IsEnum, IsString } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
import { JsonApiBodyDto, JsonApiDataDto } from "@terramatch-microservices/common/util/json-api-update-dto";
import { VALID_LOCALES, ValidLocale } from "@terramatch-microservices/database/constants/locale";

class UserUpdateAttributes {
export class UserUpdateAttributes {
@ApiProperty({ description: "Organisation UUID", nullable: true, required: false, format: "uuid" })
organisationUuid?: string | null;

@ApiProperty({ description: "First name", nullable: true, required: false })
firstName?: string | null;

@ApiProperty({ description: "Last name", nullable: true, required: false })
lastName?: string | null;

@ApiProperty({ description: "Email address", nullable: true, required: false, format: "email" })
emailAddress?: string | null;

@ApiProperty({ description: "Job role", nullable: true, required: false })
jobRole?: string | null;

@ApiProperty({ description: "Phone number", nullable: true, required: false })
phoneNumber?: string | null;

@ApiProperty({ description: "Country", nullable: true, required: false })
country?: string | null;

@ApiProperty({ description: "Program", nullable: true, required: false })
program?: string | null;

@IsEnum(VALID_LOCALES)
@ApiProperty({ description: "New default locale for the given user", nullable: true, enum: VALID_LOCALES })
locale?: ValidLocale | null;

@ApiProperty()
primaryRole?: string | null;

@ApiProperty({ isArray: true, type: String })
@IsArray()
@IsString({ each: true })
directFrameworks?: string[] | null;
}

export class UserUpdateBody extends JsonApiBodyDto(
Expand Down
Loading
Loading