openapi: 3.0.3 info: title: CEH Score API version: 1.0.0 description: | API for a single-user dashboard with shared assessments, settings, and polls. The supported browser UI deployment sets `ALLOW_OPEN_API=true` and leaves `API_SECRET` unset. In that mode all shared reads and writes are unauthenticated. When `API_SECRET` is set, non-health API requests require a bearer token; that API-only mode is incompatible with the browser UI until session authentication protects both pages and API routes. The secret must never be exposed in client code or serialized into hydration data. tags: - name: assessments - name: settings - name: polls security: - BearerAuth: [] - {} paths: /api/assessments: get: tags: [ assessments ] operationId: getAssessments summary: List all assessments responses: '200': description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/Assessment' post: tags: [ assessments ] operationId: createAssessment summary: Create a new assessment description: > `percentage` and `passed` are computed server-side and must not be sent by the client. `id` is optional; when omitted the server assigns a UUID. `domain` must be `Full Exam` or a known CEH v13 domain name. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AssessmentInput' responses: '201': description: Created content: application/json: schema: $ref: '#/components/schemas/Assessment' '400': description: Invalid request body '409': description: An assessment with this ID already exists delete: tags: [ assessments ] operationId: clearAssessments summary: Delete all assessments responses: '204': description: No Content /api/assessments/{id}: delete: tags: [ assessments ] operationId: deleteAssessment summary: Delete an assessment by ID parameters: - in: path name: id required: true schema: type: string responses: '204': description: No Content '400': description: Invalid ID /api/settings: get: tags: [ settings ] operationId: getSettings summary: Get user settings responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/UserSettings' put: tags: [ settings ] operationId: updateSettings summary: Update user settings requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UserSettings' responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/UserSettings' '400': description: Invalid request body /api/polls: get: tags: [ polls ] operationId: getPollResults summary: List poll result rows, optionally filtered by pollId parameters: - in: query name: pollId required: false schema: type: string responses: '200': description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/PollResult' '400': description: Invalid pollId post: tags: [ polls ] operationId: createPollResult summary: Create a new poll option row requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreatePollResultRequest' responses: '201': description: Created content: application/json: schema: $ref: '#/components/schemas/PollResult' '400': description: Invalid request body '409': description: A poll option with this text already exists for this poll /api/polls/{pollId}: get: tags: [ polls ] operationId: getPollStats summary: Get aggregated statistics for a poll parameters: - in: path name: pollId required: true schema: type: string responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/PollStats' '400': description: Invalid pollId '404': description: Poll not found delete: tags: [ polls ] operationId: deletePoll summary: Delete all results for a poll parameters: - in: path name: pollId required: true schema: type: string responses: '204': description: No Content '400': description: Invalid pollId /api/polls/{pollId}/votes: post: tags: [ polls ] operationId: votePoll summary: Record a vote for a poll option (rate-limited) parameters: - in: path name: pollId required: true schema: type: string requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/VoteRequest' responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/PollResult' '400': description: Invalid request body or pollId '429': description: Too many requests components: securitySchemes: BearerAuth: type: http scheme: bearer description: | Enabled only when `API_SECRET` is configured. Send `Authorization: Bearer `. The alternative empty security requirement represents intentional open mode (`ALLOW_OPEN_API=true`), where every shared read and write is unauthenticated. schemas: AssessmentInput: type: object required: [ date, type, score, maxScore, timeTaken, domain ] properties: id: type: string description: > Optional client-supplied id. When omitted the server assigns a UUID. date: type: string format: date type: type: string enum: [ practice, official, mock ] score: type: integer maxScore: type: integer timeTaken: type: integer domain: type: string description: Must be `Full Exam` or a known CEH v13 domain name. notes: type: string Assessment: type: object required: [ id, date, type, score, maxScore, percentage, timeTaken, domain, notes, passed, createdAt ] properties: id: type: string date: type: string format: date type: type: string enum: [ practice, official, mock ] score: type: integer maxScore: type: integer percentage: type: number timeTaken: type: integer domain: type: string notes: type: string passed: type: boolean createdAt: type: string format: date-time UserSettings: type: object required: [ name, targetScore, examDate, theme ] properties: name: type: string targetScore: type: integer examDate: type: string theme: type: string enum: [ dark, light ] PollResult: type: object required: [ id, pollId, pollQuestion, optionText, voteCount, createdAt, updatedAt ] properties: id: type: integer pollId: type: string pollQuestion: type: string optionText: type: string voteCount: type: integer createdAt: type: string format: date-time updatedAt: type: string format: date-time PollStats: type: object required: [ pollId, pollQuestion, totalVotes, options, createdAt, updatedAt ] properties: pollId: type: string pollQuestion: type: string totalVotes: type: integer options: type: array items: $ref: '#/components/schemas/PollStatsOption' createdAt: type: string format: date-time updatedAt: type: string format: date-time PollStatsOption: type: object required: [ id, optionText, voteCount, percentage ] properties: id: type: integer optionText: type: string voteCount: type: integer percentage: type: integer CreatePollResultRequest: type: object required: [ pollId, pollQuestion, optionText ] properties: pollId: type: string description: Must match a configured community poll id. pollQuestion: type: string optionText: type: string description: Must match an option on the configured poll. VoteRequest: type: object required: [ optionText ] properties: optionText: type: string description: Must match an option on the configured poll. pollQuestion: type: string description: Ignored for known polls; the server uses the configured question.