Skip to content

Commit 76e81b2

Browse files
authored
Merge branch 'develop' into pr/plg-monitoring-stack
2 parents 372bdca + 80f4e3b commit 76e81b2

13 files changed

Lines changed: 8425 additions & 1541 deletions

File tree

.github/workflows/build-instance-images.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ on:
88
required: false
99
type: string
1010

11+
permissions:
12+
contents: read
13+
1114
jobs:
1215
metadata:
1316
name: Prepare Build Metadata

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
- Do not create features that are not explicitly described in specifications, if there is a gap, include it summary notes after implementing the task. If there is a question regarding the implementation, do not make assumptions, stop and clarify from the user.
66
- When creating or modifying features, create/update documentation in /docs-md folder
77
- If you need to run `npx prisma generate`, run `npm run db:generate` from `apps/backend-services` - it's a special script that writes models into apps/temporal/src and apps/backend-services/src. Don't forget to run migrations as normal if necessary.
8-
- Do not include any document-specific implementation, the system is generic and must support arbitrary workloads
8+
- Do not include any document-specific implementation, the system is generic and must support arbitrary workloads
9+
- All backend controllers must have full Swagger/OpenAPI documentation: use specific decorators (`@ApiOkResponse`, `@ApiForbiddenResponse`, `@ApiUnauthorizedResponse`, `@ApiConflictResponse`, etc.) instead of generic `@ApiResponse`, create dedicated DTO classes with `@ApiProperty` decorators for all request/response shapes, and reference those DTOs via the `type` field in response decorators.

apps/backend-services/src/bootstrap/bootstrap.controller.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,39 @@ import {
77
Req,
88
} from "@nestjs/common";
99
import {
10+
ApiConflictResponse,
11+
ApiForbiddenResponse,
12+
ApiOkResponse,
1013
ApiOperation,
11-
ApiResponse,
1214
ApiTags,
15+
ApiUnauthorizedResponse,
1316
} from "@nestjs/swagger";
1417
import { Request } from "express";
15-
import { KeycloakSSOAuth } from "@/decorators/custom-auth-decorators";
18+
import { Identity } from "@/auth/identity.decorator";
1619
import { User } from "../auth/types";
1720
import { BootstrapService } from "./bootstrap.service";
21+
import { BootstrapResultDto, BootstrapStatusResponseDto } from "./dto";
1822

