Skip to content

Commit 5a1a73c

Browse files
committed
Add and cleanup response documentation for api routes
1 parent 318edd7 commit 5a1a73c

File tree

16 files changed

+308
-60
lines changed

16 files changed

+308
-60
lines changed

src/api/components/index.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ export function withRoles<T extends FastifyZodOpenApiSchema>(
205205
...schema.response,
206206
};
207207
return {
208+
...schema,
208209
security,
209210
"x-required-roles": roles,
210211
"x-disable-api-key-auth": disableApiKeyAuth,
@@ -232,7 +233,6 @@ ${schema.description}
232233
<hr />
233234
${roles.length > 0 ? `Requires any of the following roles:\n\n${roles.map((item) => `* ${AppRoleHumanMapper[item]} (<code>${item}</code>)`).join("\n")}` : "Requires valid authentication but no specific authorization."}
234235
`,
235-
...schema,
236236
response: responses,
237237
};
238238
}
@@ -241,11 +241,21 @@ export function withTags<T extends FastifyZodOpenApiSchema>(
241241
tags: string[],
242242
schema: T,
243243
) {
244+
const cleanedResponse = schema.response
245+
? Object.fromEntries(
246+
Object.entries(schema.response).filter(([_, value]) => value !== null),
247+
)
248+
: {};
244249
const responses = {
245-
500: internalServerError,
246-
429: rateLimitExceededError,
247-
400: validationError,
248-
...schema.response,
250+
...(schema.response && schema.response["500"] !== null
251+
? { 500: internalServerError }
252+
: {}),
253+
...(schema.response && schema.response["429"] !== null
254+
? { 429: rateLimitExceededError }
255+
: {}),
256+
...(schema.response &&
257+
schema.response["400"] !== null && { 400: validationError }),
258+
...cleanedResponse,
249259
};
250260
return {
251261
tags,

src/api/index.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
import { type ZodOpenApiVersion } from "zod-openapi";
2727
import { withTags } from "./components/index.js";
2828
import RedisModule from "ioredis";
29+
import * as z from "zod/v4";
2930

3031
/** BEGIN EXTERNAL PLUGINS */
3132
import fastifyIp from "fastify-ip";
@@ -356,7 +357,21 @@ Otherwise, email [[email protected]](mailto:[email protected]) for sup
356357
"/api/v1/healthz",
357358
{
358359
schema: withTags(["Generic"], {
359-
summary: "Verify that the API server is healthy.",
360+
summary: "Get API server health status",
361+
response: {
362+
200: {
363+
description: "The API server is healthy.",
364+
content: {
365+
"application/json": {
366+
schema: z.object({
367+
message: z.literal("UP").meta({ example: "UP" }),
368+
}),
369+
},
370+
},
371+
},
372+
400: null,
373+
429: null,
374+
},
360375
}),
361376
},
362377
async (_, reply) => {

src/api/routes/apiKey.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,26 @@ const apiKeyRoute: FastifyPluginAsync = async (fastify, _options) => {
3737
schema: withRoles(
3838
[AppRoles.MANAGE_ORG_API_KEYS],
3939
withTags(["API Keys"], {
40-
summary: "Create an organization API key.",
40+
summary: "Create an organization API key",
41+
description:
42+
"Organization API keys are not tied to the permissions of a specific user, but rather are assigned their own permissions with optional restrictions.",
4143
body: apiKeyPostBody,
44+
response: {
45+
201: {
46+
description: "The organization API key has been created.",
47+
content: {
48+
"application/json": {
49+
schema: z.object({
50+
apiKey: z.string().min(1).meta({
51+
description: "The API key.",
52+
example:
53+
"acmuiuc_sample435be_example244b8607e7665319c221496ad7282edbb09558f720e0e634ab77615b_d305c1",
54+
}),
55+
}),
56+
},
57+
},
58+
},
59+
},
4260
}),
4361
{ disableApiKeyAuth: true },
4462
),
@@ -143,13 +161,23 @@ If you did not create this API key, please secure your account and notify the AC
143161
schema: withRoles(
144162
[AppRoles.MANAGE_ORG_API_KEYS],
145163
withTags(["API Keys"], {
146-
summary: "Delete an organization API key.",
164+
summary: "Delete an organization API key",
147165
params: z.object({
148166
keyId: z.string().min(1).meta({
149167
description:
150168
"Key ID to delete. The key ID is the second segment of the API key.",
151169
}),
152170
}),
171+
response: {
172+
204: {
173+
description: "The organization API key has been deleted.",
174+
content: {
175+
"application/json": {
176+
schema: z.null(),
177+
},
178+
},
179+
},
180+
},
153181
}),
154182
{ disableApiKeyAuth: true },
155183
),
@@ -242,7 +270,18 @@ If you did not delete this API key, please secure your account and notify the AC
242270
schema: withRoles(
243271
[AppRoles.MANAGE_ORG_API_KEYS],
244272
withTags(["API Keys"], {
245-
summary: "Get all organization API keys.",
273+
summary: "Get all organization API keys",
274+
response: {
275+
200: {
276+
description:
277+
"The list of organization API keys has been retrieved.",
278+
content: {
279+
"application/json": {
280+
schema: z.array(apiKeyPostBody),
281+
},
282+
},
283+
},
284+
},
246285
}),
247286
{ disableApiKeyAuth: true },
248287
),

src/api/routes/iam.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import {
3131
groupModificationPatchSchema,
3232
EntraGroupActions,
3333
entraProfilePatchRequest,
34+
entraActionResponseSchema,
35+
entraGroupMembershipListResponse,
3436
} from "../../common/types/iam.js";
3537
import { clearAuthCache, getGroupRoles } from "../functions/authorization.js";
3638
import { getRoleCredentials } from "api/functions/sts.js";
@@ -220,8 +222,17 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
220222
[AppRoles.IAM_INVITE_ONLY, AppRoles.IAM_ADMIN],
221223
withTags(["IAM"], {
222224
body: invitePostRequestSchema,
223-
summary: "Invite a user to the ACM @ UIUC Entra ID tenant.",
224-
// response: { 202: entraActionResponseSchema },
225+
summary: "Invite users to the ACM @ UIUC Entra ID tenant",
226+
response: {
227+
202: {
228+
description: "At least one of the users have been invited.",
229+
content: {
230+
"application/json": {
231+
schema: entraActionResponseSchema,
232+
},
233+
},
234+
},
235+
},
225236
}),
226237
),
227238
onRequest: fastify.authorizeFromSchema,
@@ -290,7 +301,12 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
290301
}
291302
}
292303
await Promise.allSettled(logPromises);
293-
reply.status(202).send(response);
304+
if (response.success.length > 0) {
305+
reply.status(202);
306+
} else {
307+
reply.status(500);
308+
}
309+
reply.send(response);
294310
},
295311
);
296312
fastify.withTypeProvider<FastifyZodOpenApiTypeProvider>().patch(
@@ -553,7 +569,16 @@ No action is required from you at this time.
553569
schema: withRoles(
554570
[AppRoles.IAM_ADMIN],
555571
withTags(["IAM"], {
556-
// response: { 200: entraGroupMembershipListResponse },
572+
response: {
573+
200: {
574+
description: "The members of the group have been retrieved.",
575+
content: {
576+
"application/json": {
577+
schema: entraGroupMembershipListResponse,
578+
},
579+
},
580+
},
581+
},
557582
params: z.object({
558583
groupId,
559584
}),

src/api/routes/ics.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,21 @@ const icalPlugin: FastifyPluginAsync = async (fastify, _options) => {
5656
description: "Organization to retrieve calendar for",
5757
}),
5858
}),
59-
summary:
60-
"Retrieve the calendar for ACM @ UIUC or a specific sub-organization.",
59+
summary: "Retrieve an organization's calendar",
60+
response: {
61+
200: {
62+
description: "The organization's calendar has been generated.",
63+
content: {
64+
"text/calendar": {
65+
schema: z.string().meta({
66+
description: "Calendar in iCalendar format.",
67+
example:
68+
"BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//sebbo.net//ical-generator//EN\nMETHOD:PUBLISH\nNAME:ACM@UIUC - Infrastructure Committee Events\nX-WR-CALNAME:ACM@UIUC - Infrastructure Committee Events\nTIMEZONE-ID:America/Chicago\nX-WR-TIMEZONE:America/Chicago\nEND:VCALENDAR",
69+
}),
70+
},
71+
},
72+
},
73+
},
6174
} satisfies FastifyZodOpenApiSchema),
6275
},
6376
async (request, reply) => {

src/api/routes/linkry.ts

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,13 @@ import {
2626
getLinkryKvArn,
2727
setKey,
2828
} from "api/functions/cloudfrontKvStore.js";
29-
import { createRequest, linkrySlug } from "common/types/linkry.js";
29+
import {
30+
createRequest,
31+
linkryAccessList,
32+
linkryRecordWithOwner,
33+
linkryRedirectTarget,
34+
linkrySlug,
35+
} from "common/types/linkry.js";
3036
import {
3137
extractUniqueSlugs,
3238
fetchOwnerRecords,
@@ -82,7 +88,40 @@ const linkryRoutes: FastifyPluginAsync = async (fastify, _options) => {
8288
{
8389
schema: withRoles(
8490
[AppRoles.LINKS_MANAGER, AppRoles.LINKS_ADMIN],
85-
withTags(["Linkry"], {}),
91+
withTags(["Linkry"], {
92+
summary: "Get current user's links",
93+
description:
94+
"If the user has bypass permissions, all links will be returned. Otherwise, only links owned by the current user, or delegated to the current user, will be returned.",
95+
response: {
96+
200: {
97+
description: "The current user's links have been retrieved.",
98+
content: {
99+
"application/json": {
100+
schema: z.object({
101+
ownedLinks: z
102+
.array(
103+
z.object({
104+
slug: linkrySlug,
105+
createdAt: z.iso.datetime(),
106+
updatedAt: z.iso.datetime(),
107+
redirect: linkryRedirectTarget,
108+
access: linkryAccessList,
109+
}),
110+
)
111+
.meta({
112+
description:
113+
"A list of all links that the current user owns.",
114+
}),
115+
delegatedLinks: z.array(linkryRecordWithOwner).meta({
116+
description:
117+
"A list of all links that the current user has delegated access to (including superuser access).",
118+
}),
119+
}),
120+
},
121+
},
122+
},
123+
},
124+
}),
86125
),
87126
onRequest: fastify.authorizeFromSchema,
88127
},
@@ -177,7 +216,27 @@ const linkryRoutes: FastifyPluginAsync = async (fastify, _options) => {
177216
schema: withRoles(
178217
[AppRoles.LINKS_MANAGER, AppRoles.LINKS_ADMIN],
179218
withTags(["Linkry"], {
219+
summary: "Create short link record",
180220
body: createRequest,
221+
response: {
222+
201: {
223+
description: "The short link has been created.",
224+
headers: {
225+
Location: z
226+
.url()
227+
.min(1)
228+
.meta({
229+
description: "The resource URL for the shortened link.",
230+
example: `${fastify.environmentConfig.UserFacingUrl}/api/v1/linkry/redir/healthz`,
231+
}),
232+
},
233+
content: {
234+
"application/json": {
235+
schema: z.null(),
236+
},
237+
},
238+
},
239+
},
181240
}),
182241
),
183242
preValidation: async (request, reply) => {
@@ -450,7 +509,13 @@ const linkryRoutes: FastifyPluginAsync = async (fastify, _options) => {
450509
message: `Created redirect to "${request.body.redirect}"`,
451510
},
452511
});
453-
return reply.status(201).send();
512+
return reply
513+
.header(
514+
"Location",
515+
`${fastify.environmentConfig.UserFacingUrl}/api/v1/linkry/redir/${request.body.slug}`,
516+
)
517+
.status(201)
518+
.send();
454519
},
455520
);
456521

@@ -463,6 +528,17 @@ const linkryRoutes: FastifyPluginAsync = async (fastify, _options) => {
463528
params: z.object({
464529
slug: linkrySlug,
465530
}),
531+
summary: "Get a short link record",
532+
response: {
533+
200: {
534+
description: "The short link record has been retrieved.",
535+
content: {
536+
"application/json": {
537+
schema: linkryRecordWithOwner,
538+
},
539+
},
540+
},
541+
},
466542
}),
467543
),
468544
onRequest: fastify.authorizeFromSchema,
@@ -512,9 +588,20 @@ const linkryRoutes: FastifyPluginAsync = async (fastify, _options) => {
512588
schema: withRoles(
513589
[AppRoles.LINKS_MANAGER, AppRoles.LINKS_ADMIN],
514590
withTags(["Linkry"], {
591+
summary: "Delete a short link record",
515592
params: z.object({
516593
slug: linkrySlug,
517594
}),
595+
response: {
596+
204: {
597+
description: "The short link record has been deleted.",
598+
content: {
599+
"application/json": {
600+
schema: z.null(),
601+
},
602+
},
603+
},
604+
},
518605
}),
519606
),
520607
onRequest: async (request, reply) => {

0 commit comments

Comments
 (0)