{
 "openapi": "3.1.0",
 "info": {
  "title": "Rikskampen X Theme 5 API - staff sign-in",
  "version": "0.1.0",
  "description": "Theme 5 staff sign-in (API03, decision D12: option B of docs/THEME5_O3_AUTHENTICATION_OPTIONS.md, chosen by the owner on 2026-10-07). Coaches and admins sign in with email and password and receive a 15-minute bearer access token (the coachAuth/adminAuth schemes of common.openapi.json) and a 12-hour refresh token in an HttpOnly cookie, rotated on every use. The token carries no role: the server reads the role and the coach-client assignments on every request. Client sign-in (O9) is not part of this contract. Coordinator-reviewed document."
 },
 "paths": {
  "/api/theme5/auth/sign-in": {
   "post": {
    "operationId": "signIn",
    "tags": [
     "auth"
    ],
    "security": [],
    "summary": "Sign in a coach or admin with email and password.",
    "description": "Validates the body (VALIDATION_FAILED), refuses after five failed sign-ins for the email within 15 minutes (TOO_MANY_ATTEMPTS), and answers INVALID_CREDENTIALS with the same message for an unknown email, an inactive account and a wrong password. Every attempt is written to the append-only authentication audit, never with the password.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "additionalProperties": false,
        "required": [
         "email",
         "password"
        ],
        "properties": {
         "email": {
          "type": "string",
          "minLength": 3,
          "maxLength": 254,
          "pattern": "@"
         },
         "password": {
          "type": "string",
          "minLength": 1,
          "maxLength": 256
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Signed in: a short-lived bearer access token, and the rotated refresh token in the cookie.",
      "headers": {
       "Set-Cookie": {
        "$ref": "#/components/headers/SetRefreshCookie"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/SessionGrant"
        }
       }
      }
     },
     "400": {
      "description": "The body is not acceptable (VALIDATION_FAILED, details name the field).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/AuthError"
        }
       }
      }
     },
     "401": {
      "description": "Email or password not accepted (INVALID_CREDENTIALS).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/AuthError"
        }
       }
      }
     },
     "429": {
      "description": "Five failed sign-ins for this email within 15 minutes (TOO_MANY_ATTEMPTS).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/AuthError"
        }
       }
      }
     }
    }
   }
  },
  "/api/theme5/auth/refresh": {
   "post": {
    "operationId": "refreshSession",
    "tags": [
     "auth"
    ],
    "security": [],
    "summary": "Exchange the refresh cookie for a new access token and a rotated refresh token.",
    "description": "A missing, unknown, expired or revoked refresh token, or an inactive account, is UNAUTHENTICATED and clears the cookie. Presenting an already rotated token revokes its whole family (reuse detection).",
    "parameters": [
     {
      "name": "theme5_refresh",
      "in": "cookie",
      "required": false,
      "description": "The refresh token set by sign-in or refresh (HttpOnly, Secure, SameSite=Strict, Path=/api/theme5/auth).",
      "schema": {
       "type": "string",
       "pattern": "^[A-Za-z0-9_-]{43}$"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Signed in: a short-lived bearer access token, and the rotated refresh token in the cookie.",
      "headers": {
       "Set-Cookie": {
        "$ref": "#/components/headers/SetRefreshCookie"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/SessionGrant"
        }
       }
      }
     },
     "401": {
      "description": "No valid refresh token (UNAUTHENTICATED); the cookie is cleared.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/AuthError"
        }
       }
      },
      "headers": {
       "Set-Cookie": {
        "$ref": "#/components/headers/ClearRefreshCookie"
       }
      }
     }
    }
   }
  },
  "/api/theme5/auth/sign-out": {
   "post": {
    "operationId": "signOut",
    "tags": [
     "auth"
    ],
    "security": [],
    "summary": "Sign out: revoke the refresh token family and clear the cookie.",
    "description": "Always succeeds (idempotent). A known refresh token's whole family is revoked and the sign-out is audited.",
    "parameters": [
     {
      "name": "theme5_refresh",
      "in": "cookie",
      "required": false,
      "description": "The refresh token set by sign-in or refresh (HttpOnly, Secure, SameSite=Strict, Path=/api/theme5/auth).",
      "schema": {
       "type": "string",
       "pattern": "^[A-Za-z0-9_-]{43}$"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "Signed out; the cookie is cleared.",
      "headers": {
       "Set-Cookie": {
        "$ref": "#/components/headers/ClearRefreshCookie"
       }
      }
     }
    }
   }
  }
 },
 "components": {
  "schemas": {
   "SessionGrant": {
    "type": "object",
    "additionalProperties": false,
    "required": [
     "accessToken",
     "tokenType",
     "expiresIn",
     "staff"
    ],
    "properties": {
     "accessToken": {
      "type": "string",
      "description": "HS256 JWT; send as Authorization: Bearer <token>."
     },
     "tokenType": {
      "type": "string",
      "enum": [
       "Bearer"
      ]
     },
     "expiresIn": {
      "type": "integer",
      "minimum": 1,
      "description": "Seconds until the access token expires. This server always sends 900; a client schedules its refresh from the value it receives."
     },
     "staff": {
      "type": "object",
      "additionalProperties": false,
      "required": [
       "staffId",
       "role"
      ],
      "properties": {
       "staffId": {
        "type": "string",
        "pattern": "^[A-Za-z0-9_-]{1,64}$"
       },
       "role": {
        "type": "string",
        "enum": [
         "coach",
         "admin"
        ]
       }
      }
     }
    }
   },
   "AuthError": {
    "type": "object",
    "additionalProperties": false,
    "required": [
     "code",
     "message"
    ],
    "properties": {
     "code": {
      "type": "string",
      "enum": [
       "VALIDATION_FAILED",
       "INVALID_CREDENTIALS",
       "TOO_MANY_ATTEMPTS",
       "UNAUTHENTICATED"
      ]
     },
     "message": {
      "type": "string"
     },
     "details": {
      "type": [
       "array",
       "null"
      ],
      "items": {
       "type": "object",
       "additionalProperties": false,
       "required": [
        "field",
        "reason"
       ],
       "properties": {
        "field": {
         "type": "string"
        },
        "reason": {
         "type": "string"
        }
       }
      }
     }
    }
   }
  },
  "headers": {
   "SetRefreshCookie": {
    "description": "theme5_refresh=<token>; Path=/api/theme5/auth; HttpOnly; Secure; SameSite=Strict; Max-Age=43200",
    "schema": {
     "type": "string"
    }
   },
   "ClearRefreshCookie": {
    "description": "theme5_refresh=; Path=/api/theme5/auth; HttpOnly; Secure; SameSite=Strict; Max-Age=0",
    "schema": {
     "type": "string"
    }
   }
  }
 }
}
