From b8057a17835f29af859feb13aedc55674733f9f1 Mon Sep 17 00:00:00 2001 From: JackAttack-365 <142643773+jackattack-4@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:25:59 -0800 Subject: [PATCH 01/11] starting api --- package-lock.json | 95 +++++++++ package.json | 4 + src/app.ts | 10 + src/lib/openapi.ts | 44 ++++ src/lib/prisma-zod.ts | 267 +++++++++++++++++++++++++ src/routes/analysis/analysis.routes.ts | 152 ++++++++++++++ src/routes/manager/manager.routes.ts | 89 +++++++++ src/routes/manager/picklists.routes.ts | 138 +++++++++++++ 8 files changed, 799 insertions(+) create mode 100644 src/lib/openapi.ts create mode 100644 src/lib/prisma-zod.ts diff --git a/package-lock.json b/package-lock.json index 19d74bce..08b3c252 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "license": "ISC", "dependencies": { + "@asteasolutions/zod-to-openapi": "^8.4.0", "@prisma/client": "^6.19.0", "@prisma/extension-accelerate": "^1.0.0", "@slack/web-api": "^7.10.0", @@ -24,14 +25,17 @@ "posthog-node": "^5.9.5", "redis": "^5.9.0", "resend": "^2.0.0", + "swagger-ui-express": "^5.0.1", "ts-node": "^10.9.2", "zod": "^4.1.12" }, "devDependencies": { "@eslint/js": "^9.8.0", + "@types/cookie-parser": "^1.4.10", "@types/crypto-js": "^4.2.1", "@types/express": "^4.17.21", "@types/node": "^20.9.2", + "@types/swagger-ui-express": "^4.1.8", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "eslint": "^9.8.0", @@ -47,6 +51,18 @@ "node": "22.20.0" } }, + "node_modules/@asteasolutions/zod-to-openapi": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-8.4.0.tgz", + "integrity": "sha512-Ckp971tmTw4pnv+o7iK85ldBHBKk6gxMaoNyLn3c2Th/fKoTG8G3jdYuOanpdGqwlDB0z01FOjry2d32lfTqrA==", + "license": "MIT", + "dependencies": { + "openapi3-ts": "^4.1.2" + }, + "peerDependencies": { + "zod": "^4.0.0" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -1056,6 +1072,13 @@ "@redis/client": "^5.9.0" } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@selderee/plugin-htmlparser2": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", @@ -1168,6 +1191,16 @@ "@types/node": "*" } }, + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, "node_modules/@types/crypto-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", @@ -1188,6 +1221,7 @@ "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -1306,6 +1340,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.46.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", @@ -2752,6 +2797,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -4018,6 +4064,15 @@ "wrappy": "1" } }, + "node_modules/openapi3-ts": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.5.0.tgz", + "integrity": "sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==", + "license": "MIT", + "dependencies": { + "yaml": "^2.8.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5107,6 +5162,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-ui-dist": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.31.0.tgz", + "integrity": "sha512-zSUTIck02fSga6rc0RZP3b7J7wgHXwLea8ZjgLA3Vgnb8QeOl3Wou2/j5QkzSGeoz6HusP/coYuJl33aQxQZpg==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/tinyexec": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", @@ -5577,6 +5656,21 @@ "node": ">=0.4" } }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -5604,6 +5698,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index d4080b97..5decb5fd 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "author": "", "license": "ISC", "dependencies": { + "@asteasolutions/zod-to-openapi": "^8.4.0", "@prisma/client": "^6.19.0", "@prisma/extension-accelerate": "^1.0.0", "@slack/web-api": "^7.10.0", @@ -30,14 +31,17 @@ "posthog-node": "^5.9.5", "redis": "^5.9.0", "resend": "^2.0.0", + "swagger-ui-express": "^5.0.1", "ts-node": "^10.9.2", "zod": "^4.1.12" }, "devDependencies": { "@eslint/js": "^9.8.0", + "@types/cookie-parser": "^1.4.10", "@types/crypto-js": "^4.2.1", "@types/express": "^4.17.21", "@types/node": "^20.9.2", + "@types/swagger-ui-express": "^4.1.8", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "eslint": "^9.8.0", diff --git a/src/app.ts b/src/app.ts index 62030ecb..e39157a6 100644 --- a/src/app.ts +++ b/src/app.ts @@ -8,6 +8,8 @@ import { posthog } from "./posthogClient.js"; import posthogReporter from "./lib/middleware/posthogMiddleware.js"; import routes from "./routes/index.js"; +import swaggerUi from "swagger-ui-express"; +import { generateOpenApiDocument } from "./lib/openapi.js"; export const app = express(); @@ -26,3 +28,11 @@ app.use("/v1", routes); app.get("/status", (req, res) => { res.status(200).send("Server running"); }); + +// OpenAPI docs +const openApiDocument = generateOpenApiDocument(); +app.get("/doc.json", (_req, res) => { + res.json(openApiDocument); +}); + +app.use("/doc", swaggerUi.serve, swaggerUi.setup(openApiDocument)); diff --git a/src/lib/openapi.ts b/src/lib/openapi.ts new file mode 100644 index 00000000..b7f93504 --- /dev/null +++ b/src/lib/openapi.ts @@ -0,0 +1,44 @@ +import { OpenAPIRegistry, OpenApiGeneratorV31, extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi"; +import { z } from "zod"; + +// Enable .openapi() on Zod types +extendZodWithOpenApi(z); + +// Shared OpenAPI registry for the app. Other modules can import and register. +export const registry = new OpenAPIRegistry(); + +// Import and register Prisma-derived schemas +import { registerPrismaSchemas } from "./prisma-zod.js"; +registerPrismaSchemas(registry); + +// Minimal example: document the /status health check route +registry.registerPath({ + method: "get", + path: "/status", + summary: "Health check", + description: "Returns a simple message indicating the server is running.", + responses: { + 200: { + description: "Server is running", + content: { + "text/plain": { + schema: z.string().openapi({ example: "Server running" }), + }, + }, + }, + }, +}); + +export function generateOpenApiDocument() { + const generator = new OpenApiGeneratorV31(registry.definitions); + return generator.generateDocument({ + openapi: "3.1.0", + info: { + title: "Lovat API", + version: "1.0.0", + description: + "Bare-bones OpenAPI spec generated from Zod schemas using zod-to-openapi.", + }, + servers: [{ url: "/" }], + }); +} diff --git a/src/lib/prisma-zod.ts b/src/lib/prisma-zod.ts new file mode 100644 index 00000000..bd3e4aac --- /dev/null +++ b/src/lib/prisma-zod.ts @@ -0,0 +1,267 @@ +import { z } from "zod"; +import { OpenAPIRegistry, extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi"; + +// Ensure this module's Zod instance has .openapi +extendZodWithOpenApi(z); + +// Enums (mirrored from prisma/schema.prisma) +export const PositionSchema = z.enum([ + "LEFT_TRENCH", + "LEFT_BUMP", + "HUB", + "RIGHT_TRENCH", + "RIGHT_BUMP", + "NEUTRAL_ZONE", + "DEPOT", + "OUTPOST", + "NONE", +]); + +export const EventActionSchema = z.enum([ + "START_SCORING", + "STOP_SCORING", + "START_MATCH", + "START_CAMPING", + "STOP_CAMPING", + "START_DEFENDING", + "STOP_DEFENDING", + "INTAKE", + "OUTTAKE", + "DISRUPT", + "CROSS", + "CLIMB", + "START_FEEDING", + "STOP_FEEDING", +]); + +export const MobilitySchema = z.enum(["TRENCH", "BUMP", "BOTH", "NONE"]); +export const BeachedSchema = z.enum(["ON_FUEL", "ON_BUMP", "BOTH", "NEITHER"]); +export const EndgameClimbSchema = z.enum(["NOT_ATTEMPTED", "FAILED", "L1", "L2", "L3"]); +export const ClimbPositionSchema = z.enum(["SIDE", "MIDDLE"]); +export const ClimbSideSchema = z.enum(["FRONT", "BACK"]); +export const AutoClimbSchema = z.enum(["NOT_ATTEMPTED", "FAILED", "SUCCEEDED"]); +export const FeederTypeSchema = z.enum(["CONTINUOUS", "STOP_TO_SHOOT", "DUMP"]); +export const IntakeTypeSchema = z.enum(["GROUND", "OUTPOST", "BOTH", "NEITHER"]); +export const RobotRoleSchema = z.enum(["CYCLING", "SCORING", "FEEDING", "DEFENDING", "IMMOBILE"]); +export const WarningTypeSchema = z.enum(["BREAK"]); +export const UserRoleSchema = z.enum(["ANALYST", "SCOUTING_LEAD"]); +export const MatchTypeSchema = z.enum(["QUALIFICATION", "ELIMINATION"]); + +// Common JSON rule shapes used in User +export const DataSourceRuleNumberSchema = z.object({ + mode: z.enum(["INCLUDE", "EXCLUDE"]), + items: z.array(z.number()), +}); +export const DataSourceRuleStringSchema = z.object({ + mode: z.enum(["INCLUDE", "EXCLUDE"]), + items: z.array(z.string()), +}); + +// Models: include scalar fields and foreign keys. Relation objects are omitted to avoid cycles. +export const EventSchema = z.object({ + eventUuid: z.string().uuid(), + time: z.number().int(), + action: EventActionSchema, + position: PositionSchema, + points: z.number().int(), + scoutReportUuid: z.string().uuid(), +}); + +export const FeatureToggleSchema = z.object({ + feature: z.string(), + enabled: z.boolean(), +}); + +export const TeamMatchDataSchema = z.object({ + key: z.string(), + tournamentKey: z.string(), + matchNumber: z.number().int(), + teamNumber: z.number().int(), + matchType: MatchTypeSchema, +}); + +export const MutablePicklistSchema = z.object({ + uuid: z.string().uuid(), + teams: z.array(z.number().int()), + authorId: z.string(), + name: z.string(), + tournamentKey: z.string().nullable().optional(), +}); + +export const ScoutReportSchema = z.object({ + uuid: z.string().uuid(), + teamMatchKey: z.string(), + startTime: z.string().datetime(), + notes: z.string(), + robotRoles: z.array(RobotRoleSchema), + driverAbility: z.number().int(), + scouterUuid: z.string().uuid(), + robotBrokeDescription: z.string().optional().nullable(), + // year-specific fields + accuracy: z.number().int(), + beached: BeachedSchema, + climbPosition: ClimbPositionSchema.optional().nullable(), + climbSide: ClimbSideSchema.optional().nullable(), + defenseEffectiveness: z.number().int(), + feederTypes: z.array(FeederTypeSchema), + intakeType: IntakeTypeSchema, + mobility: MobilitySchema, + scoresWhileMoving: z.boolean(), + disrupts: z.boolean(), + endgameClimb: EndgameClimbSchema, + autoClimb: AutoClimbSchema, +}); + +export const ScouterScheduleShiftSchema = z.object({ + uuid: z.string().uuid(), + sourceTeamNumber: z.number().int(), + tournamentKey: z.string(), + startMatchOrdinalNumber: z.number().int(), + endMatchOrdinalNumber: z.number().int(), +}); + +export const ScouterSchema = z.object({ + uuid: z.string().uuid(), + name: z.string().optional().nullable(), + sourceTeamNumber: z.number().int(), + strikes: z.number().int(), + scouterReliability: z.number().int(), + archived: z.boolean(), +}); + +export const SharedPicklistSchema = z.object({ + uuid: z.string().uuid(), + name: z.string(), + totalPoints: z.number(), + autoPoints: z.number(), + teleopPoints: z.number(), + authorId: z.string(), + autoClimb: z.number(), + campingDefenseTime: z.number(), + climbResult: z.number(), + contactDefenseTime: z.number(), + driverAbility: z.number(), + defenseEffectiveness: z.number(), + estimatedSuccessfulFuelRate: z.number(), + estimatedTotalFuelScored: z.number(), + feedingRate: z.number(), + scoringRate: z.number(), + totalDefensiveTime: z.number(), + totalFuelFed: z.number(), + totalFuelThroughput: z.number(), +}); + +export const TeamSchema = z.object({ + number: z.number().int(), + name: z.string(), +}); + +export const RegisteredTeamSchema = z.object({ + number: z.number().int(), + code: z.string(), + email: z.string(), + emailVerified: z.boolean(), + teamApproved: z.boolean(), + website: z.string().optional().nullable(), +}); + +export const EmailVerificationRequestSchema = z.object({ + verificationCode: z.string(), + email: z.string(), + expiresAt: z.string().datetime(), + teamNumber: z.number().int(), +}); + +export const SlackWorkspaceSchema = z.object({ + workspaceId: z.string(), + owner: z.number().int(), + name: z.string(), + authToken: z.string(), + botUserId: z.string(), + authUserId: z.string(), +}); + +export const SlackSubscriptionSchema = z.object({ + subscriptionId: z.string(), + channelId: z.string(), + workspaceId: z.string(), + subscribedEvent: WarningTypeSchema, +}); + +export const SlackNotificationThreadSchema = z.object({ + messageId: z.string(), + matchNumber: z.number().int(), + teamNumber: z.number().int(), + subscriptionId: z.string(), + channelId: z.string(), +}); + +export const TournamentSchema = z.object({ + key: z.string(), + name: z.string(), + location: z.string().optional().nullable(), + date: z.string().optional().nullable(), +}); + +export const UserSchema = z.object({ + id: z.string(), + teamNumber: z.number().int().optional().nullable(), + email: z.string(), + emailVerified: z.boolean(), + username: z.string().optional().nullable(), + role: UserRoleSchema, + teamSourceRule: DataSourceRuleNumberSchema, + tournamentSourceRule: DataSourceRuleStringSchema, +}); + +export const ApiKeySchema = z.object({ + uuid: z.string().uuid(), + keyHash: z.string(), + name: z.string(), + userId: z.string(), + createdAt: z.string().datetime(), + lastUsed: z.string().datetime().optional().nullable(), + requests: z.number().int(), +}); + +export const CachedAnalysisSchema = z.object({ + key: z.string(), + teamDependencies: z.array(z.number().int()).default([]), + tournamentDependencies: z.array(z.string()).default([]), +}); + +export function registerPrismaSchemas(registry: OpenAPIRegistry) { + registry.register("Position", PositionSchema); + registry.register("EventAction", EventActionSchema); + registry.register("Mobility", MobilitySchema); + registry.register("Beached", BeachedSchema); + registry.register("EndgameClimb", EndgameClimbSchema); + registry.register("ClimbPosition", ClimbPositionSchema); + registry.register("ClimbSide", ClimbSideSchema); + registry.register("AutoClimb", AutoClimbSchema); + registry.register("FeederType", FeederTypeSchema); + registry.register("IntakeType", IntakeTypeSchema); + registry.register("RobotRole", RobotRoleSchema); + registry.register("WarningType", WarningTypeSchema); + registry.register("UserRole", UserRoleSchema); + registry.register("MatchType", MatchTypeSchema); + + registry.register("Event", EventSchema); + registry.register("FeatureToggle", FeatureToggleSchema); + registry.register("TeamMatchData", TeamMatchDataSchema); + registry.register("MutablePicklist", MutablePicklistSchema); + registry.register("ScoutReport", ScoutReportSchema); + registry.register("ScouterScheduleShift", ScouterScheduleShiftSchema); + registry.register("Scouter", ScouterSchema); + registry.register("SharedPicklist", SharedPicklistSchema); + registry.register("Team", TeamSchema); + registry.register("RegisteredTeam", RegisteredTeamSchema); + registry.register("EmailVerificationRequest", EmailVerificationRequestSchema); + registry.register("SlackWorkspace", SlackWorkspaceSchema); + registry.register("SlackSubscription", SlackSubscriptionSchema); + registry.register("SlackNotificationThread", SlackNotificationThreadSchema); + registry.register("Tournament", TournamentSchema); + registry.register("User", UserSchema); + registry.register("ApiKey", ApiKeySchema); + registry.register("CachedAnalysis", CachedAnalysisSchema); +} diff --git a/src/routes/analysis/analysis.routes.ts b/src/routes/analysis/analysis.routes.ts index 6ee48919..6a1fea21 100644 --- a/src/routes/analysis/analysis.routes.ts +++ b/src/routes/analysis/analysis.routes.ts @@ -7,9 +7,161 @@ import { Router } from "express"; import teamLookup from "./teamLookup.routes.js"; import csv from "./csv.routes.js"; import scoutReport from "./scoutreport.routes.js"; +import { registry } from "../../lib/openapi.js"; +import { z } from "zod"; const router = Router(); +// OpenAPI docs for analysis endpoints +const AlliancePathPosition = z.object({ + location: z.number(), + event: z.number(), + time: z.number().optional(), +}); +const AlliancePath = z.object({ + positions: z.array(AlliancePathPosition), + matches: z.array(z.object({ matchKey: z.string(), tournamentName: z.string() })), + score: z.array(z.number()), + frequency: z.number(), + maxScore: z.number(), +}); +const AllianceTeam = z.object({ + team: z.number(), + role: z.number(), + averagePoints: z.number(), + paths: z.array(AlliancePath), +}); +const AllianceResponseSchema = z.object({ + totalPoints: z.number(), + teams: z.array(AllianceTeam), + l1StartTime: z.array(z.number().nullable()).length(3), + l2StartTime: z.array(z.number().nullable()).length(3), + l3StartTime: z.array(z.number().nullable()).length(3), + totalFuelOutputted: z.number(), + totalBallThroughput: z.number(), +}); + +registry.registerPath({ + method: "get", + path: "/v1/analysis/pitdisplay", + tags: ["Analysis"], + summary: "Public pit display data for a team/event", + request: { + query: z.object({ + team: z.coerce.number().int(), + tournamentKey: z.string(), + topTeamCount: z.coerce.number().int(), + teamsAboveCount: z.coerce.number().int(), + }), + }, + responses: { + 200: { description: "Pit display payload", content: { "application/json": { schema: z.any() } } }, + 400: { description: "Invalid parameters" }, + 500: { description: "Error generating display" }, + }, +}); + +registry.registerPath({ + method: "get", + path: "/v1/analysis/alliance", + tags: ["Analysis"], + summary: "Alliance analysis for three teams", + request: { + query: z.object({ + teamOne: z.coerce.number().int(), + teamTwo: z.coerce.number().int(), + teamThree: z.coerce.number().int(), + }), + }, + responses: { + 200: { + description: "Alliance composition and metrics", + content: { "application/json": { schema: AllianceResponseSchema } }, + }, + 400: { description: "Invalid parameters" }, + }, +}); + +const MatchPredictionResponseSchema = z.union([ + z.object({ + red1: z.number(), + red2: z.number(), + red3: z.number(), + blue1: z.number(), + blue2: z.number(), + blue3: z.number(), + redWinning: z.number(), + blueWinning: z.number(), + winningAlliance: z.number(), + redAlliance: AllianceResponseSchema, + blueAlliance: AllianceResponseSchema, + }), + z.object({ error: z.literal("not enough data") }), +]); + +registry.registerPath({ + method: "get", + path: "/v1/analysis/matchprediction", + tags: ["Analysis"], + summary: "Predict match outcome for two alliances", + request: { + query: z.object({ + red1: z.coerce.number().int(), + red2: z.coerce.number().int(), + red3: z.coerce.number().int(), + blue1: z.coerce.number().int(), + blue2: z.coerce.number().int(), + blue3: z.coerce.number().int(), + }), + }, + responses: { + 200: { description: "Prediction and alliance details", content: { "application/json": { schema: MatchPredictionResponseSchema } } }, + 400: { description: "Invalid parameters" }, + }, +}); + +const PicklistEntrySchema = z.object({ + team: z.number(), + result: z.number(), + breakdown: z.array(z.object({ type: z.string(), result: z.number() })), + unweighted: z.array(z.object({ type: z.string(), result: z.number() })), + flags: z.array(z.object({ type: z.string(), result: z.number() })), +}); + +registry.registerPath({ + method: "get", + path: "/v1/analysis/picklist", + tags: ["Analysis"], + summary: "Compute picklist rankings for a tournament", + request: { + query: z.object({ + tournamentKey: z.string().optional(), + flags: z.string().optional(), + stage: z.string().optional(), + totalPoints: z.coerce.number().optional(), + autoPoints: z.coerce.number().optional(), + teleopPoints: z.coerce.number().optional(), + driverAbility: z.coerce.number().optional(), + climbResult: z.coerce.number().optional(), + autoClimb: z.coerce.number().optional(), + defenseEffectiveness: z.coerce.number().optional(), + contactDefenseTime: z.coerce.number().optional(), + campingDefenseTime: z.coerce.number().optional(), + totalDefensiveTime: z.coerce.number().optional(), + totalFuelThroughput: z.coerce.number().optional(), + totalFuelFed: z.coerce.number().optional(), + feedingRate: z.coerce.number().optional(), + scoringRate: z.coerce.number().optional(), + estimatedSuccessfulFuelRate: z.coerce.number().optional(), + estimatedTotalFuelScored: z.coerce.number().optional(), + }), + }, + responses: { + 200: { description: "Picklist ranking results", content: { "application/json": { schema: z.object({ teams: z.array(PicklistEntrySchema) }) } } }, + 400: { description: "Invalid parameters" }, + }, +}); + router.get("/pitdisplay", pitDisplay); router.use(requireAuth); diff --git a/src/routes/manager/manager.routes.ts b/src/routes/manager/manager.routes.ts index 1cdcbb9e..495f678d 100644 --- a/src/routes/manager/manager.routes.ts +++ b/src/routes/manager/manager.routes.ts @@ -26,9 +26,98 @@ import { getTeamCode } from "../../handler/manager/getTeamCode.js"; import { addScoutReportDashboard } from "../../handler/manager/scoutreports/addScoutReportDashboard.js"; import { getTeamTournamentStatus } from "../../handler/manager/getTeamTournamentStatus.js"; import { getMatchResults } from "../../handler/manager/getMatchResults.js"; +import { registry } from "../../lib/openapi.js"; +import { z } from "zod"; +import { TeamSchema, TournamentSchema } from "../../lib/prisma-zod.js"; const router = Router(); +// OpenAPI documentation for selected manager endpoints +const PaginationQuery = z.object({ + take: z.coerce.number().int().optional(), + skip: z.coerce.number().int().optional(), +}); + +registry.registerPath({ + method: "get", + path: "/v1/manager/teams", + tags: ["Manager - Teams"], + summary: "List teams with optional pagination and filter", + request: { + query: PaginationQuery.extend({ filter: z.string().optional() }), + }, + responses: { + 200: { + description: "Teams and total count", + content: { + "application/json": { + schema: z.object({ teams: z.array(TeamSchema), count: z.number() }), + }, + }, + }, + 401: { description: "Unauthorized" }, + }, +}); + +registry.registerPath({ + method: "get", + path: "/v1/manager/tournaments", + tags: ["Manager - Tournaments"], + summary: "List tournaments with optional pagination and filter", + request: { + query: PaginationQuery.extend({ filter: z.string().optional() }), + }, + responses: { + 200: { + description: "Tournaments and total count", + content: { + "application/json": { + schema: z.object({ tournaments: z.array(TournamentSchema), count: z.number() }), + }, + }, + }, + 401: { description: "Unauthorized" }, + }, +}); + +const MatchTeamSchema = z.object({ + number: z.number().int(), + scouters: z.array(z.object({ name: z.string(), scouted: z.boolean() })), + externalReports: z.number().int(), +}); + +const MatchSchema = z.object({ + matchNumber: z.number().int(), + matchType: z.number().int(), + scouted: z.boolean(), + finished: z.boolean(), + team1: MatchTeamSchema, + team2: MatchTeamSchema, + team3: MatchTeamSchema, + team4: MatchTeamSchema, + team5: MatchTeamSchema, + team6: MatchTeamSchema, +}); + +registry.registerPath({ + method: "get", + path: "/v1/manager/matches/{tournament}", + tags: ["Manager - Matches"], + summary: "List matches for a tournament", + request: { + params: z.object({ tournament: z.string() }), + query: z.object({ teams: z.string().optional() }), + }, + responses: { + 200: { + description: "Formatted match list", + content: { "application/json": { schema: z.array(MatchSchema) } }, + }, + 400: { description: "Invalid parameters" }, + 401: { description: "Unauthorized" }, + }, +}); + router.use("/onboarding", onboarding); router.use("/picklists", picklists); router.use("/mutablepicklists", mutablepicklist); diff --git a/src/routes/manager/picklists.routes.ts b/src/routes/manager/picklists.routes.ts index d3943b1f..218001af 100644 --- a/src/routes/manager/picklists.routes.ts +++ b/src/routes/manager/picklists.routes.ts @@ -5,6 +5,8 @@ import { deletePicklist } from "../../handler/manager/picklists/deletePicklist.j import { getPicklists } from "../../handler/manager/picklists/getPicklists.js"; import { getSinglePicklist } from "../../handler/manager/picklists/getSinglePicklist.js"; import { updatePicklist } from "../../handler/manager/picklists/updatePicklist.js"; +import { registry } from "../../lib/openapi.js"; +import { z } from "zod"; /* @@ -18,6 +20,142 @@ DELETE /manager/picklists/:uuid */ +// Schemas for requests/responses +const PicklistCreateBodySchema = z.object({ + name: z.string(), + totalPoints: z.number().default(0).optional(), + autoPoints: z.number().default(0).optional(), + teleopPoints: z.number().default(0).optional(), + climbResult: z.number().default(0).optional(), + autoClimb: z.number().default(0).optional(), + defenseEffectiveness: z.number().default(0).optional(), + contactDefenseTime: z.number().default(0).optional(), + campingDefenseTime: z.number().default(0).optional(), + totalDefensiveTime: z.number().default(0).optional(), + totalFuelThroughput: z.number().default(0).optional(), + totalFuelFed: z.number().default(0).optional(), + feedingRate: z.number().default(0).optional(), + scoringRate: z.number().default(0).optional(), + estimatedSuccessfulFuelRate: z.number().default(0).optional(), + estimatedTotalFuelScored: z.number().default(0).optional(), + driverAbility: z.number().default(0).optional(), +}); + +const PicklistSummarySchema = z.object({ + name: z.string(), + uuid: z.string(), + author: z.object({ username: z.string().nullable().optional() }), +}); + +const PicklistDetailSchema = z.object({ + uuid: z.string(), + name: z.string(), + authorId: z.string(), + totalPoints: z.number(), + autoPoints: z.number(), + teleopPoints: z.number(), + climbResult: z.number(), + autoClimb: z.number(), + defenseEffectiveness: z.number(), + contactDefenseTime: z.number(), + campingDefenseTime: z.number(), + totalDefensiveTime: z.number(), + totalFuelThroughput: z.number(), + totalFuelFed: z.number(), + feedingRate: z.number(), + scoringRate: z.number(), + estimatedSuccessfulFuelRate: z.number(), + estimatedTotalFuelScored: z.number(), +}); + +const PicklistUpdateBodySchema = PicklistCreateBodySchema; + +// OpenAPI: Register paths for these routes +registry.registerPath({ + method: "post", + path: "/v1/manager/picklists", + tags: ["Manager - Picklists"], + summary: "Create shared picklist", + request: { + body: { + content: { + "application/json": { schema: PicklistCreateBodySchema }, + }, + required: true, + }, + }, + responses: { + 200: { description: "Picklist created", content: { "text/plain": { schema: z.string() } } }, + 400: { description: "Invalid body" }, + 401: { description: "Unauthorized" }, + 403: { description: "Forbidden" }, + }, +}); + +registry.registerPath({ + method: "get", + path: "/v1/manager/picklists", + tags: ["Manager - Picklists"], + summary: "List picklists by team", + responses: { + 200: { + description: "List of picklists", + content: { "application/json": { schema: z.array(PicklistSummarySchema) } }, + }, + 401: { description: "Unauthorized" }, + 403: { description: "Forbidden" }, + }, +}); + +registry.registerPath({ + method: "get", + path: "/v1/manager/picklists/{uuid}", + tags: ["Manager - Picklists"], + summary: "Get picklist by UUID", + request: { + params: z.object({ uuid: z.string() }), + }, + responses: { + 200: { description: "Picklist detail", content: { "application/json": { schema: PicklistDetailSchema } } }, + 400: { description: "Invalid UUID" }, + 401: { description: "Unauthorized" }, + 403: { description: "Forbidden" }, + 404: { description: "Not found" }, + }, +}); + +registry.registerPath({ + method: "put", + path: "/v1/manager/picklists/{uuid}", + tags: ["Manager - Picklists"], + summary: "Update picklist", + request: { + params: z.object({ uuid: z.string() }), + body: { content: { "application/json": { schema: PicklistUpdateBodySchema } } }, + }, + responses: { + 200: { description: "Picklist updated", content: { "text/plain": { schema: z.string() } } }, + 400: { description: "Invalid input" }, + 401: { description: "Unauthorized" }, + 403: { description: "Forbidden" }, + }, +}); + +registry.registerPath({ + method: "delete", + path: "/v1/manager/picklists/{uuid}", + tags: ["Manager - Picklists"], + summary: "Delete picklist", + request: { params: z.object({ uuid: z.string() }) }, + responses: { + 200: { description: "Deleted", content: { "text/plain": { schema: z.string() } } }, + 400: { description: "Invalid UUID" }, + 401: { description: "Unauthorized" }, + 403: { description: "Forbidden" }, + 404: { description: "Not found" }, + }, +}); + const router = Router(); router.use(requireAuth); From ed95f11fe31f3dd6d1c12f078084a1c410d844fa Mon Sep 17 00:00:00 2001 From: JackAttack-365 <142643773+jackattack-4@users.noreply.github.com> Date: Sat, 24 Jan 2026 15:17:55 -0800 Subject: [PATCH 02/11] done maybe ish --- public/swaggerTheme.css | 227 ++++++++++++++++++ src/app.ts | 11 +- src/lib/openapi.ts | 27 ++- src/routes/analysis/analysis.routes.ts | 4 + src/routes/analysis/csv.routes.ts | 30 +++ src/routes/analysis/scoutreport.routes.ts | 30 +++ src/routes/analysis/teamLookup.routes.ts | 61 +++++ src/routes/manager/apikey.routes.ts | 61 +++++ src/routes/manager/manager.routes.ts | 189 +++++++++++++++ src/routes/manager/mutablepicklists.routes.ts | 51 ++++ src/routes/manager/onboarding.routes.ts | 56 +++++ src/routes/manager/picklists.routes.ts | 5 + src/routes/manager/registeredteams.routes.ts | 32 +++ src/routes/manager/scouters.routes.ts | 132 ++++++++++ src/routes/manager/scoutershifts.routes.ts | 21 ++ src/routes/manager/scoutreports.routes.ts | 48 +++- src/routes/manager/settings.routes.ts | 55 +++++ src/routes/manager/tournaments.routes.ts | 54 ++++- src/routes/slack/slack.routes.ts | 30 +++ 19 files changed, 1112 insertions(+), 12 deletions(-) create mode 100644 public/swaggerTheme.css diff --git a/public/swaggerTheme.css b/public/swaggerTheme.css new file mode 100644 index 00000000..20e27e9e --- /dev/null +++ b/public/swaggerTheme.css @@ -0,0 +1,227 @@ +/* ========================================================= + BASE + ========================================================= */ +body, +.swagger-ui { + background-color: #0f1118 !important; + color: #e5e7eb !important; + font-family: + Inter, + system-ui, + -apple-system, + BlinkMacSystemFont, + sans-serif; +} + +/* ========================================================= + TOP BAR + ========================================================= */ +.swagger-ui .topbar { + background-color: #13151c !important; + border-bottom: 1px solid #2a2c36 !important; +} + +.swagger-ui .topbar-wrapper img { + filter: brightness(0) invert(1); +} + +/* ========================================================= + AUTHORIZE BUTTON + ========================================================= */ +.swagger-ui .authorize { + background-color: #1e1a2e !important; + border: 1px solid #6f7cfc !important; + color: #e6e8ff !important; + border-radius: 8px !important; +} + +.swagger-ui .authorize:hover { + background-color: #26213d !important; +} + +/* Lock icon (closed) */ +.swagger-ui .authorize svg path { + fill: #6f7cfc !important; +} + +/* Lock icon (authorized) */ +.swagger-ui .authorize.authorized svg path { + fill: #22c55e !important; +} + +/* ========================================================= + HEADERS & TAGS + ========================================================= */ +.swagger-ui h1, +.swagger-ui h2, +.swagger-ui h3, +.swagger-ui h4, +.swagger-ui .model-title { + color: #d6d8ff !important; +} + +.swagger-ui .opblock-tag, +.swagger-ui .opblock-tag small { + color: #c8cbff !important; +} + +/* ========================================================= + OPERATION BLOCKS + ========================================================= */ +.swagger-ui .opblock { + background-color: #161920 !important; + border: 1px solid #2d2f39 !important; + border-radius: 10px !important; +} + +.swagger-ui .opblock-summary { + border-bottom: 1px solid #2d2f39 !important; +} + +/* Endpoint paths (NOT purple) */ +.swagger-ui .opblock-summary-path, +.swagger-ui .opblock-summary-path span { + color: #e5e7eb !important; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; +} + +/* Endpoint description */ +.swagger-ui .opblock-summary-description { + color: #9ca3af !important; +} + +/* ========================================================= + HTTP METHOD COLORS + ========================================================= */ +.swagger-ui .opblock-summary-method { + color: #ffffff !important; + font-weight: 600 !important; +} + +/* GET */ +.swagger-ui .opblock.opblock-get { + border-left: 5px solid #7aa7ff !important; + background-color: #161a2b !important; +} +.swagger-ui .opblock-get .opblock-summary-method { + background-color: #7aa7ff !important; +} + +/* POST */ +.swagger-ui .opblock.opblock-post { + border-left: 5px solid #a77cff !important; + background-color: #1a1830 !important; +} +.swagger-ui .opblock-post .opblock-summary-method { + background-color: #a77cff !important; +} + +/* PUT */ +.swagger-ui .opblock.opblock-put { + border-left: 5px solid #22c55e !important; + background-color: #18152a !important; +} +.swagger-ui .opblock-put .opblock-summary-method { + background-color: #22c55e !important; +} +/* PATCH */ +.swagger-ui .opblock.opblock-patch { + border-left: 5px solid #ffd166 !important; + background-color: #161b1d !important; +} +.swagger-ui .opblock-patch .opblock-summary-method { + background-color: #ffd166 !important; +} +/* DELETE */ +.swagger-ui .opblock.opblock-delete { + border-left: 5px solid #e06c75 !important; + background-color: #2a1518 !important; +} +.swagger-ui .opblock-delete .opblock-summary-method { + background-color: #e06c75 !important; +} + +/* ========================================================= + BUTTONS + ========================================================= */ +.swagger-ui .btn { + background-color: #6f7cfc !important; + border-color: #6f7cfc !important; + color: #ffffff !important; + border-radius: 6px !important; +} + +.swagger-ui .btn:hover { + background-color: #8e7fff !important; + border-color: #8e7fff !important; +} + +/* ========================================================= + INPUTS & FORMS + ========================================================= */ +.swagger-ui input, +.swagger-ui select, +.swagger-ui textarea { + background-color: #1a1d24 !important; + color: #e5e7eb !important; + border: 1px solid #2d2f39 !important; + border-radius: 6px !important; +} + +.swagger-ui input::placeholder { + color: #9ca3af !important; +} + +/* ========================================================= + TABLES + ========================================================= */ +.swagger-ui table { + background-color: #0f1118 !important; +} + +.swagger-ui table thead th { + color: #c8cbff !important; + border-bottom: 1px solid #2d2f39 !important; +} + +.swagger-ui table tbody td { + border-top: 1px solid #2d2f39 !important; +} + +/* ========================================================= + PARAMETERS & RESPONSES + ========================================================= */ +.swagger-ui .parameter__name, +.swagger-ui .response-col_status, +.swagger-ui .response-col_description, +.swagger-ui .prop-type { + color: #bfc3ff !important; +} + +/* ========================================================= + CODE BLOCKS + ========================================================= */ +.swagger-ui pre, +.swagger-ui .highlight-code { + background-color: #12141a !important; + color: #e5e7eb !important; + border-radius: 8px !important; +} + +/* ========================================================= + LINKS + ========================================================= */ +.swagger-ui a { + color: #8e7fff !important; +} + +.swagger-ui a:hover { + color: #a99cff !important; +} + +/* ========================================================= + HOVER POLISH + ========================================================= */ +.swagger-ui .opblock:hover { + box-shadow: 0 0 0 1px rgba(111, 124, 252, 0.25); +} diff --git a/src/app.ts b/src/app.ts index e39157a6..50e21834 100644 --- a/src/app.ts +++ b/src/app.ts @@ -10,6 +10,7 @@ import posthogReporter from "./lib/middleware/posthogMiddleware.js"; import routes from "./routes/index.js"; import swaggerUi from "swagger-ui-express"; import { generateOpenApiDocument } from "./lib/openapi.js"; +import path from "path"; export const app = express(); @@ -19,6 +20,8 @@ app.set("trust proxy", true); app.use(bodyParser.json()); app.use(cookieParser()); +app.use(express.static(path.resolve("public"))); + // Logs requests using posthog app.use(posthogReporter); @@ -35,4 +38,10 @@ app.get("/doc.json", (_req, res) => { res.json(openApiDocument); }); -app.use("/doc", swaggerUi.serve, swaggerUi.setup(openApiDocument)); +app.use( + "/doc", + swaggerUi.serve, + swaggerUi.setup(openApiDocument, { + customCssUrl: "/swaggerTheme.css", + }), +); diff --git a/src/lib/openapi.ts b/src/lib/openapi.ts index b7f93504..8b643ab1 100644 --- a/src/lib/openapi.ts +++ b/src/lib/openapi.ts @@ -1,4 +1,8 @@ -import { OpenAPIRegistry, OpenApiGeneratorV31, extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi"; +import { + OpenAPIRegistry, + OpenApiGeneratorV31, + extendZodWithOpenApi, +} from "@asteasolutions/zod-to-openapi"; import { z } from "zod"; // Enable .openapi() on Zod types @@ -11,6 +15,25 @@ export const registry = new OpenAPIRegistry(); import { registerPrismaSchemas } from "./prisma-zod.js"; registerPrismaSchemas(registry); +// Register security schemes +registry.registerComponent("securitySchemes", "bearerAuth", { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", +}); +registry.registerComponent("securitySchemes", "slackToken", { + type: "apiKey", + in: "header", + name: "x-slack-token", + description: "Slack verification token header", +}); +registry.registerComponent("securitySchemes", "lovatSignature", { + type: "apiKey", + in: "header", + name: "x-signature", + description: "HMAC signature header (requires accompanying x-timestamp)", +}); + // Minimal example: document the /status health check route registry.registerPath({ method: "get", @@ -37,7 +60,7 @@ export function generateOpenApiDocument() { title: "Lovat API", version: "1.0.0", description: - "Bare-bones OpenAPI spec generated from Zod schemas using zod-to-openapi.", + "API Documentation for Lovat, a scouting system used to scout teams and matches in the First Robotics Competition", }, servers: [{ url: "/" }], }); diff --git a/src/routes/analysis/analysis.routes.ts b/src/routes/analysis/analysis.routes.ts index 6a1fea21..48103265 100644 --- a/src/routes/analysis/analysis.routes.ts +++ b/src/routes/analysis/analysis.routes.ts @@ -80,6 +80,7 @@ registry.registerPath({ }, 400: { description: "Invalid parameters" }, }, + security: [{ bearerAuth: [] }], }); const MatchPredictionResponseSchema = z.union([ @@ -117,7 +118,9 @@ registry.registerPath({ responses: { 200: { description: "Prediction and alliance details", content: { "application/json": { schema: MatchPredictionResponseSchema } } }, 400: { description: "Invalid parameters" }, + 401: { description: "Unauthorized" }, }, + security: [{ bearerAuth: [] }], }); const PicklistEntrySchema = z.object({ @@ -160,6 +163,7 @@ registry.registerPath({ 200: { description: "Picklist ranking results", content: { "application/json": { schema: z.object({ teams: z.array(PicklistEntrySchema) }) } } }, 400: { description: "Invalid parameters" }, }, + security: [{ bearerAuth: [] }], }); router.get("/pitdisplay", pitDisplay); diff --git a/src/routes/analysis/csv.routes.ts b/src/routes/analysis/csv.routes.ts index 8225a788..26bcfa8e 100644 --- a/src/routes/analysis/csv.routes.ts +++ b/src/routes/analysis/csv.routes.ts @@ -3,9 +3,39 @@ import { requireAuth } from "../../lib/middleware/requireAuth.js"; import { getReportCSV } from "../../handler/analysis/csv/getReportCSV.js"; import { getTeamCSV } from "../../handler/analysis/csv/getTeamCSV.js"; import { getTeamMatchCSV } from "../../handler/analysis/csv/getTeamMatchCSV.js"; +import { registry } from "../../lib/openapi.js"; +import { z } from "zod"; const router = Router(); +registry.registerPath({ + method: "get", + path: "/v1/analysis/csvplain", + tags: ["Analysis - CSV"], + summary: "Team CSV", + request: { query: z.object({ team: z.coerce.number().int(), tournamentKey: z.string().optional() }) }, + responses: { 200: { description: "CSV", content: { "text/csv": { schema: z.string() } } } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "get", + path: "/v1/analysis/matchcsv", + tags: ["Analysis - CSV"], + summary: "Team match CSV", + request: { query: z.object({ team: z.coerce.number().int(), tournamentKey: z.string().optional() }) }, + responses: { 200: { description: "CSV", content: { "text/csv": { schema: z.string() } } } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "get", + path: "/v1/analysis/reportcsv", + tags: ["Analysis - CSV"], + summary: "Report CSV", + request: { query: z.object({ tournamentKey: z.string() }) }, + responses: { 200: { description: "CSV", content: { "text/csv": { schema: z.string() } } } }, + security: [{ bearerAuth: [] }], +}); + router.use(requireAuth); router.get("/csvplain", getTeamCSV); diff --git a/src/routes/analysis/scoutreport.routes.ts b/src/routes/analysis/scoutreport.routes.ts index 43cc24a7..f47a4a5f 100644 --- a/src/routes/analysis/scoutreport.routes.ts +++ b/src/routes/analysis/scoutreport.routes.ts @@ -3,9 +3,39 @@ import { requireAuth } from "../../lib/middleware/requireAuth.js"; import { matchPageSpecificScouter } from "../../handler/analysis/specificMatchPage/matchPageSpecificScouter.js"; import { scoutReportForMatch } from "../../handler/analysis/specificMatchPage/scoutReportForMatch.js"; import { timelineForScoutReport } from "../../handler/analysis/specificMatchPage/timelineForScoutReport.js"; +import { registry } from "../../lib/openapi.js"; +import { z } from "zod"; const router = Router(); +registry.registerPath({ + method: "get", + path: "/v1/analysis/metrics/scoutreport/{uuid}", + security: [{ bearerAuth: [] }], + tags: ["Analysis - Scout Report"], + summary: "Metrics for specific scout report", + request: { params: z.object({ uuid: z.string() }) }, + responses: { 200: { description: "Metrics", content: { "application/json": { schema: z.record(z.string(), z.any()) } } }, 404: { description: "Not found" } }, +}); +registry.registerPath({ + method: "get", + path: "/v1/analysis/scoutreports/match/{match}", + security: [{ bearerAuth: [] }], + tags: ["Analysis - Scout Report"], + summary: "All scout reports for a match", + request: { params: z.object({ match: z.coerce.number().int() }) }, + responses: { 200: { description: "Reports", content: { "application/json": { schema: z.array(z.record(z.string(), z.any())) } } } }, +}); +registry.registerPath({ + method: "get", + path: "/v1/analysis/timeline/scoutreport/{uuid}", + security: [{ bearerAuth: [] }], + tags: ["Analysis - Scout Report"], + summary: "Timeline data for scout report", + request: { params: z.object({ uuid: z.string() }) }, + responses: { 200: { description: "Timeline", content: { "application/json": { schema: z.record(z.string(), z.any()) } } } }, +}); + router.use(requireAuth); router.get("/metrics/scoutreport/:uuid", matchPageSpecificScouter); diff --git a/src/routes/analysis/teamLookup.routes.ts b/src/routes/analysis/teamLookup.routes.ts index ab7b5675..abd97c2c 100644 --- a/src/routes/analysis/teamLookup.routes.ts +++ b/src/routes/analysis/teamLookup.routes.ts @@ -6,9 +6,70 @@ import { categoryMetrics } from "../../handler/analysis/teamLookUp/categoryMetri import { detailsPage } from "../../handler/analysis/teamLookUp/detailsPage.js"; import { getNotes } from "../../handler/analysis/teamLookUp/getNotes.js"; import { multipleFlags } from "../../handler/analysis/teamLookUp/multipleFlags.js"; +import { registry } from "../../lib/openapi.js"; +import { z } from "zod"; const router = Router(); +const TeamParam = z.object({ team: z.string() }); +const MetricTeamParam = z.object({ metric: z.string(), team: z.string() }); +const BreakdownParam = z.object({ team: z.string(), breakdown: z.string() }); + +registry.registerPath({ + method: "get", + path: "/v1/analysis/metric/{metric}/team/{team}", + security: [{ bearerAuth: [] }], + tags: ["Analysis - Team Lookup"], + summary: "Metric details for team", + request: { params: MetricTeamParam }, + responses: { 200: { description: "Details", content: { "application/json": { schema: z.record(z.string(), z.any()) } } } }, +}); +registry.registerPath({ + method: "get", + path: "/v1/analysis/category/team/{team}", + security: [{ bearerAuth: [] }], + tags: ["Analysis - Team Lookup"], + summary: "Category metrics for team", + request: { params: TeamParam }, + responses: { 200: { description: "Categories", content: { "application/json": { schema: z.record(z.string(), z.any()) } } } }, +}); +registry.registerPath({ + method: "get", + path: "/v1/analysis/breakdown/team/{team}", + security: [{ bearerAuth: [] }], + tags: ["Analysis - Team Lookup"], + summary: "Breakdown metrics for team", + request: { params: TeamParam }, + responses: { 200: { description: "Breakdown", content: { "application/json": { schema: z.record(z.string(), z.any()) } } } }, +}); +registry.registerPath({ + method: "get", + path: "/v1/analysis/breakdown/team/{team}/{breakdown}", + security: [{ bearerAuth: [] }], + tags: ["Analysis - Team Lookup"], + summary: "Specific breakdown details", + request: { params: BreakdownParam }, + responses: { 200: { description: "Details", content: { "application/json": { schema: z.record(z.string(), z.any()) } } } }, +}); +registry.registerPath({ + method: "get", + path: "/v1/analysis/notes/team/{team}", + security: [{ bearerAuth: [] }], + tags: ["Analysis - Team Lookup"], + summary: "Notes for team", + request: { params: TeamParam }, + responses: { 200: { description: "Notes", content: { "application/json": { schema: z.array(z.object({ note: z.string() })) } } } }, +}); +registry.registerPath({ + method: "get", + path: "/v1/analysis/flag/team/{team}", + security: [{ bearerAuth: [] }], + tags: ["Analysis - Team Lookup"], + summary: "Flags for team", + request: { params: TeamParam }, + responses: { 200: { description: "Flags", content: { "application/json": { schema: z.array(z.object({ flag: z.string() })) } } } }, +}); + router.use(requireAuth); router.get("/metric/:metric/team/:team", detailsPage); diff --git a/src/routes/manager/apikey.routes.ts b/src/routes/manager/apikey.routes.ts index e66aacc9..081e31f5 100644 --- a/src/routes/manager/apikey.routes.ts +++ b/src/routes/manager/apikey.routes.ts @@ -4,6 +4,67 @@ import { addApiKey } from "../../handler/manager/apikey/addApiKey.js"; import { getApiKeys } from "../../handler/manager/apikey/getApiKeys.js"; import { renameApiKey } from "../../handler/manager/apikey/renameApiKey.js"; import { revokeApiKey } from "../../handler/manager/apikey/revokeApiKey.js"; +import { registry } from "../../lib/openapi.js"; +import { z } from "zod"; + +// OpenAPI docs for API key management (JWT required) +registry.registerPath({ + method: "post", + path: "/v1/manager/apikey", + tags: ["Manager - API Keys"], + summary: "Create a new API key", + description: "JWT required; API keys (lvt-...) are not permitted for this endpoint.", + request: { query: z.object({ name: z.string() }) }, + responses: { + 200: { description: "Created", content: { "application/json": { schema: z.object({ apiKey: z.string() }) } } }, + 400: { description: "Invalid request parameters" }, + 401: { description: "Unauthorized" }, + 403: { description: "Forbidden for API key auth" }, + }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "delete", + path: "/v1/manager/apikey", + tags: ["Manager - API Keys"], + summary: "Revoke an API key", + description: "JWT required; API keys (lvt-...) are not permitted for this endpoint.", + request: { query: z.object({ uuid: z.string() }) }, + responses: { + 200: { description: "Revoked", content: { "application/json": { schema: z.string() } } }, + 400: { description: "Invalid request parameters" }, + 401: { description: "Unauthorized" }, + 403: { description: "Forbidden for API key auth" }, + }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "patch", + path: "/v1/manager/apikey", + tags: ["Manager - API Keys"], + summary: "Rename an API key", + description: "JWT required; API keys (lvt-...) are not permitted for this endpoint.", + request: { query: z.object({ uuid: z.string(), newName: z.string() }) }, + responses: { + 200: { description: "Renamed", content: { "application/json": { schema: z.string() } } }, + 400: { description: "Invalid request parameters" }, + 401: { description: "Unauthorized" }, + 403: { description: "Forbidden for API key auth" }, + }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "get", + path: "/v1/manager/apikey", + tags: ["Manager - API Keys"], + summary: "List API keys", + description: "Returns current user's API keys. Accepts JWT or API keys.", + responses: { + 200: { description: "Keys", content: { "application/json": { schema: z.array(z.object({ uuid: z.string(), name: z.string(), requests: z.number().int() })) } } }, + 401: { description: "Unauthorized" }, + }, + security: [{ bearerAuth: [] }], +}); const router = Router(); diff --git a/src/routes/manager/manager.routes.ts b/src/routes/manager/manager.routes.ts index 495f678d..273a15e7 100644 --- a/src/routes/manager/manager.routes.ts +++ b/src/routes/manager/manager.routes.ts @@ -57,6 +57,7 @@ registry.registerPath({ }, 401: { description: "Unauthorized" }, }, + security: [{ bearerAuth: [] }], }); registry.registerPath({ @@ -78,6 +79,7 @@ registry.registerPath({ }, 401: { description: "Unauthorized" }, }, + security: [{ bearerAuth: [] }], }); const MatchTeamSchema = z.object({ @@ -116,6 +118,193 @@ registry.registerPath({ 400: { description: "Invalid parameters" }, 401: { description: "Unauthorized" }, }, + security: [{ bearerAuth: [] }], +}); + +// Profile +const ProfileSchema = z.object({ + id: z.string(), + username: z.string().nullable(), + email: z.string().email(), + role: z.string(), + team: z + .object({ + team: z.object({ number: z.number().int(), name: z.string().nullable() }), + }) + .nullable(), +}); +registry.registerPath({ + method: "get", + path: "/v1/manager/profile", + tags: ["Manager - Account"], + summary: "Get current user profile", + responses: { + 200: { description: "Profile", content: { "application/json": { schema: ProfileSchema.nullable() } } }, + 401: { description: "Unauthorized" }, + }, + security: [{ bearerAuth: [] }], +}); + +// Users list +registry.registerPath({ + method: "get", + path: "/v1/manager/users", + tags: ["Manager - Users"], + summary: "List users", + responses: { + 200: { description: "Users", content: { "application/json": { schema: z.array(z.object({ id: z.string(), email: z.string().email(), username: z.string().nullable(), role: z.string() })) } } }, + 401: { description: "Unauthorized" }, + }, + security: [{ bearerAuth: [] }], +}); + +// Delete user +registry.registerPath({ + method: "delete", + path: "/v1/manager/user", + tags: ["Manager - Users"], + summary: "Delete the current user", + responses: { + 200: { description: "Deleted" }, + 401: { description: "Unauthorized" }, + }, + security: [{ bearerAuth: [] }], +}); + +// Upgrade user role to scouting lead +registry.registerPath({ + method: "post", + path: "/v1/manager/upgradeuser", + tags: ["Manager - Users"], + summary: "Upgrade current user to Scouting Lead", + responses: { + 200: { description: "Upgraded" }, + 401: { description: "Unauthorized" }, + 400: { description: "Invalid request" }, + }, + security: [{ bearerAuth: [] }], +}); + +// Analysts +registry.registerPath({ + method: "get", + path: "/v1/manager/analysts", + tags: ["Manager - Users"], + summary: "List analysts", + responses: { + 200: { description: "Analysts", content: { "application/json": { schema: z.array(z.object({ id: z.string(), username: z.string().nullable() })) } } }, + 401: { description: "Unauthorized" }, + }, + security: [{ bearerAuth: [] }], +}); + +// Team code +registry.registerPath({ + method: "get", + path: "/v1/manager/code", + tags: ["Manager - Teams"], + summary: "Get team code for current user", + responses: { + 200: { description: "Team code", content: { "application/json": { schema: z.object({ code: z.string() }) } } }, + 401: { description: "Unauthorized" }, + }, + security: [{ bearerAuth: [] }], +}); + +// Dashboard scoutreport creation +registry.registerPath({ + method: "post", + path: "/v1/manager/dashboard/scoutreport", + tags: ["Manager - Scout Reports"], + summary: "Create scout report from dashboard", + request: { + body: { + content: { + "application/json": { + schema: z.object({ match: z.number().int(), team: z.number().int(), notes: z.string().optional() }), + }, + }, + }, + }, + responses: { + 200: { description: "Created", content: { "application/json": { schema: z.object({ uuid: z.string() }) } } }, + 401: { description: "Unauthorized" }, + 400: { description: "Invalid request" }, + }, + security: [{ bearerAuth: [] }], +}); + +// Team tournament status +registry.registerPath({ + method: "get", + path: "/v1/manager/team-tournament-status", + tags: ["Manager - Tournaments"], + summary: "Get status of current team in tournaments", + responses: { + 200: { description: "Status", content: { "application/json": { schema: z.object({ tournaments: z.array(z.object({ id: z.string(), code: z.string(), status: z.string() })) }) } } }, + 401: { description: "Unauthorized" }, + }, + security: [{ bearerAuth: [] }], +}); + +// Match results page +registry.registerPath({ + method: "get", + path: "/v1/manager/match-results-page", + tags: ["Manager - Matches"], + summary: "Get match results page data", + responses: { + 200: { description: "Results", content: { "application/json": { schema: z.object({ matches: z.array(MatchSchema) }) } } }, + 401: { description: "Unauthorized" }, + }, + security: [{ bearerAuth: [] }], +}); + +// Update notes (Scouting Lead only) +registry.registerPath({ + method: "put", + path: "/v1/manager/notes/{uuid}", + tags: ["Manager - Notes"], + summary: "Update scout report notes (SCOUTING_LEAD)", + request: { + params: z.object({ uuid: z.string() }), + body: { content: { "application/json": { schema: z.object({ note: z.string() }) } } }, + }, + responses: { + 200: { description: "Note updated" }, + 400: { description: "Invalid request" }, + 401: { description: "Unauthorized" }, + 403: { description: "Not authorized" }, + }, + security: [{ bearerAuth: [] }], +}); + +// Scoutershift scouters +registry.registerPath({ + method: "get", + path: "/v1/manager/scoutershift/scouters", + tags: ["Manager - Scouters"], + summary: "List scouters for current team", + request: { query: z.object({ archived: z.string().optional() }) }, + responses: { + 200: { description: "Scouters", content: { "application/json": { schema: z.array(z.object({ uuid: z.string(), name: z.string().nullable() })) } } }, + 401: { description: "Unauthorized" }, + 403: { description: "User not affiliated with a team" }, + }, + security: [{ bearerAuth: [] }], +}); + +// Set user as not on a team +registry.registerPath({ + method: "post", + path: "/v1/manager/noteam", + tags: ["Manager - Users"], + summary: "Set current user to ANALYST and remove team", + responses: { + 200: { description: "Updated", content: { "application/json": { schema: z.object({ id: z.string(), role: z.string(), teamNumber: z.number().nullable() }) } } }, + 401: { description: "Unauthorized" }, + }, + security: [{ bearerAuth: [] }], }); router.use("/onboarding", onboarding); diff --git a/src/routes/manager/mutablepicklists.routes.ts b/src/routes/manager/mutablepicklists.routes.ts index 0e7ac396..b61a6bc0 100644 --- a/src/routes/manager/mutablepicklists.routes.ts +++ b/src/routes/manager/mutablepicklists.routes.ts @@ -5,6 +5,8 @@ import { deleteMutablePicklist } from "../../handler/manager/mutablepicklists/de import { getMutablePicklists } from "../../handler/manager/mutablepicklists/getMutablePicklists.js"; import { getSingleMutablePicklist } from "../../handler/manager/mutablepicklists/getSingleMutablePicklist.js"; import { updateMutablePicklist } from "../../handler/manager/mutablepicklists/updateMutablePicklist.js"; +import { registry } from "../../lib/openapi.js"; +import { z } from "zod"; /* @@ -18,6 +20,55 @@ DELETE /manager/mutablepicklists/:uuid */ +const MutablePicklistCreateSchema = z.object({ name: z.string() }); +const MutablePicklistSummarySchema = z.object({ uuid: z.string(), name: z.string() }); +const MutablePicklistDetailSchema = z.object({ uuid: z.string(), name: z.string(), authorId: z.string() }); + +registry.registerPath({ + method: "post", + path: "/v1/manager/mutablepicklists", + tags: ["Manager - Mutable Picklists"], + summary: "Create mutable picklist", + request: { body: { content: { "application/json": { schema: MutablePicklistCreateSchema } } } }, + responses: { 200: { description: "Created" }, 400: { description: "Invalid request" }, 401: { description: "Unauthorized" } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "get", + path: "/v1/manager/mutablepicklists", + tags: ["Manager - Mutable Picklists"], + summary: "List mutable picklists", + responses: { 200: { description: "List", content: { "application/json": { schema: z.array(MutablePicklistSummarySchema) } } }, 401: { description: "Unauthorized" } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "get", + path: "/v1/manager/mutablepicklists/{uuid}", + tags: ["Manager - Mutable Picklists"], + summary: "Get mutable picklist", + request: { params: z.object({ uuid: z.string() }) }, + responses: { 200: { description: "Detail", content: { "application/json": { schema: MutablePicklistDetailSchema } } }, 404: { description: "Not found" } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "put", + path: "/v1/manager/mutablepicklists/{uuid}", + tags: ["Manager - Mutable Picklists"], + summary: "Update mutable picklist", + request: { params: z.object({ uuid: z.string() }), body: { content: { "application/json": { schema: MutablePicklistCreateSchema.partial() } } } }, + responses: { 200: { description: "Updated" }, 400: { description: "Invalid request" } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "delete", + path: "/v1/manager/mutablepicklists/{uuid}", + tags: ["Manager - Mutable Picklists"], + summary: "Delete mutable picklist", + request: { params: z.object({ uuid: z.string() }) }, + responses: { 200: { description: "Deleted" }, 404: { description: "Not found" } }, + security: [{ bearerAuth: [] }], +}); + const router = Router(); router.use(requireAuth); diff --git a/src/routes/manager/onboarding.routes.ts b/src/routes/manager/onboarding.routes.ts index 26805a7d..de128669 100644 --- a/src/routes/manager/onboarding.routes.ts +++ b/src/routes/manager/onboarding.routes.ts @@ -8,6 +8,8 @@ import { addRegisteredTeam } from "../../handler/manager/registeredteams/addRegi import rateLimit from "express-rate-limit"; import { addWebsite } from "../../handler/manager/onboarding/addWebsite.js"; import { resendEmail } from "../../handler/manager/onboarding/resendEmail.js"; +import { registry } from "../../lib/openapi.js"; +import { z } from "zod"; /* @@ -30,6 +32,60 @@ const resendEmailLimiter = rateLimit({ validate: { trustProxy: false }, }); +registry.registerPath({ + method: "post", + path: "/v1/manager/onboarding/verifyemail", + tags: ["Manager - Onboarding"], + summary: "Approve team email (signed)", + request: { body: { content: { "application/json": { schema: z.object({ token: z.string() }) } } } }, + responses: { 200: { description: "Approved" }, 400: { description: "Invalid" }, 403: { description: "Invalid signature" } }, + security: [{ lovatSignature: [] }], +}); +registry.registerPath({ + method: "post", + path: "/v1/manager/onboarding/username", + tags: ["Manager - Onboarding"], + summary: "Set username", + request: { body: { content: { "application/json": { schema: z.object({ username: z.string() }) } } } }, + responses: { 200: { description: "Updated" }, 400: { description: "Invalid" }, 401: { description: "Unauthorized" } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "post", + path: "/v1/manager/onboarding/teamcode", + tags: ["Manager - Onboarding"], + summary: "Check team code", + request: { body: { content: { "application/json": { schema: z.object({ code: z.string() }) } } } }, + responses: { 200: { description: "Valid" }, 400: { description: "Invalid" }, 401: { description: "Unauthorized" } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "post", + path: "/v1/manager/onboarding/team", + tags: ["Manager - Onboarding"], + summary: "Add registered team", + request: { body: { content: { "application/json": { schema: z.object({ number: z.number().int(), name: z.string().optional() }) } } } }, + responses: { 200: { description: "Added" }, 400: { description: "Invalid" }, 401: { description: "Unauthorized" } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "post", + path: "/v1/manager/onboarding/teamwebsite", + tags: ["Manager - Onboarding"], + summary: "Add team website", + request: { body: { content: { "application/json": { schema: z.object({ url: z.string().url() }) } } } }, + responses: { 200: { description: "Added" }, 400: { description: "Invalid" }, 401: { description: "Unauthorized" } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "post", + path: "/v1/manager/onboarding/resendverificationemail", + tags: ["Manager - Onboarding"], + summary: "Resend verification email", + responses: { 200: { description: "Sent" }, 401: { description: "Unauthorized" }, 429: { description: "Rate limited" } }, + security: [{ bearerAuth: [] }], +}); + const router = Router(); router.post("/verifyemail", requireLovatSignature, approveTeamEmail); diff --git a/src/routes/manager/picklists.routes.ts b/src/routes/manager/picklists.routes.ts index 218001af..cc021689 100644 --- a/src/routes/manager/picklists.routes.ts +++ b/src/routes/manager/picklists.routes.ts @@ -90,6 +90,7 @@ registry.registerPath({ 401: { description: "Unauthorized" }, 403: { description: "Forbidden" }, }, + security: [{ bearerAuth: [] }], }); registry.registerPath({ @@ -105,6 +106,7 @@ registry.registerPath({ 401: { description: "Unauthorized" }, 403: { description: "Forbidden" }, }, + security: [{ bearerAuth: [] }], }); registry.registerPath({ @@ -122,6 +124,7 @@ registry.registerPath({ 403: { description: "Forbidden" }, 404: { description: "Not found" }, }, + security: [{ bearerAuth: [] }], }); registry.registerPath({ @@ -139,6 +142,7 @@ registry.registerPath({ 401: { description: "Unauthorized" }, 403: { description: "Forbidden" }, }, + security: [{ bearerAuth: [] }], }); registry.registerPath({ @@ -154,6 +158,7 @@ registry.registerPath({ 403: { description: "Forbidden" }, 404: { description: "Not found" }, }, + security: [{ bearerAuth: [] }], }); const router = Router(); diff --git a/src/routes/manager/registeredteams.routes.ts b/src/routes/manager/registeredteams.routes.ts index d6b0e43d..6c5b8129 100644 --- a/src/routes/manager/registeredteams.routes.ts +++ b/src/routes/manager/registeredteams.routes.ts @@ -4,6 +4,38 @@ import { rejectRegisteredTeam } from "../../handler/manager/registeredteams/reje import { approveRegisteredTeam } from "../../handler/manager/registeredteams/approveRegisteredTeam.js"; import { checkRegisteredTeam } from "../../handler/manager/registeredteams/checkRegisteredTeam.js"; import requireLovatSignature from "../../lib/middleware/requireLovatSignature.js"; +import { registry } from "../../lib/openapi.js"; +import { z } from "zod"; + +const TeamParamSchema = z.object({ team: z.string() }); + +registry.registerPath({ + method: "get", + path: "/v1/manager/registeredteams/{team}/registrationstatus", + tags: ["Manager - Registered Teams"], + summary: "Check team registration status", + request: { params: TeamParamSchema }, + responses: { 200: { description: "Status", content: { "application/json": { schema: z.object({ status: z.string() }) } } }, 401: { description: "Unauthorized" } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "post", + path: "/v1/manager/registeredteams/{team}/approve", + tags: ["Manager - Registered Teams"], + summary: "Approve team", + request: { params: TeamParamSchema }, + responses: { 200: { description: "Approved" }, 403: { description: "Invalid signature" } }, + security: [{ lovatSignature: [] }], +}); +registry.registerPath({ + method: "post", + path: "/v1/manager/registeredteams/{team}/reject", + tags: ["Manager - Registered Teams"], + summary: "Reject team", + request: { params: TeamParamSchema }, + responses: { 200: { description: "Rejected" }, 403: { description: "Invalid signature" } }, + security: [{ lovatSignature: [] }], +}); const router = Router(); diff --git a/src/routes/manager/scouters.routes.ts b/src/routes/manager/scouters.routes.ts index cb56337f..037b0d76 100644 --- a/src/routes/manager/scouters.routes.ts +++ b/src/routes/manager/scouters.routes.ts @@ -15,8 +15,12 @@ import { scouterScoutReports } from "../../handler/analysis/scoutingLead/scouter import { deleteScouter } from "../../handler/manager/scouters/deleteScouter.js"; import { scoutingLeadProgressPage } from "../../handler/manager/scouters/scoutingLeadProgressPage.js"; import { updateScouterName } from "../../handler/manager/scouters/updateScouterName.js"; +import { registry } from "../../lib/openapi.js"; +import { z } from "zod"; const router = Router(); + +// Public/unauthenticated endpoints router.post("/emailTeamCode", emailTeamCode); router.get("/scouter/checkcode", checkCodeScouter); router.post("/name/uuid/:uuid", changeNameScouter); @@ -27,6 +31,134 @@ router.get("/scouters/:uuid/tournaments", getScouterTournaments); router.get("/scouterschedules/:tournament", getScheduleForScouter); router.get("/scouter/tournaments", getTournamentForScouterWithSchedule); +// Public/unauthenticated endpoints docs +registry.registerPath({ + method: "post", + path: "/v1/manager/emailTeamCode", + tags: ["Manager - Scouters (Public)"], + summary: "Email team code to registered address", + request: { query: z.object({ teamNumber: z.number().int() }) }, + responses: { 200: { description: "Email sent", content: { "application/json": { schema: z.object({ email: z.string().email() }) } } }, 400: { description: "Invalid request" } }, +}); +registry.registerPath({ + method: "get", + path: "/v1/manager/scouter/checkcode", + tags: ["Manager - Scouters (Public)"], + summary: "Check team code", + request: { query: z.object({ code: z.string() }) }, + responses: { 200: { description: "Valid or team row", content: { "application/json": { schema: z.any() } } }, 400: { description: "Invalid request" } }, +}); +registry.registerPath({ + method: "post", + path: "/v1/manager/name/uuid/{uuid}", + tags: ["Manager - Scouters (Public)"], + summary: "Change scouter name by UUID", + request: { params: z.object({ uuid: z.string() }), body: { content: { "application/json": { schema: z.object({ name: z.string() }) } } } }, + responses: { 200: { description: "Updated" }, 400: { description: "Invalid request" } }, +}); +registry.registerPath({ + method: "get", + path: "/v1/manager/scouters", + tags: ["Manager - Scouters (Public)"], + summary: "List active scouters for team code", + request: { headers: z.object({ "x-team-code": z.string() }) }, + responses: { 200: { description: "Scouters", content: { "application/json": { schema: z.array(z.any()) } } }, 400: { description: "Invalid request" }, 404: { description: "Team code not found" } }, +}); +registry.registerPath({ + method: "post", + path: "/v1/manager/scouter", + tags: ["Manager - Scouters (Public)"], + summary: "Create scouter", + request: { body: { content: { "application/json": { schema: z.object({ teamNumber: z.number().int(), name: z.string() }) } } } }, + responses: { 200: { description: "Created", content: { "application/json": { schema: z.any() } } }, 400: { description: "Invalid request" } }, +}); +registry.registerPath({ + method: "get", + path: "/v1/manager/scouters/{uuid}/tournaments", + tags: ["Manager - Scouters (Public)"], + summary: "List tournaments", + request: { params: z.object({ uuid: z.string() }) }, + responses: { 200: { description: "Tournaments", content: { "application/json": { schema: z.array(z.any()) } } } }, +}); +registry.registerPath({ + method: "get", + path: "/v1/manager/scouterschedules/{tournament}", + tags: ["Manager - Scouters (Public)"], + summary: "Get scouter schedule", + request: { params: z.object({ tournament: z.string() }), headers: z.object({ "x-team-code": z.string() }) }, + responses: { 200: { description: "Schedule", content: { "application/json": { schema: z.any() } } }, 400: { description: "Invalid request" } }, +}); +registry.registerPath({ + method: "get", + path: "/v1/manager/scouter/tournaments", + tags: ["Manager - Scouters (Public)"], + summary: "List scouter tournaments with schedule", + request: { headers: z.object({ "x-team-code": z.string() }) }, + responses: { 200: { description: "Tournaments", content: { "application/json": { schema: z.array(z.any()) } } }, 400: { description: "Invalid request" } }, +}); + +// OpenAPI docs for protected scouters endpoints +registry.registerPath({ + method: "post", + path: "/v1/manager/unarchive/uuid/{uuid}", + tags: ["Manager - Scouters"], + summary: "Unarchive scouter", + request: { params: z.object({ uuid: z.string() }) }, + responses: { 200: { description: "Unarchived" }, 401: { description: "Unauthorized" }, 404: { description: "Not found" } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "post", + path: "/v1/manager/archive/uuid/{uuid}", + tags: ["Manager - Scouters"], + summary: "Archive scouter", + request: { params: z.object({ uuid: z.string() }) }, + responses: { 200: { description: "Archived" }, 401: { description: "Unauthorized" }, 404: { description: "Not found" } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "put", + path: "/v1/manager/scoutername", + tags: ["Manager - Scouters"], + summary: "Update scouter name", + request: { body: { content: { "application/json": { schema: z.object({ uuid: z.string().optional(), name: z.string() }) } } } }, + responses: { 200: { description: "Updated" }, 400: { description: "Invalid request" } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "delete", + path: "/v1/manager/scouterdashboard", + tags: ["Manager - Scouters"], + summary: "Delete scouter from dashboard", + responses: { 200: { description: "Deleted" }, 401: { description: "Unauthorized" }, 404: { description: "Not found" } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "get", + path: "/v1/manager/scouterspage", + tags: ["Manager - Scouters"], + summary: "Scouting lead progress page", + responses: { 200: { description: "Page data", content: { "application/json": { schema: z.any() } } } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "post", + path: "/v1/manager/scouterdashboard", + tags: ["Manager - Scouters"], + summary: "Add scouter on dashboard", + request: { body: { content: { "application/json": { schema: z.object({ scouterId: z.string(), tournament: z.string().optional() }) } } } }, + responses: { 200: { description: "Created" }, 400: { description: "Invalid request" }, 401: { description: "Unauthorized" } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "get", + path: "/v1/manager/scouterreports", + tags: ["Manager - Scouters"], + summary: "List scouter reports", + responses: { 200: { description: "Reports", content: { "application/json": { schema: z.any() } } } }, + security: [{ bearerAuth: [] }], +}); + router.post("/unarchive/uuid/:uuid", requireAuth, unarchiveScouter); router.post("/archive/uuid/:uuid", requireAuth, archiveScouter); diff --git a/src/routes/manager/scoutershifts.routes.ts b/src/routes/manager/scoutershifts.routes.ts index 75f7de09..9bc6cb1e 100644 --- a/src/routes/manager/scoutershifts.routes.ts +++ b/src/routes/manager/scoutershifts.routes.ts @@ -2,9 +2,30 @@ import { Router } from "express"; import { requireAuth } from "../../lib/middleware/requireAuth.js"; import { updateScouterShift } from "../../handler/manager/scoutershifts/updateScouterShift.js"; import { deleteScouterShift } from "../../handler/manager/scoutershifts/deleteScouterShift.js"; +import { registry } from "../../lib/openapi.js"; +import { z } from "zod"; const router = Router(); +registry.registerPath({ + method: "post", + path: "/v1/manager/scoutershift/{uuid}", + tags: ["Manager - Scouter Shifts"], + summary: "Update scouter shift", + request: { params: z.object({ uuid: z.string() }), body: { content: { "application/json": { schema: z.object({ scouterId: z.string(), matchNumber: z.number().int() }) } } } }, + responses: { 200: { description: "Updated" }, 400: { description: "Invalid request" }, 401: { description: "Unauthorized" } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "delete", + path: "/v1/manager/scoutershift/{uuid}", + tags: ["Manager - Scouter Shifts"], + summary: "Delete scouter shift", + request: { params: z.object({ uuid: z.string() }) }, + responses: { 200: { description: "Deleted" }, 401: { description: "Unauthorized" }, 404: { description: "Not found" } }, + security: [{ bearerAuth: [] }], +}); + router.use(requireAuth); router.post("/:uuid", updateScouterShift); diff --git a/src/routes/manager/scoutreports.routes.ts b/src/routes/manager/scoutreports.routes.ts index 04a30867..b9827907 100644 --- a/src/routes/manager/scoutreports.routes.ts +++ b/src/routes/manager/scoutreports.routes.ts @@ -4,14 +4,56 @@ import { addScoutReport } from "../../handler/manager/scoutreports/addScoutRepor import { deleteScoutReport } from "../../handler/manager/scoutreports/deleteScoutReport.js"; import { getScoutReport } from "../../handler/manager/scoutreports/getScoutReport.js"; -const router = Router(); +import { registry } from "../../lib/openapi.js"; +import { z } from "zod"; -router.post("/", addScoutReport); +const ScoutReportCreateSchema = z.object({ + match: z.number().int(), + team: z.number().int(), + data: z.record(z.string(), z.any()).optional(), +}); +const ScoutReportSchema = z.object({ uuid: z.string(), match: z.number().int(), team: z.number().int(), data: z.record(z.string(), z.any()).optional() }); + +registry.registerPath({ + method: "post", + path: "/v1/manager/scoutreports", + security: [{ bearerAuth: [] }], + tags: ["Manager - Scout Reports"], + summary: "Create scout report", + request: { body: { content: { "application/json": { schema: ScoutReportCreateSchema } } } }, + responses: { + 200: { description: "Created", content: { "application/json": { schema: z.object({ uuid: z.string() }) } } }, + 400: { description: "Invalid request" }, + }, +}); +registry.registerPath({ + method: "get", + path: "/v1/manager/scoutreports/{uuid}", + security: [{ bearerAuth: [] }], + tags: ["Manager - Scout Reports"], + summary: "Get scout report", + request: { params: z.object({ uuid: z.string() }) }, + responses: { + 200: { description: "Scout report", content: { "application/json": { schema: ScoutReportSchema } } }, + 404: { description: "Not found" }, + }, +}); +registry.registerPath({ + method: "delete", + path: "/v1/manager/scoutreports/{uuid}", + security: [{ bearerAuth: [] }], + tags: ["Manager - Scout Reports"], + summary: "Delete scout report", + request: { params: z.object({ uuid: z.string() }) }, + responses: { 200: { description: "Deleted" }, 404: { description: "Not found" } }, +}); + +const router = Router(); router.use(requireAuth); +router.post("/", addScoutReport); router.get("/:uuid", getScoutReport); - router.delete("/:uuid", deleteScoutReport); export default router; diff --git a/src/routes/manager/settings.routes.ts b/src/routes/manager/settings.routes.ts index a084d39b..619dbe36 100644 --- a/src/routes/manager/settings.routes.ts +++ b/src/routes/manager/settings.routes.ts @@ -7,6 +7,8 @@ import { getTeamSource } from "../../handler/manager/settings/getTeamSource.js"; import { addTeamSource } from "../../handler/manager/settings/addTeamSource.js"; import { getTournamentSource } from "../../handler/manager/settings/getTournamentSource.js"; import { addTournamentSource } from "../../handler/manager/settings/addTournamentSource.js"; +import { registry } from "../../lib/openapi.js"; +import { z } from "zod"; const updateTeamEmails = rateLimit({ windowMs: 2 * 60 * 1000, @@ -18,6 +20,59 @@ const updateTeamEmails = rateLimit({ const router = Router(); +registry.registerPath({ + method: "put", + path: "/v1/manager/settings", + tags: ["Manager - Settings"], + summary: "Update settings", + request: { body: { content: { "application/json": { schema: z.object({ timezone: z.string().optional() }) } } } }, + responses: { 200: { description: "Updated" }, 400: { description: "Invalid request" }, 401: { description: "Unauthorized" } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "get", + path: "/v1/manager/settings/teamsource", + tags: ["Manager - Settings"], + summary: "Get team source", + responses: { 200: { description: "Team source", content: { "application/json": { schema: z.object({ source: z.string() }) } } }, 401: { description: "Unauthorized" } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "post", + path: "/v1/manager/settings/teamsource", + tags: ["Manager - Settings"], + summary: "Add team source", + request: { body: { content: { "application/json": { schema: z.object({ source: z.string() }) } } } }, + responses: { 200: { description: "Added" }, 400: { description: "Invalid request" } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "get", + path: "/v1/manager/settings/tournamentsource", + tags: ["Manager - Settings"], + summary: "Get tournament source", + responses: { 200: { description: "Tournament source", content: { "application/json": { schema: z.object({ source: z.string() }) } } }, 401: { description: "Unauthorized" } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "post", + path: "/v1/manager/settings/tournamentsource", + tags: ["Manager - Settings"], + summary: "Add tournament source", + request: { body: { content: { "application/json": { schema: z.object({ source: z.string() }) } } } }, + responses: { 200: { description: "Added" }, 400: { description: "Invalid request" } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "put", + path: "/v1/manager/settings/teamemail", + tags: ["Manager - Settings"], + summary: "Update team email", + request: { body: { content: { "application/json": { schema: z.object({ email: z.string().email() }) } } } }, + responses: { 200: { description: "Updated" }, 400: { description: "Invalid request" }, 401: { description: "Unauthorized" } }, + security: [{ bearerAuth: [] }], +}); + router.use(requireAuth); router.put("/", updateSettings); diff --git a/src/routes/manager/tournaments.routes.ts b/src/routes/manager/tournaments.routes.ts index bb89f731..046423c2 100644 --- a/src/routes/manager/tournaments.routes.ts +++ b/src/routes/manager/tournaments.routes.ts @@ -4,17 +4,58 @@ import { getTeamsInTournament } from "../../handler/manager/tournament/getTeamsI import { getTeamRankings } from "../../handler/manager/tournament/getTeamRankings.js"; import { addScouterShift } from "../../handler/manager/tournament/addScouterShift.js"; import { getScouterSchedule } from "../../handler/manager/tournament/getScouterSchedule.js"; +import { registry } from "../../lib/openapi.js"; +import { z } from "zod"; /* -tournaments.routes.ts + tournaments.routes.ts + + GET /manager/tournaments + GET /manager/tournament/:tournament/teams + GET /manager/tournament/:tournament/rankedTeams + GET /manager/tournament/:tournament/scoutershifts + + */ -GET /manager/tournaments -GET /manager/tournament/:tournament/teams -GET /manager/tournament/:tournament/rankedTeams -GET /manager/tournament/:tournament/scoutershifts +const TournamentParamSchema = z.object({ tournament: z.string() }); -*/ +registry.registerPath({ + method: "get", + path: "/v1/manager/tournament/{tournament}/teams", + tags: ["Manager - Tournaments"], + summary: "List teams in tournament", + request: { params: TournamentParamSchema }, + responses: { 200: { description: "Teams", content: { "application/json": { schema: z.array(z.object({ number: z.number().int(), name: z.string().nullable() })) } } }, 401: { description: "Unauthorized" } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "get", + path: "/v1/manager/tournament/{tournament}/rankedTeams", + tags: ["Manager - Tournaments"], + summary: "Ranked teams", + request: { params: TournamentParamSchema }, + responses: { 200: { description: "Rankings", content: { "application/json": { schema: z.array(z.object({ number: z.number().int(), rank: z.number().int() })) } } } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "post", + path: "/v1/manager/tournament/{tournament}/scoutershifts", + tags: ["Manager - Tournaments"], + summary: "Create scouter shift", + request: { params: TournamentParamSchema, body: { content: { "application/json": { schema: z.object({ uuid: z.string().optional(), scouterId: z.string(), matchNumber: z.number().int() }) } } } }, + responses: { 200: { description: "Created" }, 400: { description: "Invalid request" } }, + security: [{ bearerAuth: [] }], +}); +registry.registerPath({ + method: "get", + path: "/v1/manager/tournament/{tournament}/scoutershifts", + tags: ["Manager - Tournaments"], + summary: "List scouter shifts", + request: { params: TournamentParamSchema }, + responses: { 200: { description: "Shifts", content: { "application/json": { schema: z.array(z.object({ scouterId: z.string(), matchNumber: z.number().int() })) } } } }, + security: [{ bearerAuth: [] }], +}); const router = Router(); @@ -29,3 +70,4 @@ router.post("/:tournament/scoutershifts", addScouterShift); router.get("/:tournament/scoutershifts", getScouterSchedule); export default router; + diff --git a/src/routes/slack/slack.routes.ts b/src/routes/slack/slack.routes.ts index 83fbfdbb..b4da814e 100644 --- a/src/routes/slack/slack.routes.ts +++ b/src/routes/slack/slack.routes.ts @@ -5,6 +5,8 @@ import { requireSlackToken } from "../../lib/middleware/requireSlackToken.js"; import { addSlackWorkspace } from "../../handler/slack/addSlackWorkspace.js"; import { processCommand } from "../../handler/slack/processCommands.js"; import { processEvent } from "../../handler/slack/processEvents.js"; +import { registry } from "../../lib/openapi.js"; +import { z } from "zod"; /* @@ -17,6 +19,34 @@ POST /slack/event */ const router = Router(); + +registry.registerPath({ + method: "get", + path: "/v1/slack/add-workspace", + tags: ["Slack"], + summary: "Add Slack workspace (OAuth flow)", + responses: { 200: { description: "Redirect or JSON" } }, + security: [{ slackToken: [] }], +}); +registry.registerPath({ + method: "post", + path: "/v1/slack/command", + tags: ["Slack"], + summary: "Slack slash command", + request: { body: { content: { "application/x-www-form-urlencoded": { schema: z.object({ command: z.string(), text: z.string().optional() }) } } } }, + responses: { 200: { description: "Processed" } }, + security: [{ slackToken: [] }], +}); +registry.registerPath({ + method: "post", + path: "/v1/slack/event", + tags: ["Slack"], + summary: "Slack event callback", + request: { body: { content: { "application/json": { schema: z.object({ type: z.string(), event: z.record(z.string(), z.any()) }) } } } }, + responses: { 200: { description: "Processed" } }, + security: [{ slackToken: [] }], +}); + router.use(requireSlackToken); router.get("/add-workspace", addSlackWorkspace); From 50e8ceead74c1d174d0b01a432ccd7ee2ac0350b Mon Sep 17 00:00:00 2001 From: JackAttack-365 <142643773+jackattack-4@users.noreply.github.com> Date: Sun, 25 Jan 2026 11:19:41 -0800 Subject: [PATCH 03/11] update docs/version --- package.json | 2 +- public/swaggerTheme.css | 10 +- src/lib/openapi.ts | 8 +- src/routes/manager/apikey.routes.ts | 24 ++++- src/routes/manager/manager.routes.ts | 4 +- src/routes/manager/mutablepicklists.routes.ts | 63 +++++++++++-- src/routes/manager/onboarding.routes.ts | 18 ++-- src/routes/manager/picklists.routes.ts | 6 +- src/routes/manager/registeredteams.routes.ts | 50 +++++++++- src/routes/manager/scouters.routes.ts | 39 +++++--- src/routes/manager/scoutershifts.routes.ts | 4 +- src/routes/manager/scoutreports.routes.ts | 87 ++++++++++++++++-- src/routes/manager/settings.routes.ts | 92 ++++++++++++++++--- src/routes/manager/tournaments.routes.ts | 9 +- 14 files changed, 345 insertions(+), 71 deletions(-) diff --git a/package.json b/package.json index 5decb5fd..1ce81d9f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lovat-server", - "version": "0.1.0", + "version": "26.0.0", "description": "", "type": "module", "scripts": { diff --git a/public/swaggerTheme.css b/public/swaggerTheme.css index 20e27e9e..cd63d76a 100644 --- a/public/swaggerTheme.css +++ b/public/swaggerTheme.css @@ -25,12 +25,16 @@ body, filter: brightness(0) invert(1); } +.swagger-ui pre.version { + background-color: rgba(0, 0, 0, 0) !important; + color: #e5e7eb !important; +} /* ========================================================= AUTHORIZE BUTTON ========================================================= */ .swagger-ui .authorize { background-color: #1e1a2e !important; - border: 1px solid #6f7cfc !important; + border: 1px solid #ffffff !important; color: #e6e8ff !important; border-radius: 8px !important; } @@ -41,12 +45,12 @@ body, /* Lock icon (closed) */ .swagger-ui .authorize svg path { - fill: #6f7cfc !important; + fill: #ffffff !important; } /* Lock icon (authorized) */ .swagger-ui .authorize.authorized svg path { - fill: #22c55e !important; + fill: #ffffff !important; } /* ========================================================= diff --git a/src/lib/openapi.ts b/src/lib/openapi.ts index 8b643ab1..88b42101 100644 --- a/src/lib/openapi.ts +++ b/src/lib/openapi.ts @@ -4,6 +4,7 @@ import { extendZodWithOpenApi, } from "@asteasolutions/zod-to-openapi"; import { z } from "zod"; +import { version } from "../../package.json"; // Enable .openapi() on Zod types extendZodWithOpenApi(z); @@ -24,8 +25,9 @@ registry.registerComponent("securitySchemes", "bearerAuth", { registry.registerComponent("securitySchemes", "slackToken", { type: "apiKey", in: "header", - name: "x-slack-token", - description: "Slack verification token header", + name: "x-slack-signature", + description: + "Slack signature header (requires accompanying x-slack-request-timestamp)", }); registry.registerComponent("securitySchemes", "lovatSignature", { type: "apiKey", @@ -58,7 +60,7 @@ export function generateOpenApiDocument() { openapi: "3.1.0", info: { title: "Lovat API", - version: "1.0.0", + version: version, description: "API Documentation for Lovat, a scouting system used to scout teams and matches in the First Robotics Competition", }, diff --git a/src/routes/manager/apikey.routes.ts b/src/routes/manager/apikey.routes.ts index 081e31f5..91a5caa2 100644 --- a/src/routes/manager/apikey.routes.ts +++ b/src/routes/manager/apikey.routes.ts @@ -20,6 +20,7 @@ registry.registerPath({ 400: { description: "Invalid request parameters" }, 401: { description: "Unauthorized" }, 403: { description: "Forbidden for API key auth" }, + 500: { description: "Server error" }, }, security: [{ bearerAuth: [] }], }); @@ -35,6 +36,7 @@ registry.registerPath({ 400: { description: "Invalid request parameters" }, 401: { description: "Unauthorized" }, 403: { description: "Forbidden for API key auth" }, + 500: { description: "Server error" }, }, security: [{ bearerAuth: [] }], }); @@ -50,6 +52,7 @@ registry.registerPath({ 400: { description: "Invalid request parameters" }, 401: { description: "Unauthorized" }, 403: { description: "Forbidden for API key auth" }, + 500: { description: "Server error" }, }, security: [{ bearerAuth: [] }], }); @@ -60,8 +63,27 @@ registry.registerPath({ summary: "List API keys", description: "Returns current user's API keys. Accepts JWT or API keys.", responses: { - 200: { description: "Keys", content: { "application/json": { schema: z.array(z.object({ uuid: z.string(), name: z.string(), requests: z.number().int() })) } } }, + 200: { + description: "Keys", + content: { + "application/json": { + schema: z.object({ + apiKeys: z.array( + z.object({ + uuid: z.string(), + name: z.string(), + createdAt: z.string().datetime(), + lastUsed: z.string().datetime().nullable(), + requests: z.number().int(), + user: z.object({ username: z.string() }), + }) + ), + }), + }, + }, + }, 401: { description: "Unauthorized" }, + 500: { description: "Server error" }, }, security: [{ bearerAuth: [] }], }); diff --git a/src/routes/manager/manager.routes.ts b/src/routes/manager/manager.routes.ts index 273a15e7..beebbba0 100644 --- a/src/routes/manager/manager.routes.ts +++ b/src/routes/manager/manager.routes.ts @@ -8,7 +8,7 @@ import scouters from "./scouters.routes.js"; import tournaments from "./tournaments.routes.js"; import scoutreports from "./scoutreports.routes.js"; import settings from "./settings.routes.js"; -//import apikey from "./apikey.routes.js"; +import apikey from "./apikey.routes.js"; import { getTournaments } from "../../handler/manager/getTournaments.js"; import { getTeams } from "../../handler/manager/getTeams.js"; @@ -315,7 +315,7 @@ router.use("/", scouters); router.use("/tournament", tournaments); router.use("/scoutreports", scoutreports); router.use("/settings", settings); -//router.use("/apikey", apikey); +router.use("/apikey", apikey); router.get("/teams", requireAuth, getTeams); router.get("/tournaments", requireAuth, getTournaments); diff --git a/src/routes/manager/mutablepicklists.routes.ts b/src/routes/manager/mutablepicklists.routes.ts index b61a6bc0..5381b852 100644 --- a/src/routes/manager/mutablepicklists.routes.ts +++ b/src/routes/manager/mutablepicklists.routes.ts @@ -7,6 +7,7 @@ import { getSingleMutablePicklist } from "../../handler/manager/mutablepicklists import { updateMutablePicklist } from "../../handler/manager/mutablepicklists/updateMutablePicklist.js"; import { registry } from "../../lib/openapi.js"; import { z } from "zod"; +import { MutablePicklistSchema } from "../../lib/prisma-zod.js"; /* @@ -20,9 +21,18 @@ DELETE /manager/mutablepicklists/:uuid */ -const MutablePicklistCreateSchema = z.object({ name: z.string() }); -const MutablePicklistSummarySchema = z.object({ uuid: z.string(), name: z.string() }); -const MutablePicklistDetailSchema = z.object({ uuid: z.string(), name: z.string(), authorId: z.string() }); +const MutablePicklistCreateSchema = z.object({ + name: z.string(), + teams: z.array(z.number().int()), + tournamentKey: z.string().optional(), +}); + +const MutablePicklistListItemSchema = z.object({ + uuid: z.string(), + name: z.string(), + tournamentKey: z.string().nullable().optional(), + author: z.object({ username: z.string().nullable().optional() }), +}); registry.registerPath({ method: "post", @@ -30,42 +40,77 @@ registry.registerPath({ tags: ["Manager - Mutable Picklists"], summary: "Create mutable picklist", request: { body: { content: { "application/json": { schema: MutablePicklistCreateSchema } } } }, - responses: { 200: { description: "Created" }, 400: { description: "Invalid request" }, 401: { description: "Unauthorized" } }, + responses: { + 200: { description: "Created", content: { "text/plain": { schema: z.string() } } }, + 400: { description: "Invalid request" }, + 401: { description: "Unauthorized" }, + 403: { description: "Not on a team" }, + 500: { description: "Server error" }, + }, security: [{ bearerAuth: [] }], }); + registry.registerPath({ method: "get", path: "/v1/manager/mutablepicklists", tags: ["Manager - Mutable Picklists"], summary: "List mutable picklists", - responses: { 200: { description: "List", content: { "application/json": { schema: z.array(MutablePicklistSummarySchema) } } }, 401: { description: "Unauthorized" } }, + responses: { + 200: { description: "List", content: { "application/json": { schema: z.array(MutablePicklistListItemSchema) } } }, + 401: { description: "Unauthorized" }, + 500: { description: "Server error" }, + }, security: [{ bearerAuth: [] }], }); + registry.registerPath({ method: "get", path: "/v1/manager/mutablepicklists/{uuid}", tags: ["Manager - Mutable Picklists"], summary: "Get mutable picklist", request: { params: z.object({ uuid: z.string() }) }, - responses: { 200: { description: "Detail", content: { "application/json": { schema: MutablePicklistDetailSchema } } }, 404: { description: "Not found" } }, + responses: { + 200: { description: "Detail", content: { "application/json": { schema: MutablePicklistSchema.nullable() } } }, + 400: { description: "Invalid request" }, + 404: { description: "Not found" }, + 500: { description: "Server error" }, + }, security: [{ bearerAuth: [] }], }); + registry.registerPath({ method: "put", path: "/v1/manager/mutablepicklists/{uuid}", tags: ["Manager - Mutable Picklists"], summary: "Update mutable picklist", - request: { params: z.object({ uuid: z.string() }), body: { content: { "application/json": { schema: MutablePicklistCreateSchema.partial() } } } }, - responses: { 200: { description: "Updated" }, 400: { description: "Invalid request" } }, + request: { + params: z.object({ uuid: z.string() }), + body: { content: { "application/json": { schema: z.object({ name: z.string(), teams: z.array(z.number().int()) }) } } }, + }, + responses: { + 200: { description: "Updated", content: { "text/plain": { schema: z.string() } } }, + 400: { description: "Invalid request" }, + 401: { description: "Unauthorized" }, + 403: { description: "Not authorized" }, + 500: { description: "Server error" }, + }, security: [{ bearerAuth: [] }], }); + registry.registerPath({ method: "delete", path: "/v1/manager/mutablepicklists/{uuid}", tags: ["Manager - Mutable Picklists"], summary: "Delete mutable picklist", request: { params: z.object({ uuid: z.string() }) }, - responses: { 200: { description: "Deleted" }, 404: { description: "Not found" } }, + responses: { + 200: { description: "Deleted", content: { "text/plain": { schema: z.string() } } }, + 400: { description: "Invalid request" }, + 401: { description: "Unauthorized" }, + 403: { description: "Unauthorized" }, + 404: { description: "Not found" }, + 500: { description: "Server error" }, + }, security: [{ bearerAuth: [] }], }); diff --git a/src/routes/manager/onboarding.routes.ts b/src/routes/manager/onboarding.routes.ts index de128669..9dfbd10c 100644 --- a/src/routes/manager/onboarding.routes.ts +++ b/src/routes/manager/onboarding.routes.ts @@ -37,8 +37,8 @@ registry.registerPath({ path: "/v1/manager/onboarding/verifyemail", tags: ["Manager - Onboarding"], summary: "Approve team email (signed)", - request: { body: { content: { "application/json": { schema: z.object({ token: z.string() }) } } } }, - responses: { 200: { description: "Approved" }, 400: { description: "Invalid" }, 403: { description: "Invalid signature" } }, + request: { body: { content: { "application/json": { schema: z.object({ code: z.string() }) } } } }, + responses: { 200: { description: "Approved" }, 400: { description: "Invalid" }, 403: { description: "Invalid signature" }, 404: { description: "Code not recognized" }, 500: { description: "Server error" } }, security: [{ lovatSignature: [] }], }); registry.registerPath({ @@ -47,7 +47,7 @@ registry.registerPath({ tags: ["Manager - Onboarding"], summary: "Set username", request: { body: { content: { "application/json": { schema: z.object({ username: z.string() }) } } } }, - responses: { 200: { description: "Updated" }, 400: { description: "Invalid" }, 401: { description: "Unauthorized" } }, + responses: { 200: { description: "Updated" }, 400: { description: "Invalid" }, 401: { description: "Unauthorized" }, 500: { description: "Server error" } }, security: [{ bearerAuth: [] }], }); registry.registerPath({ @@ -55,8 +55,8 @@ registry.registerPath({ path: "/v1/manager/onboarding/teamcode", tags: ["Manager - Onboarding"], summary: "Check team code", - request: { body: { content: { "application/json": { schema: z.object({ code: z.string() }) } } } }, - responses: { 200: { description: "Valid" }, 400: { description: "Invalid" }, 401: { description: "Unauthorized" } }, + request: { query: z.object({ team: z.string(), code: z.string() }) }, + responses: { 200: { description: "Valid" }, 400: { description: "Invalid" }, 401: { description: "Unauthorized" }, 404: { description: "Team/code not found" }, 500: { description: "Server error" } }, security: [{ bearerAuth: [] }], }); registry.registerPath({ @@ -65,7 +65,7 @@ registry.registerPath({ tags: ["Manager - Onboarding"], summary: "Add registered team", request: { body: { content: { "application/json": { schema: z.object({ number: z.number().int(), name: z.string().optional() }) } } } }, - responses: { 200: { description: "Added" }, 400: { description: "Invalid" }, 401: { description: "Unauthorized" } }, + responses: { 200: { description: "Added" }, 400: { description: "Invalid" }, 401: { description: "Unauthorized" }, 500: { description: "Server error" } }, security: [{ bearerAuth: [] }], }); registry.registerPath({ @@ -73,8 +73,8 @@ registry.registerPath({ path: "/v1/manager/onboarding/teamwebsite", tags: ["Manager - Onboarding"], summary: "Add team website", - request: { body: { content: { "application/json": { schema: z.object({ url: z.string().url() }) } } } }, - responses: { 200: { description: "Added" }, 400: { description: "Invalid" }, 401: { description: "Unauthorized" } }, + request: { body: { content: { "application/json": { schema: z.object({ website: z.string() }) } } } }, + responses: { 200: { description: "Added" }, 400: { description: "Invalid" }, 401: { description: "Unauthorized" }, 500: { description: "Server error" } }, security: [{ bearerAuth: [] }], }); registry.registerPath({ @@ -82,7 +82,7 @@ registry.registerPath({ path: "/v1/manager/onboarding/resendverificationemail", tags: ["Manager - Onboarding"], summary: "Resend verification email", - responses: { 200: { description: "Sent" }, 401: { description: "Unauthorized" }, 429: { description: "Rate limited" } }, + responses: { 200: { description: "Sent" }, 401: { description: "Unauthorized" }, 404: { description: "Team not found" }, 429: { description: "Rate limited" }, 500: { description: "Server error" } }, security: [{ bearerAuth: [] }], }); diff --git a/src/routes/manager/picklists.routes.ts b/src/routes/manager/picklists.routes.ts index cc021689..836c87a9 100644 --- a/src/routes/manager/picklists.routes.ts +++ b/src/routes/manager/picklists.routes.ts @@ -89,6 +89,7 @@ registry.registerPath({ 400: { description: "Invalid body" }, 401: { description: "Unauthorized" }, 403: { description: "Forbidden" }, + 500: { description: "Server error" }, }, security: [{ bearerAuth: [] }], }); @@ -105,6 +106,7 @@ registry.registerPath({ }, 401: { description: "Unauthorized" }, 403: { description: "Forbidden" }, + 500: { description: "Server error" }, }, security: [{ bearerAuth: [] }], }); @@ -122,7 +124,7 @@ registry.registerPath({ 400: { description: "Invalid UUID" }, 401: { description: "Unauthorized" }, 403: { description: "Forbidden" }, - 404: { description: "Not found" }, + 500: { description: "Server error" }, }, security: [{ bearerAuth: [] }], }); @@ -141,6 +143,7 @@ registry.registerPath({ 400: { description: "Invalid input" }, 401: { description: "Unauthorized" }, 403: { description: "Forbidden" }, + 500: { description: "Server error" }, }, security: [{ bearerAuth: [] }], }); @@ -157,6 +160,7 @@ registry.registerPath({ 401: { description: "Unauthorized" }, 403: { description: "Forbidden" }, 404: { description: "Not found" }, + 500: { description: "Server error" }, }, security: [{ bearerAuth: [] }], }); diff --git a/src/routes/manager/registeredteams.routes.ts b/src/routes/manager/registeredteams.routes.ts index 6c5b8129..f9d540b5 100644 --- a/src/routes/manager/registeredteams.routes.ts +++ b/src/routes/manager/registeredteams.routes.ts @@ -6,34 +6,78 @@ import { checkRegisteredTeam } from "../../handler/manager/registeredteams/check import requireLovatSignature from "../../lib/middleware/requireLovatSignature.js"; import { registry } from "../../lib/openapi.js"; import { z } from "zod"; +import { RegisteredTeamSchema } from "../../lib/prisma-zod.js"; const TeamParamSchema = z.object({ team: z.string() }); +// Status variants returned by checkRegisteredTeam handler +const RegistrationStatusSchema = z.discriminatedUnion("status", [ + z.object({ status: z.literal("NOT_STARTED") }), + z.object({ status: z.literal("PENDING_EMAIL_VERIFICATION"), email: z.string() }), + z.object({ status: z.literal("PENDING_WEBSITE") }), + z.object({ status: z.literal("PENDING_TEAM_VERIFICATION"), teamEmail: z.string() }), + z.object({ status: z.literal("REGISTERED_ON_TEAM") }), + z.object({ status: z.literal("REGISTERED_OFF_TEAM") }), + z.object({ status: z.literal("PENDING") }), +]); + registry.registerPath({ method: "get", path: "/v1/manager/registeredteams/{team}/registrationstatus", tags: ["Manager - Registered Teams"], summary: "Check team registration status", request: { params: TeamParamSchema }, - responses: { 200: { description: "Status", content: { "application/json": { schema: z.object({ status: z.string() }) } } }, 401: { description: "Unauthorized" } }, + responses: { + 200: { + description: "Registration status", + content: { "application/json": { schema: RegistrationStatusSchema } }, + }, + 400: { description: "Invalid team" }, + 401: { description: "Unauthorized" }, + 500: { description: "Server error" }, + }, security: [{ bearerAuth: [] }], }); + registry.registerPath({ method: "post", path: "/v1/manager/registeredteams/{team}/approve", tags: ["Manager - Registered Teams"], summary: "Approve team", + description: + "Requires `x-signature` and `x-timestamp` headers. Signs request body, method, path.", request: { params: TeamParamSchema }, - responses: { 200: { description: "Approved" }, 403: { description: "Invalid signature" } }, + responses: { + 200: { + description: "Approved registered team row", + content: { "application/json": { schema: RegisteredTeamSchema } }, + }, + 400: { description: "Invalid team" }, + 401: { description: "Unauthorized or signature expired" }, + 403: { description: "Invalid signature" }, + 500: { description: "Server error" }, + }, security: [{ lovatSignature: [] }], }); + registry.registerPath({ method: "post", path: "/v1/manager/registeredteams/{team}/reject", tags: ["Manager - Registered Teams"], summary: "Reject team", + description: + "Requires `x-signature` and `x-timestamp` headers. Signs request body, method, path.", request: { params: TeamParamSchema }, - responses: { 200: { description: "Rejected" }, 403: { description: "Invalid signature" } }, + responses: { + 200: { + description: "Rejection message", + content: { "text/plain": { schema: z.string() } }, + }, + 400: { description: "Invalid team" }, + 401: { description: "Unauthorized or signature expired" }, + 403: { description: "Invalid signature" }, + 500: { description: "Server error" }, + }, security: [{ lovatSignature: [] }], }); diff --git a/src/routes/manager/scouters.routes.ts b/src/routes/manager/scouters.routes.ts index 037b0d76..52b29873 100644 --- a/src/routes/manager/scouters.routes.ts +++ b/src/routes/manager/scouters.routes.ts @@ -17,6 +17,7 @@ import { scoutingLeadProgressPage } from "../../handler/manager/scouters/scoutin import { updateScouterName } from "../../handler/manager/scouters/updateScouterName.js"; import { registry } from "../../lib/openapi.js"; import { z } from "zod"; +import { ScouterSchema } from "../../lib/prisma-zod.js"; const router = Router(); @@ -38,7 +39,7 @@ registry.registerPath({ tags: ["Manager - Scouters (Public)"], summary: "Email team code to registered address", request: { query: z.object({ teamNumber: z.number().int() }) }, - responses: { 200: { description: "Email sent", content: { "application/json": { schema: z.object({ email: z.string().email() }) } } }, 400: { description: "Invalid request" } }, + responses: { 200: { description: "Email sent", content: { "application/json": { schema: z.object({ email: z.string().email() }) } } }, 400: { description: "Invalid request" }, 500: { description: "Server error" } }, }); registry.registerPath({ method: "get", @@ -46,7 +47,7 @@ registry.registerPath({ tags: ["Manager - Scouters (Public)"], summary: "Check team code", request: { query: z.object({ code: z.string() }) }, - responses: { 200: { description: "Valid or team row", content: { "application/json": { schema: z.any() } } }, 400: { description: "Invalid request" } }, + responses: { 200: { description: "Valid or team row", content: { "application/json": { schema: z.any() } } }, 400: { description: "Invalid request" }, 500: { description: "Server error" } }, }); registry.registerPath({ method: "post", @@ -54,7 +55,7 @@ registry.registerPath({ tags: ["Manager - Scouters (Public)"], summary: "Change scouter name by UUID", request: { params: z.object({ uuid: z.string() }), body: { content: { "application/json": { schema: z.object({ name: z.string() }) } } } }, - responses: { 200: { description: "Updated" }, 400: { description: "Invalid request" } }, + responses: { 200: { description: "Updated" }, 400: { description: "Invalid request" }, 500: { description: "Server error" } }, }); registry.registerPath({ method: "get", @@ -62,7 +63,15 @@ registry.registerPath({ tags: ["Manager - Scouters (Public)"], summary: "List active scouters for team code", request: { headers: z.object({ "x-team-code": z.string() }) }, - responses: { 200: { description: "Scouters", content: { "application/json": { schema: z.array(z.any()) } } }, 400: { description: "Invalid request" }, 404: { description: "Team code not found" } }, + responses: { + 200: { + description: "Scouters", + content: { "application/json": { schema: z.array(ScouterSchema) } }, + }, + 400: { description: "Invalid request" }, + 404: { description: "Team code not found" }, + 500: { description: "Server error" }, + }, }); registry.registerPath({ method: "post", @@ -70,7 +79,7 @@ registry.registerPath({ tags: ["Manager - Scouters (Public)"], summary: "Create scouter", request: { body: { content: { "application/json": { schema: z.object({ teamNumber: z.number().int(), name: z.string() }) } } } }, - responses: { 200: { description: "Created", content: { "application/json": { schema: z.any() } } }, 400: { description: "Invalid request" } }, + responses: { 200: { description: "Created", content: { "application/json": { schema: z.any() } } }, 400: { description: "Invalid request" }, 500: { description: "Server error" } }, }); registry.registerPath({ method: "get", @@ -78,7 +87,7 @@ registry.registerPath({ tags: ["Manager - Scouters (Public)"], summary: "List tournaments", request: { params: z.object({ uuid: z.string() }) }, - responses: { 200: { description: "Tournaments", content: { "application/json": { schema: z.array(z.any()) } } } }, + responses: { 200: { description: "Tournaments", content: { "application/json": { schema: z.array(z.any()) } } }, 500: { description: "Server error" } }, }); registry.registerPath({ method: "get", @@ -86,7 +95,7 @@ registry.registerPath({ tags: ["Manager - Scouters (Public)"], summary: "Get scouter schedule", request: { params: z.object({ tournament: z.string() }), headers: z.object({ "x-team-code": z.string() }) }, - responses: { 200: { description: "Schedule", content: { "application/json": { schema: z.any() } } }, 400: { description: "Invalid request" } }, + responses: { 200: { description: "Schedule", content: { "application/json": { schema: z.any() } } }, 400: { description: "Invalid request" }, 500: { description: "Server error" } }, }); registry.registerPath({ method: "get", @@ -94,7 +103,7 @@ registry.registerPath({ tags: ["Manager - Scouters (Public)"], summary: "List scouter tournaments with schedule", request: { headers: z.object({ "x-team-code": z.string() }) }, - responses: { 200: { description: "Tournaments", content: { "application/json": { schema: z.array(z.any()) } } }, 400: { description: "Invalid request" } }, + responses: { 200: { description: "Tournaments", content: { "application/json": { schema: z.array(z.any()) } } }, 400: { description: "Invalid request" }, 500: { description: "Server error" } }, }); // OpenAPI docs for protected scouters endpoints @@ -104,7 +113,7 @@ registry.registerPath({ tags: ["Manager - Scouters"], summary: "Unarchive scouter", request: { params: z.object({ uuid: z.string() }) }, - responses: { 200: { description: "Unarchived" }, 401: { description: "Unauthorized" }, 404: { description: "Not found" } }, + responses: { 200: { description: "Unarchived" }, 401: { description: "Unauthorized" }, 404: { description: "Not found" }, 500: { description: "Server error" } }, security: [{ bearerAuth: [] }], }); registry.registerPath({ @@ -113,7 +122,7 @@ registry.registerPath({ tags: ["Manager - Scouters"], summary: "Archive scouter", request: { params: z.object({ uuid: z.string() }) }, - responses: { 200: { description: "Archived" }, 401: { description: "Unauthorized" }, 404: { description: "Not found" } }, + responses: { 200: { description: "Archived" }, 401: { description: "Unauthorized" }, 404: { description: "Not found" }, 500: { description: "Server error" } }, security: [{ bearerAuth: [] }], }); registry.registerPath({ @@ -122,7 +131,7 @@ registry.registerPath({ tags: ["Manager - Scouters"], summary: "Update scouter name", request: { body: { content: { "application/json": { schema: z.object({ uuid: z.string().optional(), name: z.string() }) } } } }, - responses: { 200: { description: "Updated" }, 400: { description: "Invalid request" } }, + responses: { 200: { description: "Updated" }, 400: { description: "Invalid request" }, 401: { description: "Unauthorized" }, 500: { description: "Server error" } }, security: [{ bearerAuth: [] }], }); registry.registerPath({ @@ -130,7 +139,7 @@ registry.registerPath({ path: "/v1/manager/scouterdashboard", tags: ["Manager - Scouters"], summary: "Delete scouter from dashboard", - responses: { 200: { description: "Deleted" }, 401: { description: "Unauthorized" }, 404: { description: "Not found" } }, + responses: { 200: { description: "Deleted" }, 401: { description: "Unauthorized" }, 404: { description: "Not found" }, 500: { description: "Server error" } }, security: [{ bearerAuth: [] }], }); registry.registerPath({ @@ -138,7 +147,7 @@ registry.registerPath({ path: "/v1/manager/scouterspage", tags: ["Manager - Scouters"], summary: "Scouting lead progress page", - responses: { 200: { description: "Page data", content: { "application/json": { schema: z.any() } } } }, + responses: { 200: { description: "Page data", content: { "application/json": { schema: z.any() } } } , 401: { description: "Unauthorized" }, 500: { description: "Server error" } }, security: [{ bearerAuth: [] }], }); registry.registerPath({ @@ -147,7 +156,7 @@ registry.registerPath({ tags: ["Manager - Scouters"], summary: "Add scouter on dashboard", request: { body: { content: { "application/json": { schema: z.object({ scouterId: z.string(), tournament: z.string().optional() }) } } } }, - responses: { 200: { description: "Created" }, 400: { description: "Invalid request" }, 401: { description: "Unauthorized" } }, + responses: { 200: { description: "Created" }, 400: { description: "Invalid request" }, 401: { description: "Unauthorized" }, 500: { description: "Server error" } }, security: [{ bearerAuth: [] }], }); registry.registerPath({ @@ -155,7 +164,7 @@ registry.registerPath({ path: "/v1/manager/scouterreports", tags: ["Manager - Scouters"], summary: "List scouter reports", - responses: { 200: { description: "Reports", content: { "application/json": { schema: z.any() } } } }, + responses: { 200: { description: "Reports", content: { "application/json": { schema: z.any() } } } , 401: { description: "Unauthorized" }, 500: { description: "Server error" } }, security: [{ bearerAuth: [] }], }); diff --git a/src/routes/manager/scoutershifts.routes.ts b/src/routes/manager/scoutershifts.routes.ts index 9bc6cb1e..5f3ef15b 100644 --- a/src/routes/manager/scoutershifts.routes.ts +++ b/src/routes/manager/scoutershifts.routes.ts @@ -13,7 +13,7 @@ registry.registerPath({ tags: ["Manager - Scouter Shifts"], summary: "Update scouter shift", request: { params: z.object({ uuid: z.string() }), body: { content: { "application/json": { schema: z.object({ scouterId: z.string(), matchNumber: z.number().int() }) } } } }, - responses: { 200: { description: "Updated" }, 400: { description: "Invalid request" }, 401: { description: "Unauthorized" } }, + responses: { 200: { description: "Updated" }, 400: { description: "Invalid request" }, 401: { description: "Unauthorized" }, 500: { description: "Server error" } }, security: [{ bearerAuth: [] }], }); registry.registerPath({ @@ -22,7 +22,7 @@ registry.registerPath({ tags: ["Manager - Scouter Shifts"], summary: "Delete scouter shift", request: { params: z.object({ uuid: z.string() }) }, - responses: { 200: { description: "Deleted" }, 401: { description: "Unauthorized" }, 404: { description: "Not found" } }, + responses: { 200: { description: "Deleted" }, 401: { description: "Unauthorized" }, 404: { description: "Not found" }, 500: { description: "Server error" } }, security: [{ bearerAuth: [] }], }); diff --git a/src/routes/manager/scoutreports.routes.ts b/src/routes/manager/scoutreports.routes.ts index b9827907..21ed85ba 100644 --- a/src/routes/manager/scoutreports.routes.ts +++ b/src/routes/manager/scoutreports.routes.ts @@ -7,12 +7,66 @@ import { getScoutReport } from "../../handler/manager/scoutreports/getScoutRepor import { registry } from "../../lib/openapi.js"; import { z } from "zod"; +import { EventSchema, ScoutReportSchema as PrismaScoutReportSchema } from "../../lib/prisma-zod.js"; + const ScoutReportCreateSchema = z.object({ - match: z.number().int(), - team: z.number().int(), - data: z.record(z.string(), z.any()).optional(), + uuid: z.string(), + tournamentKey: z.string(), + matchType: z.enum(["QUALIFICATION", "ELIMINATION"]), + matchNumber: z.number().int(), + startTime: z.number().int(), + notes: z.string(), + robotRoles: z.array(z.enum(["CYCLING", "SCORING", "FEEDING", "DEFENDING", "IMMOBILE"])), + mobility: z.enum(["TRENCH", "BUMP", "BOTH", "NONE"]), + climbPosition: z.enum(["SIDE", "MIDDLE"]).optional(), + climbSide: z.enum(["FRONT", "BACK"]).optional(), + beached: z.enum(["ON_FUEL", "ON_BUMP", "BOTH", "NEITHER"]), + feederTypes: z.array(z.enum(["CONTINUOUS", "STOP_TO_SHOOT", "DUMP"])), + intakeType: z.enum(["GROUND", "OUTPOST", "BOTH", "NEITHER"]), + robotBrokeDescription: z.string().nullable().optional(), + driverAbility: z.number().int(), + accuracy: z.number().int(), + disrupts: z.boolean(), + defenseEffectiveness: z.number().int(), + scoresWhileMoving: z.boolean(), + autoClimb: z.enum(["NOT_ATTEMPTED", "FAILED", "SUCCEEDED"]), + endgameClimb: z.enum(["NOT_ATTEMPTED", "FAILED", "L1", "L2", "L3"]), + scouterUuid: z.string(), + teamNumber: z.number().int(), + events: z.array( + z.object({ + time: z.number().int(), + action: z.enum([ + "START_SCORING", + "STOP_SCORING", + "START_MATCH", + "START_CAMPING", + "STOP_CAMPING", + "START_DEFENDING", + "STOP_DEFENDING", + "INTAKE", + "OUTTAKE", + "DISRUPT", + "CROSS", + "CLIMB", + "START_FEEDING", + "STOP_FEEDING", + ]), + position: z.enum([ + "LEFT_TRENCH", + "LEFT_BUMP", + "HUB", + "RIGHT_TRENCH", + "RIGHT_BUMP", + "NEUTRAL_ZONE", + "DEPOT", + "OUTPOST", + "NONE", + ]), + points: z.number().int(), + }), + ), }); -const ScoutReportSchema = z.object({ uuid: z.string(), match: z.number().int(), team: z.number().int(), data: z.record(z.string(), z.any()).optional() }); registry.registerPath({ method: "post", @@ -22,10 +76,14 @@ registry.registerPath({ summary: "Create scout report", request: { body: { content: { "application/json": { schema: ScoutReportCreateSchema } } } }, responses: { - 200: { description: "Created", content: { "application/json": { schema: z.object({ uuid: z.string() }) } } }, + 200: { description: "Created", content: { "text/plain": { schema: z.string() } } }, 400: { description: "Invalid request" }, + 401: { description: "Unauthorized" }, + 404: { description: "Match not found" }, + 500: { description: "Server error" }, }, }); + registry.registerPath({ method: "get", path: "/v1/manager/scoutreports/{uuid}", @@ -34,10 +92,20 @@ registry.registerPath({ summary: "Get scout report", request: { params: z.object({ uuid: z.string() }) }, responses: { - 200: { description: "Scout report", content: { "application/json": { schema: ScoutReportSchema } } }, + 200: { + description: "Scout report and events", + content: { + "application/json": { + schema: z.object({ scoutReport: PrismaScoutReportSchema, events: z.array(EventSchema) }), + }, + }, + }, + 400: { description: "Invalid request" }, 404: { description: "Not found" }, + 500: { description: "Server error" }, }, }); + registry.registerPath({ method: "delete", path: "/v1/manager/scoutreports/{uuid}", @@ -45,7 +113,12 @@ registry.registerPath({ tags: ["Manager - Scout Reports"], summary: "Delete scout report", request: { params: z.object({ uuid: z.string() }) }, - responses: { 200: { description: "Deleted" }, 404: { description: "Not found" } }, + responses: { + 200: { description: "Deleted", content: { "text/plain": { schema: z.string() } } }, + 400: { description: "Invalid request" }, + 404: { description: "Not found" }, + 500: { description: "Server error" }, + }, }); const router = Router(); diff --git a/src/routes/manager/settings.routes.ts b/src/routes/manager/settings.routes.ts index 619dbe36..b0505a46 100644 --- a/src/routes/manager/settings.routes.ts +++ b/src/routes/manager/settings.routes.ts @@ -24,52 +24,122 @@ registry.registerPath({ method: "put", path: "/v1/manager/settings", tags: ["Manager - Settings"], - summary: "Update settings", - request: { body: { content: { "application/json": { schema: z.object({ timezone: z.string().optional() }) } } } }, - responses: { 200: { description: "Updated" }, 400: { description: "Invalid request" }, 401: { description: "Unauthorized" } }, + summary: "Update sources", + request: { + body: { + content: { + "application/json": { + schema: z.object({ + teamSource: z.array(z.number().int()), + tournamentSource: z.array(z.string()), + }), + }, + }, + }, + }, + responses: { + 200: { description: "Updated", content: { "text/plain": { schema: z.string() } } }, + 400: { description: "Invalid request" }, + 401: { description: "Unauthorized" }, + 500: { description: "Server error" }, + }, security: [{ bearerAuth: [] }], }); + registry.registerPath({ method: "get", path: "/v1/manager/settings/teamsource", tags: ["Manager - Settings"], summary: "Get team source", - responses: { 200: { description: "Team source", content: { "application/json": { schema: z.object({ source: z.string() }) } } }, 401: { description: "Unauthorized" } }, + responses: { + 200: { + description: "Team source", + content: { + "application/json": { + schema: z.union([ + z.literal("THIS_TEAM"), + z.literal("ALL_TEAMS"), + z.array(z.number().int()), + ]), + }, + }, + }, + 401: { description: "Unauthorized" }, + 500: { description: "Server error" }, + }, security: [{ bearerAuth: [] }], }); + registry.registerPath({ method: "post", path: "/v1/manager/settings/teamsource", tags: ["Manager - Settings"], summary: "Add team source", - request: { body: { content: { "application/json": { schema: z.object({ source: z.string() }) } } } }, - responses: { 200: { description: "Added" }, 400: { description: "Invalid request" } }, + request: { + body: { + content: { + "application/json": { + schema: z.union([ + z.object({ mode: z.literal("ALL_TEAMS") }), + z.object({ mode: z.literal("THIS_TEAM") }), + z.object({ teams: z.array(z.number().int()) }), + ]), + }, + }, + }, + }, + responses: { + 200: { description: "Added", content: { "text/plain": { schema: z.string() } } }, + 400: { description: "Invalid request" }, + 401: { description: "Unauthorized" }, + 403: { description: "Not affiliated with a team" }, + 500: { description: "Server error" }, + }, security: [{ bearerAuth: [] }], }); + registry.registerPath({ method: "get", path: "/v1/manager/settings/tournamentsource", tags: ["Manager - Settings"], summary: "Get tournament source", - responses: { 200: { description: "Tournament source", content: { "application/json": { schema: z.object({ source: z.string() }) } } }, 401: { description: "Unauthorized" } }, + responses: { + 200: { description: "Tournament source", content: { "application/json": { schema: z.array(z.string()) } } }, + 401: { description: "Unauthorized" }, + 500: { description: "Server error" }, + }, security: [{ bearerAuth: [] }], }); + registry.registerPath({ method: "post", path: "/v1/manager/settings/tournamentsource", tags: ["Manager - Settings"], summary: "Add tournament source", - request: { body: { content: { "application/json": { schema: z.object({ source: z.string() }) } } } }, - responses: { 200: { description: "Added" }, 400: { description: "Invalid request" } }, + request: { body: { content: { "application/json": { schema: z.object({ tournaments: z.array(z.string()) }) } } } }, + responses: { + 200: { description: "Added", content: { "text/plain": { schema: z.string() } } }, + 400: { description: "Invalid request" }, + 401: { description: "Unauthorized" }, + 500: { description: "Server error" }, + }, security: [{ bearerAuth: [] }], }); + registry.registerPath({ method: "put", path: "/v1/manager/settings/teamemail", tags: ["Manager - Settings"], summary: "Update team email", - request: { body: { content: { "application/json": { schema: z.object({ email: z.string().email() }) } } } }, - responses: { 200: { description: "Updated" }, 400: { description: "Invalid request" }, 401: { description: "Unauthorized" } }, + request: { query: z.object({ email: z.string().email() }) }, + responses: { + 200: { description: "Verification sent", content: { "text/plain": { schema: z.string() } } }, + 400: { description: "Invalid request" }, + 401: { description: "Unauthorized" }, + 404: { description: "Team not found" }, + 429: { description: "Rate limited" }, + 500: { description: "Server error" }, + }, security: [{ bearerAuth: [] }], }); diff --git a/src/routes/manager/tournaments.routes.ts b/src/routes/manager/tournaments.routes.ts index 046423c2..b27af09e 100644 --- a/src/routes/manager/tournaments.routes.ts +++ b/src/routes/manager/tournaments.routes.ts @@ -6,6 +6,7 @@ import { addScouterShift } from "../../handler/manager/tournament/addScouterShif import { getScouterSchedule } from "../../handler/manager/tournament/getScouterSchedule.js"; import { registry } from "../../lib/openapi.js"; import { z } from "zod"; +import { TeamSchema, TournamentSchema, ScouterScheduleShiftSchema } from "../../lib/prisma-zod.js"; /* @@ -26,7 +27,7 @@ registry.registerPath({ tags: ["Manager - Tournaments"], summary: "List teams in tournament", request: { params: TournamentParamSchema }, - responses: { 200: { description: "Teams", content: { "application/json": { schema: z.array(z.object({ number: z.number().int(), name: z.string().nullable() })) } } }, 401: { description: "Unauthorized" } }, + responses: { 200: { description: "Teams", content: { "application/json": { schema: z.array(TeamSchema) } } }, 401: { description: "Unauthorized" }, 500: { description: "Server error" } }, security: [{ bearerAuth: [] }], }); registry.registerPath({ @@ -35,7 +36,7 @@ registry.registerPath({ tags: ["Manager - Tournaments"], summary: "Ranked teams", request: { params: TournamentParamSchema }, - responses: { 200: { description: "Rankings", content: { "application/json": { schema: z.array(z.object({ number: z.number().int(), rank: z.number().int() })) } } } }, + responses: { 200: { description: "Rankings", content: { "application/json": { schema: z.array(z.object({ number: z.number().int(), name: z.string(), rank: z.number().int().nullable(), rankingPoints: z.number().int().nullable(), matchesPlayed: z.number().int().nullable() })) } } } , 500: { description: "Server error" } }, security: [{ bearerAuth: [] }], }); registry.registerPath({ @@ -44,7 +45,7 @@ registry.registerPath({ tags: ["Manager - Tournaments"], summary: "Create scouter shift", request: { params: TournamentParamSchema, body: { content: { "application/json": { schema: z.object({ uuid: z.string().optional(), scouterId: z.string(), matchNumber: z.number().int() }) } } } }, - responses: { 200: { description: "Created" }, 400: { description: "Invalid request" } }, + responses: { 200: { description: "Created" }, 400: { description: "Invalid request" }, 500: { description: "Server error" } }, security: [{ bearerAuth: [] }], }); registry.registerPath({ @@ -53,7 +54,7 @@ registry.registerPath({ tags: ["Manager - Tournaments"], summary: "List scouter shifts", request: { params: TournamentParamSchema }, - responses: { 200: { description: "Shifts", content: { "application/json": { schema: z.array(z.object({ scouterId: z.string(), matchNumber: z.number().int() })) } } } }, + responses: { 200: { description: "Shifts", content: { "application/json": { schema: z.object({ hash: z.string(), data: z.array(ScouterScheduleShiftSchema) }) } } } , 500: { description: "Server error" } }, security: [{ bearerAuth: [] }], }); From 3dace303fe3517ed2a81edc9587009f19368c3ad Mon Sep 17 00:00:00 2001 From: JackAttack-365 <142643773+jackattack-4@users.noreply.github.com> Date: Sun, 25 Jan 2026 11:37:50 -0800 Subject: [PATCH 04/11] add servers --- src/lib/openapi.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/openapi.ts b/src/lib/openapi.ts index 88b42101..ad993b93 100644 --- a/src/lib/openapi.ts +++ b/src/lib/openapi.ts @@ -64,6 +64,8 @@ export function generateOpenApiDocument() { description: "API Documentation for Lovat, a scouting system used to scout teams and matches in the First Robotics Competition", }, - servers: [{ url: "/" }], + servers: [ + { url: `${process.env.BASE_URL || "https://api.lovat.app"}/v1/` }, + ], }); } From e73aec5850fb6187fb688260a09fe6174d6aae5d Mon Sep 17 00:00:00 2001 From: JackAttack-365 <142643773+jackattack-4@users.noreply.github.com> Date: Sun, 25 Jan 2026 11:50:47 -0800 Subject: [PATCH 05/11] just for posterity --- src/lib/openapi.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib/openapi.ts b/src/lib/openapi.ts index ad993b93..f10675ea 100644 --- a/src/lib/openapi.ts +++ b/src/lib/openapi.ts @@ -64,8 +64,6 @@ export function generateOpenApiDocument() { description: "API Documentation for Lovat, a scouting system used to scout teams and matches in the First Robotics Competition", }, - servers: [ - { url: `${process.env.BASE_URL || "https://api.lovat.app"}/v1/` }, - ], + servers: [{ url: `${process.env.BASE_URL || "https://api.lovat.app"}` }], }); } From d2e740b1e762fcf44ee72f2ad5a2943744a72d9e Mon Sep 17 00:00:00 2001 From: JackAttack-365 <142643773+jackattack-4@users.noreply.github.com> Date: Sun, 25 Jan 2026 15:02:56 -0800 Subject: [PATCH 06/11] api docs --- src/routes/manager/settings.routes.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/routes/manager/settings.routes.ts b/src/routes/manager/settings.routes.ts index 2f7e07db..5be06983 100644 --- a/src/routes/manager/settings.routes.ts +++ b/src/routes/manager/settings.routes.ts @@ -147,6 +147,25 @@ registry.registerPath({ security: [{ bearerAuth: [] }], }); +registry.registerPath({ + method: "get", + path: "/v1/manager/settings/teamemail", + tags: ["Manager - Settings"], + summary: "Get team email", + responses: { + 200: { + description: "Team email", + content: { "text/plain": { schema: z.string() } }, + }, + 400: { description: "Invalid request" }, + 401: { description: "Unauthorized" }, + 404: { description: "Team not found" }, + 429: { description: "Rate limited" }, + 500: { description: "Server error" }, + }, + security: [{ bearerAuth: [] }], +}); + registry.registerPath({ method: "put", path: "/v1/manager/settings/teamemail", From 982b405720526940f90bcf941cbebf871b7a4154 Mon Sep 17 00:00:00 2001 From: JackAttack-365 <142643773+jackattack-4@users.noreply.github.com> Date: Tue, 27 Jan 2026 20:06:19 -0800 Subject: [PATCH 07/11] add redis ratelimiting --- src/lib/middleware/requireAuth.ts | 14 +++++++------- src/redisClient.ts | 12 ++++++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/lib/middleware/requireAuth.ts b/src/lib/middleware/requireAuth.ts index 202443af..35dacfe7 100644 --- a/src/lib/middleware/requireAuth.ts +++ b/src/lib/middleware/requireAuth.ts @@ -4,6 +4,8 @@ import { User } from "@prisma/client"; import { Request as ExpressRequest, Response, NextFunction } from "express"; import * as jose from "jose"; import { createHash } from "crypto"; +import rateLimit from "express-rate-limit"; +import { kv } from "../../redisClient.js"; export interface AuthenticatedRequest extends ExpressRequest { user: User; @@ -35,13 +37,12 @@ export const requireAuth = async ( const keyHash = createHash("sha256").update(tokenString).digest("hex"); - const rateLimit = await prisma.apiKey.findUnique({ - where: { - keyHash: keyHash, - }, - }); + const redisKey = `auth:apikey:${keyHash}:rate`; + + const count = Number(await kv.incr(redisKey)); + if (count === 1) await kv.exp(redisKey); - if (Date.now() - rateLimit.lastUsed.getTime() <= 3 * 1000) { + if (count <= 1) { res.status(429).json({ message: "You have exceeded the rate limit for an API Key. Please wait before making more requests.", @@ -54,7 +55,6 @@ export const requireAuth = async ( keyHash: keyHash, }, data: { - lastUsed: new Date(), requests: { increment: 1, }, diff --git a/src/redisClient.ts b/src/redisClient.ts index 6220288f..4d02703f 100644 --- a/src/redisClient.ts +++ b/src/redisClient.ts @@ -25,9 +25,21 @@ const flush = async (): ReturnType["flushDb"]> => { return await (await redis).flushDb(); }; +const incr = async (key: string): ReturnType["incr"]> => { + return await (await redis).incr(key); +}; + +const exp = async ( + key: string, +): ReturnType["expire"]> => { + return await (await redis).expire(key, 3); +}; + export const kv = { set, get, del, flush, + incr, + exp, }; From 5430c6b2a576d87ae631ddb3befa9ee6050ffb54 Mon Sep 17 00:00:00 2001 From: JackAttack-365 <142643773+jackattack-4@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:30:28 -0800 Subject: [PATCH 08/11] add version --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 08b3c252..89e1534a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lovat-server", - "version": "0.1.0", + "version": "26.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lovat-server", - "version": "0.1.0", + "version": "26.0.0", "license": "ISC", "dependencies": { "@asteasolutions/zod-to-openapi": "^8.4.0", From a0920cd62ffd74d725ce509e33ffee8f3b0c0228 Mon Sep 17 00:00:00 2001 From: jackattack-4 <142643773+jackattack-4@users.noreply.github.com> Date: Sat, 7 Feb 2026 12:54:41 -0800 Subject: [PATCH 09/11] Update requireAuth.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/lib/middleware/requireAuth.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/middleware/requireAuth.ts b/src/lib/middleware/requireAuth.ts index 35dacfe7..f7ae5f4e 100644 --- a/src/lib/middleware/requireAuth.ts +++ b/src/lib/middleware/requireAuth.ts @@ -4,7 +4,6 @@ import { User } from "@prisma/client"; import { Request as ExpressRequest, Response, NextFunction } from "express"; import * as jose from "jose"; import { createHash } from "crypto"; -import rateLimit from "express-rate-limit"; import { kv } from "../../redisClient.js"; export interface AuthenticatedRequest extends ExpressRequest { From 27e9bb4d503e98412544a11a1c66826aa7575d91 Mon Sep 17 00:00:00 2001 From: jackattack-4 <142643773+jackattack-4@users.noreply.github.com> Date: Sat, 7 Feb 2026 12:54:59 -0800 Subject: [PATCH 10/11] Update tournaments.routes.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/routes/manager/tournaments.routes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/manager/tournaments.routes.ts b/src/routes/manager/tournaments.routes.ts index b27af09e..b1c02b51 100644 --- a/src/routes/manager/tournaments.routes.ts +++ b/src/routes/manager/tournaments.routes.ts @@ -6,7 +6,7 @@ import { addScouterShift } from "../../handler/manager/tournament/addScouterShif import { getScouterSchedule } from "../../handler/manager/tournament/getScouterSchedule.js"; import { registry } from "../../lib/openapi.js"; import { z } from "zod"; -import { TeamSchema, TournamentSchema, ScouterScheduleShiftSchema } from "../../lib/prisma-zod.js"; +import { TeamSchema, ScouterScheduleShiftSchema } from "../../lib/prisma-zod.js"; /* From b59b7b2f0e9d850cc3578087b30b8b67f3fdfbb0 Mon Sep 17 00:00:00 2001 From: JackAttack-365 <142643773+jackattack-4@users.noreply.github.com> Date: Sun, 8 Feb 2026 20:29:07 -0800 Subject: [PATCH 11/11] implement suggestions --- src/routes/manager/manager.routes.ts | 115 ++++++++++++++++++++++++--- tsconfig.json | 1 + 2 files changed, 104 insertions(+), 12 deletions(-) diff --git a/src/routes/manager/manager.routes.ts b/src/routes/manager/manager.routes.ts index beebbba0..cd63aaeb 100644 --- a/src/routes/manager/manager.routes.ts +++ b/src/routes/manager/manager.routes.ts @@ -7,6 +7,7 @@ import registeredteams from "./registeredteams.routes.js"; import scouters from "./scouters.routes.js"; import tournaments from "./tournaments.routes.js"; import scoutreports from "./scoutreports.routes.js"; +import scoutershifts from "./scoutershifts.routes.js"; import settings from "./settings.routes.js"; import apikey from "./apikey.routes.js"; @@ -73,7 +74,10 @@ registry.registerPath({ description: "Tournaments and total count", content: { "application/json": { - schema: z.object({ tournaments: z.array(TournamentSchema), count: z.number() }), + schema: z.object({ + tournaments: z.array(TournamentSchema), + count: z.number(), + }), }, }, }, @@ -139,7 +143,10 @@ registry.registerPath({ tags: ["Manager - Account"], summary: "Get current user profile", responses: { - 200: { description: "Profile", content: { "application/json": { schema: ProfileSchema.nullable() } } }, + 200: { + description: "Profile", + content: { "application/json": { schema: ProfileSchema.nullable() } }, + }, 401: { description: "Unauthorized" }, }, security: [{ bearerAuth: [] }], @@ -152,7 +159,21 @@ registry.registerPath({ tags: ["Manager - Users"], summary: "List users", responses: { - 200: { description: "Users", content: { "application/json": { schema: z.array(z.object({ id: z.string(), email: z.string().email(), username: z.string().nullable(), role: z.string() })) } } }, + 200: { + description: "Users", + content: { + "application/json": { + schema: z.array( + z.object({ + id: z.string(), + email: z.string().email(), + username: z.string().nullable(), + role: z.string(), + }), + ), + }, + }, + }, 401: { description: "Unauthorized" }, }, security: [{ bearerAuth: [] }], @@ -192,7 +213,16 @@ registry.registerPath({ tags: ["Manager - Users"], summary: "List analysts", responses: { - 200: { description: "Analysts", content: { "application/json": { schema: z.array(z.object({ id: z.string(), username: z.string().nullable() })) } } }, + 200: { + description: "Analysts", + content: { + "application/json": { + schema: z.array( + z.object({ id: z.string(), username: z.string().nullable() }), + ), + }, + }, + }, 401: { description: "Unauthorized" }, }, security: [{ bearerAuth: [] }], @@ -205,7 +235,12 @@ registry.registerPath({ tags: ["Manager - Teams"], summary: "Get team code for current user", responses: { - 200: { description: "Team code", content: { "application/json": { schema: z.object({ code: z.string() }) } } }, + 200: { + description: "Team code", + content: { + "application/json": { schema: z.object({ code: z.string() }) }, + }, + }, 401: { description: "Unauthorized" }, }, security: [{ bearerAuth: [] }], @@ -221,13 +256,22 @@ registry.registerPath({ body: { content: { "application/json": { - schema: z.object({ match: z.number().int(), team: z.number().int(), notes: z.string().optional() }), + schema: z.object({ + match: z.number().int(), + team: z.number().int(), + notes: z.string().optional(), + }), }, }, }, }, responses: { - 200: { description: "Created", content: { "application/json": { schema: z.object({ uuid: z.string() }) } } }, + 200: { + description: "Created", + content: { + "application/json": { schema: z.object({ uuid: z.string() }) }, + }, + }, 401: { description: "Unauthorized" }, 400: { description: "Invalid request" }, }, @@ -241,7 +285,22 @@ registry.registerPath({ tags: ["Manager - Tournaments"], summary: "Get status of current team in tournaments", responses: { - 200: { description: "Status", content: { "application/json": { schema: z.object({ tournaments: z.array(z.object({ id: z.string(), code: z.string(), status: z.string() })) }) } } }, + 200: { + description: "Status", + content: { + "application/json": { + schema: z.object({ + tournaments: z.array( + z.object({ + id: z.string(), + code: z.string(), + status: z.string(), + }), + ), + }), + }, + }, + }, 401: { description: "Unauthorized" }, }, security: [{ bearerAuth: [] }], @@ -254,7 +313,14 @@ registry.registerPath({ tags: ["Manager - Matches"], summary: "Get match results page data", responses: { - 200: { description: "Results", content: { "application/json": { schema: z.object({ matches: z.array(MatchSchema) }) } } }, + 200: { + description: "Results", + content: { + "application/json": { + schema: z.object({ matches: z.array(MatchSchema) }), + }, + }, + }, 401: { description: "Unauthorized" }, }, security: [{ bearerAuth: [] }], @@ -268,7 +334,11 @@ registry.registerPath({ summary: "Update scout report notes (SCOUTING_LEAD)", request: { params: z.object({ uuid: z.string() }), - body: { content: { "application/json": { schema: z.object({ note: z.string() }) } } }, + body: { + content: { + "application/json": { schema: z.object({ note: z.string() }) }, + }, + }, }, responses: { 200: { description: "Note updated" }, @@ -287,7 +357,16 @@ registry.registerPath({ summary: "List scouters for current team", request: { query: z.object({ archived: z.string().optional() }) }, responses: { - 200: { description: "Scouters", content: { "application/json": { schema: z.array(z.object({ uuid: z.string(), name: z.string().nullable() })) } } }, + 200: { + description: "Scouters", + content: { + "application/json": { + schema: z.array( + z.object({ uuid: z.string(), name: z.string().nullable() }), + ), + }, + }, + }, 401: { description: "Unauthorized" }, 403: { description: "User not affiliated with a team" }, }, @@ -301,7 +380,18 @@ registry.registerPath({ tags: ["Manager - Users"], summary: "Set current user to ANALYST and remove team", responses: { - 200: { description: "Updated", content: { "application/json": { schema: z.object({ id: z.string(), role: z.string(), teamNumber: z.number().nullable() }) } } }, + 200: { + description: "Updated", + content: { + "application/json": { + schema: z.object({ + id: z.string(), + role: z.string(), + teamNumber: z.number().nullable(), + }), + }, + }, + }, 401: { description: "Unauthorized" }, }, security: [{ bearerAuth: [] }], @@ -312,6 +402,7 @@ router.use("/picklists", picklists); router.use("/mutablepicklists", mutablepicklist); router.use("/registeredteams", registeredteams); router.use("/", scouters); +router.use("/scoutershifts", scoutershifts); router.use("/tournament", tournaments); router.use("/scoutreports", scoutreports); router.use("/settings", settings); diff --git a/tsconfig.json b/tsconfig.json index 83fa46ef..35b6a094 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "moduleResolution": "bundler", "target": "esnext", "allowImportingTsExtensions": false, + "resolveJsonModule": true, "esModuleInterop": true, "outDir": "dist" },