@@ -4,18 +4,26 @@ import {
44 Get ,
55 Body ,
66 Param ,
7- Delete ,
87 Query ,
98 HttpCode ,
109 HttpStatus ,
1110} from '@nestjs/common' ;
11+ import {
12+ ApiTags ,
13+ ApiOperation ,
14+ ApiResponse ,
15+ ApiParam ,
16+ ApiQuery ,
17+ ApiBody ,
18+ } from '@nestjs/swagger' ;
1219import {
1320 ApiKeyService ,
1421 CreateApiKeyRequest ,
1522 ListApiKeysRequest ,
1623} from './api-key.service' ;
1724import { CreateApiKeyDto } from './dto/create-api-key.dto' ;
1825
26+ @ApiTags ( 'api-keys' )
1927@Controller ( 'api-keys' )
2028export class ApiKeyController {
2129 constructor ( private readonly apiKeyService : ApiKeyService ) { }
@@ -25,6 +33,79 @@ export class ApiKeyController {
2533 */
2634 @Post ( )
2735 @HttpCode ( HttpStatus . CREATED )
36+ @ApiOperation ( {
37+ summary : 'Create a new API key' ,
38+ description :
39+ 'Generates a new API key for a project. The plain-text key is returned **only once** — store it securely.' ,
40+ } )
41+ @ApiBody ( {
42+ type : CreateApiKeyDto ,
43+ examples : {
44+ basic : {
45+ summary : 'Basic key creation' ,
46+ value : { name : 'production-key' , projectId : 'project-abc123' } ,
47+ } ,
48+ withExpiry : {
49+ summary : 'Key with expiration date' ,
50+ value : {
51+ name : 'temporary-key' ,
52+ projectId : 'project-abc123' ,
53+ expiresAt : '2027-01-01T00:00:00.000Z' ,
54+ } ,
55+ } ,
56+ } ,
57+ } )
58+ @ApiResponse ( {
59+ status : 201 ,
60+ description : 'API key created — plain-text key returned only here.' ,
61+ schema : {
62+ type : 'object' ,
63+ properties : {
64+ message : {
65+ type : 'string' ,
66+ example : 'Store this key securely — it will not be shown again' ,
67+ } ,
68+ apiKey : {
69+ type : 'object' ,
70+ properties : {
71+ id : { type : 'string' , example : 'apikey-uuid-here' } ,
72+ name : { type : 'string' , example : 'production-key' } ,
73+ keyPrefix : { type : 'string' , example : 'mux_live_' } ,
74+ lastFour : { type : 'string' , example : 'Ab1C' } ,
75+ status : { type : 'string' , example : 'ACTIVE' } ,
76+ createdAt : { type : 'string' , format : 'date-time' } ,
77+ } ,
78+ } ,
79+ plainTextKey : {
80+ type : 'string' ,
81+ example : 'mux_live_AbCdEfGhIjKlMnOpQrStUvWx' ,
82+ } ,
83+ } ,
84+ } ,
85+ } )
86+ @ApiResponse ( {
87+ status : 400 ,
88+ description : 'Bad request — missing or invalid fields.' ,
89+ schema : {
90+ type : 'object' ,
91+ properties : {
92+ statusCode : { type : 'number' , example : 400 } ,
93+ message : { type : 'array' , items : { type : 'string' } } ,
94+ error : { type : 'string' , example : 'Bad Request' } ,
95+ } ,
96+ } ,
97+ } )
98+ @ApiResponse ( {
99+ status : 404 ,
100+ description : 'Project not found.' ,
101+ schema : {
102+ type : 'object' ,
103+ properties : {
104+ statusCode : { type : 'number' , example : 404 } ,
105+ message : { type : 'string' , example : 'Project project-abc123 not found' } ,
106+ } ,
107+ } ,
108+ } )
28109 async createApiKey ( @Body ( ) request : CreateApiKeyDto ) {
29110 const result = await this . apiKeyService . createApiKey (
30111 request as CreateApiKeyRequest ,
@@ -49,6 +130,53 @@ export class ApiKeyController {
49130 * Lists all API keys for a project with pagination
50131 */
51132 @Get ( )
133+ @ApiOperation ( {
134+ summary : 'List API keys for a project' ,
135+ description : 'Returns paginated API key metadata. Plain-text keys are never exposed here.' ,
136+ } )
137+ @ApiQuery ( { name : 'projectId' , required : true , description : 'Project ID to list keys for' , example : 'project-abc123' } )
138+ @ApiQuery ( { name : 'page' , required : false , description : 'Page number (1-based)' , example : 1 } )
139+ @ApiQuery ( { name : 'pageSize' , required : false , description : 'Number of results per page' , example : 10 } )
140+ @ApiQuery ( { name : 'developerId' , required : false , description : 'Optional developer ID for ownership check' } )
141+ @ApiResponse ( {
142+ status : 200 ,
143+ description : 'Paginated list of API key metadata.' ,
144+ schema : {
145+ type : 'object' ,
146+ properties : {
147+ keys : {
148+ type : 'array' ,
149+ items : {
150+ type : 'object' ,
151+ properties : {
152+ id : { type : 'string' , example : 'apikey-uuid-here' } ,
153+ name : { type : 'string' , example : 'production-key' } ,
154+ keyPrefix : { type : 'string' , example : 'mux_live_' } ,
155+ lastFour : { type : 'string' , example : 'Ab1C' } ,
156+ status : { type : 'string' , example : 'ACTIVE' } ,
157+ lastUsedAt : { type : 'string' , format : 'date-time' , nullable : true } ,
158+ createdAt : { type : 'string' , format : 'date-time' } ,
159+ expiresAt : { type : 'string' , format : 'date-time' , nullable : true } ,
160+ projectId : { type : 'string' , example : 'project-abc123' } ,
161+ } ,
162+ } ,
163+ } ,
164+ pagination : {
165+ type : 'object' ,
166+ properties : {
167+ page : { type : 'number' , example : 1 } ,
168+ pageSize : { type : 'number' , example : 10 } ,
169+ total : { type : 'number' , example : 42 } ,
170+ totalPages : { type : 'number' , example : 5 } ,
171+ } ,
172+ } ,
173+ } ,
174+ } ,
175+ } )
176+ @ApiResponse ( {
177+ status : 401 ,
178+ description : 'Unauthorized — developer does not own this project.' ,
179+ } )
52180 async listApiKeys (
53181 @Query ( 'projectId' ) projectId : string ,
54182 @Query ( 'page' ) page ?: string ,
@@ -88,6 +216,39 @@ export class ApiKeyController {
88216 */
89217 @Post ( ':apiKeyId/revoke' )
90218 @HttpCode ( HttpStatus . OK )
219+ @ApiOperation ( {
220+ summary : 'Revoke an API key' ,
221+ description : 'Marks the key as REVOKED. Idempotent — revoking an already-revoked key succeeds.' ,
222+ } )
223+ @ApiParam ( { name : 'apiKeyId' , description : 'ID of the API key to revoke' , example : 'apikey-uuid-here' } )
224+ @ApiBody ( {
225+ schema : {
226+ type : 'object' ,
227+ properties : {
228+ reason : { type : 'string' , example : 'Key compromised' , description : 'Optional revocation reason' } ,
229+ developerId : { type : 'string' , example : 'dev-uuid' , description : 'Optional: verify ownership before revoking' } ,
230+ } ,
231+ } ,
232+ examples : {
233+ basic : { summary : 'Revoke without reason' , value : { } } ,
234+ withReason : { summary : 'Revoke with reason' , value : { reason : 'Key compromised during security incident' } } ,
235+ } ,
236+ } )
237+ @ApiResponse ( {
238+ status : 200 ,
239+ description : 'API key revoked successfully.' ,
240+ schema : {
241+ type : 'object' ,
242+ properties : {
243+ id : { type : 'string' , example : 'apikey-uuid-here' } ,
244+ status : { type : 'string' , example : 'REVOKED' } ,
245+ revokedAt : { type : 'string' , format : 'date-time' } ,
246+ revokedReason : { type : 'string' , nullable : true , example : 'Key compromised' } ,
247+ } ,
248+ } ,
249+ } )
250+ @ApiResponse ( { status : 401 , description : 'Not authorized to revoke this key.' } )
251+ @ApiResponse ( { status : 404 , description : 'API key not found.' } )
91252 async revokeApiKey (
92253 @Param ( 'apiKeyId' ) apiKeyId : string ,
93254 @Body ( ) body : { reason ?: string ; developerId ?: string } ,
@@ -111,6 +272,50 @@ export class ApiKeyController {
111272 */
112273 @Post ( ':apiKeyId/rotate' )
113274 @HttpCode ( HttpStatus . OK )
275+ @ApiOperation ( {
276+ summary : 'Rotate an API key' ,
277+ description :
278+ 'Creates a new API key and sets the old key into a grace-period window (configurable via `API_KEY_ROTATION_GRACE_SECONDS`). ' +
279+ 'Both keys remain valid during the grace period so clients can migrate without downtime.' ,
280+ } )
281+ @ApiParam ( { name : 'apiKeyId' , description : 'ID of the API key to rotate' , example : 'apikey-uuid-here' } )
282+ @ApiBody ( {
283+ schema : {
284+ type : 'object' ,
285+ properties : {
286+ name : { type : 'string' , example : 'production-key-v2' , description : 'Optional name for the new key' } ,
287+ developerId : { type : 'string' , example : 'dev-uuid' , description : 'Optional: verify ownership before rotating' } ,
288+ } ,
289+ } ,
290+ examples : {
291+ basic : { summary : 'Rotate without renaming' , value : { } } ,
292+ withName : { summary : 'Rotate with new name' , value : { name : 'production-key-v2' } } ,
293+ } ,
294+ } )
295+ @ApiResponse ( {
296+ status : 200 ,
297+ description : 'New API key returned. Old key stays valid during the grace period.' ,
298+ schema : {
299+ type : 'object' ,
300+ properties : {
301+ message : { type : 'string' , example : 'Store this key securely — it will not be shown again' } ,
302+ apiKey : {
303+ type : 'object' ,
304+ properties : {
305+ id : { type : 'string' , example : 'apikey-new-uuid' } ,
306+ name : { type : 'string' , example : 'production-key-v2' } ,
307+ keyPrefix : { type : 'string' , example : 'mux_live_' } ,
308+ lastFour : { type : 'string' , example : 'Xy9Z' } ,
309+ status : { type : 'string' , example : 'ACTIVE' } ,
310+ createdAt : { type : 'string' , format : 'date-time' } ,
311+ } ,
312+ } ,
313+ plainTextKey : { type : 'string' , example : 'mux_live_XyZaBcDeFgHiJkLmNoPqRsTuV' } ,
314+ } ,
315+ } ,
316+ } )
317+ @ApiResponse ( { status : 401 , description : 'Not authorized to rotate this key.' } )
318+ @ApiResponse ( { status : 404 , description : 'API key not found.' } )
114319 async rotateApiKey (
115320 @Param ( 'apiKeyId' ) apiKeyId : string ,
116321 @Body ( ) body : { name ?: string ; developerId ?: string } ,
0 commit comments