{ "openapi": "3.0.3", "info": { "title": "Dash API", "version": "1.0.0", "description": "Backend API for Dash — Google SSO auth, MSSQL, session cookies.\n\nBrowser clients must send cookies (`credentials: 'include'`). Sessions use an HTTP-only `token` cookie (`Secure`, `SameSite=Strict`, 24h); HTTPS is required for login." }, "servers": [ { "url": "http://localhost:4208/api", "description": "Local default (PORT / .env)" } ], "tags": [ { "name": "Auth", "description": "Google SSO and session" }, { "name": "Health", "description": "Connectivity checks" }, { "name": "Article", "description": "Articles under categories" }, { "name": "Category", "description": "Category tree and membership" }, { "name": "Company", "description": "Company CRUD" } ], "paths": { "/health": { "get": { "tags": [ "Health" ], "summary": "Database health check", "description": "Verifies the database client can establish a connection.", "security": [], "x-markdown": "health.md", "responses": { "200": { "description": "Database reachable", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean", "example": true } } } } } }, "503": { "description": "Database unreachable", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean", "example": false }, "error": { "type": "string" } } } } } } } } }, "/company": { "get": { "tags": [ "Company" ], "summary": "List companies", "description": "Lists companies for the authenticated user.", "security": [ { "cookieAuth": [] } ], "x-markdown": "company/list.md", "responses": { "200": { "description": "Company list", "content": { "application/json": { "schema": { "type": "object", "properties": { "companies": { "type": "array", "items": { "$ref": "#/components/schemas/Company" } } } } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "post": { "tags": [ "Company" ], "summary": "Create company", "description": "Creates a company and selects it for the current user.", "security": [ { "cookieAuth": [] } ], "x-markdown": "company/create.md", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "iso639", "name", "taxrate", "reducedtaxrate" ], "properties": { "iso639": { "type": "string" }, "name": { "type": "string" }, "taxrate": { "type": "number" }, "reducedtaxrate": { "type": "number" } }, "example": { "iso639": "de", "name": "Acme GmbH", "taxrate": 19, "reducedtaxrate": 7 } } } } }, "responses": { "201": { "description": "Created; selected company updated", "content": { "application/json": { "schema": { "type": "object", "properties": { "company": { "$ref": "#/components/schemas/Company" }, "user": { "$ref": "#/components/schemas/User" } } } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Company name not available", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/company/{id}": { "put": { "tags": [ "Company" ], "summary": "Update company", "description": "Updates a company.", "security": [ { "cookieAuth": [] } ], "x-markdown": "company/update.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "iso639", "name", "taxrate", "reducedtaxrate" ], "properties": { "iso639": { "type": "string" }, "name": { "type": "string" }, "taxrate": { "type": "number" }, "reducedtaxrate": { "type": "number" } } } } } }, "responses": { "200": { "description": "Updated", "content": { "application/json": { "schema": { "type": "object", "properties": { "company": { "$ref": "#/components/schemas/Company" } } } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Company name not available", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "delete": { "tags": [ "Company" ], "summary": "Delete company", "description": "Deletes a company.", "security": [ { "cookieAuth": [] } ], "x-markdown": "company/delete.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "responses": { "204": { "description": "Deleted" }, "400": { "description": "Invalid id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/category/tree": { "get": { "tags": [ "Category" ], "summary": "Category tree", "description": "Returns the category tree for the authenticated user's selected company.", "security": [ { "cookieAuth": [] } ], "x-markdown": "category/tree.md", "responses": { "200": { "description": "Tree and articles for the selected company", "content": { "application/json": { "schema": { "type": "object", "properties": { "languageId": { "type": "integer" }, "tree": { "type": "array", "items": { "type": "object", "additionalProperties": true } }, "articles": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } } } }, "400": { "description": "No company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/category": { "post": { "tags": [ "Category" ], "summary": "Create category", "description": "Creates a category under an authorized parent.", "security": [ { "cookieAuth": [] } ], "x-markdown": "category/create.md", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "parentId", "name" ], "properties": { "parentId": { "type": "integer" }, "name": { "type": "string" } }, "example": { "parentId": 1, "name": "New category" } } } } }, "responses": { "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "category": { "$ref": "#/components/schemas/Category" } } } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Parent not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/category/{id}/articles": { "get": { "tags": [ "Category" ], "summary": "Articles in category", "description": "Returns articles in a category the user is authorized for.", "security": [ { "cookieAuth": [] } ], "x-markdown": "category/articles.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" }, "description": "Category id" } ], "responses": { "200": { "description": "Articles list", "content": { "application/json": { "schema": { "type": "object", "properties": { "articles": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } } } }, "400": { "description": "Invalid id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/category/{id}": { "delete": { "tags": [ "Category" ], "summary": "Delete category", "description": "Deletes a category the user is authorized for (not the company root).", "security": [ { "cookieAuth": [] } ], "x-markdown": "category/delete.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" }, "description": "Category id" } ], "responses": { "204": { "description": "Deleted" }, "400": { "description": "Invalid id or cannot delete root", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/article": { "post": { "tags": [ "Article" ], "summary": "Create article", "description": "Creates an article under an authorized category (`parentId`).", "security": [ { "cookieAuth": [] } ], "x-markdown": "article/create.md", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "parentId", "name" ], "properties": { "parentId": { "type": "integer", "description": "Category id" }, "name": { "type": "string" } }, "example": { "parentId": 10, "name": "New article" } } } } }, "responses": { "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "article": { "$ref": "#/components/schemas/ArticleSummary" } } } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized for parent category", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Parent not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/article/{sku}": { "get": { "tags": [ "Article" ], "summary": "Get article by SKU", "description": "Returns article detail by SKU if the user may access its category.", "security": [ { "cookieAuth": [] } ], "x-markdown": "article/get.md", "parameters": [ { "in": "path", "name": "sku", "required": true, "schema": { "type": "string" }, "description": "Article SKU" } ], "responses": { "200": { "description": "Article detail", "content": { "application/json": { "schema": { "type": "object", "properties": { "article": { "$ref": "#/components/schemas/ArticleDetail" } } } } } }, "400": { "description": "Missing sku", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Article not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "patch": { "tags": [ "Article" ], "summary": "Update article by SKU", "description": "Updates an article name by SKU; returns the same detail shape as GET.", "security": [ { "cookieAuth": [] } ], "x-markdown": "article/update.md", "parameters": [ { "in": "path", "name": "sku", "required": true, "schema": { "type": "string" }, "description": "Article SKU" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string" } }, "example": { "name": "Updated name" } } } } }, "responses": { "200": { "description": "Updated article detail", "content": { "application/json": { "schema": { "type": "object", "properties": { "article": { "$ref": "#/components/schemas/ArticleDetail" } } } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Article not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/article/{sku}/image": { "post": { "tags": [ "Article" ], "summary": "Add article image", "description": "Accepts a full-size AVIF body (`Content-Type: image/avif`).\nFull: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000.\nThumb always scaled to max 200×200. Stores hash/thumbhash, links `articlepic`.\nAuth like GET by SKU.\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "article/add-image.md", "parameters": [ { "in": "path", "name": "sku", "required": true, "schema": { "type": "string" }, "description": "Article SKU" } ], "requestBody": { "required": true, "content": { "image/avif": { "schema": { "type": "string", "format": "binary" } } } }, "responses": { "201": { "description": "Image stored and linked", "content": { "application/json": { "schema": { "type": "object", "properties": { "image": { "$ref": "#/components/schemas/ArticleImage" } } } } } }, "400": { "description": "Missing body or invalid image", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Article not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/auth/selected-company": { "put": { "tags": [ "Auth" ], "summary": "Set selected company", "description": "Sets the authenticated user's selected company.", "security": [ { "cookieAuth": [] } ], "x-markdown": "auth/selected-company.md", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "companyId" ], "properties": { "companyId": { "type": "integer", "minimum": 1 } }, "example": { "companyId": 1 } } } } }, "responses": { "200": { "description": "Updated user", "content": { "application/json": { "schema": { "type": "object", "properties": { "user": { "$ref": "#/components/schemas/User" } } } } } }, "400": { "description": "Invalid companyId", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/auth/me": { "get": { "tags": [ "Auth" ], "summary": "Current user", "description": "Returns the authenticated user from the session cookie and the Google client ID. Unauthenticated calls still receive `googleClientId` with 401.", "security": [ { "cookieAuth": [] } ], "x-markdown": "auth/me.md", "responses": { "200": { "description": "Authenticated", "content": { "application/json": { "schema": { "type": "object", "properties": { "user": { "$ref": "#/components/schemas/User" }, "googleClientId": { "type": "string" } } } } } }, "401": { "description": "Unauthorized — still includes `googleClientId` for the frontend", "content": { "application/json": { "schema": { "type": "object", "properties": { "error": { "type": "string" }, "googleClientId": { "type": "string" } } } } } } } } }, "/auth/logout": { "post": { "tags": [ "Auth" ], "summary": "Logout", "description": "Clears the HTTP-only `token` cookie set at login.", "security": [], "x-markdown": "auth/logout.md", "responses": { "200": { "description": "Logged out", "content": { "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string", "example": "Logged out successfully" } } } } } } } } }, "/auth/login": { "post": { "tags": [ "Auth" ], "summary": "Login with Google ID token", "description": "Verifies a Google ID token, checks allowlist (or creates entry when `IMPLICIT_CREATE=true`), and sets an HTTP-only `token` cookie.", "security": [], "x-markdown": "auth/login.md", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "idToken" ], "properties": { "idToken": { "type": "string", "description": "Google ID token from Google Identity Services" } }, "example": { "idToken": "" } } } } }, "responses": { "200": { "description": "Login successful; `token` cookie set", "content": { "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string", "example": "Login successful" }, "user": { "$ref": "#/components/schemas/User" } } } } } }, "400": { "description": "Missing token or Google client not configured", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Invalid Google token or account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Email not on allowlist", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "securitySchemes": { "cookieAuth": { "type": "apiKey", "in": "cookie", "name": "token", "description": "HTTP-only session cookie set by POST /auth/login" } }, "schemas": { "Error": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string" } } }, "User": { "type": "object", "required": [ "id", "email", "selectedCompanyId" ], "properties": { "id": { "type": "integer" }, "email": { "type": "string", "format": "email" }, "selectedCompanyId": { "type": "integer", "nullable": true }, "picture": { "type": "string", "format": "uri" } } }, "Company": { "type": "object", "properties": { "id": { "type": "integer" }, "iso639": { "type": "string" }, "name": { "type": "string" }, "taxrate": { "type": "number" }, "reducedtaxrate": { "type": "number" } } }, "Category": { "type": "object", "properties": { "id": { "type": "integer" }, "parentId": { "type": "integer", "nullable": true }, "sort": { "type": "integer" }, "name": { "type": "string" } } }, "ArticleSummary": { "type": "object", "properties": { "id": { "type": "integer" }, "parentId": { "type": "integer" }, "name": { "type": "string" }, "sku": { "type": "string" } } }, "ArticleDetail": { "type": "object", "description": "Full article payload (unit, brand, categoryIds, photos, eans, prices, …)", "additionalProperties": true }, "ArticleImage": { "type": "object", "required": [ "picId", "sort", "full", "thumb", "hash", "thumbHash" ], "properties": { "picId": { "type": "integer" }, "sort": { "type": "integer" }, "full": { "type": "object", "required": [ "w", "h", "size" ], "properties": { "w": { "type": "integer" }, "h": { "type": "integer" }, "size": { "type": "integer", "description": "Byte length of full AVIF (as-is when already AVIF ≤1000×1000)" } } }, "thumb": { "type": "object", "required": [ "w", "h" ], "properties": { "w": { "type": "integer" }, "h": { "type": "integer" } } }, "hash": { "type": "string", "description": "SHA-256 hex of full image bytes" }, "thumbHash": { "type": "string", "description": "Base64 thumbhash of thumb preview" } } } } } }