Skip to content

Commit b977e49

Browse files
authored
Merge branch 'main' into aakash/accept-req
2 parents 505516a + bd79e3e commit b977e49

58 files changed

Lines changed: 2138 additions & 250 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎packages/api-gateway/src/app.module.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { FileBucketModule } from './modules/file_bucket/file_bucket.module';
1111
import { FileConfigModule } from './modules/file_config/file_config.module';
1212
import { AnalyticsModule } from './modules/analytics/analytics.module';
1313
import { AnalyticsConfigModule } from './modules/analytics_config/analytics_config.module';
14+
import { FileDeleteModule } from './modules/file_delete/file_delete.module';
1415

1516
@Module({
1617
imports: [
@@ -24,6 +25,7 @@ import { AnalyticsConfigModule } from './modules/analytics_config/analytics_conf
2425
FileUploadModule,
2526
FileBucketModule,
2627
FileConfigModule,
28+
FileDeleteModule,
2729
AnalyticsModule,
2830
AnalyticsConfigModule,
2931
],

‎packages/api-gateway/src/middleware/api_key.middleware.ts‎

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
Injectable,
3+
Logger,
34
NestMiddleware,
45
Inject,
56
OnModuleInit,
@@ -20,6 +21,7 @@ const { JWT_SERVICE_NAME } = JwtProto;
2021

2122
@Injectable()
2223
export class ApiKeyMiddleware implements NestMiddleware, OnModuleInit {
24+
private readonly logger = new Logger(ApiKeyMiddleware.name);
2325
private apiKeyService: ApiKeyProto.ApiKeyServiceClient;
2426
private jwtService: JwtProto.JwtServiceClient;
2527

@@ -58,19 +60,7 @@ export class ApiKeyMiddleware implements NestMiddleware, OnModuleInit {
5860
});
5961
const res = await lastValueFrom(apiKeyValidation);
6062
req.apiKey = res.key;
61-
62-
const userJwt = req.headers['x-user-jwt'] as string | undefined;
63-
if (userJwt) {
64-
try {
65-
const userJwtValidation = this.jwtService.validateUserJwt({
66-
jwt: userJwt,
67-
});
68-
const userJwtRes = await lastValueFrom(userJwtValidation);
69-
if (userJwtRes.valid && userJwtRes.user) {
70-
req.user = userJwtRes.user;
71-
}
72-
} catch (error) {}
73-
}
63+
await this.resolveUserFromJwt(req);
7464

7565
next();
7666
} catch (error) {
@@ -85,18 +75,7 @@ export class ApiKeyMiddleware implements NestMiddleware, OnModuleInit {
8575

8676
req.apiKey = jwtRes.apiKey;
8777

88-
const userJwt = req.headers['x-user-jwt'] as string | undefined;
89-
if (userJwt) {
90-
try {
91-
const userJwtValidation = this.jwtService.validateUserJwt({
92-
jwt: userJwt,
93-
});
94-
const userJwtRes = await lastValueFrom(userJwtValidation);
95-
if (userJwtRes.valid && userJwtRes.user) {
96-
req.user = userJwtRes.user;
97-
}
98-
} catch (error) {}
99-
}
78+
await this.resolveUserFromJwt(req);
10079

10180
next();
10281
} catch (error) {
@@ -105,6 +84,24 @@ export class ApiKeyMiddleware implements NestMiddleware, OnModuleInit {
10584
}
10685
}
10786

87+
private async resolveUserFromJwt(req: ApiKeyReq): Promise<void> {
88+
const userJwt = req.headers['x-user-jwt'] as string | undefined;
89+
if (!userJwt) return;
90+
91+
try {
92+
const result = await lastValueFrom(
93+
this.jwtService.validateUserJwt({ jwt: userJwt }),
94+
);
95+
if (result.valid && result.user) {
96+
req.user = result.user;
97+
}
98+
} catch (error) {
99+
this.logger.warn(
100+
`x-user-jwt validation failed: ${(error as Error).message}`,
101+
);
102+
}
103+
}
104+
108105
private extractTokenFromHeader(request: Request): string | undefined {
109106
const [type, token] = request.headers.authorization?.split(' ') ?? [];
110107
return type === 'Bearer' ? token : undefined;
Lines changed: 161 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,124 @@
1-
import { ApiProperty } from '@nestjs/swagger';
1+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
22
import { IsNotEmpty } from 'class-validator';
3-
import { ApiKeyProto, IdentifierProto, JwtProto } from 'juno-proto';
3+
import {
4+
ApiKeyProto,
5+
AuthCommonProto,
6+
IdentifierProto,
7+
JwtProto,
8+
} from 'juno-proto';
49

510
export class IssueApiKeyRequest {
6-
@ApiProperty({ description: 'Optional description for key' })
11+
@ApiPropertyOptional({
12+
description: 'Optional description for key',
13+
example: 'Production API key for mobile app',
14+
})
715
description?: string | undefined;
816

9-
@ApiProperty({ description: 'Environment the key will be tied to' })
17+
@ApiProperty({
18+
description: 'Environment the key will be tied to',
19+
example: 'production',
20+
})
1021
@IsNotEmpty()
1122
environment: string;
1223

1324
@IsNotEmpty()
14-
@ApiProperty({ description: 'Project identifier' })
25+
@ApiProperty({
26+
description: 'Project identifier',
27+
example: { name: 'my-project' },
28+
})
1529
project: IdentifierProto.ProjectIdentifier;
1630
}
1731

1832
export class IssueApiKeyResponse {
19-
@ApiProperty({ type: 'string', description: 'The generated API key' })
33+
@ApiProperty({
34+
description:
35+
'The generated API key value (store immediately, not retrievable again)',
36+
example: 'a1b2c3d4e5f6...',
37+
})
2038
apiKey: string;
2139

40+
@ApiProperty({
41+
description: 'Environment this key was issued for',
42+
example: 'production',
43+
})
44+
environment: string;
45+
46+
@ApiProperty({
47+
description: 'Description provided at creation',
48+
example: 'Production API key for mobile app',
49+
})
50+
description: string;
51+
52+
@ApiProperty({
53+
description: 'ISO timestamp of key creation',
54+
example: '2026-01-01T00:00:00.000Z',
55+
})
56+
createdAt: string;
57+
58+
@ApiProperty({ description: 'project identifier for the API key' })
59+
project: string;
60+
2261
constructor(res: ApiKeyProto.IssueApiKeyResponse) {
2362
this.apiKey = res.apiKey;
63+
this.environment = res.info?.environment;
64+
this.description = res.info?.description;
65+
this.createdAt = res.info?.createdAt;
66+
this.project = res.info.project.id.toString();
67+
}
68+
}
69+
70+
export class ApiKey {
71+
@ApiProperty({
72+
description: "The API key's ID in the databse",
73+
example: '5',
74+
})
75+
id: string;
76+
77+
@ApiProperty({
78+
description:
79+
'The generated API key value (store immediately, not retrievable again)',
80+
example: 'a1b2c3d4e5f6...',
81+
})
82+
hash: string;
83+
84+
@ApiProperty({
85+
description: 'Description provided at creation',
86+
example: 'Production API key for mobile app',
87+
})
88+
description: string;
89+
90+
@ApiProperty({
91+
description: 'Scopes tied to this API key',
92+
example: ['read:projects', 'write:analytics'],
93+
type: [String],
94+
})
95+
scopes: string[];
96+
97+
@ApiProperty({ description: 'project identifier for the API key' })
98+
project: string;
99+
100+
@ApiProperty({
101+
description: 'Environment this key was issued for',
102+
example: 'production',
103+
})
104+
environment: string;
105+
106+
@ApiProperty({
107+
description: 'ISO timestamp of key creation',
108+
example: '2026-01-01T00:00:00.000Z',
109+
})
110+
createdAt: string;
111+
112+
constructor(res: AuthCommonProto.ApiKey) {
113+
this.id = res.id;
114+
this.hash = res.hash;
115+
this.description = res.description;
116+
this.scopes = res.scopes
117+
? res.scopes.map((scope) => scope.toString())
118+
: [''];
119+
this.environment = res.environment;
120+
this.project = res.project.id.toString();
121+
this.createdAt = res.createdAt;
24122
}
25123
}
26124

@@ -32,3 +130,60 @@ export class IssueJWTResponse {
32130
this.token = res.jwt;
33131
}
34132
}
133+
134+
export class PaginationParams {
135+
@ApiProperty({
136+
description: 'first page of results',
137+
example: '/auth/key/?offset=0&limit=5',
138+
})
139+
first: string;
140+
@ApiProperty({
141+
description: 'previous page of results',
142+
example: '/auth/key/?offset=15&limit=5',
143+
})
144+
prev: string;
145+
@ApiProperty({
146+
description: 'next page of results',
147+
example: '/auth/key/?offset=20&limit=5',
148+
})
149+
next: string;
150+
@ApiProperty({
151+
description: 'last page of results',
152+
example: '/auth/key/?offset=25&limit=5',
153+
})
154+
last: string;
155+
156+
constructor(res: {
157+
first: string;
158+
prev: string;
159+
next: string;
160+
last: string;
161+
}) {
162+
this.first = res.first;
163+
this.prev = res.prev;
164+
this.next = res.next;
165+
this.last = res.last;
166+
}
167+
}
168+
export class GetAllApiKeysResponse {
169+
@ApiProperty({
170+
type: ApiKey,
171+
isArray: true,
172+
description: 'List of API keys belonging to a project',
173+
})
174+
keys: ApiKey[];
175+
176+
@ApiProperty({
177+
type: PaginationParams,
178+
description: 'Pagination parameters',
179+
})
180+
links: PaginationParams;
181+
182+
constructor(res: {
183+
keys: AuthCommonProto.ApiKey[];
184+
links: PaginationParams;
185+
}) {
186+
this.keys = (res.keys ?? []).map((key) => new ApiKey(key));
187+
this.links = res.links;
188+
}
189+
}

‎packages/api-gateway/src/models/file_bucket.dto.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,11 @@ export class FileBucket implements FileBucketProto.Bucket {
109109
this.FileServiceFile = fileBucket.FileServiceFile ?? [];
110110
}
111111
}
112+
113+
export class BucketWithFiles {
114+
[bucketName: string]: string[];
115+
116+
constructor(entry: FileBucketProto.Files) {
117+
this[entry.bucketName] = entry.files;
118+
}
119+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsArray, IsInt, IsNotEmpty, IsString, Min } from 'class-validator';
3+
import { FileProto } from 'juno-proto';
4+
5+
export class DeleteFilesModel {
6+
@ApiProperty({
7+
type: 'string',
8+
description: 'Name of the bucket containing the files',
9+
})
10+
@IsNotEmpty()
11+
@IsString()
12+
bucketName: string;
13+
14+
@ApiProperty({
15+
type: 'number',
16+
description: 'Configuration ID for the bucket',
17+
})
18+
@IsInt()
19+
@Min(0)
20+
@IsNotEmpty()
21+
configId: number;
22+
23+
@ApiProperty({
24+
type: [String],
25+
description: 'List of file names to delete',
26+
})
27+
@IsArray()
28+
@IsNotEmpty()
29+
@IsString({ each: true })
30+
@IsNotEmpty({ each: true })
31+
fileNames: string[];
32+
}
33+
34+
export class DeleteFilesResponse {
35+
@ApiProperty({
36+
type: 'string',
37+
description: 'The name of the bucket from which files were deleted',
38+
})
39+
bucketName: string;
40+
41+
@ApiProperty({
42+
type: [String],
43+
description: 'List of file names that were deleted',
44+
})
45+
fileNames: string[];
46+
47+
constructor(res: FileProto.DeleteFilesResponse) {
48+
this.bucketName = res.bucketName;
49+
this.fileNames = res.fileNames;
50+
}
51+
}

0 commit comments

Comments
 (0)