Extend PATCH /tags/{id}/rules/{then_id} to accept apply_to_existing bool.
When a rule is activated with apply_to_existing=true, a single recursive
CTE retroactively inserts the full transitive expansion of then_tag into
data.file_tag for all files already carrying when_tag:
WITH RECURSIVE expansion(tag_id) AS (
SELECT then_tag_id
UNION
SELECT r.then_tag_id FROM data.tag_rules r
JOIN expansion e ON r.when_tag_id = e.tag_id
WHERE r.is_active = true
)
INSERT INTO data.file_tag ... ON CONFLICT DO NOTHING
Changes:
- port/repository.go: add applyToExisting param to TagRuleRepo.SetActive
- db/postgres/tag_repo.go: implement recursive CTE retroactive apply
- service/tag_service.go: thread applyToExisting through SetRuleActive
- handler/tag_handler.go: parse apply_to_existing from PATCH body
- openapi.yaml: document apply_to_existing on PATCH endpoint
- integration test: add TestTagRuleActivateApplyToExisting covering
no-op when false, direct+transitive apply when true
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2069 lines
51 KiB
YAML
2069 lines
51 KiB
YAML
openapi: 3.0.3
|
|
info:
|
|
title: Tanabata File Manager API
|
|
description: |
|
|
REST API for Tanabata File Manager — a multi-user, tag-based web file manager.
|
|
|
|
## Authentication
|
|
All endpoints except `POST /auth/login` require a Bearer JWT token
|
|
in the `Authorization` header.
|
|
|
|
## Pagination
|
|
- **Files**: cursor-based (`cursor` parameter, returned in `next_cursor`).
|
|
- **All other lists**: offset-based (`offset` + `limit`).
|
|
|
|
## Error format
|
|
```json
|
|
{
|
|
"code": "string",
|
|
"message": "string",
|
|
"details": [{ "field": "string", "message": "string" }]
|
|
}
|
|
```
|
|
|
|
## File filter DSL
|
|
Files can be filtered via a DSL in the `filter` query parameter.
|
|
Tokens are comma-separated inside braces: `{token1,token2,...}`.
|
|
|
|
Operators: `(`, `)`, `&` (AND), `|` (OR), `!` (NOT).
|
|
Conditions: `t=<tag_uuid>` (has tag), `t=00000000-0000-0000-0000-000000000000` (untagged),
|
|
`m=<mime_id>` (exact MIME), `m~<pattern>` (MIME LIKE pattern, e.g. `m~image%`).
|
|
|
|
Example: `{t=uuid1,&,!,t=uuid2}` → has tag1 AND NOT tag2.
|
|
version: 1.0.0
|
|
license:
|
|
name: Proprietary
|
|
|
|
servers:
|
|
- url: /api/v1
|
|
|
|
security:
|
|
- bearerAuth: []
|
|
|
|
tags:
|
|
- name: Auth
|
|
description: Authentication and session management
|
|
- name: Files
|
|
description: File management, upload, trash
|
|
- name: Tags
|
|
description: Tag CRUD and tag rules
|
|
- name: Categories
|
|
description: Category CRUD
|
|
- name: Pools
|
|
description: Pool CRUD, file ordering
|
|
- name: ACL
|
|
description: Access control
|
|
- name: Users
|
|
description: User management (admin)
|
|
- name: Audit
|
|
description: Audit log (admin)
|
|
|
|
# ===========================================================================
|
|
# Paths
|
|
# ===========================================================================
|
|
paths:
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Auth
|
|
# -------------------------------------------------------------------------
|
|
/auth/login:
|
|
post:
|
|
tags: [Auth]
|
|
summary: Log in
|
|
security: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [name, password]
|
|
properties:
|
|
name:
|
|
type: string
|
|
password:
|
|
type: string
|
|
responses:
|
|
'200':
|
|
description: JWT token pair
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/TokenPair'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
|
|
/auth/refresh:
|
|
post:
|
|
tags: [Auth]
|
|
summary: Refresh access token
|
|
security: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [refresh_token]
|
|
properties:
|
|
refresh_token:
|
|
type: string
|
|
responses:
|
|
'200':
|
|
description: New token pair
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/TokenPair'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
|
|
/auth/logout:
|
|
post:
|
|
tags: [Auth]
|
|
summary: Log out (invalidate current session)
|
|
responses:
|
|
'204':
|
|
description: Logged out
|
|
|
|
/auth/sessions:
|
|
get:
|
|
tags: [Auth]
|
|
summary: List my active sessions
|
|
parameters:
|
|
- $ref: '#/components/parameters/offset'
|
|
- $ref: '#/components/parameters/limit'
|
|
responses:
|
|
'200':
|
|
description: Session list
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/SessionList'
|
|
|
|
/auth/sessions/{session_id}:
|
|
delete:
|
|
tags: [Auth]
|
|
summary: Terminate a session
|
|
parameters:
|
|
- name: session_id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
responses:
|
|
'204':
|
|
description: Session terminated
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Files
|
|
# -------------------------------------------------------------------------
|
|
/files:
|
|
get:
|
|
tags: [Files]
|
|
summary: List files (cursor-based pagination)
|
|
parameters:
|
|
- name: cursor
|
|
in: query
|
|
description: Cursor from previous response's `next_cursor` or `prev_cursor`
|
|
schema:
|
|
type: string
|
|
- name: direction
|
|
in: query
|
|
description: Pagination direction relative to cursor
|
|
schema:
|
|
type: string
|
|
enum: [forward, backward]
|
|
default: forward
|
|
- name: anchor
|
|
in: query
|
|
description: |
|
|
File UUID to anchor the listing around. The response will
|
|
include this file and its neighbors. Use with `direction`
|
|
and `limit` to load items before/after the anchor.
|
|
Mutually exclusive with `cursor`.
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
- name: limit
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
default: 50
|
|
maximum: 200
|
|
- name: sort
|
|
in: query
|
|
description: Sort field
|
|
schema:
|
|
type: string
|
|
enum: [content_datetime, created, original_name, mime]
|
|
default: created
|
|
- name: order
|
|
in: query
|
|
schema:
|
|
type: string
|
|
enum: [asc, desc]
|
|
default: desc
|
|
- name: filter
|
|
in: query
|
|
description: Filter DSL expression (see API description)
|
|
schema:
|
|
type: string
|
|
- name: search
|
|
in: query
|
|
description: Search by original_name (substring match)
|
|
schema:
|
|
type: string
|
|
- name: trash
|
|
in: query
|
|
description: If true, return only soft-deleted files (trash)
|
|
schema:
|
|
type: boolean
|
|
default: false
|
|
responses:
|
|
'200':
|
|
description: Paginated file list
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/FileCursorPage'
|
|
|
|
post:
|
|
tags: [Files]
|
|
summary: Upload a new file
|
|
description: |
|
|
Upload a file via multipart form. If `content_datetime` is not provided,
|
|
it is extracted from EXIF data (DateTimeOriginal). EXIF metadata is
|
|
automatically extracted and stored in the `exif` field (immutable).
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
multipart/form-data:
|
|
schema:
|
|
type: object
|
|
required: [file]
|
|
properties:
|
|
file:
|
|
type: string
|
|
format: binary
|
|
content_datetime:
|
|
type: string
|
|
format: date-time
|
|
notes:
|
|
type: string
|
|
metadata:
|
|
type: string
|
|
description: JSON string of key-value pairs
|
|
is_public:
|
|
type: boolean
|
|
default: false
|
|
tag_ids:
|
|
type: string
|
|
description: Comma-separated tag UUIDs to assign
|
|
responses:
|
|
'201':
|
|
description: File created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/File'
|
|
'400':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'415':
|
|
description: Unsupported MIME type
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
|
|
/files/{file_id}:
|
|
get:
|
|
tags: [Files]
|
|
summary: Get file metadata
|
|
parameters:
|
|
- $ref: '#/components/parameters/file_id'
|
|
responses:
|
|
'200':
|
|
description: File metadata
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/File'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
|
|
patch:
|
|
tags: [Files]
|
|
summary: Update file metadata
|
|
parameters:
|
|
- $ref: '#/components/parameters/file_id'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/FileUpdate'
|
|
responses:
|
|
'200':
|
|
description: Updated file
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/File'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
|
|
delete:
|
|
tags: [Files]
|
|
summary: Soft-delete file (move to trash)
|
|
parameters:
|
|
- $ref: '#/components/parameters/file_id'
|
|
responses:
|
|
'204':
|
|
description: Moved to trash
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
|
|
/files/{file_id}/content:
|
|
get:
|
|
tags: [Files]
|
|
summary: Download file content
|
|
parameters:
|
|
- $ref: '#/components/parameters/file_id'
|
|
responses:
|
|
'200':
|
|
description: File binary
|
|
content:
|
|
application/octet-stream:
|
|
schema:
|
|
type: string
|
|
format: binary
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
|
|
put:
|
|
tags: [Files]
|
|
summary: Replace file content (keep same ID)
|
|
parameters:
|
|
- $ref: '#/components/parameters/file_id'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
multipart/form-data:
|
|
schema:
|
|
type: object
|
|
required: [file]
|
|
properties:
|
|
file:
|
|
type: string
|
|
format: binary
|
|
responses:
|
|
'200':
|
|
description: File replaced
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/File'
|
|
'415':
|
|
description: Unsupported MIME type
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
|
|
/files/{file_id}/thumbnail:
|
|
get:
|
|
tags: [Files]
|
|
summary: Get file thumbnail
|
|
parameters:
|
|
- $ref: '#/components/parameters/file_id'
|
|
responses:
|
|
'200':
|
|
description: Thumbnail image
|
|
content:
|
|
image/jpeg:
|
|
schema:
|
|
type: string
|
|
format: binary
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
|
|
/files/{file_id}/preview:
|
|
get:
|
|
tags: [Files]
|
|
summary: Get file preview (larger than thumbnail)
|
|
parameters:
|
|
- $ref: '#/components/parameters/file_id'
|
|
responses:
|
|
'200':
|
|
description: Preview image
|
|
content:
|
|
image/jpeg:
|
|
schema:
|
|
type: string
|
|
format: binary
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
|
|
/files/{file_id}/restore:
|
|
post:
|
|
tags: [Files]
|
|
summary: Restore file from trash
|
|
parameters:
|
|
- $ref: '#/components/parameters/file_id'
|
|
responses:
|
|
'200':
|
|
description: File restored
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/File'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
|
|
/files/{file_id}/permanent:
|
|
delete:
|
|
tags: [Files]
|
|
summary: Permanently delete file (from trash only)
|
|
parameters:
|
|
- $ref: '#/components/parameters/file_id'
|
|
responses:
|
|
'204':
|
|
description: Permanently deleted
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
description: File is not in trash
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
|
|
# --- File-Tag relations ---
|
|
/files/{file_id}/tags:
|
|
get:
|
|
tags: [Files, Tags]
|
|
summary: List tags assigned to a file
|
|
parameters:
|
|
- $ref: '#/components/parameters/file_id'
|
|
responses:
|
|
'200':
|
|
description: Tag list
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Tag'
|
|
|
|
put:
|
|
tags: [Files, Tags]
|
|
summary: Set tags on a file (replaces all)
|
|
parameters:
|
|
- $ref: '#/components/parameters/file_id'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [tag_ids]
|
|
properties:
|
|
tag_ids:
|
|
type: array
|
|
items:
|
|
type: string
|
|
format: uuid
|
|
responses:
|
|
'200':
|
|
description: Updated tag list (including auto-applied tags)
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Tag'
|
|
|
|
/files/{file_id}/tags/{tag_id}:
|
|
put:
|
|
tags: [Files, Tags]
|
|
summary: Add a tag to a file
|
|
parameters:
|
|
- $ref: '#/components/parameters/file_id'
|
|
- $ref: '#/components/parameters/tag_id'
|
|
responses:
|
|
'200':
|
|
description: Tags after addition (including auto-applied)
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Tag'
|
|
|
|
delete:
|
|
tags: [Files, Tags]
|
|
summary: Remove a tag from a file
|
|
parameters:
|
|
- $ref: '#/components/parameters/file_id'
|
|
- $ref: '#/components/parameters/tag_id'
|
|
responses:
|
|
'204':
|
|
description: Tag removed
|
|
|
|
# --- Bulk file operations ---
|
|
/files/bulk/tags:
|
|
post:
|
|
tags: [Files, Tags]
|
|
summary: Bulk add/remove tags on multiple files
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [file_ids, action, tag_ids]
|
|
properties:
|
|
file_ids:
|
|
type: array
|
|
items:
|
|
type: string
|
|
format: uuid
|
|
action:
|
|
type: string
|
|
enum: [add, remove]
|
|
tag_ids:
|
|
type: array
|
|
items:
|
|
type: string
|
|
format: uuid
|
|
responses:
|
|
'200':
|
|
description: Result with all applied tag IDs (including auto-applied, for add)
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
applied_tag_ids:
|
|
type: array
|
|
items:
|
|
type: string
|
|
format: uuid
|
|
|
|
/files/bulk/delete:
|
|
post:
|
|
tags: [Files]
|
|
summary: Bulk soft-delete files
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [file_ids]
|
|
properties:
|
|
file_ids:
|
|
type: array
|
|
items:
|
|
type: string
|
|
format: uuid
|
|
responses:
|
|
'204':
|
|
description: Files moved to trash
|
|
|
|
/files/bulk/common-tags:
|
|
post:
|
|
tags: [Files, Tags]
|
|
summary: Get tags for multiple files, split into common (all) and partial (some)
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [file_ids]
|
|
properties:
|
|
file_ids:
|
|
type: array
|
|
items:
|
|
type: string
|
|
format: uuid
|
|
responses:
|
|
'200':
|
|
description: Tags grouped by coverage
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
common_tag_ids:
|
|
type: array
|
|
description: Tags present on ALL specified files
|
|
items:
|
|
type: string
|
|
format: uuid
|
|
partial_tag_ids:
|
|
type: array
|
|
description: Tags present on SOME but not all specified files
|
|
items:
|
|
type: string
|
|
format: uuid
|
|
|
|
# --- File import ---
|
|
/files/import:
|
|
post:
|
|
tags: [Files]
|
|
summary: Import files from a server directory
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
path:
|
|
type: string
|
|
description: Server directory path (uses user's configured import path if omitted)
|
|
responses:
|
|
'200':
|
|
description: Import result
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
imported:
|
|
type: integer
|
|
skipped:
|
|
type: integer
|
|
errors:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
filename:
|
|
type: string
|
|
reason:
|
|
type: string
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Tags
|
|
# -------------------------------------------------------------------------
|
|
/tags:
|
|
get:
|
|
tags: [Tags]
|
|
summary: List tags
|
|
parameters:
|
|
- $ref: '#/components/parameters/offset'
|
|
- $ref: '#/components/parameters/limit'
|
|
- name: sort
|
|
in: query
|
|
schema:
|
|
type: string
|
|
enum: [name, color, category_name, created]
|
|
default: created
|
|
- name: order
|
|
in: query
|
|
schema:
|
|
type: string
|
|
enum: [asc, desc]
|
|
default: desc
|
|
- name: search
|
|
in: query
|
|
description: Search by name (substring)
|
|
schema:
|
|
type: string
|
|
responses:
|
|
'200':
|
|
description: Tag list
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/TagOffsetPage'
|
|
|
|
post:
|
|
tags: [Tags]
|
|
summary: Create a tag
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/TagCreate'
|
|
responses:
|
|
'201':
|
|
description: Tag created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Tag'
|
|
'400':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'409':
|
|
description: Tag name already exists
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
|
|
/tags/{tag_id}:
|
|
get:
|
|
tags: [Tags]
|
|
summary: Get a tag
|
|
parameters:
|
|
- $ref: '#/components/parameters/tag_id'
|
|
responses:
|
|
'200':
|
|
description: Tag
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Tag'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
|
|
patch:
|
|
tags: [Tags]
|
|
summary: Update a tag
|
|
parameters:
|
|
- $ref: '#/components/parameters/tag_id'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/TagUpdate'
|
|
responses:
|
|
'200':
|
|
description: Updated tag
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Tag'
|
|
|
|
delete:
|
|
tags: [Tags]
|
|
summary: Delete a tag
|
|
parameters:
|
|
- $ref: '#/components/parameters/tag_id'
|
|
responses:
|
|
'204':
|
|
description: Tag deleted
|
|
|
|
/tags/{tag_id}/files:
|
|
get:
|
|
tags: [Tags, Files]
|
|
summary: List files with this tag
|
|
parameters:
|
|
- $ref: '#/components/parameters/tag_id'
|
|
- name: cursor
|
|
in: query
|
|
schema:
|
|
type: string
|
|
- name: limit
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
default: 50
|
|
responses:
|
|
'200':
|
|
description: File list
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/FileCursorPage'
|
|
|
|
# --- Tag rules ---
|
|
/tags/{tag_id}/rules:
|
|
get:
|
|
tags: [Tags]
|
|
summary: List tag rules for a tag (when this tag is applied, which tags follow)
|
|
parameters:
|
|
- $ref: '#/components/parameters/tag_id'
|
|
responses:
|
|
'200':
|
|
description: Tag rules
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/TagRule'
|
|
|
|
post:
|
|
tags: [Tags]
|
|
summary: Add a tag rule
|
|
parameters:
|
|
- $ref: '#/components/parameters/tag_id'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [then_tag_id]
|
|
properties:
|
|
then_tag_id:
|
|
type: string
|
|
format: uuid
|
|
is_active:
|
|
type: boolean
|
|
default: true
|
|
apply_to_existing:
|
|
type: boolean
|
|
default: true
|
|
description: Apply rule retroactively to files already tagged
|
|
responses:
|
|
'201':
|
|
description: Rule created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/TagRule'
|
|
|
|
/tags/{tag_id}/rules/{then_tag_id}:
|
|
patch:
|
|
tags: [Tags]
|
|
summary: Update a tag rule (activate / deactivate)
|
|
parameters:
|
|
- $ref: '#/components/parameters/tag_id'
|
|
- name: then_tag_id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [is_active]
|
|
properties:
|
|
is_active:
|
|
type: boolean
|
|
apply_to_existing:
|
|
type: boolean
|
|
default: false
|
|
description: When activating, apply rule retroactively to files already tagged
|
|
responses:
|
|
'200':
|
|
description: Rule updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/TagRule'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
|
|
delete:
|
|
tags: [Tags]
|
|
summary: Remove a tag rule
|
|
parameters:
|
|
- $ref: '#/components/parameters/tag_id'
|
|
- name: then_tag_id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
responses:
|
|
'204':
|
|
description: Rule removed
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Categories
|
|
# -------------------------------------------------------------------------
|
|
/categories:
|
|
get:
|
|
tags: [Categories]
|
|
summary: List categories
|
|
parameters:
|
|
- $ref: '#/components/parameters/offset'
|
|
- $ref: '#/components/parameters/limit'
|
|
- name: sort
|
|
in: query
|
|
schema:
|
|
type: string
|
|
enum: [name, created]
|
|
default: created
|
|
- name: order
|
|
in: query
|
|
schema:
|
|
type: string
|
|
enum: [asc, desc]
|
|
default: desc
|
|
- name: search
|
|
in: query
|
|
schema:
|
|
type: string
|
|
responses:
|
|
'200':
|
|
description: Category list
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CategoryOffsetPage'
|
|
|
|
post:
|
|
tags: [Categories]
|
|
summary: Create a category
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CategoryCreate'
|
|
responses:
|
|
'201':
|
|
description: Category created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Category'
|
|
'409':
|
|
description: Name already exists
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
|
|
/categories/{category_id}:
|
|
get:
|
|
tags: [Categories]
|
|
summary: Get a category
|
|
parameters:
|
|
- $ref: '#/components/parameters/category_id'
|
|
responses:
|
|
'200':
|
|
description: Category
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Category'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
|
|
patch:
|
|
tags: [Categories]
|
|
summary: Update a category
|
|
parameters:
|
|
- $ref: '#/components/parameters/category_id'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CategoryUpdate'
|
|
responses:
|
|
'200':
|
|
description: Updated category
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Category'
|
|
|
|
delete:
|
|
tags: [Categories]
|
|
summary: Delete a category
|
|
parameters:
|
|
- $ref: '#/components/parameters/category_id'
|
|
responses:
|
|
'204':
|
|
description: Category deleted
|
|
|
|
/categories/{category_id}/tags:
|
|
get:
|
|
tags: [Categories, Tags]
|
|
summary: List tags in a category
|
|
parameters:
|
|
- $ref: '#/components/parameters/category_id'
|
|
- $ref: '#/components/parameters/offset'
|
|
- $ref: '#/components/parameters/limit'
|
|
responses:
|
|
'200':
|
|
description: Tag list
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/TagOffsetPage'
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Pools
|
|
# -------------------------------------------------------------------------
|
|
/pools:
|
|
get:
|
|
tags: [Pools]
|
|
summary: List pools
|
|
parameters:
|
|
- $ref: '#/components/parameters/offset'
|
|
- $ref: '#/components/parameters/limit'
|
|
- name: sort
|
|
in: query
|
|
schema:
|
|
type: string
|
|
enum: [name, created]
|
|
default: created
|
|
- name: order
|
|
in: query
|
|
schema:
|
|
type: string
|
|
enum: [asc, desc]
|
|
default: desc
|
|
- name: search
|
|
in: query
|
|
schema:
|
|
type: string
|
|
responses:
|
|
'200':
|
|
description: Pool list
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/PoolOffsetPage'
|
|
|
|
post:
|
|
tags: [Pools]
|
|
summary: Create a pool
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/PoolCreate'
|
|
responses:
|
|
'201':
|
|
description: Pool created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Pool'
|
|
|
|
/pools/{pool_id}:
|
|
get:
|
|
tags: [Pools]
|
|
summary: Get a pool
|
|
parameters:
|
|
- $ref: '#/components/parameters/pool_id'
|
|
responses:
|
|
'200':
|
|
description: Pool
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Pool'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
|
|
patch:
|
|
tags: [Pools]
|
|
summary: Update a pool
|
|
parameters:
|
|
- $ref: '#/components/parameters/pool_id'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/PoolUpdate'
|
|
responses:
|
|
'200':
|
|
description: Updated pool
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Pool'
|
|
|
|
delete:
|
|
tags: [Pools]
|
|
summary: Delete a pool
|
|
parameters:
|
|
- $ref: '#/components/parameters/pool_id'
|
|
responses:
|
|
'204':
|
|
description: Pool deleted
|
|
|
|
/pools/{pool_id}/files:
|
|
get:
|
|
tags: [Pools, Files]
|
|
summary: List files in a pool (ordered by position)
|
|
parameters:
|
|
- $ref: '#/components/parameters/pool_id'
|
|
- name: cursor
|
|
in: query
|
|
schema:
|
|
type: string
|
|
- name: limit
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
default: 50
|
|
- name: filter
|
|
in: query
|
|
description: Filter DSL (same syntax as /files)
|
|
schema:
|
|
type: string
|
|
responses:
|
|
'200':
|
|
description: Ordered file list
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/PoolFileCursorPage'
|
|
|
|
post:
|
|
tags: [Pools, Files]
|
|
summary: Add files to a pool
|
|
parameters:
|
|
- $ref: '#/components/parameters/pool_id'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [file_ids]
|
|
properties:
|
|
file_ids:
|
|
type: array
|
|
items:
|
|
type: string
|
|
format: uuid
|
|
position:
|
|
type: integer
|
|
description: Insert position for the first file (rest follow sequentially; appended at end if omitted)
|
|
responses:
|
|
'201':
|
|
description: Files added to pool
|
|
|
|
/pools/{pool_id}/files/remove:
|
|
post:
|
|
tags: [Pools, Files]
|
|
summary: Remove files from a pool
|
|
parameters:
|
|
- $ref: '#/components/parameters/pool_id'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [file_ids]
|
|
properties:
|
|
file_ids:
|
|
type: array
|
|
items:
|
|
type: string
|
|
format: uuid
|
|
responses:
|
|
'204':
|
|
description: Files removed from pool
|
|
|
|
/pools/{pool_id}/files/reorder:
|
|
put:
|
|
tags: [Pools]
|
|
summary: Reorder files in a pool
|
|
parameters:
|
|
- $ref: '#/components/parameters/pool_id'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [file_ids]
|
|
properties:
|
|
file_ids:
|
|
type: array
|
|
description: File UUIDs in desired order
|
|
items:
|
|
type: string
|
|
format: uuid
|
|
responses:
|
|
'204':
|
|
description: Reordered
|
|
|
|
# -------------------------------------------------------------------------
|
|
# ACL
|
|
# -------------------------------------------------------------------------
|
|
/acl/{object_type}/{object_id}:
|
|
get:
|
|
tags: [ACL]
|
|
summary: List permissions for an object
|
|
parameters:
|
|
- name: object_type
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum: [file, tag, category, pool]
|
|
- name: object_id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
responses:
|
|
'200':
|
|
description: Permission list
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Permission'
|
|
|
|
put:
|
|
tags: [ACL]
|
|
summary: Set permissions for an object (replaces all)
|
|
parameters:
|
|
- name: object_type
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum: [file, tag, category, pool]
|
|
- name: object_id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [permissions]
|
|
properties:
|
|
permissions:
|
|
type: array
|
|
items:
|
|
type: object
|
|
required: [user_id]
|
|
properties:
|
|
user_id:
|
|
type: integer
|
|
can_view:
|
|
type: boolean
|
|
default: true
|
|
can_edit:
|
|
type: boolean
|
|
default: false
|
|
responses:
|
|
'200':
|
|
description: Updated permissions
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Permission'
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Users (admin)
|
|
# -------------------------------------------------------------------------
|
|
/users:
|
|
get:
|
|
tags: [Users]
|
|
summary: List users (admin only)
|
|
parameters:
|
|
- $ref: '#/components/parameters/offset'
|
|
- $ref: '#/components/parameters/limit'
|
|
responses:
|
|
'200':
|
|
description: User list
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/UserOffsetPage'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
|
|
post:
|
|
tags: [Users]
|
|
summary: Create a user (admin only)
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/UserCreate'
|
|
responses:
|
|
'201':
|
|
description: User created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/User'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
|
|
/users/me:
|
|
get:
|
|
tags: [Users]
|
|
summary: Get current user profile
|
|
responses:
|
|
'200':
|
|
description: Current user
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/User'
|
|
|
|
patch:
|
|
tags: [Users]
|
|
summary: Update current user profile (name, password)
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
name:
|
|
type: string
|
|
password:
|
|
type: string
|
|
responses:
|
|
'200':
|
|
description: Updated user
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/User'
|
|
|
|
/users/{user_id}:
|
|
get:
|
|
tags: [Users]
|
|
summary: Get a user (admin only)
|
|
parameters:
|
|
- $ref: '#/components/parameters/user_id'
|
|
responses:
|
|
'200':
|
|
description: User
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/User'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
|
|
patch:
|
|
tags: [Users]
|
|
summary: Update user role/status (admin only)
|
|
parameters:
|
|
- $ref: '#/components/parameters/user_id'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
is_admin:
|
|
type: boolean
|
|
can_create:
|
|
type: boolean
|
|
is_blocked:
|
|
type: boolean
|
|
responses:
|
|
'200':
|
|
description: Updated user
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/User'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
|
|
delete:
|
|
tags: [Users]
|
|
summary: Delete a user (admin only)
|
|
parameters:
|
|
- $ref: '#/components/parameters/user_id'
|
|
responses:
|
|
'204':
|
|
description: User deleted
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Audit log (admin)
|
|
# -------------------------------------------------------------------------
|
|
/audit:
|
|
get:
|
|
tags: [Audit]
|
|
summary: Query audit log (admin only)
|
|
parameters:
|
|
- $ref: '#/components/parameters/offset'
|
|
- name: limit
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
default: 50
|
|
maximum: 200
|
|
- name: user_id
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
- name: action
|
|
in: query
|
|
description: Filter by action type name
|
|
schema:
|
|
type: string
|
|
- name: object_type
|
|
in: query
|
|
schema:
|
|
type: string
|
|
enum: [file, tag, category, pool]
|
|
- name: object_id
|
|
in: query
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
- name: from
|
|
in: query
|
|
schema:
|
|
type: string
|
|
format: date-time
|
|
- name: to
|
|
in: query
|
|
schema:
|
|
type: string
|
|
format: date-time
|
|
responses:
|
|
'200':
|
|
description: Audit log entries
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/AuditLogOffsetPage'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
|
|
# ===========================================================================
|
|
# Components
|
|
# ===========================================================================
|
|
components:
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Security schemes
|
|
# -------------------------------------------------------------------------
|
|
securitySchemes:
|
|
bearerAuth:
|
|
type: http
|
|
scheme: bearer
|
|
bearerFormat: JWT
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Parameters
|
|
# -------------------------------------------------------------------------
|
|
parameters:
|
|
file_id:
|
|
name: file_id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
|
|
tag_id:
|
|
name: tag_id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
|
|
category_id:
|
|
name: category_id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
|
|
pool_id:
|
|
name: pool_id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
|
|
user_id:
|
|
name: user_id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
|
|
offset:
|
|
name: offset
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
default: 0
|
|
minimum: 0
|
|
|
|
limit:
|
|
name: limit
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
default: 50
|
|
minimum: 1
|
|
maximum: 200
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Responses
|
|
# -------------------------------------------------------------------------
|
|
responses:
|
|
Unauthorized:
|
|
description: Authentication required or token invalid
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
|
|
Forbidden:
|
|
description: Insufficient permissions
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
|
|
NotFound:
|
|
description: Resource not found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
|
|
ValidationError:
|
|
description: Validation error
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Schemas
|
|
# -------------------------------------------------------------------------
|
|
schemas:
|
|
|
|
# --- Error ---
|
|
Error:
|
|
type: object
|
|
required: [code, message]
|
|
properties:
|
|
code:
|
|
type: string
|
|
example: not_found
|
|
message:
|
|
type: string
|
|
example: File not found
|
|
details:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
field:
|
|
type: string
|
|
message:
|
|
type: string
|
|
|
|
# --- Auth ---
|
|
TokenPair:
|
|
type: object
|
|
properties:
|
|
access_token:
|
|
type: string
|
|
refresh_token:
|
|
type: string
|
|
expires_in:
|
|
type: integer
|
|
description: Access token TTL in seconds
|
|
|
|
Session:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
user_agent:
|
|
type: string
|
|
started_at:
|
|
type: string
|
|
format: date-time
|
|
expires_at:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|
|
last_activity:
|
|
type: string
|
|
format: date-time
|
|
is_current:
|
|
type: boolean
|
|
|
|
SessionList:
|
|
type: object
|
|
properties:
|
|
items:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Session'
|
|
total:
|
|
type: integer
|
|
|
|
# --- File ---
|
|
File:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
format: uuid
|
|
original_name:
|
|
type: string
|
|
nullable: true
|
|
mime_type:
|
|
type: string
|
|
example: image/jpeg
|
|
mime_extension:
|
|
type: string
|
|
example: jpg
|
|
content_datetime:
|
|
type: string
|
|
format: date-time
|
|
notes:
|
|
type: string
|
|
nullable: true
|
|
metadata:
|
|
type: object
|
|
nullable: true
|
|
exif:
|
|
type: object
|
|
readOnly: true
|
|
description: EXIF data extracted at upload time (immutable, empty object if no EXIF)
|
|
phash:
|
|
type: integer
|
|
format: int64
|
|
nullable: true
|
|
creator_id:
|
|
type: integer
|
|
creator_name:
|
|
type: string
|
|
is_public:
|
|
type: boolean
|
|
is_deleted:
|
|
type: boolean
|
|
created_at:
|
|
type: string
|
|
format: date-time
|
|
description: Extracted from UUID v7
|
|
|
|
FileUpdate:
|
|
type: object
|
|
properties:
|
|
original_name:
|
|
type: string
|
|
content_datetime:
|
|
type: string
|
|
format: date-time
|
|
notes:
|
|
type: string
|
|
metadata:
|
|
type: object
|
|
is_public:
|
|
type: boolean
|
|
|
|
FileCursorPage:
|
|
type: object
|
|
properties:
|
|
items:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/File'
|
|
next_cursor:
|
|
type: string
|
|
nullable: true
|
|
description: Cursor for loading next (forward) page; null if no more items ahead
|
|
prev_cursor:
|
|
type: string
|
|
nullable: true
|
|
description: Cursor for loading previous (backward) page; null if at the beginning
|
|
|
|
# --- Tag ---
|
|
Tag:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
format: uuid
|
|
name:
|
|
type: string
|
|
notes:
|
|
type: string
|
|
nullable: true
|
|
color:
|
|
type: string
|
|
nullable: true
|
|
example: "5DCAA5"
|
|
category_id:
|
|
type: string
|
|
format: uuid
|
|
nullable: true
|
|
category_name:
|
|
type: string
|
|
nullable: true
|
|
category_color:
|
|
type: string
|
|
nullable: true
|
|
metadata:
|
|
type: object
|
|
nullable: true
|
|
creator_id:
|
|
type: integer
|
|
creator_name:
|
|
type: string
|
|
is_public:
|
|
type: boolean
|
|
created_at:
|
|
type: string
|
|
format: date-time
|
|
|
|
TagCreate:
|
|
type: object
|
|
required: [name]
|
|
properties:
|
|
name:
|
|
type: string
|
|
notes:
|
|
type: string
|
|
color:
|
|
type: string
|
|
pattern: '^[A-Fa-f0-9]{6}$'
|
|
category_id:
|
|
type: string
|
|
format: uuid
|
|
metadata:
|
|
type: object
|
|
is_public:
|
|
type: boolean
|
|
default: false
|
|
|
|
TagUpdate:
|
|
type: object
|
|
properties:
|
|
name:
|
|
type: string
|
|
notes:
|
|
type: string
|
|
color:
|
|
type: string
|
|
nullable: true
|
|
description: Hex color or null to clear
|
|
category_id:
|
|
type: string
|
|
format: uuid
|
|
nullable: true
|
|
description: Category UUID or null to unassign
|
|
metadata:
|
|
type: object
|
|
is_public:
|
|
type: boolean
|
|
|
|
TagOffsetPage:
|
|
type: object
|
|
properties:
|
|
items:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Tag'
|
|
total:
|
|
type: integer
|
|
offset:
|
|
type: integer
|
|
limit:
|
|
type: integer
|
|
|
|
# --- Tag rule ---
|
|
TagRule:
|
|
type: object
|
|
properties:
|
|
when_tag_id:
|
|
type: string
|
|
format: uuid
|
|
then_tag_id:
|
|
type: string
|
|
format: uuid
|
|
then_tag_name:
|
|
type: string
|
|
is_active:
|
|
type: boolean
|
|
|
|
# --- Category ---
|
|
Category:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
format: uuid
|
|
name:
|
|
type: string
|
|
notes:
|
|
type: string
|
|
nullable: true
|
|
color:
|
|
type: string
|
|
nullable: true
|
|
metadata:
|
|
type: object
|
|
nullable: true
|
|
creator_id:
|
|
type: integer
|
|
creator_name:
|
|
type: string
|
|
is_public:
|
|
type: boolean
|
|
created_at:
|
|
type: string
|
|
format: date-time
|
|
|
|
CategoryCreate:
|
|
type: object
|
|
required: [name]
|
|
properties:
|
|
name:
|
|
type: string
|
|
notes:
|
|
type: string
|
|
color:
|
|
type: string
|
|
pattern: '^[A-Fa-f0-9]{6}$'
|
|
metadata:
|
|
type: object
|
|
is_public:
|
|
type: boolean
|
|
default: false
|
|
|
|
CategoryUpdate:
|
|
type: object
|
|
properties:
|
|
name:
|
|
type: string
|
|
notes:
|
|
type: string
|
|
color:
|
|
type: string
|
|
nullable: true
|
|
metadata:
|
|
type: object
|
|
is_public:
|
|
type: boolean
|
|
|
|
CategoryOffsetPage:
|
|
type: object
|
|
properties:
|
|
items:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Category'
|
|
total:
|
|
type: integer
|
|
offset:
|
|
type: integer
|
|
limit:
|
|
type: integer
|
|
|
|
# --- Pool ---
|
|
Pool:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
format: uuid
|
|
name:
|
|
type: string
|
|
notes:
|
|
type: string
|
|
nullable: true
|
|
metadata:
|
|
type: object
|
|
nullable: true
|
|
creator_id:
|
|
type: integer
|
|
creator_name:
|
|
type: string
|
|
is_public:
|
|
type: boolean
|
|
file_count:
|
|
type: integer
|
|
created_at:
|
|
type: string
|
|
format: date-time
|
|
|
|
PoolCreate:
|
|
type: object
|
|
required: [name]
|
|
properties:
|
|
name:
|
|
type: string
|
|
notes:
|
|
type: string
|
|
metadata:
|
|
type: object
|
|
is_public:
|
|
type: boolean
|
|
default: false
|
|
|
|
PoolUpdate:
|
|
type: object
|
|
properties:
|
|
name:
|
|
type: string
|
|
notes:
|
|
type: string
|
|
metadata:
|
|
type: object
|
|
is_public:
|
|
type: boolean
|
|
|
|
PoolOffsetPage:
|
|
type: object
|
|
properties:
|
|
items:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Pool'
|
|
total:
|
|
type: integer
|
|
offset:
|
|
type: integer
|
|
limit:
|
|
type: integer
|
|
|
|
PoolFile:
|
|
type: object
|
|
allOf:
|
|
- $ref: '#/components/schemas/File'
|
|
- type: object
|
|
properties:
|
|
position:
|
|
type: integer
|
|
|
|
PoolFileCursorPage:
|
|
type: object
|
|
properties:
|
|
items:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/PoolFile'
|
|
next_cursor:
|
|
type: string
|
|
nullable: true
|
|
|
|
# --- ACL ---
|
|
Permission:
|
|
type: object
|
|
properties:
|
|
user_id:
|
|
type: integer
|
|
user_name:
|
|
type: string
|
|
can_view:
|
|
type: boolean
|
|
can_edit:
|
|
type: boolean
|
|
|
|
# --- User ---
|
|
User:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
name:
|
|
type: string
|
|
is_admin:
|
|
type: boolean
|
|
can_create:
|
|
type: boolean
|
|
is_blocked:
|
|
type: boolean
|
|
|
|
UserCreate:
|
|
type: object
|
|
required: [name, password]
|
|
properties:
|
|
name:
|
|
type: string
|
|
password:
|
|
type: string
|
|
is_admin:
|
|
type: boolean
|
|
default: false
|
|
can_create:
|
|
type: boolean
|
|
default: false
|
|
|
|
UserOffsetPage:
|
|
type: object
|
|
properties:
|
|
items:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/User'
|
|
total:
|
|
type: integer
|
|
offset:
|
|
type: integer
|
|
limit:
|
|
type: integer
|
|
|
|
# --- Audit ---
|
|
AuditLogEntry:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
format: int64
|
|
user_id:
|
|
type: integer
|
|
user_name:
|
|
type: string
|
|
action:
|
|
type: string
|
|
example: file_create
|
|
object_type:
|
|
type: string
|
|
nullable: true
|
|
object_id:
|
|
type: string
|
|
format: uuid
|
|
nullable: true
|
|
details:
|
|
type: object
|
|
nullable: true
|
|
performed_at:
|
|
type: string
|
|
format: date-time
|
|
|
|
AuditLogOffsetPage:
|
|
type: object
|
|
properties:
|
|
items:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/AuditLogEntry'
|
|
total:
|
|
type: integer
|
|
offset:
|
|
type: integer
|
|
limit:
|
|
type: integer
|