1923
@ApiTags("Bootstrap")
2024
@Controller("api/bootstrap")
2125
export class BootstrapController {
2226
constructor(private readonly bootstrapService: BootstrapService) {}
2327

2428
@ApiOperation({
25-
summary: "Check if system bootstrap is needed and if the caller is eligible",
29+
summary:
30+
"Check if system bootstrap is needed and if the caller is eligible",
2631
})
27-
@ApiResponse({
28-
status: 200,
32+
@ApiOkResponse({
33+
type: BootstrapStatusResponseDto,
2934
description:
3035
"Returns whether bootstrap is needed and whether the caller is eligible.",
3136
})
32-
@KeycloakSSOAuth()
37+
@ApiUnauthorizedResponse({ description: "User is not authenticated." })
38+
@Identity()
3339
@Get("status")
3440
async getBootstrapStatus(
3541
@Req() req: Request & { user?: User },
36-
): Promise<{ needed: boolean; eligible: boolean }> {
42+
): Promise<BootstrapStatusResponseDto> {
3743
const userEmail = req.user?.email;
3844
return this.bootstrapService.getBootstrapStatus(userEmail);
3945
}
@@ -42,23 +48,22 @@ export class BootstrapController {
4248
summary:
4349
"Bootstrap the system: promote caller to admin, create Default group",
4450
})
45-
@ApiResponse({
46-
status: 200,
51+
@ApiOkResponse({
52+
type: BootstrapResultDto,
4753
description: "Bootstrap completed successfully.",
4854
})
49-
@ApiResponse({
50-
status: 403,
55+
@ApiUnauthorizedResponse({ description: "User is not authenticated." })
56+
@ApiForbiddenResponse({
5157
description: "Caller email does not match BOOTSTRAP_ADMIN_EMAIL.",
5258
})
53-
@ApiResponse({
54-
status: 409,
59+
@ApiConflictResponse({
5560
description: "Bootstrap already completed — a system admin exists.",
5661
})
57-
@KeycloakSSOAuth()
62+
@Identity()
5863
@Post()
5964
async performBootstrap(
6065
@Req() req: Request & { user?: User },
61-
): Promise<{ success: boolean; groupId: string; groupName: string }> {
66+
): Promise<BootstrapResultDto> {
6267
const userId = req.resolvedIdentity?.userId;
6368
if (!userId) {
6469
throw new HttpException("Unauthorized", HttpStatus.UNAUTHORIZED);

apps/backend-services/src/bootstrap/bootstrap.service.spec.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,7 @@ function createMockPrisma(adminCount = 0) {
2727
};
2828
}
2929

30-
function createService(
31-
adminCount: number,
32-
bootstrapEmail: string | undefined,
33-
) {
30+
function createService(adminCount: number, bootstrapEmail: string | undefined) {
3431
const mockPrisma = createMockPrisma(adminCount);
3532
const configService = {
3633
get: jest.fn((key: string) => {

apps/backend-services/src/bootstrap/bootstrap.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { GroupRole } from "@generated/client";
22
import {
3+
ConflictException,
34
ForbiddenException,
45
Injectable,
5-
ConflictException,
66
} from "@nestjs/common";
77
import { ConfigService } from "@nestjs/config";
88
import { AuditService } from "../audit/audit.service";
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { ApiProperty } from "@nestjs/swagger";
2+
3+
export class BootstrapResultDto {
4+
@ApiProperty({ description: "Whether bootstrap completed successfully" })
5+
success: boolean;
6+
7+
@ApiProperty({ description: "ID of the created Default group" })
8+
groupId: string;
9+
10+
@ApiProperty({ description: "Name of the created Default group" })
11+
groupName: string;
12+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { ApiProperty } from "@nestjs/swagger";
2+
3+
export class BootstrapStatusResponseDto {
4+
@ApiProperty({
5+
description:
6+
"Whether system bootstrap is still needed (no system admins exist)",
7+
})
8+
needed: boolean;
9+
10+
@ApiProperty({
11+
description: "Whether the current caller is eligible to perform bootstrap",
12+
})
13+
eligible: boolean;
14+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { BootstrapResultDto } from "./bootstrap-result.dto";
2+
export { BootstrapStatusResponseDto } from "./bootstrap-status-response.dto";

apps/frontend/src/data/hooks/useBootstrap.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@ export function useBootstrapStatus(enabled: boolean) {
2323
const response =
2424
await apiService.get<BootstrapStatus>("/bootstrap/status");
2525
if (!response.success) {
26-
throw new Error(
27-
response.message ?? "Failed to check bootstrap status",
28-
);
26+
throw new Error(response.message ?? "Failed to check bootstrap status");
2927
}
3028
return response.data ?? { needed: false, eligible: false };
3129
},
@@ -41,8 +39,7 @@ export function usePerformBootstrap() {
4139
const queryClient = useQueryClient();
4240
return useMutation({
4341
mutationFn: async (): Promise<BootstrapResult> => {
44-
const response =
45-
await apiService.post<BootstrapResult>("/bootstrap", {});
42+
const response = await apiService.post<BootstrapResult>("/bootstrap", {});
4643
if (!response.success || !response.data) {
4744
throw new Error(response.message ?? "Failed to perform bootstrap");
4845
}

apps/frontend/src/pages/RequestMembershipPage.test.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ vi.mock("../data/hooks/useGroups", () => ({
2424
useMyRequests: () => mockUseMyRequests(),
2525
}));
2626

27+
vi.mock("../data/hooks/useBootstrap", () => ({
28+
useBootstrapStatus: () => ({
29+
data: { needed: false, eligible: false },
30+
isLoading: false,
31+
}),
32+
}));
33+
2734
// ---------------------------------------------------------------------------
2835
// Helpers
2936
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)