Files
shooting-event/backend/openapi/openapi.json
2026-04-29 00:45:48 +04:00

1409 lines
34 KiB
JSON

{
"openapi": "3.0.3",
"info": {
"title": "Shooting Event API",
"description": "REST API for live scoring, admin control, proof images, and AI score advice.",
"version": "1.0.0"
},
"servers": [
{
"url": "/api",
"description": "Same host API base path"
}
],
"tags": [
{
"name": "Public",
"description": "Public endpoints available without admin authentication"
},
{
"name": "Admin",
"description": "Admin endpoints protected by bearer token"
}
],
"paths": {
"/health": {
"get": {
"tags": [
"Public"
],
"summary": "Health check",
"operationId": "getHealth",
"responses": {
"200": {
"description": "Service is healthy",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HealthResponse"
}
}
}
}
}
}
},
"/state": {
"get": {
"tags": [
"Public"
],
"summary": "Get application state",
"description": "Returns public state by default. If a valid admin bearer token is sent, `scoreProofs` is included.",
"operationId": "getState",
"security": [
{},
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "Current application state",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StateResponse"
}
}
}
},
"500": {
"$ref": "#/components/responses/InternalServerError"
}
}
}
},
"/events": {
"get": {
"tags": [
"Public"
],
"summary": "Live update stream (SSE)",
"description": "Server-Sent Events stream. Emits `ready` and `state` events plus periodic keepalive pings.",
"operationId": "streamEvents",
"responses": {
"200": {
"description": "Event stream opened",
"content": {
"text/event-stream": {
"schema": {
"type": "string",
"example": "event: ready\\ndata: {\"ts\":\"2026-04-28T10:00:00Z\"}\\n\\n"
}
}
}
},
"500": {
"$ref": "#/components/responses/InternalServerError"
}
}
}
},
"/admin/login": {
"post": {
"tags": [
"Admin"
],
"summary": "Admin login",
"description": "Authenticates admin credentials and returns a bearer token valid for 8 hours.",
"operationId": "adminLogin",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminLoginRequest"
}
}
}
},
"responses": {
"200": {
"description": "Login successful",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminLoginResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"500": {
"$ref": "#/components/responses/InternalServerError"
}
}
}
},
"/admin/logout": {
"post": {
"tags": [
"Admin"
],
"summary": "Admin logout",
"operationId": "adminLogout",
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "Token invalidated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OkResponse"
}
}
}
},
"401": {
"$ref": "#/components/responses/Unauthorized"
}
}
}
},
"/admin/state": {
"get": {
"tags": [
"Admin"
],
"summary": "Get full admin state",
"description": "Same shape as `/state`, always includes admin-visible fields such as `scoreProofs`.",
"operationId": "getAdminState",
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "Full admin state",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StateResponse"
}
}
}
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"500": {
"$ref": "#/components/responses/InternalServerError"
}
}
}
},
"/admin/settings": {
"put": {
"tags": [
"Admin"
],
"summary": "Update app settings",
"operationId": "updateAdminSettings",
"security": [
{
"bearerAuth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminSettingsUpdateRequest"
}
}
}
},
"responses": {
"200": {
"description": "Updated state",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StateResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"500": {
"$ref": "#/components/responses/InternalServerError"
}
}
}
},
"/admin/players": {
"post": {
"tags": [
"Admin"
],
"summary": "Create player",
"operationId": "createPlayer",
"security": [
{
"bearerAuth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PlayerCreateRequest"
}
}
}
},
"responses": {
"201": {
"description": "Player created; full updated state returned",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StateResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"500": {
"$ref": "#/components/responses/InternalServerError"
}
}
}
},
"/admin/players/auto-group": {
"post": {
"tags": [
"Admin"
],
"summary": "Auto-assign player groups",
"description": "Randomly shuffles players and assigns group codes in round-robin order.",
"operationId": "autoGroupPlayers",
"security": [
{
"bearerAuth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AutoGroupPlayersRequest"
}
}
}
},
"responses": {
"200": {
"description": "Updated state",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StateResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"500": {
"$ref": "#/components/responses/InternalServerError"
}
}
}
},
"/admin/players/{id}": {
"put": {
"tags": [
"Admin"
],
"summary": "Update player",
"operationId": "updatePlayer",
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/PlayerIdParam"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PlayerUpdateRequest"
}
}
}
},
"responses": {
"200": {
"description": "Updated state",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StateResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"404": {
"$ref": "#/components/responses/NotFound"
},
"500": {
"$ref": "#/components/responses/InternalServerError"
}
}
},
"delete": {
"tags": [
"Admin"
],
"summary": "Delete player",
"operationId": "deletePlayer",
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/PlayerIdParam"
}
],
"responses": {
"200": {
"description": "Updated state",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StateResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"404": {
"$ref": "#/components/responses/NotFound"
},
"500": {
"$ref": "#/components/responses/InternalServerError"
}
}
}
},
"/admin/scores/{stage}/{id}": {
"put": {
"tags": [
"Admin"
],
"summary": "Set a score",
"description": "Upserts score for a given stage/player. Score must be 0..100.",
"operationId": "updateScore",
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/ScoreStageParam"
},
{
"$ref": "#/components/parameters/PlayerIdParam"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ScoreUpdateRequest"
}
}
}
},
"responses": {
"200": {
"description": "Updated state",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StateResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"500": {
"$ref": "#/components/responses/InternalServerError"
}
}
}
},
"/admin/scores/{stage}/{id}/proof": {
"put": {
"tags": [
"Admin"
],
"summary": "Upload score proof image",
"description": "Stores proof image data (typically a data URL string).",
"operationId": "updateScoreProof",
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/ScoreStageParam"
},
{
"$ref": "#/components/parameters/PlayerIdParam"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ScoreProofUpdateRequest"
}
}
}
},
"responses": {
"200": {
"description": "Updated state",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StateResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"500": {
"$ref": "#/components/responses/InternalServerError"
}
}
},
"delete": {
"tags": [
"Admin"
],
"summary": "Delete score proof image",
"operationId": "deleteScoreProof",
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/ScoreStageParam"
},
{
"$ref": "#/components/parameters/PlayerIdParam"
}
],
"responses": {
"200": {
"description": "Updated state",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StateResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"500": {
"$ref": "#/components/responses/InternalServerError"
}
}
}
},
"/admin/scores/{stage}/{id}/advice": {
"post": {
"tags": [
"Admin"
],
"summary": "Get AI score advice",
"description": "Uses configured Gemini model with uploaded proof image to propose a score and reason.",
"operationId": "getScoreAdvice",
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/ScoreStageParam"
},
{
"$ref": "#/components/parameters/PlayerIdParam"
}
],
"responses": {
"200": {
"description": "Advice generated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ScoreAdviceResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"502": {
"description": "AI upstream returned an error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"503": {
"description": "AI service not configured",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"500": {
"$ref": "#/components/responses/InternalServerError"
}
}
}
},
"/admin/scores/{stage}/reset": {
"post": {
"tags": [
"Admin"
],
"summary": "Reset a stage family",
"description": "Resets scores to zero for one stage family or tie-break stage. Optionally removes proofs.",
"operationId": "resetStage",
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/ResetStageParam"
}
],
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ResetStageRequest"
}
}
}
},
"responses": {
"200": {
"description": "Updated state",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StateResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"500": {
"$ref": "#/components/responses/InternalServerError"
}
}
}
}
},
"components": {
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "Token"
}
},
"parameters": {
"PlayerIdParam": {
"name": "id",
"in": "path",
"required": true,
"description": "Player ID",
"schema": {
"type": "integer",
"minimum": 1
}
},
"ScoreStageParam": {
"name": "stage",
"in": "path",
"required": true,
"description": "Score stage key",
"schema": {
"type": "string",
"enum": [
"prelim_r1",
"prelim_r2",
"prelim_r3",
"final_r1",
"final_r2",
"prelim_tiebreak",
"final_tiebreak"
]
}
},
"ResetStageParam": {
"name": "stage",
"in": "path",
"required": true,
"description": "Reset target",
"schema": {
"type": "string",
"enum": [
"preliminary",
"final",
"prelim_tiebreak",
"final_tiebreak"
]
}
}
},
"responses": {
"BadRequest": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"Unauthorized": {
"description": "Missing or invalid bearer token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"NotFound": {
"description": "Resource not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"InternalServerError": {
"description": "Internal server error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
},
"schemas": {
"ErrorResponse": {
"type": "object",
"required": [
"message"
],
"properties": {
"message": {
"type": "string"
}
}
},
"OkResponse": {
"type": "object",
"required": [
"ok"
],
"properties": {
"ok": {
"type": "boolean"
}
}
},
"HealthResponse": {
"type": "object",
"required": [
"status"
],
"properties": {
"status": {
"type": "string",
"example": "ok"
}
}
},
"AdminLoginRequest": {
"type": "object",
"required": [
"username",
"password"
],
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
}
}
},
"AdminLoginResponse": {
"type": "object",
"required": [
"token",
"expiresAt",
"username"
],
"properties": {
"token": {
"type": "string"
},
"expiresAt": {
"type": "string",
"format": "date-time"
},
"username": {
"type": "string"
}
}
},
"CompetitionMeta": {
"type": "object",
"required": [
"titleAr",
"titleEn"
],
"properties": {
"titleAr": {
"type": "string"
},
"titleEn": {
"type": "string"
}
}
},
"Player": {
"type": "object",
"required": [
"id",
"nameAr",
"nameEn",
"groupCode",
"imageData"
],
"properties": {
"id": {
"type": "integer"
},
"nameAr": {
"type": "string"
},
"nameEn": {
"type": "string"
},
"groupCode": {
"type": "string"
},
"imageData": {
"type": "string"
}
}
},
"RankingRow": {
"type": "object",
"required": [
"playerId",
"nameAr",
"nameEn",
"groupCode",
"imageData",
"score",
"tieBreak",
"rank",
"seed",
"finalGroup"
],
"properties": {
"playerId": {
"type": "integer"
},
"nameAr": {
"type": "string"
},
"nameEn": {
"type": "string"
},
"groupCode": {
"type": "string"
},
"imageData": {
"type": "string"
},
"score": {
"type": "integer"
},
"tieBreak": {
"type": "integer"
},
"rank": {
"type": "integer"
},
"seed": {
"type": "integer"
},
"finalGroup": {
"type": "integer"
}
}
},
"TieBreakInfo": {
"type": "object",
"required": [
"required",
"resolved",
"slots",
"playerIds"
],
"properties": {
"required": {
"type": "boolean"
},
"resolved": {
"type": "boolean"
},
"slots": {
"type": "integer"
},
"playerIds": {
"type": "array",
"items": {
"type": "integer"
}
}
}
},
"RankingBundle": {
"type": "object",
"required": [
"rows",
"tieBreak",
"unresolved"
],
"properties": {
"rows": {
"type": "array",
"items": {
"$ref": "#/components/schemas/RankingRow"
}
},
"tieBreak": {
"$ref": "#/components/schemas/TieBreakInfo"
},
"unresolved": {
"type": "boolean"
}
}
},
"FinalGroups": {
"type": "object",
"required": [
"group1",
"group2"
],
"properties": {
"group1": {
"type": "array",
"items": {
"$ref": "#/components/schemas/RankingRow"
}
},
"group2": {
"type": "array",
"items": {
"$ref": "#/components/schemas/RankingRow"
}
}
}
},
"DerivedState": {
"type": "object",
"required": [
"preliminaryRanking",
"finalists",
"finalGroups",
"finalRanking",
"podium"
],
"properties": {
"preliminaryRanking": {
"$ref": "#/components/schemas/RankingBundle"
},
"finalists": {
"type": "array",
"items": {
"$ref": "#/components/schemas/RankingRow"
}
},
"finalGroups": {
"$ref": "#/components/schemas/FinalGroups"
},
"finalRanking": {
"$ref": "#/components/schemas/RankingBundle"
},
"podium": {
"type": "array",
"items": {
"$ref": "#/components/schemas/RankingRow"
}
}
}
},
"LiveModeSettings": {
"type": "object",
"required": [
"activeView",
"showGroupLive",
"groupDisplayMode",
"groupFixedCode",
"showPrelimTie",
"showPrelimOverall",
"showFinalGroups",
"finalDisplayMode",
"finalFixedGroup",
"showFinalTie",
"showPodium",
"rotationIntervalSec",
"rotationPlayerCount"
],
"properties": {
"activeView": {
"type": "string",
"enum": [
"group_live",
"prelim_tie",
"prelim_overall",
"final_groups",
"final_tie",
"podium"
]
},
"showGroupLive": {
"type": "boolean"
},
"groupDisplayMode": {
"type": "string",
"enum": [
"rotate",
"fixed"
]
},
"groupFixedCode": {
"type": "string"
},
"showPrelimTie": {
"type": "boolean"
},
"showPrelimOverall": {
"type": "boolean"
},
"showFinalGroups": {
"type": "boolean"
},
"finalDisplayMode": {
"type": "string",
"enum": [
"rotate",
"fixed"
]
},
"finalFixedGroup": {
"type": "integer",
"enum": [
1,
2
]
},
"showFinalTie": {
"type": "boolean"
},
"showPodium": {
"type": "boolean"
},
"rotationIntervalSec": {
"type": "integer",
"minimum": 3,
"maximum": 30
},
"rotationPlayerCount": {
"type": "integer",
"minimum": 3,
"maximum": 40
}
}
},
"LiveModeSettingsUpdateRequest": {
"type": "object",
"description": "Partial patch. Send only fields that need to change.",
"properties": {
"activeView": {
"type": "string",
"enum": [
"group_live",
"prelim_tie",
"prelim_overall",
"final_groups",
"final_tie",
"podium"
]
},
"showGroupLive": {
"type": "boolean"
},
"groupDisplayMode": {
"type": "string",
"enum": [
"rotate",
"fixed"
]
},
"groupFixedCode": {
"type": "string"
},
"showPrelimTie": {
"type": "boolean"
},
"showPrelimOverall": {
"type": "boolean"
},
"showFinalGroups": {
"type": "boolean"
},
"finalDisplayMode": {
"type": "string",
"enum": [
"rotate",
"fixed"
]
},
"finalFixedGroup": {
"type": "integer",
"enum": [
1,
2
]
},
"showFinalTie": {
"type": "boolean"
},
"showPodium": {
"type": "boolean"
},
"rotationIntervalSec": {
"type": "integer",
"minimum": 3,
"maximum": 30
},
"rotationPlayerCount": {
"type": "integer",
"minimum": 3,
"maximum": 40
}
}
},
"AppSettings": {
"type": "object",
"required": [
"viewProofInView",
"liveMode"
],
"properties": {
"viewProofInView": {
"type": "boolean"
},
"liveMode": {
"$ref": "#/components/schemas/LiveModeSettings"
}
}
},
"StateResponse": {
"type": "object",
"required": [
"competition",
"players",
"scores",
"settings",
"derived",
"serverTime"
],
"properties": {
"competition": {
"$ref": "#/components/schemas/CompetitionMeta"
},
"players": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Player"
}
},
"scores": {
"type": "object",
"additionalProperties": {
"type": "object",
"additionalProperties": {
"type": "integer"
}
}
},
"scoreProofs": {
"type": "object",
"additionalProperties": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"description": "Included for admin-authenticated state requests."
},
"settings": {
"$ref": "#/components/schemas/AppSettings"
},
"derived": {
"$ref": "#/components/schemas/DerivedState"
},
"serverTime": {
"type": "string",
"format": "date-time"
}
}
},
"AdminSettingsUpdateRequest": {
"type": "object",
"description": "At least one field must be provided.",
"properties": {
"viewProofInView": {
"type": "boolean"
},
"liveMode": {
"$ref": "#/components/schemas/LiveModeSettingsUpdateRequest"
}
}
},
"PlayerCreateRequest": {
"type": "object",
"required": [
"nameAr",
"nameEn",
"groupCode"
],
"properties": {
"nameAr": {
"type": "string"
},
"nameEn": {
"type": "string"
},
"groupCode": {
"type": "string"
}
}
},
"PlayerUpdateRequest": {
"type": "object",
"description": "Partial patch. Set `clearImage` true to remove image.",
"properties": {
"nameAr": {
"type": "string"
},
"nameEn": {
"type": "string"
},
"groupCode": {
"type": "string"
},
"imageData": {
"type": "string",
"description": "Base64/data URL payload. Max about 2.5MB characters."
},
"clearImage": {
"type": "boolean"
}
}
},
"AutoGroupPlayersRequest": {
"type": "object",
"required": [
"groups"
],
"properties": {
"groups": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 1
}
}
},
"ScoreUpdateRequest": {
"type": "object",
"required": [
"score"
],
"properties": {
"score": {
"type": "integer",
"minimum": 0,
"maximum": 100
}
}
},
"ScoreProofUpdateRequest": {
"type": "object",
"required": [
"imageData"
],
"properties": {
"imageData": {
"type": "string",
"description": "Base64/data URL payload. Max about 5MB characters."
}
}
},
"ResetStageRequest": {
"type": "object",
"properties": {
"resetProofs": {
"type": "boolean"
}
}
},
"ScoreAdviceResponse": {
"type": "object",
"required": [
"stage",
"playerId",
"advisedScore",
"reason",
"generatedAt"
],
"properties": {
"stage": {
"type": "string"
},
"playerId": {
"type": "integer"
},
"advisedScore": {
"type": "integer",
"minimum": 0,
"maximum": 100
},
"reason": {
"type": "string"
},
"generatedAt": {
"type": "string",
"format": "date-time"
}
}
}
}
}
}