From 768939c22fff7be4590a3089e058912ba2b79ce2 Mon Sep 17 00:00:00 2001 From: justanamelessguy Date: Sun, 7 Jun 2026 23:24:39 +0200 Subject: [PATCH] Add OpenAPI documentation --- README.md | 19 + backend/openapi.yaml | 942 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 961 insertions(+) create mode 100644 backend/openapi.yaml diff --git a/README.md b/README.md index 778455c..f8041b9 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,25 @@ http://127.0.0.1:8000/login.html Keep both terminals open while using the app. +### API Documentation + +The backend API is documented with an OpenAPI/Swagger specification: + +```text +backend/openapi.yaml +``` + +To view the interactive documentation, open [Swagger Editor](https://editor.swagger.io/) +and import `backend/openapi.yaml`. To make requests, start the backend and use +`http://127.0.0.1:5000`. Browser cookie policies may make authenticated requests +from the hosted Swagger Editor unreliable, so the app or an API client may be +easier for testing authenticated endpoints. + +The API uses a Flask session cookie for authentication. Register or log in first, +then include the returned `session` cookie in authenticated requests. The +specification documents all authentication, user, journey, comment, image upload, +uploaded-file, and health endpoints. + ### Daily Development Workflow 1. Start the backend in `backend/`. diff --git a/backend/openapi.yaml b/backend/openapi.yaml new file mode 100644 index 0000000..e10014b --- /dev/null +++ b/backend/openapi.yaml @@ -0,0 +1,942 @@ +openapi: 3.0.3 +info: + title: Journey Mapper API + version: 1.0.0 + description: | + Backend API for the Journey Mapper application. + + Authentication uses a Flask session cookie. After registering or logging in, + clients must send the returned `session` cookie with authenticated requests. + Browser requests from the frontend must use credentials, for example + `fetch(url, { credentials: "include" })`. + + Journey and marker descriptions support Markdown. Raw HTML is stored as text + and escaped by the frontend when rendered. +servers: + - url: http://127.0.0.1:5000 + description: Local development backend + +tags: + - name: Authentication + - name: Users + - name: Journeys + - name: Comments + - name: Uploads + - name: System + +paths: + /api/register: + post: + tags: [Authentication] + summary: Register a user + description: Creates a user, starts a session, and returns the new public user data. + operationId: registerUser + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AuthRequest" + example: + username: traveller + password: secret123 + responses: + "201": + description: Registration successful + headers: + Set-Cookie: + description: Flask session cookie used for authenticated requests. + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/AuthResponse" + example: + id: 1 + username: traveller + message: Registration successful + "400": + $ref: "#/components/responses/ValidationError" + "409": + description: Username is already taken + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + error: Username already taken + + /api/login: + post: + tags: [Authentication] + summary: Log in + description: Validates the credentials and starts a session. + operationId: loginUser + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AuthRequest" + example: + username: traveller + password: secret123 + responses: + "200": + description: Login successful + headers: + Set-Cookie: + description: Flask session cookie used for authenticated requests. + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/AuthResponse" + example: + id: 1 + username: traveller + message: Login successful + "400": + $ref: "#/components/responses/ValidationError" + "401": + description: Invalid username or password + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + error: Invalid username or password + + /api/logout: + post: + tags: [Authentication] + summary: Log out + description: Removes the user ID from the current session. This operation also succeeds if no session exists. + operationId: logoutUser + security: [] + responses: + "200": + description: Logged out + content: + application/json: + schema: + $ref: "#/components/schemas/Message" + example: + message: Logged out + + /api/me: + get: + tags: [Authentication] + summary: Get the current user + operationId: getCurrentUser + responses: + "200": + description: Current public user data + content: + application/json: + schema: + $ref: "#/components/schemas/User" + "401": + $ref: "#/components/responses/NotLoggedIn" + + /api/users: + get: + tags: [Users] + summary: List other users + description: Returns every public user except the currently logged-in user. + operationId: listUsers + responses: + "200": + description: Public users + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/User" + "401": + $ref: "#/components/responses/AuthenticationRequired" + + /api/journeys: + get: + tags: [Journeys] + summary: List visible journeys + description: | + Returns journeys owned by the current user, public journeys, and journeys + shared with the current user. Each returned journey contains a `can_edit` + flag calculated for the current user. + operationId: listJourneys + responses: + "200": + description: Visible journeys + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/JourneyListItem" + "401": + $ref: "#/components/responses/AuthenticationRequired" + post: + tags: [Journeys] + summary: Create a journey + description: | + Creates a journey owned by the current user. Supplied comments and + server-managed fields such as IDs and timestamps are ignored. + operationId: createJourney + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/JourneyCreate" + responses: + "201": + description: Journey created + content: + application/json: + schema: + $ref: "#/components/schemas/Journey" + "400": + $ref: "#/components/responses/ValidationError" + "401": + $ref: "#/components/responses/AuthenticationRequired" + + /api/journeys/{journeyId}: + parameters: + - $ref: "#/components/parameters/JourneyId" + get: + tags: [Journeys] + summary: Get a journey + operationId: getJourney + responses: + "200": + description: Journey data + content: + application/json: + schema: + $ref: "#/components/schemas/Journey" + "401": + $ref: "#/components/responses/AuthenticationRequired" + "403": + description: The current user cannot view this journey + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + error: Access denied + "404": + $ref: "#/components/responses/JourneyNotFound" + put: + tags: [Journeys] + summary: Update a journey + description: | + The owner and users with shared edit access may update the title, + description, and markers. Only the owner may update visibility or sharing. + Omitted fields remain unchanged. + operationId: updateJourney + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/JourneyUpdate" + responses: + "200": + description: Journey updated + content: + application/json: + schema: + $ref: "#/components/schemas/Journey" + "400": + $ref: "#/components/responses/ValidationError" + "401": + $ref: "#/components/responses/AuthenticationRequired" + "403": + description: The current user cannot edit the journey or its sharing settings + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + examples: + cannotEdit: + value: + error: Not authorized to edit this journey + sharingOwnerOnly: + value: + error: Only the owner can update sharing settings + "404": + $ref: "#/components/responses/JourneyNotFound" + delete: + tags: [Journeys] + summary: Delete a journey + description: Only the journey owner may delete it. + operationId: deleteJourney + responses: + "200": + description: Journey deleted + content: + application/json: + schema: + type: object + required: [message, journey] + properties: + message: + type: string + journey: + $ref: "#/components/schemas/Journey" + example: + message: Journey deleted successfully + journey: + id: 1 + owner_id: 1 + title: Switzerland + description: My summer journey + markers: [] + created_at: "2026-06-07T18:30:00" + visibility: private + shared_read: [] + shared_edit: [] + comments: [] + "401": + $ref: "#/components/responses/AuthenticationRequired" + "403": + description: Only the journey owner may delete it + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + error: Only the owner can delete this journey + "404": + $ref: "#/components/responses/JourneyNotFound" + + /api/journeys/{journeyId}/comments: + parameters: + - $ref: "#/components/parameters/JourneyId" + get: + tags: [Comments] + summary: List journey comments + description: The current user must be able to view the journey. + operationId: listJourneyComments + responses: + "200": + description: Journey comments + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Comment" + "401": + $ref: "#/components/responses/AuthenticationRequired" + "403": + description: The current user cannot view the journey + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + error: Access denied + "404": + $ref: "#/components/responses/JourneyNotFound" + post: + tags: [Comments] + summary: Add a journey comment + description: The current user must be able to view the journey. + operationId: addJourneyComment + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CommentCreate" + example: + text: This looks like a wonderful trip. + responses: + "201": + description: Comment created + content: + application/json: + schema: + $ref: "#/components/schemas/Comment" + "400": + $ref: "#/components/responses/ValidationError" + "401": + $ref: "#/components/responses/AuthenticationRequired" + "403": + description: The current user cannot view the journey + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + error: Access denied + "404": + $ref: "#/components/responses/JourneyNotFound" + + /api/comments/{commentId}: + parameters: + - $ref: "#/components/parameters/CommentId" + delete: + tags: [Comments] + summary: Delete a comment + description: A comment may be deleted by its author or the journey owner. + operationId: deleteComment + responses: + "200": + description: Comment deleted + content: + application/json: + schema: + $ref: "#/components/schemas/Message" + example: + message: Comment deleted + "401": + $ref: "#/components/responses/AuthenticationRequired" + "403": + description: The current user cannot delete the comment + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + error: Not authorized + "404": + description: Comment not found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + error: Comment not found + + /api/uploads/images: + post: + tags: [Uploads] + summary: Upload marker images + description: | + Uploads one or more images using repeated `images` form fields. Accepted + filename extensions are `.png`, `.jpg`, `.jpeg`, `.gif`, and `.webp`. + The returned image objects can be included in a marker's `images` array. + operationId: uploadImages + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [images] + properties: + images: + type: array + items: + type: string + format: binary + responses: + "201": + description: Images uploaded + content: + application/json: + schema: + type: object + required: [images] + properties: + images: + type: array + items: + $ref: "#/components/schemas/Image" + "400": + description: No valid images were provided or an extension is unsupported + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + examples: + noImages: + value: + error: No images provided + unsupported: + value: + error: "Unsupported image type: notes.txt" + "401": + $ref: "#/components/responses/AuthenticationRequired" + + /uploads/{filename}: + parameters: + - name: filename + in: path + required: true + description: Server-generated image filename returned by the upload endpoint. + schema: + type: string + example: 79ce8a0727d846f4bffb1fbf94365191.jpg + get: + tags: [Uploads] + summary: Get an uploaded image + description: Uploaded images are publicly accessible. + operationId: getUploadedImage + security: [] + responses: + "200": + description: Image file + content: + image/png: + schema: + type: string + format: binary + image/jpeg: + schema: + type: string + format: binary + image/gif: + schema: + type: string + format: binary + image/webp: + schema: + type: string + format: binary + "404": + description: Image not found + + /api/journeys/health: + get: + tags: [System] + summary: Check API health + operationId: getHealth + security: [] + responses: + "200": + description: Backend is healthy + content: + application/json: + schema: + $ref: "#/components/schemas/Health" + + /: + get: + tags: [System] + summary: Get the API landing page + description: Returns a small HTML page confirming that the backend is running. + operationId: getApiLandingPage + security: [] + responses: + "200": + description: API landing page + content: + text/html: + schema: + type: string + +components: + securitySchemes: + cookieAuth: + type: apiKey + in: cookie + name: session + description: Flask session cookie returned after registration or login. + + parameters: + JourneyId: + name: journeyId + in: path + required: true + description: Journey ID + schema: + type: integer + minimum: 1 + CommentId: + name: commentId + in: path + required: true + description: Millisecond timestamp used as the comment ID + schema: + type: integer + format: int64 + minimum: 1 + + responses: + ValidationError: + description: Request validation failed + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + examples: + missingTitle: + value: + error: Journey title is required + invalidCoordinates: + value: + error: Marker latitude must be between -90 and 90 + AuthenticationRequired: + description: Authentication is required + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + error: Authentication required + NotLoggedIn: + description: There is no valid current user session + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + examples: + noSession: + value: + error: Not logged in + missingUser: + value: + error: User not found + JourneyNotFound: + description: Journey not found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + error: Journey not found + + schemas: + Error: + type: object + required: [error] + additionalProperties: false + properties: + error: + type: string + + Message: + type: object + required: [message] + additionalProperties: false + properties: + message: + type: string + + User: + type: object + required: [id, username] + additionalProperties: false + properties: + id: + type: integer + minimum: 1 + username: + type: string + maxLength: 50 + + AuthRequest: + type: object + required: [username, password] + properties: + username: + type: string + minLength: 1 + maxLength: 50 + password: + type: string + format: password + minLength: 4 + maxLength: 200 + + AuthResponse: + type: object + required: [id, username, message] + additionalProperties: false + properties: + id: + type: integer + minimum: 1 + username: + type: string + maxLength: 50 + message: + type: string + + Image: + type: object + required: [filename, originalName, url] + additionalProperties: false + properties: + filename: + type: string + maxLength: 255 + description: Server-generated filename. + originalName: + type: string + maxLength: 255 + description: Sanitized original filename. + url: + type: string + maxLength: 2048 + description: Relative or absolute image URL. + example: /uploads/79ce8a0727d846f4bffb1fbf94365191.jpg + + MarkerImageInput: + description: A marker image may be supplied as a URL string or a full image object. + oneOf: + - type: string + minLength: 1 + maxLength: 2048 + - $ref: "#/components/schemas/Image" + + Marker: + type: object + required: [lat, lng, title, date, description, images] + additionalProperties: false + properties: + lat: + type: number + format: double + minimum: -90 + maximum: 90 + lng: + type: number + format: double + minimum: -180 + maximum: 180 + title: + type: string + maxLength: 200 + date: + type: string + maxLength: 20 + description: Date text, normally formatted as `YYYY-MM-DD`. + example: "2026-06-07" + description: + type: string + maxLength: 10000 + description: Markdown-supported marker description. + images: + type: array + maxItems: 20 + items: + $ref: "#/components/schemas/MarkerImageInput" + + MarkerInput: + type: object + required: [lat, lng] + properties: + lat: + type: number + format: double + minimum: -90 + maximum: 90 + lng: + type: number + format: double + minimum: -180 + maximum: 180 + title: + type: string + maxLength: 200 + default: "" + date: + type: string + maxLength: 20 + default: "" + example: "2026-06-07" + description: + type: string + maxLength: 10000 + default: "" + description: Markdown-supported marker description. + images: + type: array + maxItems: 20 + default: [] + items: + $ref: "#/components/schemas/MarkerImageInput" + + Comment: + type: object + required: [id, author_id, author_name, text, created_at] + additionalProperties: false + properties: + id: + type: integer + format: int64 + minimum: 1 + author_id: + type: integer + minimum: 1 + author_name: + type: string + maxLength: 50 + text: + type: string + minLength: 1 + maxLength: 2000 + created_at: + type: string + format: date-time + + CommentCreate: + type: object + required: [text] + properties: + text: + type: string + minLength: 1 + maxLength: 2000 + + Visibility: + type: string + enum: [private, public, shared] + default: private + + Journey: + type: object + required: + - id + - owner_id + - title + - description + - markers + - created_at + - visibility + - shared_read + - shared_edit + - comments + properties: + id: + type: integer + minimum: 1 + owner_id: + type: integer + minimum: 1 + title: + type: string + minLength: 1 + maxLength: 200 + description: + type: string + maxLength: 20000 + description: Markdown-supported journey description. + markers: + type: array + maxItems: 500 + items: + $ref: "#/components/schemas/Marker" + created_at: + type: string + format: date-time + visibility: + $ref: "#/components/schemas/Visibility" + shared_read: + type: array + uniqueItems: true + items: + type: integer + minimum: 1 + shared_edit: + type: array + uniqueItems: true + items: + type: integer + minimum: 1 + comments: + type: array + items: + $ref: "#/components/schemas/Comment" + + JourneyListItem: + allOf: + - $ref: "#/components/schemas/Journey" + - type: object + required: [can_edit] + properties: + can_edit: + type: boolean + description: Whether the current user may edit this journey. + + JourneyCreate: + type: object + required: [title] + properties: + title: + type: string + minLength: 1 + maxLength: 200 + description: + type: string + maxLength: 20000 + default: "" + description: Markdown-supported journey description. + markers: + type: array + maxItems: 500 + default: [] + items: + $ref: "#/components/schemas/MarkerInput" + visibility: + $ref: "#/components/schemas/Visibility" + shared_read: + type: array + default: [] + description: Invalid, duplicate, and unknown user IDs are silently removed. + items: + type: integer + minimum: 1 + shared_edit: + type: array + default: [] + description: Invalid, duplicate, and unknown user IDs are silently removed. + items: + type: integer + minimum: 1 + + JourneyUpdate: + type: object + properties: + title: + type: string + minLength: 1 + maxLength: 200 + description: + type: string + maxLength: 20000 + description: Markdown-supported journey description. + markers: + type: array + maxItems: 500 + items: + $ref: "#/components/schemas/MarkerInput" + visibility: + $ref: "#/components/schemas/Visibility" + shared_read: + type: array + description: Owner-only field. Invalid, duplicate, and unknown user IDs are silently removed. + items: + type: integer + minimum: 1 + shared_edit: + type: array + description: Owner-only field. Invalid, duplicate, and unknown user IDs are silently removed. + items: + type: integer + minimum: 1 + + Health: + type: object + required: [status, timestamp] + additionalProperties: false + properties: + status: + type: string + enum: [healthy] + timestamp: + type: string + format: date-time + +security: + - cookieAuth: []