- Add GET /api/articleIndex returning the article search index (listSearchIndex) - Add GET /api/category/export.zip returning a kategorien.zip with a Windows-1252 CSV (depth-first, Ebene N columns) plus one AVIF per category image (content-addressed, deduplicated) using jszip
4328 lines
114 KiB
JSON
4328 lines
114 KiB
JSON
{
|
||
"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"
|
||
},
|
||
{
|
||
"name": "Unit",
|
||
"description": "Company-scoped units of measure"
|
||
},
|
||
{
|
||
"name": "Brand",
|
||
"description": "Company-scoped brands"
|
||
},
|
||
{
|
||
"name": "Image",
|
||
"description": "Content-addressed AVIF by fullHash"
|
||
}
|
||
],
|
||
"paths": {
|
||
"/unit": {
|
||
"get": {
|
||
"tags": [
|
||
"Unit"
|
||
],
|
||
"summary": "List units",
|
||
"description": "Lists units for the authenticated user's selected company (default-language symbol/name).",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "unit/list.md",
|
||
"responses": {
|
||
"200": {
|
||
"description": "Unit list",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"units": {
|
||
"type": "array",
|
||
"items": {
|
||
"$ref": "#/components/schemas/Unit"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"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"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"put": {
|
||
"tags": [
|
||
"Unit"
|
||
],
|
||
"summary": "Upsert unit",
|
||
"description": "Insert or update unit by `si` for the selected company. Upserts `unittext` for the company language.\n`basesifactor` must be finite and `> 0`.\n",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "unit/upsert.md",
|
||
"requestBody": {
|
||
"required": true,
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"required": [
|
||
"si",
|
||
"basesi",
|
||
"basesifactor",
|
||
"symbol",
|
||
"name"
|
||
],
|
||
"properties": {
|
||
"si": {
|
||
"type": "string"
|
||
},
|
||
"basesi": {
|
||
"type": "string"
|
||
},
|
||
"basesifactor": {
|
||
"type": "number"
|
||
},
|
||
"symbol": {
|
||
"type": "string"
|
||
},
|
||
"name": {
|
||
"type": "string"
|
||
}
|
||
},
|
||
"example": {
|
||
"si": "l",
|
||
"basesi": "l",
|
||
"basesifactor": 1,
|
||
"symbol": "L",
|
||
"name": "Liter"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"responses": {
|
||
"200": {
|
||
"description": "Upserted unit",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"unit": {
|
||
"$ref": "#/components/schemas/Unit"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"400": {
|
||
"description": "Invalid unit or 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"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"delete": {
|
||
"tags": [
|
||
"Unit"
|
||
],
|
||
"summary": "Delete unit",
|
||
"description": "Deletes `si` for the selected company. Fails with `Unit in use` if articles still reference it.",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "unit/delete.md",
|
||
"parameters": [
|
||
{
|
||
"in": "query",
|
||
"name": "si",
|
||
"required": true,
|
||
"schema": {
|
||
"type": "string"
|
||
}
|
||
}
|
||
],
|
||
"responses": {
|
||
"204": {
|
||
"description": "Deleted"
|
||
},
|
||
"400": {
|
||
"description": "Missing si or 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 or unit not found",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"$ref": "#/components/schemas/Error"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"409": {
|
||
"description": "Unit in use",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"$ref": "#/components/schemas/Error"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"/posterminal": {
|
||
"get": {
|
||
"tags": [
|
||
"Posterminal"
|
||
],
|
||
"summary": "List POS terminals",
|
||
"description": "Lists POS terminals for the authenticated user's selected company.",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "posterminal/list.md",
|
||
"responses": {
|
||
"200": {
|
||
"description": "Posterminal list",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"posterminals": {
|
||
"type": "array",
|
||
"items": {
|
||
"$ref": "#/components/schemas/Posterminal"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"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"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"put": {
|
||
"tags": [
|
||
"Posterminal"
|
||
],
|
||
"summary": "Create POS terminal",
|
||
"description": "Creates a new POS terminal for the selected company.\nThe pairingpin is randomly generated server-side and returned in the response.\n",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "posterminal/upsert.md",
|
||
"requestBody": {
|
||
"required": true,
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"required": [
|
||
"name"
|
||
],
|
||
"properties": {
|
||
"name": {
|
||
"type": "string"
|
||
}
|
||
},
|
||
"example": {
|
||
"name": "Kassa 1"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"responses": {
|
||
"200": {
|
||
"description": "Created posterminal with generated pairingpin",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"posterminal": {
|
||
"$ref": "#/components/schemas/Posterminal"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"400": {
|
||
"description": "Invalid posterminal or 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"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"/posterminal/{id}": {
|
||
"delete": {
|
||
"tags": [
|
||
"Posterminal"
|
||
],
|
||
"summary": "Delete POS terminal",
|
||
"description": "Deletes a POS terminal for the selected company.",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "posterminal/delete.md",
|
||
"parameters": [
|
||
{
|
||
"in": "path",
|
||
"name": "id",
|
||
"required": true,
|
||
"schema": {
|
||
"type": "integer"
|
||
}
|
||
}
|
||
],
|
||
"responses": {
|
||
"204": {
|
||
"description": "Deleted"
|
||
},
|
||
"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": "Posterminal not found",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"$ref": "#/components/schemas/Error"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"/posterminal/{id}/category/{categoryId}": {
|
||
"put": {
|
||
"tags": [
|
||
"Posterminal"
|
||
],
|
||
"summary": "Enable category on terminal",
|
||
"description": "Toggles a single category's visibility on a terminal (idempotent).",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "posterminal/setOn.md",
|
||
"parameters": [
|
||
{
|
||
"in": "path",
|
||
"name": "id",
|
||
"required": true,
|
||
"schema": {
|
||
"type": "integer"
|
||
}
|
||
},
|
||
{
|
||
"in": "path",
|
||
"name": "categoryId",
|
||
"required": true,
|
||
"schema": {
|
||
"type": "integer"
|
||
}
|
||
}
|
||
],
|
||
"responses": {
|
||
"200": {
|
||
"description": "Updated category IDs for this terminal",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"id": {
|
||
"type": "integer"
|
||
},
|
||
"categoryIds": {
|
||
"type": "array",
|
||
"items": {
|
||
"type": "integer"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"400": {
|
||
"description": "No company selected or invalid terminal/category",
|
||
"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": "Posterminal not found",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"$ref": "#/components/schemas/Error"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"delete": {
|
||
"tags": [
|
||
"Posterminal"
|
||
],
|
||
"summary": "Disable category on terminal",
|
||
"description": "Removes a single category's visibility on a terminal (idempotent).",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "posterminal/setOff.md",
|
||
"parameters": [
|
||
{
|
||
"in": "path",
|
||
"name": "id",
|
||
"required": true,
|
||
"schema": {
|
||
"type": "integer"
|
||
}
|
||
},
|
||
{
|
||
"in": "path",
|
||
"name": "categoryId",
|
||
"required": true,
|
||
"schema": {
|
||
"type": "integer"
|
||
}
|
||
}
|
||
],
|
||
"responses": {
|
||
"200": {
|
||
"description": "Updated category IDs for this terminal",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"id": {
|
||
"type": "integer"
|
||
},
|
||
"categoryIds": {
|
||
"type": "array",
|
||
"items": {
|
||
"type": "integer"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"400": {
|
||
"description": "No company selected or invalid terminal/category",
|
||
"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": "Posterminal not found",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"$ref": "#/components/schemas/Error"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"/posterminal/{id}/all": {
|
||
"put": {
|
||
"tags": [
|
||
"Posterminal"
|
||
],
|
||
"summary": "Enable all categories on terminal",
|
||
"description": "Walks the company's category tree and inserts any missing links\nfor this terminal. Returns the updated category IDs.\n",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "posterminal/setOnAll.md",
|
||
"parameters": [
|
||
{
|
||
"in": "path",
|
||
"name": "id",
|
||
"required": true,
|
||
"schema": {
|
||
"type": "integer"
|
||
}
|
||
}
|
||
],
|
||
"responses": {
|
||
"200": {
|
||
"description": "Updated category IDs for this terminal",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"id": {
|
||
"type": "integer"
|
||
},
|
||
"categoryIds": {
|
||
"type": "array",
|
||
"items": {
|
||
"type": "integer"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"400": {
|
||
"description": "No company selected or invalid terminal",
|
||
"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": "Posterminal not found",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"$ref": "#/components/schemas/Error"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"delete": {
|
||
"tags": [
|
||
"Posterminal"
|
||
],
|
||
"summary": "Disable all categories on terminal",
|
||
"description": "Removes all category links for this terminal.",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "posterminal/setOffAll.md",
|
||
"parameters": [
|
||
{
|
||
"in": "path",
|
||
"name": "id",
|
||
"required": true,
|
||
"schema": {
|
||
"type": "integer"
|
||
}
|
||
}
|
||
],
|
||
"responses": {
|
||
"200": {
|
||
"description": "Updated category IDs for this terminal",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"id": {
|
||
"type": "integer"
|
||
},
|
||
"categoryIds": {
|
||
"type": "array",
|
||
"items": {
|
||
"type": "integer"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"400": {
|
||
"description": "No company selected or invalid terminal",
|
||
"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": "Posterminal not found",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"$ref": "#/components/schemas/Error"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"/image/{fullHash}": {
|
||
"get": {
|
||
"tags": [
|
||
"Image"
|
||
],
|
||
"summary": "Full image by hash",
|
||
"description": "Returns the full AVIF by `pic.hash` (SHA-256 hex).\nAuth via article category access. Browser-cacheable (`private`, `immutable`, `ETag`).\n",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "image/get.md",
|
||
"parameters": [
|
||
{
|
||
"in": "path",
|
||
"name": "fullHash",
|
||
"required": true,
|
||
"schema": {
|
||
"type": "string",
|
||
"pattern": "^[a-fA-F0-9]{64}$"
|
||
},
|
||
"description": "SHA-256 hex of the full image bytes"
|
||
}
|
||
],
|
||
"responses": {
|
||
"200": {
|
||
"description": "AVIF image bytes",
|
||
"headers": {
|
||
"Cache-Control": {
|
||
"schema": {
|
||
"type": "string",
|
||
"example": "private, max-age=31536000, immutable"
|
||
}
|
||
},
|
||
"ETag": {
|
||
"schema": {
|
||
"type": "string"
|
||
}
|
||
}
|
||
},
|
||
"content": {
|
||
"image/avif": {
|
||
"schema": {
|
||
"type": "string",
|
||
"format": "binary"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"304": {
|
||
"description": "Not modified (If-None-Match matched ETag)"
|
||
},
|
||
"400": {
|
||
"description": "Invalid hash",
|
||
"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": "Image not found",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"$ref": "#/components/schemas/Error"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"/thumb/{fullHash}": {
|
||
"get": {
|
||
"tags": [
|
||
"Image"
|
||
],
|
||
"summary": "Thumb image by hash",
|
||
"description": "Returns the thumb AVIF for `pic.hash` (fullHash — not thumbhash).\nAuth via article category access. Browser-cacheable (`private`, `immutable`, `ETag`).\n",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "thumb/get.md",
|
||
"parameters": [
|
||
{
|
||
"in": "path",
|
||
"name": "fullHash",
|
||
"required": true,
|
||
"schema": {
|
||
"type": "string",
|
||
"pattern": "^[a-fA-F0-9]{64}$"
|
||
},
|
||
"description": "SHA-256 hex of the full image bytes"
|
||
}
|
||
],
|
||
"responses": {
|
||
"200": {
|
||
"description": "AVIF thumb bytes",
|
||
"headers": {
|
||
"Cache-Control": {
|
||
"schema": {
|
||
"type": "string",
|
||
"example": "private, max-age=31536000, immutable"
|
||
}
|
||
},
|
||
"ETag": {
|
||
"schema": {
|
||
"type": "string"
|
||
}
|
||
}
|
||
},
|
||
"content": {
|
||
"image/avif": {
|
||
"schema": {
|
||
"type": "string",
|
||
"format": "binary"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"304": {
|
||
"description": "Not modified (If-None-Match matched ETag)"
|
||
},
|
||
"400": {
|
||
"description": "Invalid hash",
|
||
"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": "Image not found",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"$ref": "#/components/schemas/Error"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"/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/export.zip": {
|
||
"get": {
|
||
"tags": [
|
||
"Category"
|
||
],
|
||
"summary": "Export categories as ZIP",
|
||
"description": "Builds a `kategorien.zip` for the authenticated user's selected company.\nThe ZIP contains `kategorien.csv` (semicolon-separated, Windows-1252,\ndepth-first in sort order with an `Ebene N` name column per tree depth)\nplus one AVIF entry per category image, named `<fullHash>.avif`\n(content-addressed, so shared hashes are deduplicated).\n",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"responses": {
|
||
"200": {
|
||
"description": "ZIP archive",
|
||
"content": {
|
||
"application/zip": {
|
||
"schema": {
|
||
"type": "string",
|
||
"format": "binary"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"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}": {
|
||
"patch": {
|
||
"tags": [
|
||
"Category"
|
||
],
|
||
"summary": "Update category (rename or move)",
|
||
"description": "Updates a category the user is authorized for.\n`name` renames the category (re-slugifies in the company default language).\n`parentId` moves it under a new parent (cycle/self → `Invalid parent`).\nEither `name` or `parentId` (or both) is required.\n",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "category/update.md",
|
||
"parameters": [
|
||
{
|
||
"in": "path",
|
||
"name": "id",
|
||
"required": true,
|
||
"schema": {
|
||
"type": "integer"
|
||
},
|
||
"description": "Category id"
|
||
}
|
||
],
|
||
"requestBody": {
|
||
"required": true,
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"name": {
|
||
"type": "string"
|
||
},
|
||
"parentId": {
|
||
"type": "integer",
|
||
"description": "New parent category id"
|
||
}
|
||
},
|
||
"example": {
|
||
"parentId": 3
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"responses": {
|
||
"200": {
|
||
"description": "Updated category (renamed and/or moved)",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"category": {
|
||
"type": "object",
|
||
"required": [
|
||
"id",
|
||
"name",
|
||
"slug",
|
||
"parentId",
|
||
"sort"
|
||
],
|
||
"properties": {
|
||
"id": {
|
||
"type": "integer"
|
||
},
|
||
"name": {
|
||
"type": "string"
|
||
},
|
||
"slug": {
|
||
"type": "string"
|
||
},
|
||
"parentId": {
|
||
"type": "integer"
|
||
},
|
||
"sort": {
|
||
"type": "integer"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"400": {
|
||
"description": "Validation error (name or parentId required, or invalid parent)",
|
||
"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": "Category not found",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"$ref": "#/components/schemas/Error"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"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"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"/category/order": {
|
||
"put": {
|
||
"tags": [
|
||
"Category"
|
||
],
|
||
"summary": "Reorder sibling categories",
|
||
"description": "Sets the sibling sort order of `categoryIds` under `parentId`.\n`categoryIds` must be exactly the current child set of `parentId`\n(same count, no foreign/duplicate ids). Each child's `sort` is set\nto its index `0..n-1` in one transaction.\n",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "category/order.md",
|
||
"requestBody": {
|
||
"required": true,
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"required": [
|
||
"parentId",
|
||
"categoryIds"
|
||
],
|
||
"properties": {
|
||
"parentId": {
|
||
"type": "integer",
|
||
"description": "Parent category under which sibling order applies"
|
||
},
|
||
"categoryIds": {
|
||
"type": "array",
|
||
"items": {
|
||
"type": "integer"
|
||
}
|
||
}
|
||
},
|
||
"example": {
|
||
"parentId": 1,
|
||
"categoryIds": [
|
||
2,
|
||
4,
|
||
3,
|
||
5
|
||
]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"responses": {
|
||
"200": {
|
||
"description": "New sibling order",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"categoryIds": {
|
||
"type": "array",
|
||
"items": {
|
||
"type": "integer"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"400": {
|
||
"description": "Invalid category order",
|
||
"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 category not found",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"$ref": "#/components/schemas/Error"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"/category/{id}/image": {
|
||
"post": {
|
||
"tags": [
|
||
"Category"
|
||
],
|
||
"summary": "Add category 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 `categorypic`.\nAuth like GET /category/tree — user must be authorized for the category's company.\n",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "category/add-image.md",
|
||
"parameters": [
|
||
{
|
||
"in": "path",
|
||
"name": "id",
|
||
"required": true,
|
||
"schema": {
|
||
"type": "integer"
|
||
},
|
||
"description": "Category id"
|
||
}
|
||
],
|
||
"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": {
|
||
"category": {
|
||
"type": "object",
|
||
"properties": {
|
||
"id": {
|
||
"type": "integer"
|
||
},
|
||
"thumbW": {
|
||
"type": "integer"
|
||
},
|
||
"thumbH": {
|
||
"type": "integer"
|
||
},
|
||
"thumbHash": {
|
||
"type": "string"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"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": "Category not found",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"$ref": "#/components/schemas/Error"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"delete": {
|
||
"tags": [
|
||
"Category"
|
||
],
|
||
"summary": "Delete category image",
|
||
"description": "Removes the category's image. Sets thumbW, thumbH, thumbHash back to null.\nAuth like GET /category/tree — user must be authorized for the category's company.\n",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "category/delete-image.md",
|
||
"parameters": [
|
||
{
|
||
"in": "path",
|
||
"name": "id",
|
||
"required": true,
|
||
"schema": {
|
||
"type": "integer"
|
||
},
|
||
"description": "Category id"
|
||
}
|
||
],
|
||
"responses": {
|
||
"200": {
|
||
"description": "Image removed",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"category": {
|
||
"type": "object",
|
||
"properties": {
|
||
"id": {
|
||
"type": "integer"
|
||
},
|
||
"thumbW": {
|
||
"type": "integer",
|
||
"nullable": true
|
||
},
|
||
"thumbH": {
|
||
"type": "integer",
|
||
"nullable": true
|
||
},
|
||
"thumbHash": {
|
||
"type": "string",
|
||
"nullable": true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"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": "Category or image not found",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"$ref": "#/components/schemas/Error"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"/brand": {
|
||
"get": {
|
||
"tags": [
|
||
"Brand"
|
||
],
|
||
"summary": "List brands",
|
||
"description": "Lists brands for the authenticated user's selected company, sorted by name.",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "brand/list.md",
|
||
"responses": {
|
||
"200": {
|
||
"description": "Brand list",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"brands": {
|
||
"type": "array",
|
||
"items": {
|
||
"$ref": "#/components/schemas/Brand"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"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"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"put": {
|
||
"tags": [
|
||
"Brand"
|
||
],
|
||
"summary": "Upsert brand",
|
||
"description": "Insert or update brand by `name` for the selected company. `gsprpinfo` optional.\n",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "brand/upsert.md",
|
||
"requestBody": {
|
||
"required": true,
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"required": [
|
||
"name"
|
||
],
|
||
"properties": {
|
||
"name": {
|
||
"type": "string"
|
||
},
|
||
"gsprpinfo": {
|
||
"type": "string"
|
||
}
|
||
},
|
||
"example": {
|
||
"name": "Acme",
|
||
"gsprpinfo": "Acme GmbH"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"responses": {
|
||
"200": {
|
||
"description": "Upserted brand",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"brand": {
|
||
"$ref": "#/components/schemas/Brand"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"400": {
|
||
"description": "Invalid brand or 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"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"/brand/{id}": {
|
||
"delete": {
|
||
"tags": [
|
||
"Brand"
|
||
],
|
||
"summary": "Delete brand",
|
||
"description": "Deletes the brand for the selected company. Fails with `Brand in use`\nif any article still references it.\n",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "brand/delete.md",
|
||
"parameters": [
|
||
{
|
||
"in": "path",
|
||
"name": "id",
|
||
"required": true,
|
||
"schema": {
|
||
"type": "integer"
|
||
}
|
||
}
|
||
],
|
||
"responses": {
|
||
"204": {
|
||
"description": "Deleted"
|
||
},
|
||
"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": "Brand not found",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"$ref": "#/components/schemas/Error"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"409": {
|
||
"description": "Brand in use",
|
||
"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": "Patches article fields. At least one of `name`, `unitamount`, `unitSi` required.\n`unitSi` must already exist for the company. Returns the same detail shape as GET.\n",
|
||
"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",
|
||
"properties": {
|
||
"name": {
|
||
"type": "string"
|
||
},
|
||
"unitamount": {
|
||
"type": "number",
|
||
"description": "Must be finite and `> 0`"
|
||
},
|
||
"unitSi": {
|
||
"type": "string",
|
||
"description": "Existing company unit `si`"
|
||
}
|
||
},
|
||
"example": {
|
||
"unitamount": 0.24,
|
||
"unitSi": "l"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"responses": {
|
||
"200": {
|
||
"description": "Updated article detail",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"article": {
|
||
"$ref": "#/components/schemas/ArticleDetail"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"400": {
|
||
"description": "Validation error (Name required / Invalid unitamount)",
|
||
"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 or unit not found",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"$ref": "#/components/schemas/Error"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"delete": {
|
||
"tags": [
|
||
"Article"
|
||
],
|
||
"summary": "Delete article by SKU",
|
||
"description": "Deletes the article when it has no child articles. Cascades related rows;\nnulls `manifestarticle.article_id` and appends a delete comment.\nAuth like GET by SKU.\n",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "article/delete.md",
|
||
"parameters": [
|
||
{
|
||
"in": "path",
|
||
"name": "sku",
|
||
"required": true,
|
||
"schema": {
|
||
"type": "string"
|
||
},
|
||
"description": "Article SKU"
|
||
}
|
||
],
|
||
"responses": {
|
||
"204": {
|
||
"description": "Deleted"
|
||
},
|
||
"400": {
|
||
"description": "Article has children",
|
||
"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}/prices": {
|
||
"put": {
|
||
"tags": [
|
||
"Article"
|
||
],
|
||
"summary": "Replace POS prices by SKU",
|
||
"description": "Replace-all prices for the company POS customer group.\nEach entry: `price` (net), `tax` (rate, ensured via tax.ensure), optional `minmultiplier` (default 1).\nEmpty `prices` clears all POS prices. Returns the same shape as GET by SKU.\n",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "article/set-prices.md",
|
||
"parameters": [
|
||
{
|
||
"in": "path",
|
||
"name": "sku",
|
||
"required": true,
|
||
"schema": {
|
||
"type": "string"
|
||
}
|
||
}
|
||
],
|
||
"requestBody": {
|
||
"required": true,
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"required": [
|
||
"prices"
|
||
],
|
||
"properties": {
|
||
"prices": {
|
||
"type": "array",
|
||
"items": {
|
||
"type": "object",
|
||
"required": [
|
||
"price",
|
||
"tax"
|
||
],
|
||
"properties": {
|
||
"price": {
|
||
"type": "number",
|
||
"description": "Net price"
|
||
},
|
||
"tax": {
|
||
"type": "number",
|
||
"description": "Tax rate (e.g. 19)"
|
||
},
|
||
"minmultiplier": {
|
||
"type": "number",
|
||
"description": "Defaults to 1; must be unique per request"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"example": {
|
||
"prices": [
|
||
{
|
||
"price": 10.5,
|
||
"tax": 19
|
||
},
|
||
{
|
||
"price": 9.9,
|
||
"tax": 19,
|
||
"minmultiplier": 10
|
||
}
|
||
]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"responses": {
|
||
"200": {
|
||
"description": "Updated article detail",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"article": {
|
||
"$ref": "#/components/schemas/ArticleDetail"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"400": {
|
||
"description": "Invalid prices or POS customer group required",
|
||
"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"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"/article/{sku}/image/{fullHash}": {
|
||
"delete": {
|
||
"tags": [
|
||
"Article"
|
||
],
|
||
"summary": "Delete article image",
|
||
"description": "Removes `articlepic` and the `pic` row for this article (`fullHash`). Auth like GET by SKU.",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "article/delete-image.md",
|
||
"parameters": [
|
||
{
|
||
"in": "path",
|
||
"name": "sku",
|
||
"required": true,
|
||
"schema": {
|
||
"type": "string"
|
||
}
|
||
},
|
||
{
|
||
"in": "path",
|
||
"name": "fullHash",
|
||
"required": true,
|
||
"schema": {
|
||
"type": "string",
|
||
"pattern": "^[a-fA-F0-9]{64}$"
|
||
}
|
||
}
|
||
],
|
||
"responses": {
|
||
"204": {
|
||
"description": "Deleted"
|
||
},
|
||
"400": {
|
||
"description": "Invalid hash",
|
||
"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 or image not found",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"$ref": "#/components/schemas/Error"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"/article/{sku}/images/order": {
|
||
"put": {
|
||
"tags": [
|
||
"Article"
|
||
],
|
||
"summary": "Reorder article images",
|
||
"description": "Sets `articlepic.sort` to `0..n-1` in the given order.\n`fullHashes` must list every image of the article exactly once.\n",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "article/order-images.md",
|
||
"parameters": [
|
||
{
|
||
"in": "path",
|
||
"name": "sku",
|
||
"required": true,
|
||
"schema": {
|
||
"type": "string"
|
||
}
|
||
}
|
||
],
|
||
"requestBody": {
|
||
"required": true,
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"required": [
|
||
"fullHashes"
|
||
],
|
||
"properties": {
|
||
"fullHashes": {
|
||
"type": "array",
|
||
"items": {
|
||
"type": "string",
|
||
"pattern": "^[a-fA-F0-9]{64}$"
|
||
}
|
||
}
|
||
},
|
||
"example": {
|
||
"fullHashes": [
|
||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||
]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"responses": {
|
||
"200": {
|
||
"description": "New order",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"fullHashes": {
|
||
"type": "array",
|
||
"items": {
|
||
"type": "string"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"400": {
|
||
"description": "Invalid image order",
|
||
"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}/eans": {
|
||
"put": {
|
||
"tags": [
|
||
"Article"
|
||
],
|
||
"summary": "Replace article EANs by SKU",
|
||
"description": "Replace-all EANs for the article. EANs must be non-empty unique strings;\nempty array clears all EANs. Returns the same shape as GET by SKU.\n",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "article/set-eans.md",
|
||
"parameters": [
|
||
{
|
||
"in": "path",
|
||
"name": "sku",
|
||
"required": true,
|
||
"schema": {
|
||
"type": "string"
|
||
}
|
||
}
|
||
],
|
||
"requestBody": {
|
||
"required": true,
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"required": [
|
||
"eans"
|
||
],
|
||
"properties": {
|
||
"eans": {
|
||
"type": "array",
|
||
"items": {
|
||
"type": "string"
|
||
}
|
||
}
|
||
},
|
||
"example": {
|
||
"eans": [
|
||
"4001234567890"
|
||
]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"responses": {
|
||
"200": {
|
||
"description": "Updated article detail",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"article": {
|
||
"$ref": "#/components/schemas/ArticleDetail"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"400": {
|
||
"description": "Invalid eans",
|
||
"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}/brand": {
|
||
"put": {
|
||
"tags": [
|
||
"Article"
|
||
],
|
||
"summary": "Set or clear article brand by SKU",
|
||
"description": "Sets the article's `brandId` (must belong to the article's company).\nPass `null` to clear the brand. Returns the same shape as GET by SKU.\n",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "article/set-brand.md",
|
||
"parameters": [
|
||
{
|
||
"in": "path",
|
||
"name": "sku",
|
||
"required": true,
|
||
"schema": {
|
||
"type": "string"
|
||
}
|
||
}
|
||
],
|
||
"requestBody": {
|
||
"required": true,
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"required": [
|
||
"brandId"
|
||
],
|
||
"properties": {
|
||
"brandId": {
|
||
"type": "integer",
|
||
"nullable": true
|
||
}
|
||
},
|
||
"example": {
|
||
"brandId": 3
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"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 or brand not found",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"$ref": "#/components/schemas/Error"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"/article/{sku}/weight": {
|
||
"put": {
|
||
"tags": [
|
||
"Article"
|
||
],
|
||
"summary": "Set article weight by SKU",
|
||
"description": "Sets the article's `weight`. Must be finite and `>= 0`.\nReturns the same shape as GET by SKU.\n",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "article/set-weight.md",
|
||
"parameters": [
|
||
{
|
||
"in": "path",
|
||
"name": "sku",
|
||
"required": true,
|
||
"schema": {
|
||
"type": "string"
|
||
}
|
||
}
|
||
],
|
||
"requestBody": {
|
||
"required": true,
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"required": [
|
||
"weight"
|
||
],
|
||
"properties": {
|
||
"weight": {
|
||
"type": "number"
|
||
}
|
||
},
|
||
"example": {
|
||
"weight": 1.5
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"responses": {
|
||
"200": {
|
||
"description": "Updated article detail",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"article": {
|
||
"$ref": "#/components/schemas/ArticleDetail"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"400": {
|
||
"description": "Invalid weight",
|
||
"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}/categories": {
|
||
"put": {
|
||
"tags": [
|
||
"Article"
|
||
],
|
||
"summary": "Replace article categories by SKU",
|
||
"description": "Replace-all the article's category list. Each category must belong to\nthe article's company; ids must be unique. Empty array removes the\narticle from all categories. Returns the same shape as GET by SKU.\n",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"x-markdown": "article/set-categories.md",
|
||
"parameters": [
|
||
{
|
||
"in": "path",
|
||
"name": "sku",
|
||
"required": true,
|
||
"schema": {
|
||
"type": "string"
|
||
}
|
||
}
|
||
],
|
||
"requestBody": {
|
||
"required": true,
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"required": [
|
||
"categoryIds"
|
||
],
|
||
"properties": {
|
||
"categoryIds": {
|
||
"type": "array",
|
||
"items": {
|
||
"type": "integer"
|
||
}
|
||
}
|
||
},
|
||
"example": {
|
||
"categoryIds": [
|
||
4,
|
||
7
|
||
]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"responses": {
|
||
"200": {
|
||
"description": "Updated article detail",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"article": {
|
||
"$ref": "#/components/schemas/ArticleDetail"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"400": {
|
||
"description": "Invalid categoryIds",
|
||
"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"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"/articleIndex": {
|
||
"get": {
|
||
"tags": [
|
||
"Article"
|
||
],
|
||
"summary": "List article search index",
|
||
"description": "Returns every article reachable via the company's category tree.\nDesigned for client-side search / autocomplete preload.\nAuth via company access.\n",
|
||
"security": [
|
||
{
|
||
"cookieAuth": []
|
||
}
|
||
],
|
||
"responses": {
|
||
"200": {
|
||
"description": "Search index",
|
||
"content": {
|
||
"application/json": {
|
||
"schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"articles": {
|
||
"type": "array",
|
||
"items": {
|
||
"type": "object",
|
||
"properties": {
|
||
"id": {
|
||
"type": "integer"
|
||
},
|
||
"sku": {
|
||
"type": "string"
|
||
},
|
||
"name": {
|
||
"type": "string",
|
||
"nullable": true
|
||
},
|
||
"eans": {
|
||
"type": "array",
|
||
"items": {
|
||
"type": "string"
|
||
}
|
||
},
|
||
"categoryIds": {
|
||
"type": "array",
|
||
"items": {
|
||
"type": "integer"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"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"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"/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": "<google-id-token>"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"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"
|
||
}
|
||
}
|
||
},
|
||
"Unit": {
|
||
"type": "object",
|
||
"required": [
|
||
"si",
|
||
"basesi",
|
||
"basesifactor"
|
||
],
|
||
"properties": {
|
||
"si": {
|
||
"type": "string"
|
||
},
|
||
"basesi": {
|
||
"type": "string"
|
||
},
|
||
"basesifactor": {
|
||
"type": "number"
|
||
},
|
||
"symbol": {
|
||
"type": "string",
|
||
"nullable": true
|
||
},
|
||
"name": {
|
||
"type": "string",
|
||
"nullable": true
|
||
}
|
||
}
|
||
},
|
||
"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
|
||
},
|
||
"Brand": {
|
||
"type": "object",
|
||
"required": [
|
||
"id",
|
||
"name"
|
||
],
|
||
"properties": {
|
||
"id": {
|
||
"type": "integer"
|
||
},
|
||
"name": {
|
||
"type": "string"
|
||
},
|
||
"gsprpinfo": {
|
||
"type": "string",
|
||
"nullable": 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"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|