Skip to content

Commit df97e7d

Browse files
kworathurgreptile-apps[bot]aakashg00llam36
authored
Feature: List and Delete API Keys Endpoints (#246)
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: aakashg00 <aakashbb@gmail.com> Co-authored-by: Long Lam <llam36@gatech.edu>
1 parent d1a4968 commit df97e7d

37 files changed

Lines changed: 1156 additions & 229 deletions

‎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+
}

0 commit comments

Comments
 (0)