Skip to content

New API Docs #211

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/deploy-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
name: Run Unit Tests
steps:
- uses: actions/checkout@v4
Expand All @@ -35,6 +36,7 @@ jobs:

build:
runs-on: ubuntu-24.04-arm
timeout-minutes: 15
name: Build Application
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -74,6 +76,7 @@ jobs:

deploy-test-dev:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
id-token: write
contents: read
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/deploy-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
name: Run Unit Tests
steps:
- uses: actions/checkout@v4
Expand All @@ -34,6 +35,7 @@ jobs:

build:
runs-on: ubuntu-24.04-arm
timeout-minutes: 15
name: Build Application
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -73,6 +75,7 @@ jobs:

deploy-prod:
runs-on: ubuntu-latest
timeout-minutes: 30
name: Deploy to Prod and Run Health Check
concurrency:
group: ${{ github.event.repository.name }}-prod
Expand Down
154 changes: 144 additions & 10 deletions src/api/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,6 @@ export const acmCoreOrganization = z
examples: ["ACM", "Infrastructure Committee"],
});

export function withTags<T extends FastifyZodOpenApiSchema>(
tags: string[],
schema: T,
) {
return {
tags,
...schema,
};
}

export type RoleSchema = {
"x-required-roles": AppRoles[];
"x-disable-api-key-auth": boolean;
Expand All @@ -48,6 +38,128 @@ type RolesConfig = {
disableApiKeyAuth: boolean;
};

export function getCorrectJsonSchema<T, U>({
schema,
example,
description,
}: {
schema: T;
example: U;
description: string;
}) {
return {
description,
content: {
"application/json": {
example,
schema,
},
},
};
}

export const notAuthenticatedError = getCorrectJsonSchema({
schema: z
.object({
name: z.literal("UnauthenticatedError"),
id: z.literal(102),
message: z.string().min(1),
})
.meta({
id: "notAuthenticatedError",
}),
description: "The request could not be authenticated.",
example: {
name: "UnauthenticatedError",
id: 102,
message: "Token not found.",
},
});

export const notFoundError = getCorrectJsonSchema({
schema: z
.object({
name: z.literal("NotFoundError"),
id: z.literal(103),
message: z.string().min(1),
})
.meta({
id: "notFoundError",
}),
description: "The resource could not be found.",
example: {
name: "NotFoundError",
id: 103,
message: "{url} is not a valid URL.",
},
});

export const notAuthorizedError = getCorrectJsonSchema({
schema: z
.object({
name: z.literal("UnauthorizedError"),
id: z.literal(101),
message: z.string().min(1),
})
.meta({
id: "notAuthorizedError",
}),
description:
"The caller does not have the appropriate permissions for this task.",
example: {
name: "UnauthorizedError",
id: 101,
message: "User does not have the privileges for this task.",
},
});

export const internalServerError = getCorrectJsonSchema({
schema: {
content: {
"application/json": {
schema: z
.object({
name: z.literal("InternalServerError"),
id: z.literal(100),
message: z.string().min(1),
})
.meta({
id: "internalServerError",
description:
"The server encountered an error processing the request.",
}),
},
},
},
description: "The server encountered an error.",
example: {
name: "InternalServerError",
id: 100,
message:
"An internal server error occurred. Please try again or contact support.",
},
});

export const rateLimitExceededError = getCorrectJsonSchema({
schema: z
.object({
name: z.literal("RateLimitExceededError"),
id: z.literal(409),
message: z.literal("Rate limit exceeded."),
})
.meta({
id: "RateLimitExceededError",
description:
"You have sent too many requests. Check the response headers and try again.",
}),
description: "The request exceeeds the rate limit.",
example: {
name: "RateLimitExceededError",
id: 409,
message: "Rate limit exceeded.",
},
});

export function withRoles<T extends FastifyZodOpenApiSchema>(
roles: AppRoles[],
schema: T,
Expand All @@ -57,6 +169,11 @@ export function withRoles<T extends FastifyZodOpenApiSchema>(
if (!disableApiKeyAuth) {
security.push({ apiKeyAuth: [] });
}
const responses = {
401: notAuthorizedError,
403: notAuthenticatedError,
...schema.response,
};
return {
security,
"x-required-roles": roles,
Expand All @@ -66,5 +183,22 @@ export function withRoles<T extends FastifyZodOpenApiSchema>(
? `${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}` : ""}`
: "Requires valid authentication but no specific role.",
...schema,
response: responses,
};
}

export function withTags<T extends FastifyZodOpenApiSchema>(
tags: string[],
schema: T,
) {
const responses = {
500: internalServerError,
429: rateLimitExceededError,
...schema.response,
};
return {
tags,
...schema,
response: responses,
};
}
4 changes: 4 additions & 0 deletions src/api/createLambdaPackage.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export const packagesToTransfer = [
"passkit-generator",
"argon2",
"ioredis",
"fastify-zod-openapi",
"@fastify/swagger",
"zod-openapi",
"zod",
];
const filePath = `${getPath().dirname}/package.json`;
const writeFilePath = `${getPath().dirname}/package.lambda.json`;
Expand Down
52 changes: 31 additions & 21 deletions src/api/createSwagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,37 @@ import { writeFile, mkdir } from "fs/promises";
import init from "./index.js"; // Assuming this is your Fastify app initializer

const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="ACM @ UIUC Core API Docs" />
<title>ACM @ UIUC Core API</title>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/swagger-ui.css" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/[email protected]/swagger-ui-bundle.js" crossorigin></script>
<script>
window.onload = () => {
window.ui = SwaggerUIBundle({
url: '/docs/openapi.json',
dom_id: '#swagger-ui',
});
};
</script>
</body>
<!doctype html>
<html>
<head>
<title>Core API Documentation | ACM @ UIUC</title>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1" />
<meta property="og:title" content="Core API Documentation | ACM @ UIUC" />
<meta property="og:description" content="The ACM @ UIUC Core API provides services for managing chapter operations." />
<meta property="description" content="The ACM @ UIUC Core API provides services for managing chapter operations." />
<meta property="og:image" content="https://static.acm.illinois.edu/square-blue.png" />
<meta property="og:url" content="https://core.acm.illinois.edu/docs" />
</head>

<body>
<div id="app"></div>

<!-- Load the Script -->
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>

<!-- Initialize the Scalar API Reference -->
<script>
Scalar.createApiReference('#app', {
// The URL of the OpenAPI/Swagger document
url: '/docs/openapi.json',
// Avoid CORS issues
proxyUrl: 'https://proxy.scalar.com',
})
</script>
</body>
</html>
`;
/**
Expand Down
26 changes: 12 additions & 14 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ async function init(prettyPrint: boolean = false, initClients: boolean = true) {
return event.requestContext.requestId;
},
});
if (!process.env.RunEnvironment) {
process.env.RunEnvironment = "dev";
}
app.runEnvironment = process.env.RunEnvironment as RunEnvironment;
app.environmentConfig =
environmentConfig[app.runEnvironment as RunEnvironment];
app.setValidatorCompiler(validatorCompiler);
app.setSerializerCompiler(serializerCompiler);

Expand All @@ -111,8 +117,9 @@ async function init(prettyPrint: boolean = false, initClients: boolean = true) {
openapi: {
info: {
title: "ACM @ UIUC Core API",
description: "ACM @ UIUC Core Management Platform",
version: "1.0.0",
description:
"The ACM @ UIUC Core API provides services for managing chapter operations.",
version: "1.1.0",
contact: {
name: "ACM @ UIUC Infrastructure Team",
email: "[email protected]",
Expand All @@ -127,12 +134,8 @@ async function init(prettyPrint: boolean = false, initClients: boolean = true) {
},
servers: [
{
url: "https://core.acm.illinois.edu",
description: "Production API server",
},
{
url: "https://core.aws.qa.acmuiuc.org",
description: "QA API server",
url: app.environmentConfig.UserFacingUrl,
description: "Main API server",
},
],

Expand Down Expand Up @@ -215,6 +218,7 @@ async function init(prettyPrint: boolean = false, initClients: boolean = true) {
});
isSwaggerServer = true;
} catch (e) {
app.log.error(e);
app.log.warn("Fastify Swagger not created!");
}
}
Expand All @@ -229,9 +233,6 @@ async function init(prettyPrint: boolean = false, initClients: boolean = true) {
root: path.join(__dirname, "public"),
prefix: "/",
});
if (!process.env.RunEnvironment) {
process.env.RunEnvironment = "dev";
}
if (isRunningInLambda && !isSwaggerServer) {
// Serve docs from S3
app.get("/api/documentation", (_request, response) => {
Expand Down Expand Up @@ -264,9 +265,6 @@ async function init(prettyPrint: boolean = false, initClients: boolean = true) {
"Audit logging to Dynamo is disabled! Audit log statements will be logged to the console.",
);
}
app.runEnvironment = process.env.RunEnvironment as RunEnvironment;
app.environmentConfig =
environmentConfig[app.runEnvironment as RunEnvironment];
app.nodeCache = new NodeCache({ checkperiod: 30 });
if (initClients) {
app.dynamoClient = new DynamoDBClient({
Expand Down
4 changes: 2 additions & 2 deletions src/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@
"stripe": "^18.0.0",
"uuid": "^11.1.0",
"zod": "^3.25.73",
"zod-validation-error": "^3.3.1"
"zod-validation-error": "^3.3.1",
"@fastify/swagger": "^9.5.0"
},
"devDependencies": {
"@fastify/swagger": "^9.5.0",
"@fastify/swagger-ui": "^5.2.2",
"@tsconfig/node22": "^22.0.1",
"@types/aws-lambda": "^8.10.149",
Expand Down
Loading
Loading