openapi: 3.1.0 info: title: API-ritif version: 1.0.0 summary: Le bar de l'Ă©cole de dev, qui sert ses cocktails par API. description: | Bienvenue au bar **API-ritif** đŸč - Toutes les rĂ©ponses sont en JSON, les erreurs au format `application/problem+json` (RFC 9457). - Les listes sont paginĂ©es (`page`, `per_page`) et contiennent des liens `_links`. - Les routes protĂ©gĂ©es attendent une clĂ© dans le header `X-API-Key`, ou un JWT obtenu via `POST /auth/token`. - Le barman applique un rate limiting : au-delĂ , rĂ©ponse `429` avec un header `Retry-After`. - Toutes les routes rĂ©pondent Ă  `OPTIONS` avec un header `Allow`. Consommation responsable : les cocktails sont virtuels, les codes de statut sont rĂ©els. license: name: CC BY-NC-SA 4.0 url: https://creativecommons.org/licenses/by-nc-sa/4.0/deed.fr servers: - url: https://api-ritif.example/v1 description: Le bar (adresse donnĂ©e en cours) - url: http://localhost:8000/v1 description: En local tags: - name: Accueil description: Le point d'entrĂ©e de l'API - name: Clients description: Inscription et profil - name: Cocktails description: La carte du bar - name: Avis description: Les avis des clients sur les cocktails - name: Commandes description: Commander, suivre et annuler - name: Authentification description: Obtenir un JWT - name: Chasse au trĂ©sor description: Le TP1 paths: /: get: tags: [Accueil] operationId: getRoot security: [] summary: Point d'entrĂ©e de l'API description: Renvoie les liens vers les principales ressources. Lisez bien les headers. responses: '200': description: Liens vers les ressources content: application/json: schema: $ref: '#/components/schemas/Root' '429': $ref: '#/components/responses/TooManyRequests' /customers: post: tags: [Clients] operationId: createCustomer security: [] summary: Devenir client du bar description: | CrĂ©e un compte client et renvoie sa clĂ© d'API. **La clĂ© n'est affichĂ©e qu'une seule fois** : notez-la. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CustomerCreate' responses: '201': description: Client créé headers: Location: $ref: '#/components/headers/Location' content: application/json: schema: $ref: '#/components/schemas/CustomerCreated' '409': description: Ce pseudo est dĂ©jĂ  pris content: application/problem+json: schema: $ref: '#/components/schemas/Problem' '422': $ref: '#/components/responses/ValidationError' '429': $ref: '#/components/responses/TooManyRequests' /me: get: tags: [Clients] operationId: getMe summary: Mon profil security: - ApiKeyAuth: [] - BearerAuth: [] responses: '200': description: Le profil du client authentifiĂ© content: application/json: schema: $ref: '#/components/schemas/Customer' '401': $ref: '#/components/responses/Unauthorized' '429': $ref: '#/components/responses/TooManyRequests' patch: tags: [Clients] operationId: updateMe summary: Modifier mon profil description: Seul le pseudo est modifiable. security: - ApiKeyAuth: [] - BearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CustomerUpdate' responses: '200': description: Profil modifiĂ© content: application/json: schema: $ref: '#/components/schemas/Customer' '401': $ref: '#/components/responses/Unauthorized' '409': description: Ce pseudo est dĂ©jĂ  pris content: application/problem+json: schema: $ref: '#/components/schemas/Problem' '422': $ref: '#/components/responses/ValidationError' '429': $ref: '#/components/responses/TooManyRequests' /cocktails: get: tags: [Cocktails] operationId: listCocktails security: [] summary: La carte parameters: - name: tag in: query description: Ne garder que les cocktails portant ce tag schema: type: string example: classic - name: alcohol_free in: query description: Ne garder que les cocktails avec (`false`) ou sans (`true`) alcool schema: type: boolean - name: max_price in: query description: Prix maximum, en euros schema: type: number minimum: 0 example: 10 - name: sort in: query description: | Champs de tri sĂ©parĂ©s par des virgules. Un `-` devant un champ inverse l'ordre. Champs possibles : `id`, `name`, `price`. schema: type: string default: id example: -price,name - $ref: '#/components/parameters/Page' - $ref: '#/components/parameters/PerPage' responses: '200': description: Une page de la carte headers: Link: $ref: '#/components/headers/Link' content: application/json: schema: $ref: '#/components/schemas/CocktailList' '422': $ref: '#/components/responses/ValidationError' '429': $ref: '#/components/responses/TooManyRequests' /cocktails/{cocktail_id}: parameters: - $ref: '#/components/parameters/CocktailId' get: tags: [Cocktails] operationId: getCocktail security: [] summary: Un cocktail description: | NĂ©gociation de contenu : selon le header `Accept`, le cocktail est servi en JSON ou en XML (version « nostalgie SOAP 2003 »). responses: '200': description: Le cocktail content: application/json: schema: $ref: '#/components/schemas/Cocktail' application/xml: schema: $ref: '#/components/schemas/Cocktail' '404': $ref: '#/components/responses/NotFound' '406': description: Format demandĂ© dans `Accept` non disponible content: application/problem+json: schema: $ref: '#/components/schemas/Problem' '429': $ref: '#/components/responses/TooManyRequests' /cocktails/{cocktail_id}/reviews: parameters: - $ref: '#/components/parameters/CocktailId' get: tags: [Avis] operationId: listReviews security: [] summary: Les avis sur un cocktail description: Avis triĂ©s du plus rĂ©cent au plus ancien. parameters: - $ref: '#/components/parameters/Page' - $ref: '#/components/parameters/PerPage' responses: '200': description: Une page d'avis headers: Link: $ref: '#/components/headers/Link' content: application/json: schema: $ref: '#/components/schemas/ReviewList' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/ValidationError' '429': $ref: '#/components/responses/TooManyRequests' post: tags: [Avis] operationId: createReview summary: Donner son avis sur un cocktail description: Un seul avis par client et par cocktail. security: - ApiKeyAuth: [] - BearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ReviewCreate' responses: '201': description: Avis créé headers: Location: $ref: '#/components/headers/Location' content: application/json: schema: $ref: '#/components/schemas/Review' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' '409': description: Vous avez dĂ©jĂ  donnĂ© votre avis sur ce cocktail content: application/problem+json: schema: $ref: '#/components/schemas/Problem' '422': $ref: '#/components/responses/ValidationError' '429': $ref: '#/components/responses/TooManyRequests' /cocktails/{cocktail_id}/reviews/{review_id}: parameters: - $ref: '#/components/parameters/CocktailId' - name: review_id in: path required: true schema: type: integer minimum: 1 get: tags: [Avis] operationId: getReview summary: Un avis security: [] responses: '200': description: L'avis content: application/json: schema: $ref: '#/components/schemas/Review' '404': $ref: '#/components/responses/NotFound' '429': $ref: '#/components/responses/TooManyRequests' /orders: get: tags: [Commandes] operationId: listOrders summary: Les commandes description: | Par dĂ©faut, uniquement vos commandes. Avec `scope=all`, toutes les commandes de la promo, anonymisĂ©es (pseudo uniquement). security: - ApiKeyAuth: [] - BearerAuth: [] parameters: - name: scope in: query schema: type: string enum: [mine, all] default: mine - name: status in: query schema: $ref: '#/components/schemas/OrderStatus' - $ref: '#/components/parameters/Page' - $ref: '#/components/parameters/PerPage' responses: '200': description: Une page de commandes headers: Link: $ref: '#/components/headers/Link' content: application/json: schema: $ref: '#/components/schemas/OrderList' '401': $ref: '#/components/responses/Unauthorized' '422': $ref: '#/components/responses/ValidationError' '429': $ref: '#/components/responses/TooManyRequests' post: tags: [Commandes] operationId: createOrder summary: Commander un cocktail description: | La commande avance toute seule : `pending` → `preparing` (30 s) → `ready` (2 min) → `served`. Elle ne peut ĂȘtre annulĂ©e que tant qu'elle est `pending`. security: - ApiKeyAuth: [] - BearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/OrderCreate' responses: '201': description: Commande enregistrĂ©e headers: Location: $ref: '#/components/headers/Location' content: application/json: schema: $ref: '#/components/schemas/Order' '401': $ref: '#/components/responses/Unauthorized' '403': description: Cocktail rĂ©servĂ© aux barmans content: application/problem+json: schema: $ref: '#/components/schemas/Problem' '404': $ref: '#/components/responses/NotFound' '409': description: Cocktail en rupture de stock content: application/problem+json: schema: $ref: '#/components/schemas/Problem' '422': $ref: '#/components/responses/ValidationError' '429': $ref: '#/components/responses/TooManyRequests' '508': description: Loop detected. On n'en sort pas. content: application/problem+json: schema: $ref: '#/components/schemas/Problem' /orders/{order_id}: parameters: - name: order_id in: path required: true schema: type: integer minimum: 1 get: tags: [Commandes] operationId: getOrder summary: Une commande description: Renvoie `404` si la commande n'existe pas ou ne vous appartient pas. security: - ApiKeyAuth: [] - BearerAuth: [] responses: '200': description: La commande, avec les liens des actions possibles content: application/json: schema: $ref: '#/components/schemas/Order' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' '429': $ref: '#/components/responses/TooManyRequests' delete: tags: [Commandes] operationId: cancelOrder summary: Annuler une commande description: Possible uniquement tant que la commande est `pending`. security: - ApiKeyAuth: [] - BearerAuth: [] responses: '204': description: Commande annulĂ©e '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' '409': description: La commande n'est plus annulable content: application/problem+json: schema: $ref: '#/components/schemas/Problem' example: type: https://api-ritif.example/problems/order-not-cancellable title: La commande ne peut plus ĂȘtre annulĂ©e status: 409 detail: La commande 42 est en prĂ©paration. Le shaker tourne dĂ©jĂ . instance: /v1/orders/42 '429': $ref: '#/components/responses/TooManyRequests' /tea: post: tags: [Commandes] operationId: orderTea security: [] summary: Commander un thĂ© description: Ceci est un bar. responses: '418': description: I'm a teapot (RFC 2324) content: application/problem+json: schema: $ref: '#/components/schemas/Problem' /auth/token: post: tags: [Authentification] operationId: createToken summary: Échanger sa clĂ© contre un JWT description: Le token est valable 15 minutes. Ensuite, `401`. security: - ApiKeyAuth: [] responses: '201': description: Token créé content: application/json: schema: $ref: '#/components/schemas/Token' '401': $ref: '#/components/responses/Unauthorized' '429': $ref: '#/components/responses/TooManyRequests' /hunt/flag: post: tags: [Chasse au trĂ©sor] operationId: submitFlag summary: Soumettre un flag de la chasse au trĂ©sor security: - ApiKeyAuth: [] - BearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [code] properties: code: type: string example: FLAG-EXEMPLE responses: '200': description: RĂ©sultat de la soumission content: application/json: schema: $ref: '#/components/schemas/FlagResult' '401': $ref: '#/components/responses/Unauthorized' '422': $ref: '#/components/responses/ValidationError' '429': $ref: '#/components/responses/TooManyRequests' components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key description: ClĂ© obtenue Ă  l'inscription (`POST /customers`). BearerAuth: type: http scheme: bearer bearerFormat: JWT description: Token obtenu via `POST /auth/token`, valable 15 minutes. parameters: CocktailId: name: cocktail_id in: path required: true schema: type: integer minimum: 1 example: 3 Page: name: page in: query description: NumĂ©ro de page, Ă  partir de 1 schema: type: integer minimum: 1 default: 1 PerPage: name: per_page in: query description: Nombre d'Ă©lĂ©ments par page schema: type: integer minimum: 1 maximum: 50 default: 5 headers: Location: description: URL de la ressource créée schema: type: string format: uri-reference Link: description: Liens de pagination (RFC 8288) schema: type: string example: ; rel="next", ; rel="last" RetryAfter: description: Nombre de secondes Ă  attendre avant de rĂ©essayer schema: type: integer example: 10 responses: Unauthorized: description: Authentification absente, invalide ou expirĂ©e content: application/problem+json: schema: $ref: '#/components/schemas/Problem' example: type: https://api-ritif.example/problems/unauthorized title: Unauthorized status: 401 detail: Il faut ĂȘtre client du bar. Envoyez votre clĂ© dans le header X-API-Key. NotFound: description: Ressource introuvable content: application/problem+json: schema: $ref: '#/components/schemas/Problem' example: type: about:blank title: Not Found status: 404 detail: Ce cocktail n'existe pas. Pas encore. ValidationError: description: DonnĂ©es invalides content: application/problem+json: schema: $ref: '#/components/schemas/ValidationProblem' TooManyRequests: description: Trop de requĂȘtes headers: Retry-After: $ref: '#/components/headers/RetryAfter' content: application/problem+json: schema: $ref: '#/components/schemas/Problem' example: type: https://api-ritif.example/problems/rate-limited title: Too Many Requests status: 429 detail: Le barman est dĂ©bordĂ©, reviens dans 10 s schemas: Problem: type: object description: Erreur au format RFC 9457 properties: type: type: string format: uri-reference default: about:blank title: type: string status: type: integer detail: type: string instance: type: string format: uri-reference ValidationProblem: allOf: - $ref: '#/components/schemas/Problem' - type: object properties: errors: type: array items: type: object required: [field, message] properties: field: type: string message: type: string example: type: https://api-ritif.example/problems/validation-error title: DonnĂ©es invalides status: 422 detail: La requĂȘte contient 1 erreur. errors: - field: rating message: doit ĂȘtre compris entre 1 et 5 Link: type: object required: [href] properties: href: type: string format: uri-reference method: type: string description: MĂ©thode HTTP Ă  utiliser, si ce n'est pas GET enum: [POST, PUT, PATCH, DELETE] Links: type: object additionalProperties: $ref: '#/components/schemas/Link' PageMeta: type: object required: [page, per_page, total, _links] properties: page: type: integer per_page: type: integer total: type: integer description: Nombre total d'Ă©lĂ©ments _links: $ref: '#/components/schemas/Links' example: page: 2 per_page: 5 total: 13 _links: self: {href: '/v1/cocktails?page=2&per_page=5'} first: {href: '/v1/cocktails?page=1&per_page=5'} prev: {href: '/v1/cocktails?page=1&per_page=5'} next: {href: '/v1/cocktails?page=3&per_page=5'} last: {href: '/v1/cocktails?page=3&per_page=5'} Root: type: object properties: name: type: string example: API-ritif version: type: string example: 1.0.0 _links: $ref: '#/components/schemas/Links' example: name: API-ritif version: 1.0.0 _links: self: {href: /v1/} cocktails: {href: /v1/cocktails} customers: {href: /v1/customers, method: POST} me: {href: /v1/me} orders: {href: /v1/orders} docs: {href: /docs} CustomerCreate: type: object required: [nickname] additionalProperties: false properties: nickname: type: string minLength: 2 maxLength: 30 pattern: '^[a-zA-Z0-9_-]+$' example: neo CustomerUpdate: type: object additionalProperties: false properties: nickname: type: string minLength: 2 maxLength: 30 pattern: '^[a-zA-Z0-9_-]+$' Customer: type: object required: [id, nickname, role, created_at] properties: id: type: integer example: 12 nickname: type: string example: neo role: type: string enum: [customer, bartender, admin] readOnly: true created_at: type: string format: date-time _links: $ref: '#/components/schemas/Links' CustomerCreated: allOf: - $ref: '#/components/schemas/Customer' - type: object required: [api_key] properties: api_key: type: string description: AffichĂ©e une seule fois example: ak_7f3c9e1b2d4a Cocktail: type: object required: [id, name, price, alcohol_free] properties: id: type: integer example: 3 name: type: string example: Segfault Sunrise description: type: string example: Un classique. Plante rarement, mais sans prĂ©venir. price: type: number description: Prix en euros example: 9.5 alcohol_free: type: boolean example: false tags: type: array items: type: string example: [classic] rating_average: type: [number, 'null'] description: Note moyenne, `null` s'il n'y a pas encore d'avis example: 4.2 _links: $ref: '#/components/schemas/Links' example: id: 3 name: Segfault Sunrise description: Un classique. Plante rarement, mais sans prĂ©venir. price: 9.5 alcohol_free: false tags: [classic] rating_average: 4.2 _links: self: {href: /v1/cocktails/3} reviews: {href: /v1/cocktails/3/reviews} order: {href: /v1/orders, method: POST} CocktailList: allOf: - type: object required: [items] properties: items: type: array items: $ref: '#/components/schemas/Cocktail' - $ref: '#/components/schemas/PageMeta' ReviewCreate: type: object required: [rating] additionalProperties: false properties: rating: type: integer minimum: 1 maximum: 5 example: 4 comment: type: string maxLength: 500 example: Plante rarement, mais quel goĂ»t. Review: type: object required: [id, cocktail_id, author, rating, created_at] properties: id: type: integer example: 7 cocktail_id: type: integer example: 3 author: type: string description: Pseudo de l'auteur example: neo rating: type: integer minimum: 1 maximum: 5 example: 4 comment: type: string example: Plante rarement, mais quel goĂ»t. created_at: type: string format: date-time _links: $ref: '#/components/schemas/Links' ReviewList: allOf: - type: object required: [items] properties: items: type: array items: $ref: '#/components/schemas/Review' - $ref: '#/components/schemas/PageMeta' OrderStatus: type: string enum: [pending, preparing, ready, served, cancelled] OrderCreate: type: object required: [cocktail_id] additionalProperties: false properties: cocktail_id: type: integer example: 3 message_to_bartender: type: string maxLength: 140 example: pas trop de glace svp Order: type: object required: [id, status, cocktail_id, cocktail, price, created_at] properties: id: type: integer example: 42 status: $ref: '#/components/schemas/OrderStatus' cocktail_id: type: integer example: 3 cocktail: type: string description: Nom du cocktail example: Segfault Sunrise price: type: number example: 9.5 customer: type: string description: Pseudo du client example: neo message_to_bartender: type: string description: Absent dans les listes `scope=all` created_at: type: string format: date-time _links: $ref: '#/components/schemas/Links' example: id: 42 status: pending cocktail_id: 3 cocktail: Segfault Sunrise price: 9.5 customer: neo message_to_bartender: pas trop de glace svp created_at: '2026-10-14T10:32:00Z' _links: self: {href: /v1/orders/42} cancel: {href: /v1/orders/42, method: DELETE} cocktail: {href: /v1/cocktails/3} OrderList: allOf: - type: object required: [items] properties: items: type: array items: $ref: '#/components/schemas/Order' - $ref: '#/components/schemas/PageMeta' Token: type: object required: [access_token, token_type, expires_in] properties: access_token: type: string token_type: type: string const: Bearer expires_in: type: integer description: DurĂ©e de validitĂ©, en secondes example: 900 FlagResult: type: object properties: accepted: type: boolean step: type: integer description: Étape atteinte message: type: string example: accepted: true step: 4 message: Bien jouĂ©. Le barman lĂšve son verre.