Skip to content

Commit bbe27c9

Browse files
authored
New API Docs (#211)
1 parent a200ab9 commit bbe27c9

File tree

11 files changed

+303
-58
lines changed

11 files changed

+303
-58
lines changed

.github/workflows/deploy-dev.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ on:
1010
jobs:
1111
test:
1212
runs-on: ubuntu-latest
13+
timeout-minutes: 15
1314
name: Run Unit Tests
1415
steps:
1516
- uses: actions/checkout@v4
@@ -35,6 +36,7 @@ jobs:
3536

3637
build:
3738
runs-on: ubuntu-24.04-arm
39+
timeout-minutes: 15
3840
name: Build Application
3941
steps:
4042
- uses: actions/checkout@v4
@@ -74,6 +76,7 @@ jobs:
7476
7577
deploy-test-dev:
7678
runs-on: ubuntu-latest
79+
timeout-minutes: 30
7780
permissions:
7881
id-token: write
7982
contents: read

.github/workflows/deploy-prod.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ on:
99
jobs:
1010
test:
1111
runs-on: ubuntu-latest
12+
timeout-minutes: 15
1213
name: Run Unit Tests
1314
steps:
1415
- uses: actions/checkout@v4
@@ -34,6 +35,7 @@ jobs:
3435

3536
build:
3637
runs-on: ubuntu-24.04-arm
38+
timeout-minutes: 15
3739
name: Build Application
3840
steps:
3941
- uses: actions/checkout@v4
@@ -73,6 +75,7 @@ jobs:
7375
7476
deploy-prod:
7577
runs-on: ubuntu-latest
78+
timeout-minutes: 30
7679
name: Deploy to Prod and Run Health Check
7780
concurrency:
7881
group: ${{ github.event.repository.name }}-prod

src/api/components/index.ts

Lines changed: 144 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,6 @@ export const acmCoreOrganization = z
2828
examples: ["ACM", "Infrastructure Committee"],
2929
});
3030

