feat: standardize OpenAPI route docs metadata (#412)
This commit is contained in:
@@ -31,6 +31,9 @@ LOG_LEVEL=warn
|
||||
|
||||
# API documentation UI + OpenAPI JSON
|
||||
# Default behavior: enabled outside production, disabled in production
|
||||
# Recommended:
|
||||
# development/staging: OPENAPI_DOCS_ENABLED=true
|
||||
# production: OPENAPI_DOCS_ENABLED=false
|
||||
# OPENAPI_DOCS_ENABLED=true
|
||||
|
||||
# Timezone for scheduled reminders (e.g., Europe/Berlin, America/New_York)
|
||||
|
||||
@@ -198,6 +198,19 @@ All configuration is done via environment variables in `.env`. Copy `.env.exampl
|
||||
| `OPENAPI_DOCS_ENABLED` | `auto` | Enables API docs in non-production by default. Set explicitly to `true`/`false` to override. |
|
||||
| `TZ` | `Europe/Berlin` | Timezone for scheduled reminders |
|
||||
|
||||
Recommended values for API docs by environment:
|
||||
|
||||
| Environment | Recommendation |
|
||||
|-------------|----------------|
|
||||
| Development | `OPENAPI_DOCS_ENABLED=true` |
|
||||
| Staging/Test | `OPENAPI_DOCS_ENABLED=true` |
|
||||
| Production | `OPENAPI_DOCS_ENABLED=false` (or keep `auto` with `NODE_ENV=production`) |
|
||||
|
||||
Notes:
|
||||
|
||||
- `auto` means: docs enabled when `NODE_ENV` is not `production`, disabled when `NODE_ENV=production`.
|
||||
- Explicitly setting `OPENAPI_DOCS_ENABLED` always overrides `auto` behavior.
|
||||
|
||||
### Authentication
|
||||
|
||||
| Variable | Default | Description |
|
||||
|
||||
@@ -18,7 +18,10 @@ const idParamSchema = z.object({
|
||||
id: z.string().regex(/^\d+$/),
|
||||
});
|
||||
|
||||
const protectedEndpointSecurity = [{ bearerAuth: [] }];
|
||||
const protectedEndpointSecurity: ReadonlyArray<Record<string, readonly string[]>> = [
|
||||
{ bearerAuth: [] },
|
||||
{ cookieAuth: [] },
|
||||
];
|
||||
const genericErrorSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
|
||||
+56
-36
@@ -6,6 +6,7 @@ import { doseTracking, medications, shareTokens } from "../db/schema.js";
|
||||
import { getAnonymousUserId, requireAuth } from "../plugins/auth.js";
|
||||
import { env } from "../plugins/env.js";
|
||||
import type { AuthUser } from "../types/fastify.js";
|
||||
import { applyOpenApiRouteStandards } from "../utils/openapi-route-standards.js";
|
||||
import { parseIntakesJson, parseTakenByJson, personTakesMedication } from "../utils/scheduler-utils.js";
|
||||
|
||||
// =============================================================================
|
||||
@@ -23,6 +24,11 @@ const dismissDosesSchema = z.object({
|
||||
doseIds: z.array(z.string().min(1)).min(1, "At least one doseId is required"),
|
||||
});
|
||||
|
||||
const protectedEndpointSecurity: ReadonlyArray<Record<string, readonly string[]>> = [
|
||||
{ bearerAuth: [] },
|
||||
{ cookieAuth: [] },
|
||||
];
|
||||
|
||||
const doseIdPattern = /^(\d+)-(\d+)-(\d+)(?:-(.+))?$/;
|
||||
|
||||
function maskToken(token: string): string {
|
||||
@@ -135,33 +141,43 @@ async function validateShareDoseId(share: typeof shareTokens.$inferSelect, doseI
|
||||
// Dose Tracking Routes
|
||||
// =============================================================================
|
||||
export async function doseRoutes(app: FastifyInstance) {
|
||||
applyOpenApiRouteStandards(app, {
|
||||
tag: "doses",
|
||||
protectedByDefault: false,
|
||||
protectedPaths: [/^\/doses\/taken$/, /^\/doses\/taken\/:doseId$/, /^\/doses\/dismiss$/],
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /doses/taken - PROTECTED: Get all taken doses for the user
|
||||
// Suppress request logs — polled every 5s by frontend
|
||||
// ---------------------------------------------------------------------------
|
||||
app.get("/doses/taken", { preHandler: requireAuth, logLevel: "warn" }, async (request, reply) => {
|
||||
const userId = await getUserId(request, reply);
|
||||
app.get(
|
||||
"/doses/taken",
|
||||
{ preHandler: requireAuth, logLevel: "warn", schema: { tags: ["doses"], security: protectedEndpointSecurity } },
|
||||
async (request, reply) => {
|
||||
const userId = await getUserId(request, reply);
|
||||
|
||||
// Get all taken doses for this user (no time limit)
|
||||
const doses = await db.select().from(doseTracking).where(eq(doseTracking.userId, userId));
|
||||
// Get all taken doses for this user (no time limit)
|
||||
const doses = await db.select().from(doseTracking).where(eq(doseTracking.userId, userId));
|
||||
|
||||
return {
|
||||
doses: doses.map((d) => ({
|
||||
doseId: d.doseId,
|
||||
takenAt: d.takenAt?.getTime() ?? Date.now(),
|
||||
markedBy: d.markedBy,
|
||||
takenSource: d.takenSource ?? "manual",
|
||||
dismissed: d.dismissed ?? false,
|
||||
})),
|
||||
};
|
||||
});
|
||||
return {
|
||||
doses: doses.map((d) => ({
|
||||
doseId: d.doseId,
|
||||
takenAt: d.takenAt?.getTime() ?? Date.now(),
|
||||
markedBy: d.markedBy,
|
||||
takenSource: d.takenSource ?? "manual",
|
||||
dismissed: d.dismissed ?? false,
|
||||
})),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// POST /doses/taken - PROTECTED: Mark a dose as taken
|
||||
// ---------------------------------------------------------------------------
|
||||
app.post<{ Body: z.infer<typeof markDoseSchema> }>(
|
||||
"/doses/taken",
|
||||
{ preHandler: requireAuth },
|
||||
{ preHandler: requireAuth, schema: { tags: ["doses"], security: protectedEndpointSecurity } },
|
||||
async (request, reply) => {
|
||||
const userId = await getUserId(request, reply);
|
||||
|
||||
@@ -201,7 +217,7 @@ export async function doseRoutes(app: FastifyInstance) {
|
||||
// ---------------------------------------------------------------------------
|
||||
app.delete<{ Params: { doseId: string } }>(
|
||||
"/doses/taken/:doseId",
|
||||
{ preHandler: requireAuth },
|
||||
{ preHandler: requireAuth, schema: { tags: ["doses"], security: protectedEndpointSecurity } },
|
||||
async (request, reply) => {
|
||||
const userId = await getUserId(request, reply);
|
||||
|
||||
@@ -230,7 +246,7 @@ export async function doseRoutes(app: FastifyInstance) {
|
||||
// ---------------------------------------------------------------------------
|
||||
app.post<{ Body: z.infer<typeof dismissDosesSchema> }>(
|
||||
"/doses/dismiss",
|
||||
{ preHandler: requireAuth },
|
||||
{ preHandler: requireAuth, schema: { tags: ["doses"], security: protectedEndpointSecurity } },
|
||||
async (request, reply) => {
|
||||
const userId = await getUserId(request, reply);
|
||||
|
||||
@@ -281,30 +297,34 @@ export async function doseRoutes(app: FastifyInstance) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// DELETE /doses/dismiss - PROTECTED: Clear all dismissed doses (un-dismiss)
|
||||
// ---------------------------------------------------------------------------
|
||||
app.delete("/doses/dismiss", { preHandler: requireAuth }, async (request, reply) => {
|
||||
const userId = await getUserId(request, reply);
|
||||
app.delete(
|
||||
"/doses/dismiss",
|
||||
{ preHandler: requireAuth, schema: { tags: ["doses"], security: protectedEndpointSecurity } },
|
||||
async (request, reply) => {
|
||||
const userId = await getUserId(request, reply);
|
||||
|
||||
// Delete all dismissed-only records (not taken ones)
|
||||
// For taken+dismissed, just remove the dismissed flag
|
||||
const dismissed = await db
|
||||
.select()
|
||||
.from(doseTracking)
|
||||
.where(and(eq(doseTracking.userId, userId), eq(doseTracking.dismissed, true)));
|
||||
// Delete all dismissed-only records (not taken ones)
|
||||
// For taken+dismissed, just remove the dismissed flag
|
||||
const dismissed = await db
|
||||
.select()
|
||||
.from(doseTracking)
|
||||
.where(and(eq(doseTracking.userId, userId), eq(doseTracking.dismissed, true)));
|
||||
|
||||
for (const d of dismissed) {
|
||||
const hasRealTakenTimestamp = d.takenAt instanceof Date ? d.takenAt.getTime() > 0 : Boolean(d.takenAt);
|
||||
for (const d of dismissed) {
|
||||
const hasRealTakenTimestamp = d.takenAt instanceof Date ? d.takenAt.getTime() > 0 : Boolean(d.takenAt);
|
||||
|
||||
if (d.markedBy !== null || hasRealTakenTimestamp) {
|
||||
// This was also marked as taken - just remove dismissed flag
|
||||
await db.update(doseTracking).set({ dismissed: false }).where(eq(doseTracking.id, d.id));
|
||||
} else {
|
||||
// This was only dismissed - delete it
|
||||
await db.delete(doseTracking).where(eq(doseTracking.id, d.id));
|
||||
if (d.markedBy !== null || hasRealTakenTimestamp) {
|
||||
// This was also marked as taken - just remove dismissed flag
|
||||
await db.update(doseTracking).set({ dismissed: false }).where(eq(doseTracking.id, d.id));
|
||||
} else {
|
||||
// This was only dismissed - delete it
|
||||
await db.delete(doseTracking).where(eq(doseTracking.id, d.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, clearedCount: dismissed.length };
|
||||
});
|
||||
return { success: true, clearedCount: dismissed.length };
|
||||
}
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /share/:token/doses - PUBLIC: Get taken doses for a share link
|
||||
|
||||
@@ -10,6 +10,7 @@ import { doseTracking, medications, refillHistory, shareTokens, userSettings } f
|
||||
import { getAnonymousUserId, requireAuth } from "../plugins/auth.js";
|
||||
import { env } from "../plugins/env.js";
|
||||
import type { AuthUser } from "../types/fastify.js";
|
||||
import { applyOpenApiRouteStandards } from "../utils/openapi-route-standards.js";
|
||||
import { normalizePackageType, PACKAGE_TYPES } from "../utils/package-profiles.js";
|
||||
import { parseIntakesJson, parseTakenByJson } from "../utils/scheduler-utils.js";
|
||||
|
||||
@@ -272,6 +273,7 @@ function buildDoseId(medicationId: number, blisterIndex: number, timestampMs: nu
|
||||
export async function exportRoutes(app: FastifyInstance) {
|
||||
// All export routes require auth
|
||||
app.addHook("preHandler", requireAuth);
|
||||
applyOpenApiRouteStandards(app, { tag: "export", protectedByDefault: true });
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /export - Export all user data
|
||||
|
||||
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { applyOpenApiRouteStandards } from "../utils/openapi-route-standards.js";
|
||||
|
||||
// Read version from package.json at startup
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
@@ -10,6 +11,8 @@ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
||||
const backendVersion = packageJson.version || "unknown";
|
||||
|
||||
export async function healthRoutes(app: FastifyInstance) {
|
||||
applyOpenApiRouteStandards(app, { tag: "health", protectedByDefault: false });
|
||||
|
||||
// Exempt from rate limit + suppress request logs (called every 30s by Docker healthcheck)
|
||||
app.get("/health", { config: { rateLimit: false }, logLevel: "warn" }, async () => ({
|
||||
status: "ok",
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
streamToBuffer,
|
||||
writeOptimizedImageSet,
|
||||
} from "../utils/image-upload.js";
|
||||
import { applyOpenApiRouteStandards } from "../utils/openapi-route-standards.js";
|
||||
import {
|
||||
isAmountBasedPackageType,
|
||||
isLiquidContainerPackageType,
|
||||
@@ -240,6 +241,7 @@ const medicationSchema = z
|
||||
export async function medicationRoutes(app: FastifyInstance) {
|
||||
// All medication routes require auth
|
||||
app.addHook("preHandler", requireAuth);
|
||||
applyOpenApiRouteStandards(app, { tag: "medications", protectedByDefault: true });
|
||||
|
||||
// Helper to get user ID from request
|
||||
// Returns anonymous user ID when auth is disabled
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as client from "openid-client";
|
||||
import { db } from "../db/client.js";
|
||||
import { refreshTokens, users } from "../db/schema.js";
|
||||
import { env } from "../plugins/env.js";
|
||||
import { applyOpenApiRouteStandards } from "../utils/openapi-route-standards.js";
|
||||
|
||||
// =============================================================================
|
||||
// OIDC Configuration Cache
|
||||
@@ -49,6 +50,8 @@ function getFrontendUrl(): string {
|
||||
// OIDC Routes
|
||||
// =============================================================================
|
||||
export async function oidcRoutes(app: FastifyInstance) {
|
||||
applyOpenApiRouteStandards(app, { tag: "auth", protectedByDefault: false });
|
||||
|
||||
if (!env.OIDC_ENABLED) {
|
||||
// Register a disabled route that returns an error
|
||||
app.get("/auth/oidc/login", async (_request, reply) => {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { getAnonymousUserId, requireAuth } from "../plugins/auth.js";
|
||||
import { env } from "../plugins/env.js";
|
||||
import { updateReminderSentTime, updateUserReminderSentTime } from "../services/reminder-scheduler.js";
|
||||
import type { AuthUser } from "../types/fastify.js";
|
||||
import { applyOpenApiRouteStandards } from "../utils/openapi-route-standards.js";
|
||||
import {
|
||||
getPlannerUnitKind,
|
||||
isAmountBasedPackageType,
|
||||
@@ -134,6 +135,7 @@ type PrescriptionReminderBody = {
|
||||
export async function plannerRoutes(app: FastifyInstance) {
|
||||
// Add auth hook for all planner routes
|
||||
app.addHook("preHandler", requireAuth);
|
||||
applyOpenApiRouteStandards(app, { tag: "planner", protectedByDefault: true });
|
||||
|
||||
// Helper to get user ID from request
|
||||
async function getUserId(request: FastifyRequest): Promise<number> {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { medications, refillHistory } from "../db/schema.js";
|
||||
import { getAnonymousUserId, requireAuth } from "../plugins/auth.js";
|
||||
import { env } from "../plugins/env.js";
|
||||
import type { AuthUser } from "../types/fastify.js";
|
||||
import { applyOpenApiRouteStandards } from "../utils/openapi-route-standards.js";
|
||||
import { isAmountBasedPackageType, normalizePackageType } from "../utils/package-profiles.js";
|
||||
|
||||
const refillSchema = z
|
||||
@@ -21,6 +22,7 @@ const refillSchema = z
|
||||
export async function refillRoutes(app: FastifyInstance) {
|
||||
// All refill routes require auth
|
||||
app.addHook("preHandler", requireAuth);
|
||||
applyOpenApiRouteStandards(app, { tag: "refills", protectedByDefault: true });
|
||||
|
||||
// Helper to get user ID from request
|
||||
async function getUserId(request: FastifyRequest, reply: FastifyReply): Promise<number> {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { doseTracking, medications, refillHistory } from "../db/schema.js";
|
||||
import { getAnonymousUserId, requireAuth } from "../plugins/auth.js";
|
||||
import { env } from "../plugins/env.js";
|
||||
import type { AuthUser } from "../types/fastify.js";
|
||||
import { applyOpenApiRouteStandards } from "../utils/openapi-route-standards.js";
|
||||
|
||||
const reportDataSchema = z.object({
|
||||
medicationIds: z.array(z.number().int().positive()).min(1).max(100),
|
||||
@@ -13,6 +14,7 @@ const reportDataSchema = z.object({
|
||||
|
||||
export async function reportRoutes(app: FastifyInstance) {
|
||||
app.addHook("preHandler", requireAuth);
|
||||
applyOpenApiRouteStandards(app, { tag: "report", protectedByDefault: true });
|
||||
|
||||
async function getUserId(request: FastifyRequest, reply: FastifyReply): Promise<number> {
|
||||
if (!env.AUTH_ENABLED) {
|
||||
|
||||
+46
-30
@@ -8,6 +8,7 @@ import { getAnonymousUserId, requireAuth } from "../plugins/auth.js";
|
||||
import { env } from "../plugins/env.js";
|
||||
import { buildSharedMedicationOverview } from "../services/coverage.js";
|
||||
import type { AuthUser } from "../types/fastify.js";
|
||||
import { applyOpenApiRouteStandards } from "../utils/openapi-route-standards.js";
|
||||
import { isAmountBasedPackageType, normalizePackageType } from "../utils/package-profiles.js";
|
||||
import {
|
||||
getAllTakenByForMedication,
|
||||
@@ -24,6 +25,11 @@ const createShareSchema = z.object({
|
||||
scheduleDays: z.number().int().min(1).max(365).default(30),
|
||||
});
|
||||
|
||||
const protectedEndpointSecurity: ReadonlyArray<Record<string, readonly string[]>> = [
|
||||
{ bearerAuth: [] },
|
||||
{ cookieAuth: [] },
|
||||
];
|
||||
|
||||
const shareTokenPattern = /^[a-f0-9]{16}$/;
|
||||
|
||||
function maskToken(token: string): string {
|
||||
@@ -51,6 +57,12 @@ async function getUserId(request: FastifyRequest, reply: FastifyReply): Promise<
|
||||
// Share Routes
|
||||
// =============================================================================
|
||||
export async function shareRoutes(app: FastifyInstance) {
|
||||
applyOpenApiRouteStandards(app, {
|
||||
tag: "share",
|
||||
protectedByDefault: false,
|
||||
protectedPaths: [/^\/share$/, /^\/share\/people$/],
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /share/:token - PUBLIC: Get shared schedule by token
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -256,7 +268,7 @@ export async function shareRoutes(app: FastifyInstance) {
|
||||
// ---------------------------------------------------------------------------
|
||||
app.post<{ Body: z.infer<typeof createShareSchema> }>(
|
||||
"/share",
|
||||
{ preHandler: requireAuth },
|
||||
{ preHandler: requireAuth, schema: { tags: ["share"], security: protectedEndpointSecurity } },
|
||||
async (request, reply) => {
|
||||
const userId = await getUserId(request, reply);
|
||||
|
||||
@@ -337,37 +349,41 @@ export async function shareRoutes(app: FastifyInstance) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /share/people - PROTECTED: Get list of unique takenBy values
|
||||
// ---------------------------------------------------------------------------
|
||||
app.get("/share/people", { preHandler: requireAuth }, async (request, reply) => {
|
||||
const userId = await getUserId(request, reply);
|
||||
app.get(
|
||||
"/share/people",
|
||||
{ preHandler: requireAuth, schema: { tags: ["share"], security: protectedEndpointSecurity } },
|
||||
async (request, reply) => {
|
||||
const userId = await getUserId(request, reply);
|
||||
|
||||
// Get all unique takenBy values for this user (from both medication-level and intake-level)
|
||||
const meds = await db
|
||||
.select({
|
||||
takenByJson: medications.takenByJson,
|
||||
intakesJson: medications.intakesJson,
|
||||
usageJson: medications.usageJson,
|
||||
everyJson: medications.everyJson,
|
||||
startJson: medications.startJson,
|
||||
intakeRemindersEnabled: medications.intakeRemindersEnabled,
|
||||
})
|
||||
.from(medications)
|
||||
.where(eq(medications.userId, userId));
|
||||
// Get all unique takenBy values for this user (from both medication-level and intake-level)
|
||||
const meds = await db
|
||||
.select({
|
||||
takenByJson: medications.takenByJson,
|
||||
intakesJson: medications.intakesJson,
|
||||
usageJson: medications.usageJson,
|
||||
everyJson: medications.everyJson,
|
||||
startJson: medications.startJson,
|
||||
intakeRemindersEnabled: medications.intakeRemindersEnabled,
|
||||
})
|
||||
.from(medications)
|
||||
.where(eq(medications.userId, userId));
|
||||
|
||||
// Collect all unique person names from medication-level AND intake-level takenBy
|
||||
const allPeople = new Set<string>();
|
||||
for (const med of meds) {
|
||||
const takenByArray = parseTakenByJson(med.takenByJson);
|
||||
const intakes = parseIntakesJson(
|
||||
med.intakesJson,
|
||||
{ usageJson: med.usageJson, everyJson: med.everyJson, startJson: med.startJson },
|
||||
med.intakeRemindersEnabled ?? false
|
||||
);
|
||||
const allForMed = getAllTakenByForMedication(takenByArray, intakes);
|
||||
for (const person of allForMed) {
|
||||
if (person) allPeople.add(person);
|
||||
// Collect all unique person names from medication-level AND intake-level takenBy
|
||||
const allPeople = new Set<string>();
|
||||
for (const med of meds) {
|
||||
const takenByArray = parseTakenByJson(med.takenByJson);
|
||||
const intakes = parseIntakesJson(
|
||||
med.intakesJson,
|
||||
{ usageJson: med.usageJson, everyJson: med.everyJson, startJson: med.startJson },
|
||||
med.intakeRemindersEnabled ?? false
|
||||
);
|
||||
const allForMed = getAllTakenByForMedication(takenByArray, intakes);
|
||||
for (const person of allForMed) {
|
||||
if (person) allPeople.add(person);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { people: [...allPeople].sort() };
|
||||
});
|
||||
return { people: [...allPeople].sort() };
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { FastifyInstance, RouteOptions } from "fastify";
|
||||
|
||||
type SecurityEntry = Readonly<Record<string, readonly string[]>>;
|
||||
|
||||
const defaultProtectedSecurity: readonly SecurityEntry[] = [{ bearerAuth: [] }, { cookieAuth: [] }];
|
||||
|
||||
export type OpenApiRouteStandardsOptions = {
|
||||
tag: string;
|
||||
protectedByDefault: boolean;
|
||||
protectedPaths?: RegExp[];
|
||||
publicPaths?: RegExp[];
|
||||
};
|
||||
|
||||
function asMethods(method: RouteOptions["method"]): string[] {
|
||||
if (Array.isArray(method)) return method.map((m) => String(m).toUpperCase());
|
||||
return [String(method).toUpperCase()];
|
||||
}
|
||||
|
||||
function pathMatches(path: string, patterns: RegExp[] | undefined): boolean {
|
||||
if (!patterns || patterns.length === 0) return false;
|
||||
return patterns.some((pattern) => pattern.test(path));
|
||||
}
|
||||
|
||||
function buildDefaultSummary(methods: string[], path: string): string {
|
||||
const methodText = methods.join("/");
|
||||
return `${methodText} ${path}`;
|
||||
}
|
||||
|
||||
function buildDefaultDescription(requiresAuth: boolean): string {
|
||||
return requiresAuth
|
||||
? "Protected endpoint. Requires Bearer token (API key or JWT) or session cookie."
|
||||
: "Public endpoint.";
|
||||
}
|
||||
|
||||
export function applyOpenApiRouteStandards(app: FastifyInstance, options: OpenApiRouteStandardsOptions): void {
|
||||
app.addHook("onRoute", (routeOptions) => {
|
||||
const methods = asMethods(routeOptions.method);
|
||||
const path = routeOptions.url;
|
||||
|
||||
const isExplicitlyPublic = pathMatches(path, options.publicPaths);
|
||||
const isExplicitlyProtected = pathMatches(path, options.protectedPaths);
|
||||
let requiresAuth = options.protectedByDefault;
|
||||
if (isExplicitlyPublic) {
|
||||
requiresAuth = false;
|
||||
} else if (isExplicitlyProtected) {
|
||||
requiresAuth = true;
|
||||
}
|
||||
|
||||
routeOptions.schema = routeOptions.schema ?? {};
|
||||
routeOptions.schema.tags = routeOptions.schema.tags ?? [options.tag];
|
||||
routeOptions.schema.summary = routeOptions.schema.summary ?? buildDefaultSummary(methods, path);
|
||||
routeOptions.schema.description = routeOptions.schema.description ?? buildDefaultDescription(requiresAuth);
|
||||
|
||||
if (requiresAuth) {
|
||||
routeOptions.schema.security = routeOptions.schema.security ?? defaultProtectedSecurity;
|
||||
routeOptions.schema.response = {
|
||||
...(routeOptions.schema.response ?? {}),
|
||||
401: (routeOptions.schema.response as Record<number | string, unknown> | undefined)?.[401] ?? {
|
||||
type: "object",
|
||||
properties: {
|
||||
error: { type: "string" },
|
||||
code: { type: "string" },
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (routeOptions.schema.security === undefined) {
|
||||
routeOptions.schema.security = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user