31-
export function withTags<T extends FastifyZodOpenApiSchema>(
32-
tags: string[],
33-
schema: T,
34-
) {
35-
return {
36-
tags,
37-
...schema,
38-
};
39-
}
40-
4131
export type RoleSchema = {
4232
"x-required-roles": AppRoles[];
4333
"x-disable-api-key-auth": boolean;
@@ -48,6 +38,128 @@ type RolesConfig = {
4838
disableApiKeyAuth: boolean;
4939
};
5040

41+
export function getCorrectJsonSchema<T, U>({
42+
schema,
43+
example,
44+
description,
45+
}: {
46+
schema: T;
47+
example: U;
48+
description: string;
49+
}) {
50+
return {
51+
description,
52+
content: {
53+
"application/json": {
54+
example,
55+
schema,
56+
},
57+
},
58+
};
59+
}
60+
61+
export const notAuthenticatedError = getCorrectJsonSchema({
62+
schema: z
63+
.object({
64+
name: z.literal("UnauthenticatedError"),
65+
id: z.literal(102),
66+
message: z.string().min(1),
67+
})
68+
.meta({
69+
id: "notAuthenticatedError",
70+
}),
71+
description: "The request could not be authenticated.",
72+
example: {
73+
name: "UnauthenticatedError",
74+
id: 102,
75+
message: "Token not found.",
76+
},
77+
});
78+
79+
export const notFoundError = getCorrectJsonSchema({
80+
schema: z
81+
.object({
82+
name: z.literal("NotFoundError"),
83+
id: z.literal(103),
84+
message: z.string().min(1),
85+
})
86+
.meta({
87+
id: "notFoundError",
88+
}),
89+
description: "The resource could not be found.",
90+
example: {
91+
name: "NotFoundError",
92+
id: 103,
93+
message: "{url} is not a valid URL.",
94+
},
95+
});
96+
97+
export const notAuthorizedError = getCorrectJsonSchema({
98+
schema: z
99+
.object({
100+
name: z.literal("UnauthorizedError"),
101+
id: z.literal(101),
102+
message: z.string().min(1),
103+
})
104+
.meta({
105+
id: "notAuthorizedError",
106+
}),
107+
description:
108+
"The caller does not have the appropriate permissions for this task.",
109+
example: {
110+
name: "UnauthorizedError",
111+
id: 101,
112+
message: "User does not have the privileges for this task.",
113+
},
114+
});
115+
116+
export const internalServerError = getCorrectJsonSchema({
117+
schema: {
118+
content: {
119+
"application/json": {
120+
schema: z
121+
.object({
122+
name: z.literal("InternalServerError"),
123+
id: z.literal(100),
124+
message: z.string().min(1),
125+
})
126+
.meta({
127+
id: "internalServerError",
128+
description:
129+
"The server encountered an error processing the request.",
130+
}),
131+
},
132+
},
133+
},
134+
description: "The server encountered an error.",
135+
example: {
136+
name: "InternalServerError",
137+
id: 100,
138+
message:
139+
"An internal server error occurred. Please try again or contact support.",
140+
},
141+
});
142+
143+
export const rateLimitExceededError = getCorrectJsonSchema({
144+
schema: z
145+
.object({
146+
name: z.literal("RateLimitExceededError"),
147+
id: z.literal(409),
148+
message: z.literal("Rate limit exceeded."),
149+
})
150+
.meta({
151+
id: "RateLimitExceededError",
152+
description:
153+
"You have sent too many requests. Check the response headers and try again.",
154+
}),
155+
description: "The request exceeeds the rate limit.",
156+
example: {
157+
name: "RateLimitExceededError",
158+
id: 409,
159+
message: "Rate limit exceeded.",
160+
},
161+
});
162+
51163
export function withRoles<T extends FastifyZodOpenApiSchema>(
52164
roles: AppRoles[],
53165
schema: T,
@@ -57,6 +169,11 @@ export function withRoles<T extends FastifyZodOpenApiSchema>(
57169
if (!disableApiKeyAuth) {
58170
security.push({ apiKeyAuth: [] });
59171
}
172+
const responses = {
173+
401: notAuthorizedError,
174+
403: notAuthenticatedError,
175+
...schema.response,
176+
};
60177
return {
61178
security,
62179
"x-required-roles": roles,
@@ -66,5 +183,22 @@ export function withRoles<T extends FastifyZodOpenApiSchema>(
66183
? `${disableApiKeyAuth ? "API key authentication is not permitted for this route.\n\n" : ""}Requires one of the following roles: ${roles.join(", ")}.${schema.description ? `\n\n${schema.description}` : ""}`
67184
: "Requires valid authentication but no specific role.",
68185
...schema,
186+
response: responses,
187+
};
188+
}
189+
190+
export function withTags<T extends FastifyZodOpenApiSchema>(
191+
tags: string[],
192+
schema: T,
193+
) {
194+
const responses = {
195+
500: internalServerError,
196+
429: rateLimitExceededError,
197+
...schema.response,
198+
};
199+
return {
200+
tags,
201+
...schema,
202+
response: responses,
69203
};
70204
}

src/api/createLambdaPackage.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ export const packagesToTransfer = [
1515
"passkit-generator",
1616
"argon2",
1717
"ioredis",
18+
"fastify-zod-openapi",
19+
"@fastify/swagger",
20+
"zod-openapi",
21+
"zod",
1822
];
1923
const filePath = `${getPath().dirname}/package.json`;
2024
const writeFilePath = `${getPath().dirname}/package.lambda.json`;

src/api/createSwagger.ts

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,37 @@ import { writeFile, mkdir } from "fs/promises";
44
import init from "./index.js"; // Assuming this is your Fastify app initializer
55

66
const html = `
7-
<!DOCTYPE html>
8-
<html lang="en">
9-
<head>
10-
<meta charset="utf-8" />
11-
<meta name="viewport" content="width=device-width, initial-scale=1" />
12-
<meta name="description" content="ACM @ UIUC Core API Docs" />
13-
<title>ACM @ UIUC Core API</title>
14-
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css" />
15-
</head>
16-
<body>
17-
<div id="swagger-ui"></div>
18-
<script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js" crossorigin></script>
19-
<script>
20-
window.onload = () => {
21-
window.ui = SwaggerUIBundle({
22-
url: '/docs/openapi.json',
23-
dom_id: '#swagger-ui',
24-
});
25-
};
26-
</script>
27-
</body>
7+
<!doctype html>
8+
<html>
9+
<head>
10+
<title>Core API Documentation | ACM @ UIUC</title>
11+
<meta charset="utf-8" />
12+
<meta
13+
name="viewport"
14+
content="width=device-width, initial-scale=1" />
15+
<meta property="og:title" content="Core API Documentation | ACM @ UIUC" />
16+
<meta property="og:description" content="The ACM @ UIUC Core API provides services for managing chapter operations." />
17+
<meta property="description" content="The ACM @ UIUC Core API provides services for managing chapter operations." />
18+
<meta property="og:image" content="https://static.acm.illinois.edu/square-blue.png" />
19+
<meta property="og:url" content="https://core.acm.illinois.edu/docs" />
20+
</head>
21+
22+
<body>
23+
<div id="app"></div>
24+
25+
<!-- Load the Script -->
26+
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
27+
28+
<!-- Initialize the Scalar API Reference -->
29+
<script>
30+
Scalar.createApiReference('#app', {
31+
// The URL of the OpenAPI/Swagger document
32+
url: '/docs/openapi.json',
33+
// Avoid CORS issues
34+
proxyUrl: 'https://proxy.scalar.com',
35+
})
36+
</script>
37+
</body>
2838
</html>
2939
`;
3040
/**

src/api/index.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@ async function init(prettyPrint: boolean = false, initClients: boolean = true) {
100100
return event.requestContext.requestId;
101101
},
102102
});
103+
if (!process.env.RunEnvironment) {
104+
process.env.RunEnvironment = "dev";
105+
}
106+
app.runEnvironment = process.env.RunEnvironment as RunEnvironment;
107+
app.environmentConfig =
108+
environmentConfig[app.runEnvironment as RunEnvironment];
103109
app.setValidatorCompiler(validatorCompiler);
104110
app.setSerializerCompiler(serializerCompiler);
105111

@@ -111,8 +117,9 @@ async function init(prettyPrint: boolean = false, initClients: boolean = true) {
111117
openapi: {
112118
info: {
113119
title: "ACM @ UIUC Core API",
114-
description: "ACM @ UIUC Core Management Platform",
115-
version: "1.0.0",
120+
description:
121+
"The ACM @ UIUC Core API provides services for managing chapter operations.",
122+
version: "1.1.0",
116123
contact: {
117124
name: "ACM @ UIUC Infrastructure Team",
118125
email: "infra@acm.illinois.edu",
@@ -127,12 +134,8 @@ async function init(prettyPrint: boolean = false, initClients: boolean = true) {
127134
},
128135
servers: [
129136
{
130-
url: "https://core.acm.illinois.edu",
131-
description: "Production API server",
132-
},
133-
{
134-
url: "https://core.aws.qa.acmuiuc.org",
135-
description: "QA API server",
137+
url: app.environmentConfig.UserFacingUrl,
138+
description: "Main API server",
136139
},
137140
],
138141

@@ -215,6 +218,7 @@ async function init(prettyPrint: boolean = false, initClients: boolean = true) {
215218
});
216219
isSwaggerServer = true;
217220
} catch (e) {
221+
app.log.error(e);
218222
app.log.warn("Fastify Swagger not created!");
219223
}
220224
}
@@ -229,9 +233,6 @@ async function init(prettyPrint: boolean = false, initClients: boolean = true) {
229233
root: path.join(__dirname, "public"),
230234
prefix: "/",
231235
});
232-
if (!process.env.RunEnvironment) {
233-
process.env.RunEnvironment = "dev";
234-
}
235236
if (isRunningInLambda && !isSwaggerServer) {
236237
// Serve docs from S3
237238
app.get("/api/documentation", (_request, response) => {
@@ -264,9 +265,6 @@ async function init(prettyPrint: boolean = false, initClients: boolean = true) {
264265
"Audit logging to Dynamo is disabled! Audit log statements will be logged to the console.",
265266
);
266267
}
267-
app.runEnvironment = process.env.RunEnvironment as RunEnvironment;
268-
app.environmentConfig =
269-
environmentConfig[app.runEnvironment as RunEnvironment];
270268
app.nodeCache = new NodeCache({ checkperiod: 30 });
271269
if (initClients) {
272270
app.dynamoClient = new DynamoDBClient({

src/api/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,10 @@
5959
"stripe": "^18.0.0",
6060
"uuid": "^11.1.0",
6161
"zod": "^3.25.73",
62-
"zod-validation-error": "^3.3.1"
62+
"zod-validation-error": "^3.3.1",
63+
"@fastify/swagger": "^9.5.0"
6364
},
6465
"devDependencies": {
65-
"@fastify/swagger": "^9.5.0",
6666
"@fastify/swagger-ui": "^5.2.2",
6767
"@tsconfig/node22": "^22.0.1",
6868
"@types/aws-lambda": "^8.10.149",

0 commit comments

Comments
 (0)