Compare commits
No commits in common. "7956a8a961ab3e5a5b82491425531411e25cc8db" and "bbe57e07d59ed06bb8cfdae815b570c99c3886ef" have entirely different histories.
7956a8a961
...
bbe57e07d5
120 changed files with 1551 additions and 12408 deletions
|
|
@ -18,10 +18,14 @@ jobs:
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '^1.25'
|
go-version: '^1.25'
|
||||||
|
check-latest: false
|
||||||
|
cache-dependency-path: |
|
||||||
|
modules/backend/go.sum
|
||||||
|
|
||||||
- name: Build backend
|
- name: Build Go app
|
||||||
run: |
|
run: |
|
||||||
cd modules/backend
|
cd modules/backend
|
||||||
|
go mod tidy
|
||||||
go build -o nyanimedb .
|
go build -o nyanimedb .
|
||||||
tar -czvf nyanimedb-backend.tar.gz nyanimedb
|
tar -czvf nyanimedb-backend.tar.gz nyanimedb
|
||||||
|
|
||||||
|
|
@ -31,18 +35,6 @@ jobs:
|
||||||
name: nyanimedb-backend.tar.gz
|
name: nyanimedb-backend.tar.gz
|
||||||
path: modules/backend/nyanimedb-backend.tar.gz
|
path: modules/backend/nyanimedb-backend.tar.gz
|
||||||
|
|
||||||
- name: Build auth
|
|
||||||
run: |
|
|
||||||
cd modules/auth
|
|
||||||
go build -o auth .
|
|
||||||
tar -czvf nyanimedb-auth.tar.gz auth
|
|
||||||
|
|
||||||
- name: Upload built auth to artifactory
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: nyanimedb-auth.tar.gz
|
|
||||||
path: modules/auth/nyanimedb-auth.tar.gz
|
|
||||||
|
|
||||||
# Build frontend
|
# Build frontend
|
||||||
- uses: actions/setup-node@v5
|
- uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
|
|
@ -84,14 +76,6 @@ jobs:
|
||||||
push: true
|
push: true
|
||||||
tags: meowgit.nekoea.red/nihonium/nyanimedb-backend:latest
|
tags: meowgit.nekoea.red/nihonium/nyanimedb-backend:latest
|
||||||
|
|
||||||
- name: Build and push auth image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: Dockerfiles/Dockerfile_auth
|
|
||||||
push: true
|
|
||||||
tags: meowgit.nekoea.red/nihonium/nyanimedb-auth:latest
|
|
||||||
|
|
||||||
- name: Build and push frontend image
|
- name: Build and push frontend image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
|
|
@ -101,7 +85,7 @@ jobs:
|
||||||
tags: meowgit.nekoea.red/nihonium/nyanimedb-frontend:latest
|
tags: meowgit.nekoea.red/nihonium/nyanimedb-frontend:latest
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: debian-test
|
runs-on: self-hosted
|
||||||
needs: build
|
needs: build
|
||||||
env:
|
env:
|
||||||
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
|
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
|
||||||
|
|
@ -111,12 +95,6 @@ jobs:
|
||||||
POSTGRES_VERSION: 18
|
POSTGRES_VERSION: 18
|
||||||
LOG_LEVEL: ${{ vars.LOG_LEVEL }}
|
LOG_LEVEL: ${{ vars.LOG_LEVEL }}
|
||||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||||
SERVICE_ADDRESS: ${{ vars.SERVICE_ADDRESS }}
|
|
||||||
RABBITMQ_URL: ${{ secrets.RABBITMQ_URL }}
|
|
||||||
JWT_PRIVATE_KEY: ${{ secrets.JWT_PRIVATE_KEY }}
|
|
||||||
RABBITMQ_DEFAULT_USER: ${{ secrets.RABBITMQ_USER }}
|
|
||||||
RABBITMQ_DEFAULT_PASS: ${{ secrets.RABBITMQ_PASSWORD }}
|
|
||||||
AUTH_ENABLED: ${{ vars.AUTH_ENABLED }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
package: oapi
|
|
||||||
generate:
|
|
||||||
strict-server: true
|
|
||||||
gin-server: true
|
|
||||||
models: true
|
|
||||||
output: api/api.gen.go
|
|
||||||
|
|
@ -1,844 +0,0 @@
|
||||||
openapi: 3.0.4
|
|
||||||
info:
|
|
||||||
title: 'Titles, Users, Reviews, Tags, and Media API'
|
|
||||||
version: 1.0.0
|
|
||||||
servers:
|
|
||||||
- url: /api/v1
|
|
||||||
paths:
|
|
||||||
/titles:
|
|
||||||
get:
|
|
||||||
summary: Get titles
|
|
||||||
parameters:
|
|
||||||
- $ref: '#/components/parameters/cursor'
|
|
||||||
- $ref: '#/components/parameters/title_sort'
|
|
||||||
- name: sort_forward
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
- name: ext_search
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
- name: word
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- name: status
|
|
||||||
in: query
|
|
||||||
description: List of title statuses to filter
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/TitleStatus'
|
|
||||||
explode: false
|
|
||||||
style: form
|
|
||||||
- name: rating
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: number
|
|
||||||
format: double
|
|
||||||
- name: release_year
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
- name: release_season
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/ReleaseSeason'
|
|
||||||
- name: limit
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
default: 10
|
|
||||||
- name: offset
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
default: 0
|
|
||||||
- name: fields
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
default: all
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: List of titles with cursor
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
data:
|
|
||||||
description: List of titles
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/Title'
|
|
||||||
cursor:
|
|
||||||
$ref: '#/components/schemas/CursorObj'
|
|
||||||
required:
|
|
||||||
- data
|
|
||||||
- cursor
|
|
||||||
'204':
|
|
||||||
description: No titles found
|
|
||||||
'400':
|
|
||||||
description: Request params are not correct
|
|
||||||
'500':
|
|
||||||
description: Unknown server error
|
|
||||||
'/titles/{title_id}':
|
|
||||||
get:
|
|
||||||
operationId: getTitle
|
|
||||||
summary: Get title description
|
|
||||||
parameters:
|
|
||||||
- name: title_id
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
- name: fields
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
default: all
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Title description
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Title'
|
|
||||||
'204':
|
|
||||||
description: No title found
|
|
||||||
'400':
|
|
||||||
description: Request params are not correct
|
|
||||||
'404':
|
|
||||||
description: Title not found
|
|
||||||
'500':
|
|
||||||
description: Unknown server error
|
|
||||||
security:
|
|
||||||
- JwtAuthCookies: []
|
|
||||||
/users/:
|
|
||||||
get:
|
|
||||||
summary: 'Search user by nickname or dispname (both in one param), response is always sorted by id'
|
|
||||||
parameters:
|
|
||||||
- name: word
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- name: limit
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
default: 10
|
|
||||||
- name: cursor_id
|
|
||||||
in: query
|
|
||||||
description: pass cursor naked
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
default: 1
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: List of users with cursor
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
data:
|
|
||||||
description: List of users
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/User'
|
|
||||||
cursor:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
default: 1
|
|
||||||
required:
|
|
||||||
- data
|
|
||||||
- cursor
|
|
||||||
'204':
|
|
||||||
description: No users found
|
|
||||||
'400':
|
|
||||||
description: Request params are not correct
|
|
||||||
'500':
|
|
||||||
description: Unknown server error
|
|
||||||
'/users/{user_id}':
|
|
||||||
get:
|
|
||||||
operationId: getUsersId
|
|
||||||
summary: Get user info
|
|
||||||
parameters:
|
|
||||||
- name: user_id
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- name: fields
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
default: all
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: User info
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/User'
|
|
||||||
'400':
|
|
||||||
description: Request params are not correct
|
|
||||||
'404':
|
|
||||||
description: User not found
|
|
||||||
'500':
|
|
||||||
description: Unknown server error
|
|
||||||
patch:
|
|
||||||
operationId: updateUser
|
|
||||||
summary: Partially update a user account
|
|
||||||
description: |
|
|
||||||
Update selected user profile fields (excluding password).
|
|
||||||
Password updates must be done via the dedicated auth-service (`/auth/`).
|
|
||||||
Fields not provided in the request body remain unchanged.
|
|
||||||
parameters:
|
|
||||||
- name: user_id
|
|
||||||
in: path
|
|
||||||
description: User ID (primary key)
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
example: 123
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
description: Only provided fields are updated. Omitted fields remain unchanged.
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
avatar_id:
|
|
||||||
description: ID of the user avatar (references `images.id`); set to `null` to remove avatar
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
example: 42
|
|
||||||
nullable: true
|
|
||||||
mail:
|
|
||||||
description: User email (must be unique and valid)
|
|
||||||
type: string
|
|
||||||
format: email
|
|
||||||
example: john.doe.updated@example.com
|
|
||||||
pattern: '^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\\.[a-zA-Z0-9_-]+$'
|
|
||||||
nickname:
|
|
||||||
description: 'Username (alphanumeric + `_` or `-`, 3–16 chars)'
|
|
||||||
type: string
|
|
||||||
example: john_doe_43
|
|
||||||
maxLength: 16
|
|
||||||
minLength: 3
|
|
||||||
pattern: '^[a-zA-Z0-9_-]{3,16}$'
|
|
||||||
disp_name:
|
|
||||||
description: Display name
|
|
||||||
type: string
|
|
||||||
example: John Smith
|
|
||||||
maxLength: 32
|
|
||||||
user_desc:
|
|
||||||
description: User description / bio
|
|
||||||
type: string
|
|
||||||
example: Just a curious developer.
|
|
||||||
maxLength: 512
|
|
||||||
additionalProperties: false
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: User updated successfully. Returns updated user representation (excluding sensitive fields).
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/User'
|
|
||||||
'400':
|
|
||||||
description: 'Invalid input (e.g., validation failed, nickname/email conflict, malformed JSON)'
|
|
||||||
'401':
|
|
||||||
description: Unauthorized — missing or invalid authentication token
|
|
||||||
'403':
|
|
||||||
description: 'Forbidden — user is not allowed to modify this resource (e.g., not own profile & no admin rights)'
|
|
||||||
'404':
|
|
||||||
description: User not found
|
|
||||||
'409':
|
|
||||||
description: 'Conflict — e.g., requested `nickname` or `mail` already taken by another user'
|
|
||||||
'422':
|
|
||||||
description: 'Unprocessable Entity — semantic errors not caught by schema (e.g., invalid `avatar_id`)'
|
|
||||||
'500':
|
|
||||||
description: Unknown server error
|
|
||||||
security:
|
|
||||||
- XsrfAuthHeader: []
|
|
||||||
'/users/{user_id}/titles':
|
|
||||||
get:
|
|
||||||
operationId: getUserTitles
|
|
||||||
summary: Get user titles
|
|
||||||
parameters:
|
|
||||||
- $ref: '#/components/parameters/cursor'
|
|
||||||
- $ref: '#/components/parameters/title_sort'
|
|
||||||
- name: user_id
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- name: sort_forward
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
- name: word
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- name: status
|
|
||||||
in: query
|
|
||||||
description: List of title statuses to filter
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/TitleStatus'
|
|
||||||
explode: false
|
|
||||||
style: form
|
|
||||||
- name: watch_status
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/UserTitleStatus'
|
|
||||||
explode: false
|
|
||||||
style: form
|
|
||||||
- name: rating
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: number
|
|
||||||
format: double
|
|
||||||
- name: my_rate
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
- name: release_year
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
- name: release_season
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/ReleaseSeason'
|
|
||||||
- name: limit
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
default: 10
|
|
||||||
- name: fields
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
default: all
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: List of user titles
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
data:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/UserTitle'
|
|
||||||
cursor:
|
|
||||||
$ref: '#/components/schemas/CursorObj'
|
|
||||||
required:
|
|
||||||
- data
|
|
||||||
- cursor
|
|
||||||
'204':
|
|
||||||
description: No titles found
|
|
||||||
'400':
|
|
||||||
description: Request params are not correct
|
|
||||||
'404':
|
|
||||||
description: User not found
|
|
||||||
'500':
|
|
||||||
description: Unknown server error
|
|
||||||
post:
|
|
||||||
operationId: addUserTitle
|
|
||||||
summary: Add a title to a user
|
|
||||||
description: 'User adding title to list af watched, status required'
|
|
||||||
parameters:
|
|
||||||
- name: user_id
|
|
||||||
in: path
|
|
||||||
description: ID of the user to assign the title to
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
example: 123
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
title_id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
status:
|
|
||||||
$ref: '#/components/schemas/UserTitleStatus'
|
|
||||||
rate:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
ftime:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
required:
|
|
||||||
- title_id
|
|
||||||
- status
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Title successfully added to user
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/UserTitleMini'
|
|
||||||
'400':
|
|
||||||
description: 'Invalid request body (missing fields, invalid types, etc.)'
|
|
||||||
'401':
|
|
||||||
description: Unauthorized — missing or invalid auth token
|
|
||||||
'403':
|
|
||||||
description: Forbidden — user not allowed to assign titles to this user
|
|
||||||
'404':
|
|
||||||
description: User or Title not found
|
|
||||||
'409':
|
|
||||||
description: Conflict — title already assigned to user (if applicable)
|
|
||||||
'500':
|
|
||||||
description: Internal server error
|
|
||||||
'/users/{user_id}/titles/{title_id}':
|
|
||||||
get:
|
|
||||||
operationId: getUserTitle
|
|
||||||
summary: Get user title
|
|
||||||
parameters:
|
|
||||||
- name: user_id
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
- name: title_id
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: User titles
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/UserTitleMini'
|
|
||||||
'204':
|
|
||||||
description: No user title found
|
|
||||||
'400':
|
|
||||||
description: Request params are not correct
|
|
||||||
'404':
|
|
||||||
description: User or title not found
|
|
||||||
'500':
|
|
||||||
description: Unknown server error
|
|
||||||
patch:
|
|
||||||
operationId: updateUserTitle
|
|
||||||
summary: Update a usertitle
|
|
||||||
description: User updating title list of watched
|
|
||||||
parameters:
|
|
||||||
- name: user_id
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
- name: title_id
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
status:
|
|
||||||
$ref: '#/components/schemas/UserTitleStatus'
|
|
||||||
rate:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
ftime:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Title successfully updated
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/UserTitleMini'
|
|
||||||
'400':
|
|
||||||
description: 'Invalid request body (missing fields, invalid types, etc.)'
|
|
||||||
'401':
|
|
||||||
description: Unauthorized — missing or invalid auth token
|
|
||||||
'403':
|
|
||||||
description: Forbidden — user not allowed to update title
|
|
||||||
'404':
|
|
||||||
description: User or Title not found
|
|
||||||
'500':
|
|
||||||
description: Internal server error
|
|
||||||
security:
|
|
||||||
- XsrfAuthHeader: []
|
|
||||||
delete:
|
|
||||||
operationId: deleteUserTitle
|
|
||||||
summary: Delete a usertitle
|
|
||||||
description: User deleting title from list of watched
|
|
||||||
parameters:
|
|
||||||
- name: user_id
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
- name: title_id
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Title successfully deleted
|
|
||||||
'401':
|
|
||||||
description: Unauthorized — missing or invalid auth token
|
|
||||||
'403':
|
|
||||||
description: Forbidden — user not allowed to delete title
|
|
||||||
'404':
|
|
||||||
description: User or Title not found
|
|
||||||
'500':
|
|
||||||
description: Internal server error
|
|
||||||
security:
|
|
||||||
- XsrfAuthHeader: []
|
|
||||||
/media/upload:
|
|
||||||
post:
|
|
||||||
summary: 'Upload an image (PNG, JPEG, or WebP)'
|
|
||||||
description: |
|
|
||||||
Uploads a single image file. Supported formats: **PNG**, **JPEG/JPG**, **WebP**.
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
encoding:
|
|
||||||
image:
|
|
||||||
contentType: 'image/png, image/jpeg, image/webp'
|
|
||||||
multipart/form-data:
|
|
||||||
schema:
|
|
||||||
image:
|
|
||||||
type: string
|
|
||||||
format: binary
|
|
||||||
description: 'Image file (PNG, JPEG, or WebP)'
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Image uploaded successfully
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Image'
|
|
||||||
'400':
|
|
||||||
description: 'Bad request — e.g., invalid/malformed image, empty file'
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
'415':
|
|
||||||
description: |
|
|
||||||
Unsupported Media Type — e.g., request `Content-Type` is not `multipart/form-data`,
|
|
||||||
or the `image` part has an unsupported `Content-Type` (not image/png, image/jpeg, or image/webp)
|
|
||||||
'500':
|
|
||||||
description: Internal server error
|
|
||||||
components:
|
|
||||||
parameters:
|
|
||||||
cursor:
|
|
||||||
in: query
|
|
||||||
name: cursor
|
|
||||||
required: false
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
title_sort:
|
|
||||||
in: query
|
|
||||||
name: sort
|
|
||||||
required: false
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/TitleSort'
|
|
||||||
schemas:
|
|
||||||
TitleSort:
|
|
||||||
description: Title sort order
|
|
||||||
type: string
|
|
||||||
default: id
|
|
||||||
enum:
|
|
||||||
- id
|
|
||||||
- year
|
|
||||||
- rating
|
|
||||||
- views
|
|
||||||
TitleStatus:
|
|
||||||
description: Title status
|
|
||||||
type: string
|
|
||||||
enum:
|
|
||||||
- finished
|
|
||||||
- ongoing
|
|
||||||
- planned
|
|
||||||
ReleaseSeason:
|
|
||||||
description: Title release season
|
|
||||||
type: string
|
|
||||||
enum:
|
|
||||||
- winter
|
|
||||||
- spring
|
|
||||||
- summer
|
|
||||||
- fall
|
|
||||||
StorageType:
|
|
||||||
description: Image storage type
|
|
||||||
type: string
|
|
||||||
enum:
|
|
||||||
- s3
|
|
||||||
- local
|
|
||||||
Image:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
storage_type:
|
|
||||||
$ref: '#/components/schemas/StorageType'
|
|
||||||
image_path:
|
|
||||||
type: string
|
|
||||||
Studio:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
poster:
|
|
||||||
$ref: '#/components/schemas/Image'
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- id
|
|
||||||
- name
|
|
||||||
Tag:
|
|
||||||
description: 'A localized tag: keys are language codes (ISO 639-1), values are tag names'
|
|
||||||
type: object
|
|
||||||
example:
|
|
||||||
en: Shojo
|
|
||||||
ru: Сёдзё
|
|
||||||
ja: 少女
|
|
||||||
additionalProperties:
|
|
||||||
type: string
|
|
||||||
Tags:
|
|
||||||
description: Array of localized tags
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/Tag'
|
|
||||||
example:
|
|
||||||
- en: Shojo
|
|
||||||
ru: Сёдзё
|
|
||||||
ja: 少女
|
|
||||||
- en: Shounen
|
|
||||||
ru: Сёнен
|
|
||||||
ja: 少年
|
|
||||||
Title:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
description: Unique title ID (primary key)
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
example: 1
|
|
||||||
title_names:
|
|
||||||
description: 'Localized titles. Key = language (ISO 639-1), value = list of names'
|
|
||||||
type: object
|
|
||||||
example:
|
|
||||||
en:
|
|
||||||
- Attack on Titan
|
|
||||||
- AoT
|
|
||||||
ru:
|
|
||||||
- Атака титанов
|
|
||||||
- Титаны
|
|
||||||
ja:
|
|
||||||
- 進撃の巨人
|
|
||||||
additionalProperties:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
example: Attack on Titan
|
|
||||||
minItems: 1
|
|
||||||
example:
|
|
||||||
- Attack on Titan
|
|
||||||
- AoT
|
|
||||||
title_desc:
|
|
||||||
description: 'Localized description. Key = language (ISO 639-1), value = description.'
|
|
||||||
type: object
|
|
||||||
additionalProperties:
|
|
||||||
type: string
|
|
||||||
studio:
|
|
||||||
$ref: '#/components/schemas/Studio'
|
|
||||||
tags:
|
|
||||||
$ref: '#/components/schemas/Tags'
|
|
||||||
poster:
|
|
||||||
$ref: '#/components/schemas/Image'
|
|
||||||
title_status:
|
|
||||||
$ref: '#/components/schemas/TitleStatus'
|
|
||||||
rating:
|
|
||||||
type: number
|
|
||||||
format: double
|
|
||||||
rating_count:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
release_year:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
release_season:
|
|
||||||
$ref: '#/components/schemas/ReleaseSeason'
|
|
||||||
episodes_aired:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
episodes_all:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
episodes_len:
|
|
||||||
type: object
|
|
||||||
additionalProperties:
|
|
||||||
type: number
|
|
||||||
format: double
|
|
||||||
required:
|
|
||||||
- id
|
|
||||||
- title_names
|
|
||||||
- tags
|
|
||||||
CursorObj:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
param:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- id
|
|
||||||
User:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
description: Unique user ID (primary key)
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
example: 1
|
|
||||||
image:
|
|
||||||
$ref: '#/components/schemas/Image'
|
|
||||||
mail:
|
|
||||||
description: User email
|
|
||||||
type: string
|
|
||||||
format: email
|
|
||||||
example: john.doe@example.com
|
|
||||||
nickname:
|
|
||||||
description: Username (alphanumeric + _ or -)
|
|
||||||
type: string
|
|
||||||
example: john_doe_42
|
|
||||||
maxLength: 16
|
|
||||||
disp_name:
|
|
||||||
description: Display name
|
|
||||||
type: string
|
|
||||||
example: John Doe
|
|
||||||
maxLength: 32
|
|
||||||
user_desc:
|
|
||||||
description: User description
|
|
||||||
type: string
|
|
||||||
example: Just a regular user.
|
|
||||||
maxLength: 512
|
|
||||||
creation_date:
|
|
||||||
description: Timestamp when the user was created
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
example: '2025-10-10T23:45:47.908073Z'
|
|
||||||
required:
|
|
||||||
- user_id
|
|
||||||
- nickname
|
|
||||||
UserTitleStatus:
|
|
||||||
description: User's title status
|
|
||||||
type: string
|
|
||||||
enum:
|
|
||||||
- finished
|
|
||||||
- planned
|
|
||||||
- dropped
|
|
||||||
- in-progress
|
|
||||||
UserTitle:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
user_id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
title:
|
|
||||||
$ref: '#/components/schemas/Title'
|
|
||||||
status:
|
|
||||||
$ref: '#/components/schemas/UserTitleStatus'
|
|
||||||
rate:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
review_id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
ctime:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
required:
|
|
||||||
- user_id
|
|
||||||
- title_id
|
|
||||||
- status
|
|
||||||
UserTitleMini:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
user_id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
title_id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
status:
|
|
||||||
$ref: '#/components/schemas/UserTitleStatus'
|
|
||||||
rate:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
review_id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
ctime:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
required:
|
|
||||||
- user_id
|
|
||||||
- title_id
|
|
||||||
- status
|
|
||||||
Review:
|
|
||||||
type: object
|
|
||||||
additionalProperties: true
|
|
||||||
securitySchemes:
|
|
||||||
XsrfAuthHeader:
|
|
||||||
type: apiKey
|
|
||||||
in: header
|
|
||||||
name: X-XSRF-TOKEN
|
|
||||||
description: |
|
|
||||||
Anti-CSRF token. Must match the `XSRF-TOKEN` cookie.
|
|
||||||
Required for all state-changing requests (POST/PUT/PATCH/DELETE).
|
|
||||||
1715
api/api.gen.go
1715
api/api.gen.go
File diff suppressed because it is too large
Load diff
603
api/openapi.yaml
603
api/openapi.yaml
|
|
@ -1,31 +1,592 @@
|
||||||
openapi: 3.0.4
|
openapi: 3.1.1
|
||||||
info:
|
info:
|
||||||
title: Titles, Users, Reviews, Tags, and Media API
|
title: Titles, Users, Reviews, Tags, and Media API
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|
||||||
servers:
|
servers:
|
||||||
- url: /api/v1
|
- url: /api/v1
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
/titles:
|
# /title:
|
||||||
$ref: "./paths/titles.yaml"
|
# get:
|
||||||
/titles/{title_id}:
|
# summary: Get titles
|
||||||
$ref: "./paths/titles-id.yaml"
|
# parameters:
|
||||||
/users/:
|
# - in: query
|
||||||
$ref: "./paths/users.yaml"
|
# name: query
|
||||||
|
# schema:
|
||||||
|
# type: string
|
||||||
|
# - in: query
|
||||||
|
# name: limit
|
||||||
|
# schema:
|
||||||
|
# type: integer
|
||||||
|
# default: 10
|
||||||
|
# - in: query
|
||||||
|
# name: offset
|
||||||
|
# schema:
|
||||||
|
# type: integer
|
||||||
|
# default: 0
|
||||||
|
# - in: query
|
||||||
|
# name: fields
|
||||||
|
# schema:
|
||||||
|
# type: string
|
||||||
|
# default: all
|
||||||
|
# responses:
|
||||||
|
# '200':
|
||||||
|
# description: List of titles
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# type: array
|
||||||
|
# items:
|
||||||
|
# $ref: '#/components/schemas/Title'
|
||||||
|
# '204':
|
||||||
|
# description: No titles found
|
||||||
|
|
||||||
|
# /title/{title_id}:
|
||||||
|
# get:
|
||||||
|
# summary: Get title description
|
||||||
|
# parameters:
|
||||||
|
# - in: path
|
||||||
|
# name: title_id
|
||||||
|
# required: true
|
||||||
|
# schema:
|
||||||
|
# type: string
|
||||||
|
# - in: query
|
||||||
|
# name: fields
|
||||||
|
# schema:
|
||||||
|
# type: string
|
||||||
|
# default: all
|
||||||
|
# responses:
|
||||||
|
# '200':
|
||||||
|
# description: Title description
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# $ref: '#/components/schemas/Title'
|
||||||
|
# '404':
|
||||||
|
# description: Title not found
|
||||||
|
|
||||||
|
# patch:
|
||||||
|
# summary: Update title info
|
||||||
|
# parameters:
|
||||||
|
# - in: path
|
||||||
|
# name: title_id
|
||||||
|
# required: true
|
||||||
|
# schema:
|
||||||
|
# type: string
|
||||||
|
# requestBody:
|
||||||
|
# required: true
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# $ref: '#/components/schemas/Title'
|
||||||
|
# responses:
|
||||||
|
# '200':
|
||||||
|
# description: Update result
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# type: object
|
||||||
|
# properties:
|
||||||
|
# success:
|
||||||
|
# type: boolean
|
||||||
|
# error:
|
||||||
|
# type: string
|
||||||
|
# user_json:
|
||||||
|
# $ref: '#/components/schemas/User'
|
||||||
|
|
||||||
|
# /title/{title_id}/reviews:
|
||||||
|
# get:
|
||||||
|
# summary: Get title reviews
|
||||||
|
# parameters:
|
||||||
|
# - in: path
|
||||||
|
# name: title_id
|
||||||
|
# required: true
|
||||||
|
# schema:
|
||||||
|
# type: string
|
||||||
|
# - in: query
|
||||||
|
# name: limit
|
||||||
|
# schema:
|
||||||
|
# type: integer
|
||||||
|
# default: 10
|
||||||
|
# - in: query
|
||||||
|
# name: offset
|
||||||
|
# schema:
|
||||||
|
# type: integer
|
||||||
|
# default: 0
|
||||||
|
# responses:
|
||||||
|
# '200':
|
||||||
|
# description: List of reviews
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# type: array
|
||||||
|
# items:
|
||||||
|
# $ref: '#/components/schemas/Review'
|
||||||
|
# '204':
|
||||||
|
# description: No reviews found
|
||||||
|
|
||||||
/users/{user_id}:
|
/users/{user_id}:
|
||||||
$ref: "./paths/users-id.yaml"
|
get:
|
||||||
/users/{user_id}/titles:
|
summary: Get user info
|
||||||
$ref: "./paths/users-id-titles.yaml"
|
parameters:
|
||||||
/users/{user_id}/titles/{title_id}:
|
- in: path
|
||||||
$ref: "./paths/users-id-titles-id.yaml"
|
name: user_id
|
||||||
/media/upload:
|
required: true
|
||||||
$ref: "./paths/media_upload.yaml"
|
schema:
|
||||||
|
type: string
|
||||||
|
- in: query
|
||||||
|
name: fields
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
default: all
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: User info
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/User'
|
||||||
|
'404':
|
||||||
|
description: User not found
|
||||||
|
|
||||||
|
# patch:
|
||||||
|
# summary: Update user
|
||||||
|
# parameters:
|
||||||
|
# - in: path
|
||||||
|
# name: user_id
|
||||||
|
# required: true
|
||||||
|
# schema:
|
||||||
|
# type: string
|
||||||
|
# requestBody:
|
||||||
|
# required: true
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# $ref: '#/components/schemas/User'
|
||||||
|
# responses:
|
||||||
|
# '200':
|
||||||
|
# description: Update result
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# type: object
|
||||||
|
# properties:
|
||||||
|
# success:
|
||||||
|
# type: boolean
|
||||||
|
# error:
|
||||||
|
# type: string
|
||||||
|
|
||||||
|
# delete:
|
||||||
|
# summary: Delete user
|
||||||
|
# parameters:
|
||||||
|
# - in: path
|
||||||
|
# name: user_id
|
||||||
|
# required: true
|
||||||
|
# schema:
|
||||||
|
# type: string
|
||||||
|
# responses:
|
||||||
|
# '200':
|
||||||
|
# description: Delete result
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# type: object
|
||||||
|
# properties:
|
||||||
|
# success:
|
||||||
|
# type: boolean
|
||||||
|
# error:
|
||||||
|
# type: string
|
||||||
|
|
||||||
|
# /users:
|
||||||
|
# get:
|
||||||
|
# summary: Search user
|
||||||
|
# parameters:
|
||||||
|
# - in: query
|
||||||
|
# name: query
|
||||||
|
# schema:
|
||||||
|
# type: string
|
||||||
|
# - in: query
|
||||||
|
# name: fields
|
||||||
|
# schema:
|
||||||
|
# type: string
|
||||||
|
# responses:
|
||||||
|
# '200':
|
||||||
|
# description: List of users
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# type: array
|
||||||
|
# items:
|
||||||
|
# $ref: '#/components/schemas/User'
|
||||||
|
|
||||||
|
# post:
|
||||||
|
# summary: Add new user
|
||||||
|
# requestBody:
|
||||||
|
# required: true
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# $ref: '#/components/schemas/User'
|
||||||
|
# responses:
|
||||||
|
# '200':
|
||||||
|
# description: Add result
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# type: object
|
||||||
|
# properties:
|
||||||
|
# success:
|
||||||
|
# type: boolean
|
||||||
|
# error:
|
||||||
|
# type: string
|
||||||
|
# user_json:
|
||||||
|
# $ref: '#/components/schemas/User'
|
||||||
|
|
||||||
|
# /users/{user_id}/titles:
|
||||||
|
# get:
|
||||||
|
# summary: Get user titles
|
||||||
|
# parameters:
|
||||||
|
# - in: path
|
||||||
|
# name: user_id
|
||||||
|
# required: true
|
||||||
|
# schema:
|
||||||
|
# type: string
|
||||||
|
# - in: query
|
||||||
|
# name: query
|
||||||
|
# schema:
|
||||||
|
# type: string
|
||||||
|
# - in: query
|
||||||
|
# name: limit
|
||||||
|
# schema:
|
||||||
|
# type: integer
|
||||||
|
# default: 10
|
||||||
|
# - in: query
|
||||||
|
# name: offset
|
||||||
|
# schema:
|
||||||
|
# type: integer
|
||||||
|
# default: 0
|
||||||
|
# - in: query
|
||||||
|
# name: fields
|
||||||
|
# schema:
|
||||||
|
# type: string
|
||||||
|
# default: all
|
||||||
|
# responses:
|
||||||
|
# '200':
|
||||||
|
# description: List of user titles
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# type: array
|
||||||
|
# items:
|
||||||
|
# $ref: '#/components/schemas/UserTitle'
|
||||||
|
# '204':
|
||||||
|
# description: No titles found
|
||||||
|
|
||||||
|
# post:
|
||||||
|
# summary: Add user title
|
||||||
|
# parameters:
|
||||||
|
# - in: path
|
||||||
|
# name: user_id
|
||||||
|
# required: true
|
||||||
|
# schema:
|
||||||
|
# type: string
|
||||||
|
# requestBody:
|
||||||
|
# required: true
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# type: object
|
||||||
|
# properties:
|
||||||
|
# title_id:
|
||||||
|
# type: string
|
||||||
|
# status:
|
||||||
|
# type: string
|
||||||
|
# responses:
|
||||||
|
# '200':
|
||||||
|
# description: Add result
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# type: object
|
||||||
|
# properties:
|
||||||
|
# success:
|
||||||
|
# type: boolean
|
||||||
|
# error:
|
||||||
|
# type: string
|
||||||
|
|
||||||
|
# patch:
|
||||||
|
# summary: Update user title
|
||||||
|
# parameters:
|
||||||
|
# - in: path
|
||||||
|
# name: user_id
|
||||||
|
# required: true
|
||||||
|
# schema:
|
||||||
|
# type: string
|
||||||
|
# requestBody:
|
||||||
|
# required: true
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# $ref: '#/components/schemas/UserTitle'
|
||||||
|
# responses:
|
||||||
|
# '200':
|
||||||
|
# description: Update result
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# type: object
|
||||||
|
# properties:
|
||||||
|
# success:
|
||||||
|
# type: boolean
|
||||||
|
# error:
|
||||||
|
# type: string
|
||||||
|
|
||||||
|
# delete:
|
||||||
|
# summary: Delete user title
|
||||||
|
# parameters:
|
||||||
|
# - in: path
|
||||||
|
# name: user_id
|
||||||
|
# required: true
|
||||||
|
# schema:
|
||||||
|
# type: string
|
||||||
|
# - in: query
|
||||||
|
# name: title_id
|
||||||
|
# schema:
|
||||||
|
# type: string
|
||||||
|
# responses:
|
||||||
|
# '200':
|
||||||
|
# description: Delete result
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# type: object
|
||||||
|
# properties:
|
||||||
|
# success:
|
||||||
|
# type: boolean
|
||||||
|
# error:
|
||||||
|
# type: string
|
||||||
|
|
||||||
|
# /users/{user_id}/reviews:
|
||||||
|
# get:
|
||||||
|
# summary: Get user reviews
|
||||||
|
# parameters:
|
||||||
|
# - in: path
|
||||||
|
# name: user_id
|
||||||
|
# required: true
|
||||||
|
# schema:
|
||||||
|
# type: string
|
||||||
|
# - in: query
|
||||||
|
# name: limit
|
||||||
|
# schema:
|
||||||
|
# type: integer
|
||||||
|
# default: 10
|
||||||
|
# - in: query
|
||||||
|
# name: offset
|
||||||
|
# schema:
|
||||||
|
# type: integer
|
||||||
|
# default: 0
|
||||||
|
# responses:
|
||||||
|
# '200':
|
||||||
|
# description: List of reviews
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# type: array
|
||||||
|
# items:
|
||||||
|
# $ref: '#/components/schemas/Review'
|
||||||
|
|
||||||
|
# /reviews:
|
||||||
|
# post:
|
||||||
|
# summary: Add review
|
||||||
|
# requestBody:
|
||||||
|
# required: true
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# $ref: '#/components/schemas/Review'
|
||||||
|
# responses:
|
||||||
|
# '200':
|
||||||
|
# description: Add result
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# type: object
|
||||||
|
# properties:
|
||||||
|
# success:
|
||||||
|
# type: boolean
|
||||||
|
# error:
|
||||||
|
# type: string
|
||||||
|
|
||||||
|
# /reviews/{review_id}:
|
||||||
|
# patch:
|
||||||
|
# summary: Update review
|
||||||
|
# parameters:
|
||||||
|
# - in: path
|
||||||
|
# name: review_id
|
||||||
|
# required: true
|
||||||
|
# schema:
|
||||||
|
# type: string
|
||||||
|
# requestBody:
|
||||||
|
# required: true
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# $ref: '#/components/schemas/Review'
|
||||||
|
# responses:
|
||||||
|
# '200':
|
||||||
|
# description: Update result
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# type: object
|
||||||
|
# properties:
|
||||||
|
# success:
|
||||||
|
# type: boolean
|
||||||
|
# error:
|
||||||
|
# type: string
|
||||||
|
# delete:
|
||||||
|
# summary: Delete review
|
||||||
|
# parameters:
|
||||||
|
# - in: path
|
||||||
|
# name: review_id
|
||||||
|
# required: true
|
||||||
|
# schema:
|
||||||
|
# type: string
|
||||||
|
# responses:
|
||||||
|
# '200':
|
||||||
|
# description: Delete result
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# type: object
|
||||||
|
# properties:
|
||||||
|
# success:
|
||||||
|
# type: boolean
|
||||||
|
# error:
|
||||||
|
# type: string
|
||||||
|
|
||||||
|
# /tags:
|
||||||
|
# get:
|
||||||
|
# summary: Get tags
|
||||||
|
# parameters:
|
||||||
|
# - in: query
|
||||||
|
# name: limit
|
||||||
|
# schema:
|
||||||
|
# type: integer
|
||||||
|
# default: 10
|
||||||
|
# - in: query
|
||||||
|
# name: offset
|
||||||
|
# schema:
|
||||||
|
# type: integer
|
||||||
|
# default: 0
|
||||||
|
# - in: query
|
||||||
|
# name: fields
|
||||||
|
# schema:
|
||||||
|
# type: string
|
||||||
|
# responses:
|
||||||
|
# '200':
|
||||||
|
# description: List of tags
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# type: array
|
||||||
|
# items:
|
||||||
|
# $ref: '#/components/schemas/Tag'
|
||||||
|
|
||||||
|
# /media:
|
||||||
|
# post:
|
||||||
|
# summary: Upload image
|
||||||
|
# responses:
|
||||||
|
# '200':
|
||||||
|
# description: Upload result
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# type: object
|
||||||
|
# properties:
|
||||||
|
# success:
|
||||||
|
# type: boolean
|
||||||
|
# error:
|
||||||
|
# type: string
|
||||||
|
# image_id:
|
||||||
|
# type: string
|
||||||
|
|
||||||
|
# get:
|
||||||
|
# summary: Get image path
|
||||||
|
# parameters:
|
||||||
|
# - in: query
|
||||||
|
# name: image_id
|
||||||
|
# required: true
|
||||||
|
# schema:
|
||||||
|
# type: string
|
||||||
|
# responses:
|
||||||
|
# '200':
|
||||||
|
# description: Image path
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# type: object
|
||||||
|
# properties:
|
||||||
|
# success:
|
||||||
|
# type: boolean
|
||||||
|
# error:
|
||||||
|
# type: string
|
||||||
|
# image_path:
|
||||||
|
# type: string
|
||||||
|
|
||||||
components:
|
components:
|
||||||
parameters:
|
|
||||||
$ref: "./parameters/_index.yaml"
|
|
||||||
schemas:
|
schemas:
|
||||||
$ref: "./schemas/_index.yaml"
|
Title:
|
||||||
securitySchemes:
|
type: object
|
||||||
$ref: "./securitySchemes/_index.yaml"
|
additionalProperties: true
|
||||||
|
User:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
description: Unique user ID (primary key)
|
||||||
|
example: 1
|
||||||
|
avatar_id:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
description: ID of the user avatar (references images table)
|
||||||
|
nullable: true
|
||||||
|
example: null
|
||||||
|
mail:
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
description: User email
|
||||||
|
example: "john.doe@example.com"
|
||||||
|
nickname:
|
||||||
|
type: string
|
||||||
|
description: Username (alphanumeric + _ or -)
|
||||||
|
maxLength: 16
|
||||||
|
example: "john_doe_42"
|
||||||
|
disp_name:
|
||||||
|
type: string
|
||||||
|
description: Display name
|
||||||
|
maxLength: 32
|
||||||
|
example: "John Doe"
|
||||||
|
user_desc:
|
||||||
|
type: string
|
||||||
|
description: User description
|
||||||
|
maxLength: 512
|
||||||
|
example: "Just a regular user."
|
||||||
|
creation_date:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: Timestamp when the user was created
|
||||||
|
example: "2025-10-10T23:45:47.908073Z"
|
||||||
|
required:
|
||||||
|
- user_id
|
||||||
|
- nickname
|
||||||
|
- creation_date
|
||||||
|
UserTitle:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
Review:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
Tag:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
cursor:
|
|
||||||
$ref: "./cursor.yaml"
|
|
||||||
title_sort:
|
|
||||||
$ref: "./title_sort.yaml"
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
in: query
|
|
||||||
name: cursor
|
|
||||||
required: false
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
in: query
|
|
||||||
name: sort
|
|
||||||
required: false
|
|
||||||
schema:
|
|
||||||
$ref: '../schemas/TitleSort.yaml'
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
post:
|
|
||||||
summary: Upload an image (PNG, JPEG, or WebP)
|
|
||||||
description: |
|
|
||||||
Uploads a single image file. Supported formats: **PNG**, **JPEG/JPG**, **WebP**.
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
multipart/form-data:
|
|
||||||
schema:
|
|
||||||
image:
|
|
||||||
type: string
|
|
||||||
format: binary
|
|
||||||
description: Image file (PNG, JPEG, or WebP)
|
|
||||||
encoding:
|
|
||||||
image:
|
|
||||||
contentType: image/png, image/jpeg, image/webp
|
|
||||||
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Image uploaded successfully
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: "../schemas/Image.yaml"
|
|
||||||
'400':
|
|
||||||
description: Bad request — e.g., invalid/malformed image, empty file
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
'415':
|
|
||||||
description: |
|
|
||||||
Unsupported Media Type — e.g., request `Content-Type` is not `multipart/form-data`,
|
|
||||||
or the `image` part has an unsupported `Content-Type` (not image/png, image/jpeg, or image/webp)
|
|
||||||
|
|
||||||
'500':
|
|
||||||
description: Internal server error
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
get:
|
|
||||||
summary: Get title description
|
|
||||||
security:
|
|
||||||
- JwtAuthCookies: []
|
|
||||||
operationId: getTitle
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: title_id
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
- in: query
|
|
||||||
name: fields
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
default: all
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Title description
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: "../schemas/Title.yaml"
|
|
||||||
'404':
|
|
||||||
description: Title not found
|
|
||||||
'400':
|
|
||||||
description: Request params are not correct
|
|
||||||
'500':
|
|
||||||
description: Unknown server error
|
|
||||||
'204':
|
|
||||||
description: No title found
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
get:
|
|
||||||
summary: Get titles
|
|
||||||
parameters:
|
|
||||||
- $ref: "../parameters/cursor.yaml"
|
|
||||||
- $ref: "../parameters/title_sort.yaml"
|
|
||||||
- in: query
|
|
||||||
name: sort_forward
|
|
||||||
schema:
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
- in: query
|
|
||||||
name: ext_search
|
|
||||||
schema:
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
- in: query
|
|
||||||
name: word
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- in: query
|
|
||||||
name: status
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '../schemas/enums/TitleStatus.yaml'
|
|
||||||
description: List of title statuses to filter
|
|
||||||
style: form
|
|
||||||
explode: false
|
|
||||||
- in: query
|
|
||||||
name: rating
|
|
||||||
schema:
|
|
||||||
type: number
|
|
||||||
format: double
|
|
||||||
- in: query
|
|
||||||
name: release_year
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
- in: query
|
|
||||||
name: release_season
|
|
||||||
schema:
|
|
||||||
$ref: '../schemas/enums/ReleaseSeason.yaml'
|
|
||||||
- in: query
|
|
||||||
name: limit
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
default: 10
|
|
||||||
- in: query
|
|
||||||
name: offset
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
default: 0
|
|
||||||
- in: query
|
|
||||||
name: fields
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
default: all
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: List of titles with cursor
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
data:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '../schemas/Title.yaml'
|
|
||||||
description: List of titles
|
|
||||||
cursor:
|
|
||||||
$ref: '../schemas/CursorObj.yaml'
|
|
||||||
required:
|
|
||||||
- data
|
|
||||||
- cursor
|
|
||||||
'204':
|
|
||||||
description: No titles found
|
|
||||||
'400':
|
|
||||||
description: Request params are not correct
|
|
||||||
'500':
|
|
||||||
description: Unknown server error
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
get:
|
|
||||||
summary: Get user title
|
|
||||||
operationId: getUserTitle
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: user_id
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
- in: path
|
|
||||||
name: title_id
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: User titles
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '../schemas/UserTitleMini.yaml'
|
|
||||||
'204':
|
|
||||||
description: No user title found
|
|
||||||
'400':
|
|
||||||
description: Request params are not correct
|
|
||||||
'404':
|
|
||||||
description: User or title not found
|
|
||||||
'500':
|
|
||||||
description: Unknown server error
|
|
||||||
|
|
||||||
patch:
|
|
||||||
summary: Update a usertitle
|
|
||||||
description: User updating title list of watched
|
|
||||||
operationId: updateUserTitle
|
|
||||||
security:
|
|
||||||
- XsrfAuthHeader: []
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: user_id
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
- in: path
|
|
||||||
name: title_id
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
status:
|
|
||||||
$ref: '../schemas/enums/UserTitleStatus.yaml'
|
|
||||||
rate:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
ftime:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Title successfully updated
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '../schemas/UserTitleMini.yaml'
|
|
||||||
'400':
|
|
||||||
description: Invalid request body (missing fields, invalid types, etc.)
|
|
||||||
'401':
|
|
||||||
description: Unauthorized — missing or invalid auth token
|
|
||||||
'403':
|
|
||||||
description: Forbidden — user not allowed to update title
|
|
||||||
'404':
|
|
||||||
description: User or Title not found
|
|
||||||
'500':
|
|
||||||
description: Internal server error
|
|
||||||
|
|
||||||
delete:
|
|
||||||
summary: Delete a usertitle
|
|
||||||
description: User deleting title from list of watched
|
|
||||||
operationId: deleteUserTitle
|
|
||||||
security:
|
|
||||||
- XsrfAuthHeader: []
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: user_id
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
- in: path
|
|
||||||
name: title_id
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Title successfully deleted
|
|
||||||
'401':
|
|
||||||
description: Unauthorized — missing or invalid auth token
|
|
||||||
'403':
|
|
||||||
description: Forbidden — user not allowed to delete title
|
|
||||||
'404':
|
|
||||||
description: User or Title not found
|
|
||||||
'500':
|
|
||||||
description: Internal server error
|
|
||||||
|
|
@ -1,146 +0,0 @@
|
||||||
get:
|
|
||||||
summary: Get user titles
|
|
||||||
operationId: getUserTitles
|
|
||||||
parameters:
|
|
||||||
- $ref: '../parameters/cursor.yaml'
|
|
||||||
- $ref: "../parameters/title_sort.yaml"
|
|
||||||
- in: path
|
|
||||||
name: user_id
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- in: query
|
|
||||||
name: sort_forward
|
|
||||||
schema:
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
- in: query
|
|
||||||
name: word
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- in: query
|
|
||||||
name: status
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '../schemas/enums/TitleStatus.yaml'
|
|
||||||
description: List of title statuses to filter
|
|
||||||
style: form
|
|
||||||
explode: false
|
|
||||||
- in: query
|
|
||||||
name: watch_status
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '../schemas/enums/UserTitleStatus.yaml'
|
|
||||||
style: form
|
|
||||||
explode: false
|
|
||||||
- in: query
|
|
||||||
name: rating
|
|
||||||
schema:
|
|
||||||
type: number
|
|
||||||
format: double
|
|
||||||
- in: query
|
|
||||||
name: my_rate
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
- in: query
|
|
||||||
name: release_year
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
- in: query
|
|
||||||
name: release_season
|
|
||||||
schema:
|
|
||||||
$ref: '../schemas/enums/ReleaseSeason.yaml'
|
|
||||||
- in: query
|
|
||||||
name: limit
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
default: 10
|
|
||||||
- in: query
|
|
||||||
name: fields
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
default: all
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: List of user titles
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
data:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '../schemas/UserTitle.yaml'
|
|
||||||
cursor:
|
|
||||||
$ref: '../schemas/CursorObj.yaml'
|
|
||||||
required:
|
|
||||||
- data
|
|
||||||
- cursor
|
|
||||||
'204':
|
|
||||||
description: No titles found
|
|
||||||
'400':
|
|
||||||
description: Request params are not correct
|
|
||||||
'404':
|
|
||||||
description: User not found
|
|
||||||
'500':
|
|
||||||
description: Unknown server error
|
|
||||||
|
|
||||||
post:
|
|
||||||
summary: Add a title to a user
|
|
||||||
description: User adding title to list af watched, status required
|
|
||||||
operationId: addUserTitle
|
|
||||||
parameters:
|
|
||||||
- name: user_id
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
description: ID of the user to assign the title to
|
|
||||||
example: 123
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- title_id
|
|
||||||
- status
|
|
||||||
properties:
|
|
||||||
title_id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
status:
|
|
||||||
$ref: '../schemas/enums/UserTitleStatus.yaml'
|
|
||||||
rate:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
ftime:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Title successfully added to user
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '../schemas/UserTitleMini.yaml'
|
|
||||||
'400':
|
|
||||||
description: Invalid request body (missing fields, invalid types, etc.)
|
|
||||||
'401':
|
|
||||||
description: Unauthorized — missing or invalid auth token
|
|
||||||
'403':
|
|
||||||
description: Forbidden — user not allowed to assign titles to this user
|
|
||||||
'404':
|
|
||||||
description: User or Title not found
|
|
||||||
'409':
|
|
||||||
description: Conflict — title already assigned to user (if applicable)
|
|
||||||
'500':
|
|
||||||
description: Internal server error
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
get:
|
|
||||||
summary: Get user info
|
|
||||||
operationId: getUsersId
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: user_id
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- in: query
|
|
||||||
name: fields
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
default: all
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: User info
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '../schemas/User.yaml'
|
|
||||||
'404':
|
|
||||||
description: User not found
|
|
||||||
'400':
|
|
||||||
description: Request params are not correct
|
|
||||||
'500':
|
|
||||||
description: Unknown server error
|
|
||||||
|
|
||||||
patch:
|
|
||||||
summary: Partially update a user account
|
|
||||||
description: |
|
|
||||||
Update selected user profile fields (excluding password).
|
|
||||||
Password updates must be done via the dedicated auth-service (`/auth/`).
|
|
||||||
Fields not provided in the request body remain unchanged.
|
|
||||||
operationId: updateUser
|
|
||||||
security:
|
|
||||||
- XsrfAuthHeader: []
|
|
||||||
parameters:
|
|
||||||
# - $ref: '../parameters/xsrf_token_header.yaml'
|
|
||||||
- name: user_id
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
description: User ID (primary key)
|
|
||||||
example: 123
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
avatar_id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
nullable: true
|
|
||||||
description: ID of the user avatar (references `images.id`); set to `null` to remove avatar
|
|
||||||
example: 42
|
|
||||||
mail:
|
|
||||||
type: string
|
|
||||||
format: email
|
|
||||||
pattern: '^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\\.[a-zA-Z0-9_-]+$'
|
|
||||||
description: User email (must be unique and valid)
|
|
||||||
example: john.doe.updated@example.com
|
|
||||||
nickname:
|
|
||||||
type: string
|
|
||||||
pattern: '^[a-zA-Z0-9_-]{3,16}$'
|
|
||||||
description: Username (alphanumeric + `_` or `-`, 3–16 chars)
|
|
||||||
maxLength: 16
|
|
||||||
minLength: 3
|
|
||||||
example: john_doe_43
|
|
||||||
disp_name:
|
|
||||||
type: string
|
|
||||||
description: Display name
|
|
||||||
maxLength: 32
|
|
||||||
example: John Smith
|
|
||||||
user_desc:
|
|
||||||
type: string
|
|
||||||
description: User description / bio
|
|
||||||
maxLength: 512
|
|
||||||
example: Just a curious developer.
|
|
||||||
additionalProperties: false
|
|
||||||
description: Only provided fields are updated. Omitted fields remain unchanged.
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: User updated successfully. Returns updated user representation (excluding sensitive fields).
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '../schemas/User.yaml'
|
|
||||||
'400':
|
|
||||||
description: Invalid input (e.g., validation failed, nickname/email conflict, malformed JSON)
|
|
||||||
'401':
|
|
||||||
description: Unauthorized — missing or invalid authentication token
|
|
||||||
'403':
|
|
||||||
description: Forbidden — user is not allowed to modify this resource (e.g., not own profile & no admin rights)
|
|
||||||
'404':
|
|
||||||
description: User not found
|
|
||||||
'409':
|
|
||||||
description: Conflict — e.g., requested `nickname` or `mail` already taken by another user
|
|
||||||
'422':
|
|
||||||
description: Unprocessable Entity — semantic errors not caught by schema (e.g., invalid `avatar_id`)
|
|
||||||
'500':
|
|
||||||
description: Unknown server error
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
get:
|
|
||||||
summary: Search user by nickname or dispname (both in one param), response is always sorted by id
|
|
||||||
parameters:
|
|
||||||
- in: query
|
|
||||||
name: word
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- in: query
|
|
||||||
name: limit
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
default: 10
|
|
||||||
- in: query
|
|
||||||
name: cursor_id
|
|
||||||
description: pass cursor naked
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
default: 1
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: List of users with cursor
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
data:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '../schemas/User.yaml'
|
|
||||||
description: List of users
|
|
||||||
cursor:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
default: 1
|
|
||||||
required:
|
|
||||||
- data
|
|
||||||
- cursor
|
|
||||||
'204':
|
|
||||||
description: No users found
|
|
||||||
'400':
|
|
||||||
description: Request params are not correct
|
|
||||||
'500':
|
|
||||||
description: Unknown server error
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- id
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
param:
|
|
||||||
type: string
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
# id выпиливаем
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
storage_type:
|
|
||||||
$ref: './enums/StorageType.yaml'
|
|
||||||
image_path:
|
|
||||||
type: string
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
# type: apiKey
|
|
||||||
# in: cookie
|
|
||||||
# name: access_token
|
|
||||||
# scheme: bearer
|
|
||||||
# bearerFormat: JWT
|
|
||||||
# description: |
|
|
||||||
# JWT access token sent in `Cookie: access_token=...`.
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
type: object
|
|
||||||
additionalProperties: true
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- id
|
|
||||||
- name
|
|
||||||
properties:
|
|
||||||
# id не нужен
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
poster:
|
|
||||||
$ref: ./Image.yaml
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
type: object
|
|
||||||
description: 'A localized tag: keys are language codes (ISO 639-1), values are tag names'
|
|
||||||
additionalProperties:
|
|
||||||
type: string
|
|
||||||
example:
|
|
||||||
en: Shojo
|
|
||||||
ru: Сёдзё
|
|
||||||
ja: 少女
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
type: array
|
|
||||||
description: Array of localized tags
|
|
||||||
items:
|
|
||||||
$ref: ./Tag.yaml
|
|
||||||
example:
|
|
||||||
- en: Shojo
|
|
||||||
ru: Сёдзё
|
|
||||||
ja: 少女
|
|
||||||
- en: Shounen
|
|
||||||
ru: Сёнен
|
|
||||||
ja: 少年
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- id
|
|
||||||
- title_names
|
|
||||||
- tags
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
description: Unique title ID (primary key)
|
|
||||||
example: 1
|
|
||||||
title_names:
|
|
||||||
type: object
|
|
||||||
description: Localized titles. Key = language (ISO 639-1), value = list of names
|
|
||||||
additionalProperties:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
example: Attack on Titan
|
|
||||||
minItems: 1
|
|
||||||
example:
|
|
||||||
- Attack on Titan
|
|
||||||
- AoT
|
|
||||||
example:
|
|
||||||
en:
|
|
||||||
- Attack on Titan
|
|
||||||
- AoT
|
|
||||||
ru:
|
|
||||||
- Атака титанов
|
|
||||||
- Титаны
|
|
||||||
ja:
|
|
||||||
- 進撃の巨人
|
|
||||||
title_desc:
|
|
||||||
type: object
|
|
||||||
description: Localized description. Key = language (ISO 639-1), value = description.
|
|
||||||
additionalProperties:
|
|
||||||
type: string
|
|
||||||
studio:
|
|
||||||
$ref: ./Studio.yaml
|
|
||||||
tags:
|
|
||||||
$ref: ./Tags.yaml
|
|
||||||
poster:
|
|
||||||
$ref: ./Image.yaml
|
|
||||||
title_status:
|
|
||||||
$ref: ./enums/TitleStatus.yaml
|
|
||||||
rating:
|
|
||||||
type: number
|
|
||||||
format: double
|
|
||||||
rating_count:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
release_year:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
release_season:
|
|
||||||
$ref: ./enums/ReleaseSeason.yaml
|
|
||||||
episodes_aired:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
episodes_all:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
episodes_len:
|
|
||||||
type: object
|
|
||||||
additionalProperties:
|
|
||||||
type: number
|
|
||||||
format: double
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
type: string
|
|
||||||
description: Title sort order
|
|
||||||
default: id
|
|
||||||
enum:
|
|
||||||
- id
|
|
||||||
- year
|
|
||||||
- rating
|
|
||||||
- views
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
description: Unique user ID (primary key)
|
|
||||||
example: 1
|
|
||||||
image:
|
|
||||||
$ref: '../schemas/Image.yaml'
|
|
||||||
mail:
|
|
||||||
type: string
|
|
||||||
format: email
|
|
||||||
description: User email
|
|
||||||
example: john.doe@example.com
|
|
||||||
nickname:
|
|
||||||
type: string
|
|
||||||
description: Username (alphanumeric + _ or -)
|
|
||||||
maxLength: 16
|
|
||||||
example: john_doe_42
|
|
||||||
disp_name:
|
|
||||||
type: string
|
|
||||||
description: Display name
|
|
||||||
maxLength: 32
|
|
||||||
example: John Doe
|
|
||||||
user_desc:
|
|
||||||
type: string
|
|
||||||
description: User description
|
|
||||||
maxLength: 512
|
|
||||||
example: Just a regular user.
|
|
||||||
creation_date:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
description: Timestamp when the user was created
|
|
||||||
example: '2025-10-10T23:45:47.908073Z'
|
|
||||||
required:
|
|
||||||
- user_id
|
|
||||||
- nickname
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- user_id
|
|
||||||
- title_id
|
|
||||||
- status
|
|
||||||
properties:
|
|
||||||
user_id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
title:
|
|
||||||
$ref: ./Title.yaml
|
|
||||||
status:
|
|
||||||
$ref: ./enums/UserTitleStatus.yaml
|
|
||||||
rate:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
review_id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
ctime:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- user_id
|
|
||||||
- title_id
|
|
||||||
- status
|
|
||||||
properties:
|
|
||||||
user_id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
title_id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
status:
|
|
||||||
$ref: ./enums/UserTitleStatus.yaml
|
|
||||||
rate:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
review_id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
ctime:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
CursorObj:
|
|
||||||
$ref: "./CursorObj.yaml"
|
|
||||||
TitleSort:
|
|
||||||
$ref: "./TitleSort.yaml"
|
|
||||||
Image:
|
|
||||||
$ref: "./Image.yaml"
|
|
||||||
TitleStatus:
|
|
||||||
$ref: "./enums/TitleStatus.yaml"
|
|
||||||
ReleaseSeason:
|
|
||||||
$ref: "./enums/ReleaseSeason.yaml"
|
|
||||||
UserTitleStatus:
|
|
||||||
$ref: "./enums/UserTitleStatus.yaml"
|
|
||||||
Review:
|
|
||||||
$ref: "./Review.yaml"
|
|
||||||
Tag:
|
|
||||||
$ref: "./Tag.yaml"
|
|
||||||
Tags:
|
|
||||||
$ref: "./Tags.yaml"
|
|
||||||
Studio:
|
|
||||||
$ref: "./Studio.yaml"
|
|
||||||
Title:
|
|
||||||
$ref: "./Title.yaml"
|
|
||||||
User:
|
|
||||||
$ref: "./User.yaml"
|
|
||||||
UserTitle:
|
|
||||||
$ref: "./UserTitle.yaml"
|
|
||||||
# JwtAuth:
|
|
||||||
# $ref: "./JWTAuth.yaml"
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
type: string
|
|
||||||
description: Title release season
|
|
||||||
enum:
|
|
||||||
- winter
|
|
||||||
- spring
|
|
||||||
- summer
|
|
||||||
- fall
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
type: string
|
|
||||||
description: Image storage type
|
|
||||||
enum:
|
|
||||||
- s3
|
|
||||||
- local
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
type: string
|
|
||||||
description: Title status
|
|
||||||
enum:
|
|
||||||
- finished
|
|
||||||
- ongoing
|
|
||||||
- planned
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
type: string
|
|
||||||
description: User's title status
|
|
||||||
enum:
|
|
||||||
- finished
|
|
||||||
- planned
|
|
||||||
- dropped
|
|
||||||
- in-progress
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
avatar_id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
nullable: true
|
|
||||||
description: ID of the user avatar (references `images.id`); set to `null` to remove avatar
|
|
||||||
example: 42
|
|
||||||
mail:
|
|
||||||
type: string
|
|
||||||
format: email
|
|
||||||
pattern: '^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\\.[a-zA-Z0-9_-]+$'
|
|
||||||
description: User email (must be unique and valid)
|
|
||||||
example: john.doe.updated@example.com
|
|
||||||
disp_name:
|
|
||||||
type: string
|
|
||||||
description: Display name
|
|
||||||
maxLength: 32
|
|
||||||
example: John Smith
|
|
||||||
user_desc:
|
|
||||||
type: string
|
|
||||||
description: User description / bio
|
|
||||||
maxLength: 512
|
|
||||||
example: Just a curious developer.
|
|
||||||
additionalProperties: false
|
|
||||||
description: Only provided fields are updated. Omitted fields remain unchanged.
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
# accessToken:
|
|
||||||
# $ref: "./access_token.yaml"
|
|
||||||
# csrfToken:
|
|
||||||
# $ref: "./xsrf_token_cookie.yaml"
|
|
||||||
XsrfAuthHeader:
|
|
||||||
type: apiKey
|
|
||||||
in: header
|
|
||||||
name: X-XSRF-TOKEN
|
|
||||||
description: |
|
|
||||||
Anti-CSRF token. Must match the `XSRF-TOKEN` cookie.
|
|
||||||
Required for all state-changing requests (POST/PUT/PATCH/DELETE).
|
|
||||||
220
auth/auth.gen.go
220
auth/auth.gen.go
|
|
@ -13,55 +13,44 @@ import (
|
||||||
strictgin "github.com/oapi-codegen/runtime/strictmiddleware/gin"
|
strictgin "github.com/oapi-codegen/runtime/strictmiddleware/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
// PostAuthSignInJSONBody defines parameters for PostAuthSignIn.
|
||||||
BearerAuthScopes = "bearerAuth.Scopes"
|
type PostAuthSignInJSONBody struct {
|
||||||
)
|
|
||||||
|
|
||||||
// GetImpersonationTokenJSONBody defines parameters for GetImpersonationToken.
|
|
||||||
type GetImpersonationTokenJSONBody struct {
|
|
||||||
ExternalId *int64 `json:"external_id,omitempty"`
|
|
||||||
UserId *int64 `json:"user_id,omitempty"`
|
|
||||||
union json.RawMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetImpersonationTokenJSONBody0 defines parameters for GetImpersonationToken.
|
|
||||||
type GetImpersonationTokenJSONBody0 = interface{}
|
|
||||||
|
|
||||||
// GetImpersonationTokenJSONBody1 defines parameters for GetImpersonationToken.
|
|
||||||
type GetImpersonationTokenJSONBody1 = interface{}
|
|
||||||
|
|
||||||
// PostSignInJSONBody defines parameters for PostSignIn.
|
|
||||||
type PostSignInJSONBody struct {
|
|
||||||
Nickname string `json:"nickname"`
|
Nickname string `json:"nickname"`
|
||||||
Pass string `json:"pass"`
|
Pass string `json:"pass"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostSignUpJSONBody defines parameters for PostSignUp.
|
// PostAuthSignUpJSONBody defines parameters for PostAuthSignUp.
|
||||||
type PostSignUpJSONBody struct {
|
type PostAuthSignUpJSONBody struct {
|
||||||
Nickname string `json:"nickname"`
|
Nickname string `json:"nickname"`
|
||||||
Pass string `json:"pass"`
|
Pass string `json:"pass"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetImpersonationTokenJSONRequestBody defines body for GetImpersonationToken for application/json ContentType.
|
// PostAuthVerifyTokenJSONBody defines parameters for PostAuthVerifyToken.
|
||||||
type GetImpersonationTokenJSONRequestBody GetImpersonationTokenJSONBody
|
type PostAuthVerifyTokenJSONBody struct {
|
||||||
|
// Token JWT token to validate
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
// PostSignInJSONRequestBody defines body for PostSignIn for application/json ContentType.
|
// PostAuthSignInJSONRequestBody defines body for PostAuthSignIn for application/json ContentType.
|
||||||
type PostSignInJSONRequestBody PostSignInJSONBody
|
type PostAuthSignInJSONRequestBody PostAuthSignInJSONBody
|
||||||
|
|
||||||
// PostSignUpJSONRequestBody defines body for PostSignUp for application/json ContentType.
|
// PostAuthSignUpJSONRequestBody defines body for PostAuthSignUp for application/json ContentType.
|
||||||
type PostSignUpJSONRequestBody PostSignUpJSONBody
|
type PostAuthSignUpJSONRequestBody PostAuthSignUpJSONBody
|
||||||
|
|
||||||
|
// PostAuthVerifyTokenJSONRequestBody defines body for PostAuthVerifyToken for application/json ContentType.
|
||||||
|
type PostAuthVerifyTokenJSONRequestBody PostAuthVerifyTokenJSONBody
|
||||||
|
|
||||||
// ServerInterface represents all server handlers.
|
// ServerInterface represents all server handlers.
|
||||||
type ServerInterface interface {
|
type ServerInterface interface {
|
||||||
// Get service impersontaion token
|
|
||||||
// (POST /get-impersonation-token)
|
|
||||||
GetImpersonationToken(c *gin.Context)
|
|
||||||
// Sign in a user and return JWT
|
// Sign in a user and return JWT
|
||||||
// (POST /sign-in)
|
// (POST /auth/sign-in)
|
||||||
PostSignIn(c *gin.Context)
|
PostAuthSignIn(c *gin.Context)
|
||||||
// Sign up a new user
|
// Sign up a new user
|
||||||
// (POST /sign-up)
|
// (POST /auth/sign-up)
|
||||||
PostSignUp(c *gin.Context)
|
PostAuthSignUp(c *gin.Context)
|
||||||
|
// Verify JWT validity
|
||||||
|
// (POST /auth/verify-token)
|
||||||
|
PostAuthVerifyToken(c *gin.Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerInterfaceWrapper converts contexts to parameters.
|
// ServerInterfaceWrapper converts contexts to parameters.
|
||||||
|
|
@ -73,10 +62,8 @@ type ServerInterfaceWrapper struct {
|
||||||
|
|
||||||
type MiddlewareFunc func(c *gin.Context)
|
type MiddlewareFunc func(c *gin.Context)
|
||||||
|
|
||||||
// GetImpersonationToken operation middleware
|
// PostAuthSignIn operation middleware
|
||||||
func (siw *ServerInterfaceWrapper) GetImpersonationToken(c *gin.Context) {
|
func (siw *ServerInterfaceWrapper) PostAuthSignIn(c *gin.Context) {
|
||||||
|
|
||||||
c.Set(BearerAuthScopes, []string{})
|
|
||||||
|
|
||||||
for _, middleware := range siw.HandlerMiddlewares {
|
for _, middleware := range siw.HandlerMiddlewares {
|
||||||
middleware(c)
|
middleware(c)
|
||||||
|
|
@ -85,11 +72,11 @@ func (siw *ServerInterfaceWrapper) GetImpersonationToken(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
siw.Handler.GetImpersonationToken(c)
|
siw.Handler.PostAuthSignIn(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostSignIn operation middleware
|
// PostAuthSignUp operation middleware
|
||||||
func (siw *ServerInterfaceWrapper) PostSignIn(c *gin.Context) {
|
func (siw *ServerInterfaceWrapper) PostAuthSignUp(c *gin.Context) {
|
||||||
|
|
||||||
for _, middleware := range siw.HandlerMiddlewares {
|
for _, middleware := range siw.HandlerMiddlewares {
|
||||||
middleware(c)
|
middleware(c)
|
||||||
|
|
@ -98,11 +85,11 @@ func (siw *ServerInterfaceWrapper) PostSignIn(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
siw.Handler.PostSignIn(c)
|
siw.Handler.PostAuthSignUp(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostSignUp operation middleware
|
// PostAuthVerifyToken operation middleware
|
||||||
func (siw *ServerInterfaceWrapper) PostSignUp(c *gin.Context) {
|
func (siw *ServerInterfaceWrapper) PostAuthVerifyToken(c *gin.Context) {
|
||||||
|
|
||||||
for _, middleware := range siw.HandlerMiddlewares {
|
for _, middleware := range siw.HandlerMiddlewares {
|
||||||
middleware(c)
|
middleware(c)
|
||||||
|
|
@ -111,7 +98,7 @@ func (siw *ServerInterfaceWrapper) PostSignUp(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
siw.Handler.PostSignUp(c)
|
siw.Handler.PostAuthVerifyToken(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GinServerOptions provides options for the Gin server.
|
// GinServerOptions provides options for the Gin server.
|
||||||
|
|
@ -141,81 +128,76 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options
|
||||||
ErrorHandler: errorHandler,
|
ErrorHandler: errorHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
router.POST(options.BaseURL+"/get-impersonation-token", wrapper.GetImpersonationToken)
|
router.POST(options.BaseURL+"/auth/sign-in", wrapper.PostAuthSignIn)
|
||||||
router.POST(options.BaseURL+"/sign-in", wrapper.PostSignIn)
|
router.POST(options.BaseURL+"/auth/sign-up", wrapper.PostAuthSignUp)
|
||||||
router.POST(options.BaseURL+"/sign-up", wrapper.PostSignUp)
|
router.POST(options.BaseURL+"/auth/verify-token", wrapper.PostAuthVerifyToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
type UnauthorizedErrorResponse struct {
|
type PostAuthSignInRequestObject struct {
|
||||||
|
Body *PostAuthSignInJSONRequestBody
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetImpersonationTokenRequestObject struct {
|
type PostAuthSignInResponseObject interface {
|
||||||
Body *GetImpersonationTokenJSONRequestBody
|
VisitPostAuthSignInResponse(w http.ResponseWriter) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetImpersonationTokenResponseObject interface {
|
type PostAuthSignIn200JSONResponse struct {
|
||||||
VisitGetImpersonationTokenResponse(w http.ResponseWriter) error
|
Error *string `json:"error"`
|
||||||
|
Success *bool `json:"success,omitempty"`
|
||||||
|
|
||||||
|
// Token JWT token to access protected endpoints
|
||||||
|
Token *string `json:"token"`
|
||||||
|
UserId *string `json:"user_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetImpersonationToken200JSONResponse struct {
|
func (response PostAuthSignIn200JSONResponse) VisitPostAuthSignInResponse(w http.ResponseWriter) error {
|
||||||
// AccessToken JWT access token
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (response GetImpersonationToken200JSONResponse) VisitGetImpersonationTokenResponse(w http.ResponseWriter) error {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
|
|
||||||
return json.NewEncoder(w).Encode(response)
|
return json.NewEncoder(w).Encode(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetImpersonationToken401Response = UnauthorizedErrorResponse
|
type PostAuthSignUpRequestObject struct {
|
||||||
|
Body *PostAuthSignUpJSONRequestBody
|
||||||
func (response GetImpersonationToken401Response) VisitGetImpersonationTokenResponse(w http.ResponseWriter) error {
|
|
||||||
w.WriteHeader(401)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostSignInRequestObject struct {
|
type PostAuthSignUpResponseObject interface {
|
||||||
Body *PostSignInJSONRequestBody
|
VisitPostAuthSignUpResponse(w http.ResponseWriter) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostSignInResponseObject interface {
|
type PostAuthSignUp200JSONResponse struct {
|
||||||
VisitPostSignInResponse(w http.ResponseWriter) error
|
Error *string `json:"error"`
|
||||||
|
Success *bool `json:"success,omitempty"`
|
||||||
|
UserId *string `json:"user_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostSignIn200JSONResponse struct {
|
func (response PostAuthSignUp200JSONResponse) VisitPostAuthSignUpResponse(w http.ResponseWriter) error {
|
||||||
UserId int64 `json:"user_id"`
|
|
||||||
UserName string `json:"user_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (response PostSignIn200JSONResponse) VisitPostSignInResponse(w http.ResponseWriter) error {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
|
|
||||||
return json.NewEncoder(w).Encode(response)
|
return json.NewEncoder(w).Encode(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostSignIn401Response = UnauthorizedErrorResponse
|
type PostAuthVerifyTokenRequestObject struct {
|
||||||
|
Body *PostAuthVerifyTokenJSONRequestBody
|
||||||
func (response PostSignIn401Response) VisitPostSignInResponse(w http.ResponseWriter) error {
|
|
||||||
w.WriteHeader(401)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostSignUpRequestObject struct {
|
type PostAuthVerifyTokenResponseObject interface {
|
||||||
Body *PostSignUpJSONRequestBody
|
VisitPostAuthVerifyTokenResponse(w http.ResponseWriter) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostSignUpResponseObject interface {
|
type PostAuthVerifyToken200JSONResponse struct {
|
||||||
VisitPostSignUpResponse(w http.ResponseWriter) error
|
// Error Error message if token is invalid
|
||||||
|
Error *string `json:"error"`
|
||||||
|
|
||||||
|
// UserId User ID extracted from token if valid
|
||||||
|
UserId *string `json:"user_id"`
|
||||||
|
|
||||||
|
// Valid True if token is valid
|
||||||
|
Valid *bool `json:"valid,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostSignUp200JSONResponse struct {
|
func (response PostAuthVerifyToken200JSONResponse) VisitPostAuthVerifyTokenResponse(w http.ResponseWriter) error {
|
||||||
UserId int64 `json:"user_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (response PostSignUp200JSONResponse) VisitPostSignUpResponse(w http.ResponseWriter) error {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
|
|
||||||
|
|
@ -224,15 +206,15 @@ func (response PostSignUp200JSONResponse) VisitPostSignUpResponse(w http.Respons
|
||||||
|
|
||||||
// StrictServerInterface represents all server handlers.
|
// StrictServerInterface represents all server handlers.
|
||||||
type StrictServerInterface interface {
|
type StrictServerInterface interface {
|
||||||
// Get service impersontaion token
|
|
||||||
// (POST /get-impersonation-token)
|
|
||||||
GetImpersonationToken(ctx context.Context, request GetImpersonationTokenRequestObject) (GetImpersonationTokenResponseObject, error)
|
|
||||||
// Sign in a user and return JWT
|
// Sign in a user and return JWT
|
||||||
// (POST /sign-in)
|
// (POST /auth/sign-in)
|
||||||
PostSignIn(ctx context.Context, request PostSignInRequestObject) (PostSignInResponseObject, error)
|
PostAuthSignIn(ctx context.Context, request PostAuthSignInRequestObject) (PostAuthSignInResponseObject, error)
|
||||||
// Sign up a new user
|
// Sign up a new user
|
||||||
// (POST /sign-up)
|
// (POST /auth/sign-up)
|
||||||
PostSignUp(ctx context.Context, request PostSignUpRequestObject) (PostSignUpResponseObject, error)
|
PostAuthSignUp(ctx context.Context, request PostAuthSignUpRequestObject) (PostAuthSignUpResponseObject, error)
|
||||||
|
// Verify JWT validity
|
||||||
|
// (POST /auth/verify-token)
|
||||||
|
PostAuthVerifyToken(ctx context.Context, request PostAuthVerifyTokenRequestObject) (PostAuthVerifyTokenResponseObject, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type StrictHandlerFunc = strictgin.StrictGinHandlerFunc
|
type StrictHandlerFunc = strictgin.StrictGinHandlerFunc
|
||||||
|
|
@ -247,11 +229,11 @@ type strictHandler struct {
|
||||||
middlewares []StrictMiddlewareFunc
|
middlewares []StrictMiddlewareFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetImpersonationToken operation middleware
|
// PostAuthSignIn operation middleware
|
||||||
func (sh *strictHandler) GetImpersonationToken(ctx *gin.Context) {
|
func (sh *strictHandler) PostAuthSignIn(ctx *gin.Context) {
|
||||||
var request GetImpersonationTokenRequestObject
|
var request PostAuthSignInRequestObject
|
||||||
|
|
||||||
var body GetImpersonationTokenJSONRequestBody
|
var body PostAuthSignInJSONRequestBody
|
||||||
if err := ctx.ShouldBindJSON(&body); err != nil {
|
if err := ctx.ShouldBindJSON(&body); err != nil {
|
||||||
ctx.Status(http.StatusBadRequest)
|
ctx.Status(http.StatusBadRequest)
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
|
|
@ -260,10 +242,10 @@ func (sh *strictHandler) GetImpersonationToken(ctx *gin.Context) {
|
||||||
request.Body = &body
|
request.Body = &body
|
||||||
|
|
||||||
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
|
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
|
||||||
return sh.ssi.GetImpersonationToken(ctx, request.(GetImpersonationTokenRequestObject))
|
return sh.ssi.PostAuthSignIn(ctx, request.(PostAuthSignInRequestObject))
|
||||||
}
|
}
|
||||||
for _, middleware := range sh.middlewares {
|
for _, middleware := range sh.middlewares {
|
||||||
handler = middleware(handler, "GetImpersonationToken")
|
handler = middleware(handler, "PostAuthSignIn")
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := handler(ctx, request)
|
response, err := handler(ctx, request)
|
||||||
|
|
@ -271,8 +253,8 @@ func (sh *strictHandler) GetImpersonationToken(ctx *gin.Context) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
ctx.Status(http.StatusInternalServerError)
|
ctx.Status(http.StatusInternalServerError)
|
||||||
} else if validResponse, ok := response.(GetImpersonationTokenResponseObject); ok {
|
} else if validResponse, ok := response.(PostAuthSignInResponseObject); ok {
|
||||||
if err := validResponse.VisitGetImpersonationTokenResponse(ctx.Writer); err != nil {
|
if err := validResponse.VisitPostAuthSignInResponse(ctx.Writer); err != nil {
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
}
|
}
|
||||||
} else if response != nil {
|
} else if response != nil {
|
||||||
|
|
@ -280,11 +262,11 @@ func (sh *strictHandler) GetImpersonationToken(ctx *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostSignIn operation middleware
|
// PostAuthSignUp operation middleware
|
||||||
func (sh *strictHandler) PostSignIn(ctx *gin.Context) {
|
func (sh *strictHandler) PostAuthSignUp(ctx *gin.Context) {
|
||||||
var request PostSignInRequestObject
|
var request PostAuthSignUpRequestObject
|
||||||
|
|
||||||
var body PostSignInJSONRequestBody
|
var body PostAuthSignUpJSONRequestBody
|
||||||
if err := ctx.ShouldBindJSON(&body); err != nil {
|
if err := ctx.ShouldBindJSON(&body); err != nil {
|
||||||
ctx.Status(http.StatusBadRequest)
|
ctx.Status(http.StatusBadRequest)
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
|
|
@ -293,10 +275,10 @@ func (sh *strictHandler) PostSignIn(ctx *gin.Context) {
|
||||||
request.Body = &body
|
request.Body = &body
|
||||||
|
|
||||||
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
|
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
|
||||||
return sh.ssi.PostSignIn(ctx, request.(PostSignInRequestObject))
|
return sh.ssi.PostAuthSignUp(ctx, request.(PostAuthSignUpRequestObject))
|
||||||
}
|
}
|
||||||
for _, middleware := range sh.middlewares {
|
for _, middleware := range sh.middlewares {
|
||||||
handler = middleware(handler, "PostSignIn")
|
handler = middleware(handler, "PostAuthSignUp")
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := handler(ctx, request)
|
response, err := handler(ctx, request)
|
||||||
|
|
@ -304,8 +286,8 @@ func (sh *strictHandler) PostSignIn(ctx *gin.Context) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
ctx.Status(http.StatusInternalServerError)
|
ctx.Status(http.StatusInternalServerError)
|
||||||
} else if validResponse, ok := response.(PostSignInResponseObject); ok {
|
} else if validResponse, ok := response.(PostAuthSignUpResponseObject); ok {
|
||||||
if err := validResponse.VisitPostSignInResponse(ctx.Writer); err != nil {
|
if err := validResponse.VisitPostAuthSignUpResponse(ctx.Writer); err != nil {
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
}
|
}
|
||||||
} else if response != nil {
|
} else if response != nil {
|
||||||
|
|
@ -313,11 +295,11 @@ func (sh *strictHandler) PostSignIn(ctx *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostSignUp operation middleware
|
// PostAuthVerifyToken operation middleware
|
||||||
func (sh *strictHandler) PostSignUp(ctx *gin.Context) {
|
func (sh *strictHandler) PostAuthVerifyToken(ctx *gin.Context) {
|
||||||
var request PostSignUpRequestObject
|
var request PostAuthVerifyTokenRequestObject
|
||||||
|
|
||||||
var body PostSignUpJSONRequestBody
|
var body PostAuthVerifyTokenJSONRequestBody
|
||||||
if err := ctx.ShouldBindJSON(&body); err != nil {
|
if err := ctx.ShouldBindJSON(&body); err != nil {
|
||||||
ctx.Status(http.StatusBadRequest)
|
ctx.Status(http.StatusBadRequest)
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
|
|
@ -326,10 +308,10 @@ func (sh *strictHandler) PostSignUp(ctx *gin.Context) {
|
||||||
request.Body = &body
|
request.Body = &body
|
||||||
|
|
||||||
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
|
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
|
||||||
return sh.ssi.PostSignUp(ctx, request.(PostSignUpRequestObject))
|
return sh.ssi.PostAuthVerifyToken(ctx, request.(PostAuthVerifyTokenRequestObject))
|
||||||
}
|
}
|
||||||
for _, middleware := range sh.middlewares {
|
for _, middleware := range sh.middlewares {
|
||||||
handler = middleware(handler, "PostSignUp")
|
handler = middleware(handler, "PostAuthVerifyToken")
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := handler(ctx, request)
|
response, err := handler(ctx, request)
|
||||||
|
|
@ -337,8 +319,8 @@ func (sh *strictHandler) PostSignUp(ctx *gin.Context) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
ctx.Status(http.StatusInternalServerError)
|
ctx.Status(http.StatusInternalServerError)
|
||||||
} else if validResponse, ok := response.(PostSignUpResponseObject); ok {
|
} else if validResponse, ok := response.(PostAuthVerifyTokenResponseObject); ok {
|
||||||
if err := validResponse.VisitPostSignUpResponse(ctx.Writer); err != nil {
|
if err := validResponse.VisitPostAuthVerifyTokenResponse(ctx.Writer); err != nil {
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
}
|
}
|
||||||
} else if response != nil {
|
} else if response != nil {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
openapi: 3.1.1
|
openapi: 3.1.0
|
||||||
info:
|
info:
|
||||||
title: Auth Service
|
title: Auth Service
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|
||||||
servers:
|
|
||||||
- url: /auth
|
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
/sign-up:
|
/auth/sign-up:
|
||||||
post:
|
post:
|
||||||
summary: Sign up a new user
|
summary: Sign up a new user
|
||||||
operationId: postSignUp
|
|
||||||
tags: [Auth]
|
tags: [Auth]
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
|
|
@ -31,18 +27,20 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
required:
|
|
||||||
- user_id
|
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
user_id:
|
user_id:
|
||||||
type: integer
|
type: string
|
||||||
format: int64
|
nullable: true
|
||||||
|
|
||||||
/sign-in:
|
/auth/sign-in:
|
||||||
post:
|
post:
|
||||||
summary: Sign in a user and return JWT
|
summary: Sign in a user and return JWT
|
||||||
operationId: postSignIn
|
|
||||||
tags: [Auth]
|
tags: [Auth]
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
|
|
@ -58,69 +56,57 @@ paths:
|
||||||
type: string
|
type: string
|
||||||
format: password
|
format: password
|
||||||
responses:
|
responses:
|
||||||
# This one also sets two cookies: access_token and refresh_token
|
|
||||||
"200":
|
"200":
|
||||||
description: Sign-in result with JWT
|
description: Sign-in result with JWT
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
required:
|
|
||||||
- user_id
|
|
||||||
- user_name
|
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
user_id:
|
success:
|
||||||
type: integer
|
type: boolean
|
||||||
format: int64
|
error:
|
||||||
user_name:
|
|
||||||
type: string
|
type: string
|
||||||
"401":
|
nullable: true
|
||||||
$ref: '#/components/responses/UnauthorizedError'
|
user_id:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
token:
|
||||||
|
type: string
|
||||||
|
description: JWT token to access protected endpoints
|
||||||
|
nullable: true
|
||||||
|
|
||||||
/get-impersonation-token:
|
/auth/verify-token:
|
||||||
post:
|
post:
|
||||||
summary: Get service impersontaion token
|
summary: Verify JWT validity
|
||||||
operationId: getImpersonationToken
|
|
||||||
tags: [Auth]
|
tags: [Auth]
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
|
required: [token]
|
||||||
properties:
|
properties:
|
||||||
user_id:
|
token:
|
||||||
type: integer
|
type: string
|
||||||
format: int64
|
description: JWT token to validate
|
||||||
external_id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
oneOf:
|
|
||||||
- required: ["user_id"]
|
|
||||||
- required: ["external_id"]
|
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Generated impersonation access token
|
description: Token validation result
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
required:
|
|
||||||
- access_token
|
|
||||||
properties:
|
properties:
|
||||||
access_token:
|
valid:
|
||||||
|
type: boolean
|
||||||
|
description: True if token is valid
|
||||||
|
user_id:
|
||||||
type: string
|
type: string
|
||||||
description: JWT access token
|
nullable: true
|
||||||
"401":
|
description: User ID extracted from token if valid
|
||||||
$ref: '#/components/responses/UnauthorizedError'
|
error:
|
||||||
|
type: string
|
||||||
components:
|
nullable: true
|
||||||
securitySchemes:
|
description: Error message if token is invalid
|
||||||
bearerAuth:
|
|
||||||
type: http
|
|
||||||
scheme: bearer
|
|
||||||
responses:
|
|
||||||
UnauthorizedError:
|
|
||||||
description: Access token is missing or invalid
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
cd ./api
|
|
||||||
openapi-format .\openapi.yaml --output .\_build\openapi.yaml --yaml
|
|
||||||
cd ..
|
|
||||||
oapi-codegen --config=api\oapi-codegen.yaml api\_build\openapi.yaml
|
|
||||||
|
|
@ -11,50 +11,20 @@ services:
|
||||||
- "${POSTGRES_PORT}:5432"
|
- "${POSTGRES_PORT}:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql
|
- postgres_data:/var/lib/postgresql
|
||||||
networks:
|
|
||||||
- nyanimedb-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
start_period: 10s
|
|
||||||
|
|
||||||
rabbitmq:
|
# pgadmin:
|
||||||
image: rabbitmq:3-management
|
# image: dpage/pgadmin4:${PGADMIN_VERSION}
|
||||||
container_name: rabbitmq
|
# container_name: pgadmin
|
||||||
ports:
|
# restart: always
|
||||||
- "5672:5672"
|
# environment:
|
||||||
- "15672:15672"
|
# PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL}
|
||||||
environment:
|
# PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD}
|
||||||
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER}
|
# ports:
|
||||||
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
|
# - "${PGADMIN_PORT}:80"
|
||||||
volumes:
|
# depends_on:
|
||||||
- rabbitmq_data:/var/lib/rabbitmq
|
# - postgres
|
||||||
networks:
|
# volumes:
|
||||||
- nyanimedb-network
|
# - pgadmin_data:/var/lib/pgadmin
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "rabbitmqctl", "status"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
start_period: 10s
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:8.4.0-alpine
|
|
||||||
container_name: redis
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
restart: always
|
|
||||||
command: ["redis-server", "--appendonly", "yes"]
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
start_period: 5s
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
|
|
||||||
nyanimedb-backend:
|
nyanimedb-backend:
|
||||||
image: meowgit.nekoea.red/nihonium/nyanimedb-backend:latest
|
image: meowgit.nekoea.red/nihonium/nyanimedb-backend:latest
|
||||||
|
|
@ -63,33 +33,10 @@ services:
|
||||||
environment:
|
environment:
|
||||||
LOG_LEVEL: ${LOG_LEVEL}
|
LOG_LEVEL: ${LOG_LEVEL}
|
||||||
DATABASE_URL: ${DATABASE_URL}
|
DATABASE_URL: ${DATABASE_URL}
|
||||||
SERVICE_ADDRESS: ${SERVICE_ADDRESS}
|
ports:
|
||||||
RABBITMQ_URL: ${RABBITMQ_URL}
|
- "8080:8080"
|
||||||
JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY}
|
|
||||||
AUTH_ENABLED: ${AUTH_ENABLED}
|
|
||||||
# ports:
|
|
||||||
# - "8080:8080"
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres
|
- postgres
|
||||||
- rabbitmq
|
|
||||||
networks:
|
|
||||||
- nyanimedb-network
|
|
||||||
|
|
||||||
nyanimedb-auth:
|
|
||||||
image: meowgit.nekoea.red/nihonium/nyanimedb-auth:latest
|
|
||||||
container_name: nyanimedb-auth
|
|
||||||
restart: always
|
|
||||||
environment:
|
|
||||||
LOG_LEVEL: ${LOG_LEVEL}
|
|
||||||
DATABASE_URL: ${DATABASE_URL}
|
|
||||||
SERVICE_ADDRESS: ${SERVICE_ADDRESS}
|
|
||||||
JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY}
|
|
||||||
# ports:
|
|
||||||
# - "8082:8082"
|
|
||||||
depends_on:
|
|
||||||
- postgres
|
|
||||||
networks:
|
|
||||||
- nyanimedb-network
|
|
||||||
|
|
||||||
nyanimedb-frontend:
|
nyanimedb-frontend:
|
||||||
image: meowgit.nekoea.red/nihonium/nyanimedb-frontend:latest
|
image: meowgit.nekoea.red/nihonium/nyanimedb-frontend:latest
|
||||||
|
|
@ -99,13 +46,7 @@ services:
|
||||||
- "8081:80"
|
- "8081:80"
|
||||||
depends_on:
|
depends_on:
|
||||||
- nyanimedb-backend
|
- nyanimedb-backend
|
||||||
networks:
|
|
||||||
- nyanimedb-network
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
rabbitmq_data:
|
pgadmin_data:
|
||||||
redis_data:
|
|
||||||
|
|
||||||
networks:
|
|
||||||
nyanimedb-network:
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
npx openapi-typescript-codegen --input ..\..\api\openapi.yaml --output ./src/api --client axios --useUnionTypes
|
npx openapi-typescript-codegen --input ..\..\api\openapi.yaml --output ./src/api --client axios
|
||||||
oapi-codegen --config=api/oapi-codegen.yaml .\api\openapi.yaml
|
oapi-codegen --config=api/oapi-codegen.yaml .\api\openapi.yaml
|
||||||
sqlc generate -f .\sql\sqlc.yaml
|
sqlc generate -f .\sql\sqlc.yaml
|
||||||
20
go.mod
20
go.mod
|
|
@ -3,14 +3,12 @@ module nyanimedb
|
||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alexedwards/argon2id v1.0.0
|
|
||||||
github.com/gin-contrib/cors v1.7.6
|
github.com/gin-contrib/cors v1.7.6
|
||||||
github.com/gin-gonic/gin v1.11.0
|
github.com/gin-gonic/gin v1.11.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
github.com/jackc/pgx/v5 v5.7.6
|
github.com/jackc/pgx/v5 v5.7.6
|
||||||
github.com/oapi-codegen/runtime v1.1.2
|
github.com/oapi-codegen/runtime v1.1.2
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4
|
github.com/pelletier/go-toml/v2 v2.2.4
|
||||||
github.com/sirupsen/logrus v1.9.3
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|
@ -18,7 +16,6 @@ require (
|
||||||
github.com/bytedance/sonic v1.14.0 // indirect
|
github.com/bytedance/sonic v1.14.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/disintegration/imaging v1.6.2 // indirect
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
|
@ -29,7 +26,6 @@ require (
|
||||||
github.com/google/uuid v1.5.0 // indirect
|
github.com/google/uuid v1.5.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
|
@ -38,18 +34,16 @@ require (
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||||
github.com/rabbitmq/amqp091-go v1.10.0 // indirect
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
go.uber.org/mock v0.5.0 // indirect
|
go.uber.org/mock v0.5.0 // indirect
|
||||||
golang.org/x/arch v0.20.0 // indirect
|
golang.org/x/arch v0.20.0 // indirect
|
||||||
golang.org/x/crypto v0.43.0 // indirect
|
golang.org/x/crypto v0.40.0 // indirect
|
||||||
golang.org/x/image v0.33.0 // indirect
|
golang.org/x/mod v0.25.0 // indirect
|
||||||
golang.org/x/mod v0.29.0 // indirect
|
golang.org/x/net v0.42.0 // indirect
|
||||||
golang.org/x/net v0.46.0 // indirect
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
golang.org/x/sync v0.18.0 // indirect
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
golang.org/x/sys v0.37.0 // indirect
|
golang.org/x/text v0.27.0 // indirect
|
||||||
golang.org/x/text v0.31.0 // indirect
|
golang.org/x/tools v0.34.0 // indirect
|
||||||
golang.org/x/tools v0.38.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.36.9 // indirect
|
google.golang.org/protobuf v1.36.9 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
67
go.sum
67
go.sum
|
|
@ -1,6 +1,4 @@
|
||||||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||||
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
|
||||||
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
|
|
||||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||||
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||||
|
|
@ -13,8 +11,6 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
|
||||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||||
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
|
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
|
||||||
|
|
@ -74,13 +70,7 @@ github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||||
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
|
||||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
|
||||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||||
github.com/streadway/amqp v1.1.0 h1:py12iX8XSyI7aN/3dUT8DFIDJazNJsVJdxNVEpnQTZM=
|
|
||||||
github.com/streadway/amqp v1.1.0/go.mod h1:WYSrTEYHOXHd0nwFeUXAe2G2hRnQT+deZJJf88uS9Bg=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
|
@ -95,82 +85,25 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
|
||||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
|
||||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
|
||||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
|
||||||
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
|
|
||||||
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
|
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
|
||||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
|
||||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
|
||||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
|
||||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
|
||||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
|
||||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|
||||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
|
||||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
|
||||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
|
||||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|
|
||||||
|
|
@ -2,28 +2,35 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
auth "nyanimedb/auth"
|
auth "nyanimedb/auth"
|
||||||
sqlc "nyanimedb/sql"
|
sqlc "nyanimedb/sql"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alexedwards/argon2id"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var secretKey = []byte("my_secret_key")
|
||||||
|
|
||||||
|
func generateToken(userID string) (string, error) {
|
||||||
|
claims := jwt.MapClaims{
|
||||||
|
"user_id": userID,
|
||||||
|
"exp": time.Now().Add(time.Hour * 24).Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
return token.SignedString(secretKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
var UserDb = make(map[string]string) //TEMP
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
db *sqlc.Queries
|
db *sqlc.Queries
|
||||||
JwtPrivateKey string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(db *sqlc.Queries, JwtPrivatekey string) Server {
|
func NewServer(db *sqlc.Queries) Server {
|
||||||
return Server{db: db, JwtPrivateKey: JwtPrivatekey}
|
return Server{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseInt64(s string) (int32, error) {
|
func parseInt64(s string) (int32, error) {
|
||||||
|
|
@ -31,249 +38,71 @@ func parseInt64(s string) (int32, error) {
|
||||||
return int32(i), err
|
return int32(i), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func HashPassword(password string) (string, error) {
|
func (s Server) PostAuthSignIn(ctx context.Context, req auth.PostAuthSignInRequestObject) (auth.PostAuthSignInResponseObject, error) {
|
||||||
params := &argon2id.Params{
|
err := ""
|
||||||
Memory: 64 * 1024,
|
success := true
|
||||||
Iterations: 3,
|
t, _ := generateToken(req.Body.Nickname)
|
||||||
Parallelism: 2,
|
|
||||||
SaltLength: 16,
|
|
||||||
KeyLength: 32,
|
|
||||||
}
|
|
||||||
|
|
||||||
return argon2id.CreateHash(password, params)
|
UserDb[req.Body.Nickname] = req.Body.Pass
|
||||||
}
|
|
||||||
|
|
||||||
func CheckPassword(password, hash string) (bool, error) {
|
return auth.PostAuthSignIn200JSONResponse{
|
||||||
return argon2id.ComparePasswordAndHash(password, hash)
|
Error: &err,
|
||||||
}
|
Success: &success,
|
||||||
|
UserId: &req.Body.Nickname,
|
||||||
func (s Server) generateImpersonationToken(userID string, impersonated_by string) (accessToken string, err error) {
|
Token: &t,
|
||||||
accessClaims := jwt.MapClaims{
|
|
||||||
"user_id": userID,
|
|
||||||
"exp": time.Now().Add(15 * time.Minute).Unix(),
|
|
||||||
"imp_id": impersonated_by,
|
|
||||||
}
|
|
||||||
|
|
||||||
at := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
|
|
||||||
|
|
||||||
accessToken, err = at.SignedString([]byte(s.JwtPrivateKey))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return accessToken, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Server) generateTokens(userID string) (accessToken string, refreshToken string, csrfToken string, err error) {
|
|
||||||
accessClaims := jwt.MapClaims{
|
|
||||||
"user_id": userID,
|
|
||||||
"exp": time.Now().Add(15 * time.Minute).Unix(),
|
|
||||||
//TODO: add created_at
|
|
||||||
}
|
|
||||||
at := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
|
|
||||||
accessToken, err = at.SignedString([]byte(s.JwtPrivateKey))
|
|
||||||
if err != nil {
|
|
||||||
return "", "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshClaims := jwt.MapClaims{
|
|
||||||
"user_id": userID,
|
|
||||||
"exp": time.Now().Add(7 * 24 * time.Hour).Unix(),
|
|
||||||
}
|
|
||||||
rt := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
|
|
||||||
refreshToken, err = rt.SignedString([]byte(s.JwtPrivateKey))
|
|
||||||
if err != nil {
|
|
||||||
return "", "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
csrfBytes := make([]byte, 32)
|
|
||||||
_, err = rand.Read(csrfBytes)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", "", err
|
|
||||||
}
|
|
||||||
csrfToken = base64.RawURLEncoding.EncodeToString(csrfBytes)
|
|
||||||
|
|
||||||
return accessToken, refreshToken, csrfToken, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Server) PostSignUp(ctx context.Context, req auth.PostSignUpRequestObject) (auth.PostSignUpResponseObject, error) {
|
|
||||||
passhash, err := HashPassword(req.Body.Pass)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to hash password: %v", err)
|
|
||||||
// TODO: return 500
|
|
||||||
}
|
|
||||||
|
|
||||||
user_id, err := s.db.CreateNewUser(context.Background(), sqlc.CreateNewUserParams{
|
|
||||||
Passhash: passhash,
|
|
||||||
Nickname: req.Body.Nickname,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to create user %s: %v", req.Body.Nickname, err)
|
|
||||||
// TODO: check err and retyrn 400/500
|
|
||||||
}
|
|
||||||
|
|
||||||
return auth.PostSignUp200JSONResponse{
|
|
||||||
UserId: user_id,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Server) PostSignIn(ctx context.Context, req auth.PostSignInRequestObject) (auth.PostSignInResponseObject, error) {
|
func (s Server) PostAuthSignUp(ctx context.Context, req auth.PostAuthSignUpRequestObject) (auth.PostAuthSignUpResponseObject, error) {
|
||||||
ginCtx, ok := ctx.Value(gin.ContextKey).(*gin.Context)
|
err := ""
|
||||||
if !ok {
|
success := true
|
||||||
log.Print("failed to get gin context")
|
UserDb[req.Body.Nickname] = req.Body.Pass
|
||||||
// TODO: change to 500
|
|
||||||
return auth.PostSignIn200JSONResponse{}, fmt.Errorf("failed to get gin.Context from context.Context")
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := s.db.GetUserByNickname(context.Background(), req.Body.Nickname)
|
return auth.PostAuthSignUp200JSONResponse{
|
||||||
if err != nil {
|
Error: &err,
|
||||||
log.Errorf("failed to get user by nickname %s: %v", req.Body.Nickname, err)
|
Success: &success,
|
||||||
// TODO: return 400/500
|
UserId: &req.Body.Nickname,
|
||||||
}
|
}, nil
|
||||||
|
|
||||||
ok, err = CheckPassword(req.Body.Pass, user.Passhash)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to check password for user %s: %v", req.Body.Nickname, err)
|
|
||||||
// TODO: return 500
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
return auth.PostSignIn401Response{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
accessToken, refreshToken, csrfToken, err := s.generateTokens(req.Body.Nickname)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to generate tokens for user %s: %v", req.Body.Nickname, err)
|
|
||||||
// TODO: return 500
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: check cookie settings carefully
|
|
||||||
ginCtx.SetSameSite(http.SameSiteStrictMode)
|
|
||||||
ginCtx.SetCookie("access_token", accessToken, 900, "/api", "", false, true)
|
|
||||||
ginCtx.SetCookie("refresh_token", refreshToken, 1209600, "/auth", "", false, true)
|
|
||||||
ginCtx.SetCookie("xsrf_token", csrfToken, 1209600, "/", "", false, false)
|
|
||||||
|
|
||||||
result := auth.PostSignIn200JSONResponse{
|
|
||||||
UserId: user.ID,
|
|
||||||
UserName: user.Nickname,
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Server) GetImpersonationToken(ctx context.Context, req auth.GetImpersonationTokenRequestObject) (auth.GetImpersonationTokenResponseObject, error) {
|
func (s Server) PostAuthVerifyToken(ctx context.Context, req auth.PostAuthVerifyTokenRequestObject) (auth.PostAuthVerifyTokenResponseObject, error) {
|
||||||
ginCtx, ok := ctx.Value(gin.ContextKey).(*gin.Context)
|
valid := false
|
||||||
if !ok {
|
var userID *string
|
||||||
log.Print("failed to get gin context")
|
var errStr *string
|
||||||
// TODO: change to 500
|
|
||||||
return auth.GetImpersonationToken200JSONResponse{}, fmt.Errorf("failed to get gin.Context from context.Context")
|
token, err := jwt.Parse(req.Body.Token, func(t *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected signing method")
|
||||||
}
|
}
|
||||||
|
return secretKey, nil
|
||||||
token, err := ExtractBearerToken(ginCtx.Request.Header.Get("Authorization"))
|
|
||||||
if err != nil {
|
|
||||||
// TODO: return 500
|
|
||||||
log.Errorf("failed to extract bearer token: %v", err)
|
|
||||||
return auth.GetImpersonationToken401Response{}, err
|
|
||||||
}
|
|
||||||
log.Printf("got auth token: %s", token)
|
|
||||||
|
|
||||||
ext_service, err := s.db.GetExternalServiceByToken(context.Background(), &token)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to get external service by token: %v", err)
|
|
||||||
return auth.GetImpersonationToken401Response{}, err
|
|
||||||
// TODO: check err and retyrn 400/500
|
|
||||||
}
|
|
||||||
|
|
||||||
var user_id string = ""
|
|
||||||
|
|
||||||
if req.Body.ExternalId != nil {
|
|
||||||
user, err := s.db.GetUserByExternalServiceId(context.Background(), sqlc.GetUserByExternalServiceIdParams{
|
|
||||||
ExternalID: fmt.Sprintf("%d", *req.Body.ExternalId),
|
|
||||||
ServiceID: ext_service.ID,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to get user by external user id: %v", err)
|
e := err.Error()
|
||||||
return auth.GetImpersonationToken401Response{}, err
|
errStr = &e
|
||||||
// TODO: check err and retyrn 400/500
|
return auth.PostAuthVerifyToken200JSONResponse{
|
||||||
|
Valid: &valid,
|
||||||
|
UserId: userID,
|
||||||
|
Error: errStr,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
user_id = fmt.Sprintf("%d", user.ID)
|
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||||
}
|
if uid, ok := claims["user_id"].(string); ok {
|
||||||
|
valid = true
|
||||||
if req.Body.UserId != nil {
|
userID = &uid
|
||||||
// TODO: check user existence
|
|
||||||
if user_id != "" && user_id != fmt.Sprintf("%d", *req.Body.UserId) {
|
|
||||||
log.Error("user_id and external_d are incorrect")
|
|
||||||
// TODO: 405
|
|
||||||
return auth.GetImpersonationToken401Response{}, nil
|
|
||||||
} else {
|
} else {
|
||||||
user_id = fmt.Sprintf("%d", *req.Body.UserId)
|
e := "user_id not found in token"
|
||||||
|
errStr = &e
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
e := "invalid token claims"
|
||||||
|
errStr = &e
|
||||||
}
|
}
|
||||||
|
|
||||||
accessToken, err := s.generateImpersonationToken(user_id, fmt.Sprintf("%d", ext_service.ID))
|
return auth.PostAuthVerifyToken200JSONResponse{
|
||||||
if err != nil {
|
Valid: &valid,
|
||||||
log.Errorf("failed to generate impersonation token: %v", err)
|
UserId: userID,
|
||||||
return auth.GetImpersonationToken401Response{}, err
|
Error: errStr,
|
||||||
// TODO: check err and retyrn 400/500
|
}, nil
|
||||||
}
|
|
||||||
|
|
||||||
return auth.GetImpersonationToken200JSONResponse{AccessToken: accessToken}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// func (s Server) PostAuthRefreshToken(ctx context.Context, req auth.PostAuthRefreshTokenRequestObject) (auth.PostAuthRefreshTokenResponseObject, error) {
|
|
||||||
// valid := false
|
|
||||||
// var userID *string
|
|
||||||
// var errStr *string
|
|
||||||
|
|
||||||
// token, err := jwt.Parse(req.Body.Token, func(t *jwt.Token) (interface{}, error) {
|
|
||||||
// if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
||||||
// return nil, fmt.Errorf("unexpected signing method")
|
|
||||||
// }
|
|
||||||
// return refreshSecret, nil
|
|
||||||
// })
|
|
||||||
|
|
||||||
// if err != nil {
|
|
||||||
// e := err.Error()
|
|
||||||
// errStr = &e
|
|
||||||
// return auth.PostAuthVerifyToken200JSONResponse{
|
|
||||||
// Valid: &valid,
|
|
||||||
// UserId: userID,
|
|
||||||
// Error: errStr,
|
|
||||||
// }, nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
|
||||||
// if uid, ok := claims["user_id"].(string); ok {
|
|
||||||
// // Refresh token is valid, generate new tokens
|
|
||||||
// newAccessToken, newRefreshToken, _ := generateTokens(uid)
|
|
||||||
// valid = true
|
|
||||||
// userID = &uid
|
|
||||||
// return auth.PostAuthVerifyToken200JSONResponse{
|
|
||||||
// Valid: &valid,
|
|
||||||
// UserId: userID,
|
|
||||||
// Error: nil,
|
|
||||||
// Token: &newAccessToken, // return new access token
|
|
||||||
// // optionally return newRefreshToken as well
|
|
||||||
// }, nil
|
|
||||||
// } else {
|
|
||||||
// e := "user_id not found in refresh token"
|
|
||||||
// errStr = &e
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// e := "invalid refresh token claims"
|
|
||||||
// errStr = &e
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return auth.PostAuthVerifyToken200JSONResponse{
|
|
||||||
// Valid: &valid,
|
|
||||||
// UserId: userID,
|
|
||||||
// Error: errStr,
|
|
||||||
// }, nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
func ExtractBearerToken(header string) (string, error) {
|
|
||||||
const prefix = "Bearer "
|
|
||||||
if len(header) <= len(prefix) || header[:len(prefix)] != prefix {
|
|
||||||
return "", fmt.Errorf("invalid bearer token format")
|
|
||||||
}
|
|
||||||
return header[len(prefix):], nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
)
|
|
||||||
|
|
||||||
func setField(obj interface{}, name string, value interface{}) error {
|
|
||||||
v := reflect.ValueOf(obj)
|
|
||||||
|
|
||||||
if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
|
|
||||||
return fmt.Errorf("expected pointer to a struct")
|
|
||||||
}
|
|
||||||
|
|
||||||
v = v.Elem()
|
|
||||||
field := v.FieldByName(name)
|
|
||||||
|
|
||||||
if !field.IsValid() {
|
|
||||||
return fmt.Errorf("no such field: %s", name)
|
|
||||||
}
|
|
||||||
if !field.CanSet() {
|
|
||||||
return fmt.Errorf("cannot set field: %s", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
val := reflect.ValueOf(value)
|
|
||||||
|
|
||||||
if field.Type() != val.Type() {
|
|
||||||
return fmt.Errorf("provided value type (%s) doesn't match field type (%s)", val.Type(), field.Type())
|
|
||||||
}
|
|
||||||
|
|
||||||
field.Set(val)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
auth "nyanimedb/auth"
|
auth "nyanimedb/auth"
|
||||||
|
|
@ -13,40 +9,19 @@ import (
|
||||||
|
|
||||||
"github.com/gin-contrib/cors"
|
"github.com/gin-contrib/cors"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
|
||||||
"github.com/pelletier/go-toml/v2"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var AppConfig Config
|
var AppConfig Config
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) != 2 {
|
|
||||||
AppConfig.Mode = "env"
|
|
||||||
} else {
|
|
||||||
AppConfig.Mode = "argv"
|
|
||||||
}
|
|
||||||
|
|
||||||
err := InitConfig()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to init config: %v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
pool, err := pgxpool.New(context.Background(), os.Getenv("DATABASE_URL"))
|
var queries *sqlc.Queries = nil
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
var queries *sqlc.Queries = sqlc.New(pool)
|
server := handlers.NewServer(queries)
|
||||||
|
|
||||||
server := handlers.NewServer(queries, AppConfig.JwtPrivateKey)
|
|
||||||
|
|
||||||
log.Info("allow origins:", AppConfig.ServiceAddress)
|
|
||||||
r.Use(cors.New(cors.Config{
|
r.Use(cors.New(cors.Config{
|
||||||
AllowOrigins: []string{"*"},
|
AllowOrigins: []string{"*"}, // allow all origins, change to specific domains in production
|
||||||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
|
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
|
||||||
AllowHeaders: []string{"Origin", "Content-Type", "Accept"},
|
AllowHeaders: []string{"Origin", "Content-Type", "Accept"},
|
||||||
ExposeHeaders: []string{"Content-Length"},
|
ExposeHeaders: []string{"Content-Length"},
|
||||||
|
|
@ -61,41 +36,3 @@ func main() {
|
||||||
|
|
||||||
r.Run(":8082")
|
r.Run(":8082")
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitConfig() error {
|
|
||||||
if AppConfig.Mode == "argv" {
|
|
||||||
content, err := os.ReadFile(os.Args[1])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
toml.Unmarshal(content, &AppConfig)
|
|
||||||
|
|
||||||
fmt.Printf("%+v\n", AppConfig)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
} else if AppConfig.Mode == "env" {
|
|
||||||
f := reflect.ValueOf(AppConfig)
|
|
||||||
|
|
||||||
for i := 0; i < f.NumField(); i++ {
|
|
||||||
field := f.Type().Field(i)
|
|
||||||
tag := field.Tag
|
|
||||||
env_var := tag.Get("env")
|
|
||||||
fmt.Printf("Field: %v.\nEnvironment variable: %v.\n", field.Name, env_var)
|
|
||||||
if env_var != "" {
|
|
||||||
env_value, exists := os.LookupEnv(env_var)
|
|
||||||
if !exists {
|
|
||||||
return fmt.Errorf("there is no env variable %s", env_var)
|
|
||||||
}
|
|
||||||
err := setField(&AppConfig, field.Name, env_value)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to set config field %s: %v", field.Name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("incorrect config mode")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
-- name: GetUserByNickname :one
|
|
||||||
SELECT *
|
|
||||||
FROM users
|
|
||||||
WHERE nickname = sqlc.arg('nickname');
|
|
||||||
|
|
||||||
-- name: CreateNewUser :one
|
|
||||||
INSERT
|
|
||||||
INTO users (passhash, nickname)
|
|
||||||
VALUES (sqlc.arg(passhash), sqlc.arg(nickname))
|
|
||||||
RETURNING id;
|
|
||||||
|
|
||||||
-- name: GetExternalServiceByToken :one
|
|
||||||
SELECT *
|
|
||||||
FROM external_services
|
|
||||||
WHERE auth_token = sqlc.arg('auth_token');
|
|
||||||
|
|
||||||
-- name: GetUserByExternalServiceId :one
|
|
||||||
SELECT u.*
|
|
||||||
FROM users u
|
|
||||||
LEFT JOIN external_ids ei ON eu.user_id = u.id
|
|
||||||
WHERE ei.external_id = sqlc.arg('external_id') AND ei.service_id = sqlc.arg('service_id');
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Mode string
|
JwtPrivateKey string
|
||||||
ServiceAddress string `toml:"ServiceAddress" env:"SERVICE_ADDRESS"`
|
|
||||||
DdUrl string `toml:"DbUrl" env:"DATABASE_URL"`
|
|
||||||
JwtPrivateKey string `toml:"JwtPrivateKey" env:"JWT_PRIVATE_KEY"`
|
|
||||||
LogLevel string `toml:"LogLevel" env:"LOG_LEVEL"`
|
LogLevel string `toml:"LogLevel" env:"LOG_LEVEL"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,173 +0,0 @@
|
||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
oapi "nyanimedb/api"
|
|
||||||
"nyanimedb/modules/backend/rmq"
|
|
||||||
sqlc "nyanimedb/sql"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// type Handler struct {
|
|
||||||
// publisher *rmq.Publisher
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func New(publisher *rmq.Publisher) *Handler {
|
|
||||||
// return &Handler{publisher: publisher}
|
|
||||||
// }
|
|
||||||
|
|
||||||
type Server struct {
|
|
||||||
db *sqlc.Queries
|
|
||||||
// publisher *rmq.Publisher
|
|
||||||
RPCclient *rmq.RPCClient
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewServer(db *sqlc.Queries, rpcclient *rmq.RPCClient) *Server {
|
|
||||||
return &Server{
|
|
||||||
db: db,
|
|
||||||
// publisher: publisher,
|
|
||||||
RPCclient: rpcclient,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sql2StorageType(s *sqlc.StorageTypeT) (*oapi.StorageType, error) {
|
|
||||||
if s == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
var t oapi.StorageType
|
|
||||||
switch *s {
|
|
||||||
case sqlc.StorageTypeTLocal:
|
|
||||||
t = oapi.Local
|
|
||||||
case sqlc.StorageTypeTS3:
|
|
||||||
t = oapi.S3
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unexpected storage type: %s", *s)
|
|
||||||
}
|
|
||||||
return &t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Server) mapTitle(title sqlc.GetTitleByIDRow) (oapi.Title, error) {
|
|
||||||
|
|
||||||
oapi_title := oapi.Title{
|
|
||||||
EpisodesAired: title.EpisodesAired,
|
|
||||||
EpisodesAll: title.EpisodesAll,
|
|
||||||
// EpisodesLen: &episodes_lens,
|
|
||||||
Id: title.ID,
|
|
||||||
// Poster: &oapi_image,
|
|
||||||
Rating: title.Rating,
|
|
||||||
RatingCount: title.RatingCount,
|
|
||||||
// ReleaseSeason: &release_season,
|
|
||||||
ReleaseYear: title.ReleaseYear,
|
|
||||||
// Studio: &oapi_studio,
|
|
||||||
// Tags: oapi_tag_names,
|
|
||||||
// TitleNames: title_names,
|
|
||||||
// TitleStatus: oapi_status,
|
|
||||||
// AdditionalProperties:
|
|
||||||
}
|
|
||||||
|
|
||||||
title_names := make(map[string][]string, 0)
|
|
||||||
err := json.Unmarshal(title.TitleNames, &title_names)
|
|
||||||
if err != nil {
|
|
||||||
return oapi.Title{}, fmt.Errorf("unmarshal TitleNames: %v", err)
|
|
||||||
}
|
|
||||||
oapi_title.TitleNames = title_names
|
|
||||||
|
|
||||||
if len(title.TitleDesc) > 0 {
|
|
||||||
title_descs := make(map[string]string, 0)
|
|
||||||
err = json.Unmarshal(title.TitleDesc, &title_descs)
|
|
||||||
if err != nil {
|
|
||||||
return oapi.Title{}, fmt.Errorf("unmarshal TitleDesc: %v", err)
|
|
||||||
}
|
|
||||||
oapi_title.TitleDesc = &title_descs
|
|
||||||
}
|
|
||||||
if len(title.EpisodesLen) > 0 {
|
|
||||||
episodes_lens := make(map[string]float64, 0)
|
|
||||||
err = json.Unmarshal(title.EpisodesLen, &episodes_lens)
|
|
||||||
if err != nil {
|
|
||||||
return oapi.Title{}, fmt.Errorf("unmarshal EpisodesLen: %v", err)
|
|
||||||
}
|
|
||||||
oapi_title.EpisodesLen = &episodes_lens
|
|
||||||
}
|
|
||||||
|
|
||||||
oapi_tag_names := make(oapi.Tags, 0)
|
|
||||||
err = json.Unmarshal(title.TagNames, &oapi_tag_names)
|
|
||||||
if err != nil {
|
|
||||||
return oapi.Title{}, fmt.Errorf("unmarshalling title_tag: %v", err)
|
|
||||||
}
|
|
||||||
oapi_title.Tags = oapi_tag_names
|
|
||||||
|
|
||||||
var oapi_studio oapi.Studio
|
|
||||||
if title.StudioName != nil {
|
|
||||||
oapi_studio.Name = *title.StudioName
|
|
||||||
}
|
|
||||||
if title.StudioID != 0 {
|
|
||||||
oapi_studio.Id = title.StudioID
|
|
||||||
oapi_studio.Description = title.StudioDesc
|
|
||||||
if title.StudioIllustID != nil {
|
|
||||||
oapi_studio.Poster = &oapi.Image{}
|
|
||||||
oapi_studio.Poster.Id = title.StudioIllustID
|
|
||||||
oapi_studio.Poster.ImagePath = title.StudioImagePath
|
|
||||||
|
|
||||||
s, err := sql2StorageType(title.StudioStorageType)
|
|
||||||
if err != nil {
|
|
||||||
return oapi.Title{}, fmt.Errorf("mapTitle, studio storage type: %v", err)
|
|
||||||
}
|
|
||||||
oapi_studio.Poster.StorageType = s
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
oapi_title.Studio = &oapi_studio
|
|
||||||
|
|
||||||
var oapi_image oapi.Image
|
|
||||||
|
|
||||||
if title.PosterID != nil {
|
|
||||||
oapi_image.Id = title.PosterID
|
|
||||||
oapi_image.ImagePath = title.TitleImagePath
|
|
||||||
s, err := sql2StorageType(title.TitleStorageType)
|
|
||||||
if err != nil {
|
|
||||||
return oapi.Title{}, fmt.Errorf("mapTitle, title starage type: %v", err)
|
|
||||||
}
|
|
||||||
oapi_image.StorageType = s
|
|
||||||
}
|
|
||||||
oapi_title.Poster = &oapi_image
|
|
||||||
|
|
||||||
var release_season oapi.ReleaseSeason
|
|
||||||
if title.ReleaseSeason != nil {
|
|
||||||
release_season = oapi.ReleaseSeason(*title.ReleaseSeason)
|
|
||||||
}
|
|
||||||
oapi_title.ReleaseSeason = &release_season
|
|
||||||
|
|
||||||
oapi_status, err := TitleStatus2oapi(&title.TitleStatus)
|
|
||||||
if err != nil {
|
|
||||||
return oapi.Title{}, fmt.Errorf("TitleStatus2oapi: %v", err)
|
|
||||||
}
|
|
||||||
oapi_title.TitleStatus = oapi_status
|
|
||||||
|
|
||||||
return oapi_title, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseInt64(s string) (int64, error) {
|
|
||||||
i, err := strconv.ParseInt(s, 10, 64)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func TitleStatus2Sqlc(s *[]oapi.TitleStatus) ([]sqlc.TitleStatusT, error) {
|
|
||||||
var sqlc_status []sqlc.TitleStatusT
|
|
||||||
if s == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
for _, t := range *s {
|
|
||||||
switch t {
|
|
||||||
case oapi.TitleStatusFinished:
|
|
||||||
sqlc_status = append(sqlc_status, sqlc.TitleStatusTFinished)
|
|
||||||
case oapi.TitleStatusOngoing:
|
|
||||||
sqlc_status = append(sqlc_status, sqlc.TitleStatusTOngoing)
|
|
||||||
case oapi.TitleStatusPlanned:
|
|
||||||
sqlc_status = append(sqlc_status, sqlc.TitleStatusTPlanned)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unexpected tittle status: %s", t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sqlc_status, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ParseCursorInto parses an opaque base64 cursor and injects values into target struct.
|
|
||||||
//
|
|
||||||
// Supported sort types:
|
|
||||||
// - "id" → sets CursorID (must be *int64)
|
|
||||||
// - "year" → sets CursorID (*int64) + CursorYear (*int32)
|
|
||||||
// - "rating" → sets CursorID (*int64) + CursorRating (*float64)
|
|
||||||
//
|
|
||||||
// Target struct may have any subset of these fields (e.g. only CursorID).
|
|
||||||
// Unknown fields are ignored. Missing fields → values are dropped (safe).
|
|
||||||
//
|
|
||||||
// Returns error if cursor is invalid or inconsistent with sort_by.
|
|
||||||
func ParseCursorInto(sortBy, cursorStr string, target any) error {
|
|
||||||
if cursorStr == "" {
|
|
||||||
return nil // no cursor → nothing to do
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Decode cursor
|
|
||||||
payload, err := decodeCursor(cursorStr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Extract ID (required for all types)
|
|
||||||
id, err := extractInt64(payload, "id")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cursor: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Get reflect value of target (must be ptr to struct)
|
|
||||||
v := reflect.ValueOf(target)
|
|
||||||
if v.Kind() != reflect.Pointer || v.IsNil() {
|
|
||||||
return fmt.Errorf("target must be non-nil pointer to struct")
|
|
||||||
}
|
|
||||||
v = v.Elem()
|
|
||||||
if v.Kind() != reflect.Struct {
|
|
||||||
return fmt.Errorf("target must be pointer to struct")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Helper: set field if exists and compatible
|
|
||||||
setField := func(fieldName string, value any) {
|
|
||||||
f := v.FieldByName(fieldName)
|
|
||||||
if !f.IsValid() || !f.CanSet() {
|
|
||||||
return // field not found or unexported
|
|
||||||
}
|
|
||||||
ft := f.Type()
|
|
||||||
vv := reflect.ValueOf(value)
|
|
||||||
|
|
||||||
// Case: field is *T, value is T → wrap in pointer
|
|
||||||
if ft.Kind() == reflect.Pointer {
|
|
||||||
elemType := ft.Elem()
|
|
||||||
if vv.Type().AssignableTo(elemType) {
|
|
||||||
ptr := reflect.New(elemType)
|
|
||||||
ptr.Elem().Set(vv)
|
|
||||||
f.Set(ptr)
|
|
||||||
}
|
|
||||||
// nil → leave as zero (nil pointer)
|
|
||||||
} else if vv.Type().AssignableTo(ft) {
|
|
||||||
f.Set(vv)
|
|
||||||
}
|
|
||||||
// else: type mismatch → silently skip (safe)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Dispatch by sort type
|
|
||||||
switch sortBy {
|
|
||||||
case "id":
|
|
||||||
setField("CursorID", id)
|
|
||||||
|
|
||||||
case "year":
|
|
||||||
setField("CursorID", id)
|
|
||||||
param, err := extractString(payload, "param")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cursor year: %w", err)
|
|
||||||
}
|
|
||||||
year, err := strconv.Atoi(param)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cursor year: param must be integer, got %q", param)
|
|
||||||
}
|
|
||||||
setField("CursorYear", int32(year)) // or int, depending on your schema
|
|
||||||
|
|
||||||
case "rating":
|
|
||||||
setField("CursorID", id)
|
|
||||||
param, err := extractString(payload, "param")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cursor rating: %w", err)
|
|
||||||
}
|
|
||||||
rating, err := strconv.ParseFloat(param, 64)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cursor rating: param must be float, got %q", param)
|
|
||||||
}
|
|
||||||
setField("CursorRating", rating)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported sort_by: %q", sortBy)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- helpers ---
|
|
||||||
func decodeCursor(cursorStr string) (map[string]any, error) {
|
|
||||||
data, err := base64.RawURLEncoding.DecodeString(cursorStr)
|
|
||||||
if err != nil {
|
|
||||||
data, err = base64.StdEncoding.DecodeString(cursorStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid base64 cursor")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var m map[string]any
|
|
||||||
if err := json.Unmarshal(data, &m); err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid cursor JSON: %w", err)
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractInt64(m map[string]any, key string) (int64, error) {
|
|
||||||
v, ok := m[key]
|
|
||||||
if !ok {
|
|
||||||
return 0, fmt.Errorf("missing %q", key)
|
|
||||||
}
|
|
||||||
switch x := v.(type) {
|
|
||||||
case float64:
|
|
||||||
if x == float64(int64(x)) {
|
|
||||||
return int64(x), nil
|
|
||||||
}
|
|
||||||
case string:
|
|
||||||
i, err := strconv.ParseInt(x, 10, 64)
|
|
||||||
if err == nil {
|
|
||||||
return i, nil
|
|
||||||
}
|
|
||||||
case int64, int, int32:
|
|
||||||
return reflect.ValueOf(x).Int(), nil
|
|
||||||
}
|
|
||||||
return 0, fmt.Errorf("%q must be integer", key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractString(m map[string]any, key string) (string, error) {
|
|
||||||
v, ok := m[key]
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("missing %q", key)
|
|
||||||
}
|
|
||||||
s, ok := v.(string)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("%q must be string", key)
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
"image/jpeg"
|
|
||||||
"image/png"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
oapi "nyanimedb/api"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/disintegration/imaging"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"golang.org/x/image/webp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PostMediaUpload implements oapi.StrictServerInterface.
|
|
||||||
func (s *Server) PostMediaUpload(ctx context.Context, request oapi.PostMediaUploadRequestObject) (oapi.PostMediaUploadResponseObject, error) {
|
|
||||||
// Получаем multipart body
|
|
||||||
mp := request.MultipartBody
|
|
||||||
if mp == nil {
|
|
||||||
log.Errorf("PostMedia without body")
|
|
||||||
return oapi.PostMediaUpload400JSONResponse("Multipart body is required"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Парсим первую часть (предполагаем, что файл в поле "file")
|
|
||||||
part, err := mp.NextPart()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("PostMedia without file")
|
|
||||||
return oapi.PostMediaUpload400JSONResponse("File required"), nil
|
|
||||||
}
|
|
||||||
defer part.Close()
|
|
||||||
|
|
||||||
// Читаем ВЕСЬ файл в память (для небольших изображений — нормально)
|
|
||||||
// Если файлы могут быть большими — используйте лимитированный буфер (см. ниже)
|
|
||||||
data, err := io.ReadAll(part)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("PostMedia cannot read file")
|
|
||||||
return oapi.PostMediaUpload400JSONResponse("File required"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data) == 0 {
|
|
||||||
log.Errorf("PostMedia empty file")
|
|
||||||
return oapi.PostMediaUpload400JSONResponse("Empty file"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверка MIME по первым 512 байтам
|
|
||||||
mimeType := http.DetectContentType(data)
|
|
||||||
if mimeType != "image/jpeg" && mimeType != "image/png" && mimeType != "image/webp" {
|
|
||||||
log.Errorf("PostMedia bad type")
|
|
||||||
return oapi.PostMediaUpload400JSONResponse("Bad data type"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Декодируем изображение из буфера
|
|
||||||
var img image.Image
|
|
||||||
switch mimeType {
|
|
||||||
case "image/jpeg":
|
|
||||||
{
|
|
||||||
img, err = jpeg.Decode(bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("PostMedia cannot decode file: %v", err)
|
|
||||||
return oapi.PostMediaUpload500Response{}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "image/png":
|
|
||||||
{
|
|
||||||
img, err = png.Decode(bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("PostMedia cannot decode file: %v", err)
|
|
||||||
return oapi.PostMediaUpload500Response{}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "image/webp":
|
|
||||||
{
|
|
||||||
img, err = webp.Decode(bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("PostMedia cannot decode file: %v", err)
|
|
||||||
return oapi.PostMediaUpload500Response{}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
err = imaging.Encode(&buf, img, imaging.PNG)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("PostMedia failed to re-encode JPEG: %v", err)
|
|
||||||
return oapi.PostMediaUpload500Response{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: to delete
|
|
||||||
filename := part.FileName()
|
|
||||||
if filename == "" {
|
|
||||||
filename = "upload_" + generateRandomHex(8) + ".jpg"
|
|
||||||
} else {
|
|
||||||
filename = sanitizeFilename(filename)
|
|
||||||
if !strings.HasSuffix(strings.ToLower(filename), ".png") {
|
|
||||||
filename += ".png"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: пойти на хуй ( вызвать файловую помойку)
|
|
||||||
os.Mkdir("uploads", 0644)
|
|
||||||
err = os.WriteFile(filepath.Join("./uploads", filename), buf.Bytes(), 0644)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("PostMedia failed to write: %v", err)
|
|
||||||
return oapi.PostMediaUpload500Response{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return oapi.PostMediaUpload200JSONResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Вспомогательные функции — как раньше
|
|
||||||
func generateRandomHex(n int) string {
|
|
||||||
b := make([]byte, n)
|
|
||||||
for i := range b {
|
|
||||||
b[i] = byte('a' + (i % 16))
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%x", b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sanitizeFilename(name string) string {
|
|
||||||
var clean strings.Builder
|
|
||||||
for _, r := range name {
|
|
||||||
if (r >= 'a' && r <= 'z') ||
|
|
||||||
(r >= 'A' && r <= 'Z') ||
|
|
||||||
(r >= '0' && r <= '9') ||
|
|
||||||
r == '.' || r == '_' || r == '-' {
|
|
||||||
clean.WriteRune(r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s := clean.String()
|
|
||||||
if s == "" {
|
|
||||||
return "file"
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
@ -1,304 +0,0 @@
|
||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
oapi "nyanimedb/api"
|
|
||||||
"nyanimedb/modules/backend/rmq"
|
|
||||||
sqlc "nyanimedb/sql"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Word2Sqlc(s *string) *string {
|
|
||||||
if s == nil || *s == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func TitleStatus2oapi(s *sqlc.TitleStatusT) (*oapi.TitleStatus, error) {
|
|
||||||
if s == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
var t oapi.TitleStatus
|
|
||||||
switch *s {
|
|
||||||
case sqlc.TitleStatusTFinished:
|
|
||||||
t = oapi.TitleStatusFinished
|
|
||||||
case sqlc.TitleStatusTOngoing:
|
|
||||||
t = oapi.TitleStatusOngoing
|
|
||||||
case sqlc.TitleStatusTPlanned:
|
|
||||||
t = oapi.TitleStatusPlanned
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unexpected tittle status: %s", *s)
|
|
||||||
}
|
|
||||||
return &t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReleaseSeason2sqlc(s *oapi.ReleaseSeason) (*sqlc.ReleaseSeasonT, error) {
|
|
||||||
if s == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
var t sqlc.ReleaseSeasonT
|
|
||||||
switch *s {
|
|
||||||
case oapi.Winter:
|
|
||||||
t = sqlc.ReleaseSeasonTWinter
|
|
||||||
case oapi.Spring:
|
|
||||||
t = sqlc.ReleaseSeasonTSpring
|
|
||||||
case oapi.Summer:
|
|
||||||
t = sqlc.ReleaseSeasonTSummer
|
|
||||||
case oapi.Fall:
|
|
||||||
t = sqlc.ReleaseSeasonTFall
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unexpected release season: %s", *s)
|
|
||||||
}
|
|
||||||
return &t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Server) GetTagsByTitleId(ctx context.Context, id int64) (oapi.Tags, error) {
|
|
||||||
|
|
||||||
sqlc_title_tags, err := s.db.GetTitleTags(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
if err == pgx.ErrNoRows {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("query GetTitleTags: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
oapi_tag_names := make(oapi.Tags, 1)
|
|
||||||
for _, title_tag := range sqlc_title_tags {
|
|
||||||
oapi_tag_name := make(map[string]string, 1)
|
|
||||||
err = json.Unmarshal(title_tag, &oapi_tag_name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unmarshalling title_tag: %v", err)
|
|
||||||
}
|
|
||||||
oapi_tag_names = append(oapi_tag_names, oapi_tag_name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return oapi_tag_names, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// func (s Server) GetImage(ctx context.Context, id int64) (*oapi.Image, error) {
|
|
||||||
|
|
||||||
// var oapi_image oapi.Image
|
|
||||||
|
|
||||||
// sqlc_image, err := s.db.GetImageByID(ctx, id)
|
|
||||||
// if err != nil {
|
|
||||||
// if err == pgx.ErrNoRows {
|
|
||||||
// return nil, nil //todo: error reference in db
|
|
||||||
// }
|
|
||||||
// return &oapi_image, fmt.Errorf("query GetImageByID: %v", err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// //can cast and dont use brain cause all this fields required in image table
|
|
||||||
// oapi_image.Id = &sqlc_image.ID
|
|
||||||
// oapi_image.ImagePath = &sqlc_image.ImagePath
|
|
||||||
// storageTypeStr := string(sqlc_image.StorageType)
|
|
||||||
// oapi_image.StorageType = string(storageTypeStr)
|
|
||||||
|
|
||||||
// return &oapi_image, nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (s Server) GetStudio(ctx context.Context, id int64) (*oapi.Studio, error) {
|
|
||||||
|
|
||||||
// var oapi_studio oapi.Studio
|
|
||||||
|
|
||||||
// sqlc_studio, err := s.db.GetStudioByID(ctx, id)
|
|
||||||
// if err != nil {
|
|
||||||
// if err == pgx.ErrNoRows {
|
|
||||||
// return nil, nil
|
|
||||||
// }
|
|
||||||
// return &oapi_studio, fmt.Errorf("query GetStudioByID: %v", err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// oapi_studio.Id = sqlc_studio.ID
|
|
||||||
// oapi_studio.Name = sqlc_studio.StudioName
|
|
||||||
// oapi_studio.Description = sqlc_studio.StudioDesc
|
|
||||||
|
|
||||||
// if sqlc_studio.IllustID == nil {
|
|
||||||
// return &oapi_studio, nil
|
|
||||||
// }
|
|
||||||
// oapi_illust, err := s.GetImage(ctx, *sqlc_studio.IllustID)
|
|
||||||
// if err != nil {
|
|
||||||
// return &oapi_studio, fmt.Errorf("GetImage: %v", err)
|
|
||||||
// }
|
|
||||||
// if oapi_illust != nil {
|
|
||||||
// oapi_studio.Poster = oapi_illust
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return &oapi_studio, nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
func (s Server) GetTitle(ctx context.Context, request oapi.GetTitleRequestObject) (oapi.GetTitleResponseObject, error) {
|
|
||||||
var oapi_title oapi.Title
|
|
||||||
|
|
||||||
sqlc_title, err := s.db.GetTitleByID(ctx, request.TitleId)
|
|
||||||
if err != nil {
|
|
||||||
if err == pgx.ErrNoRows {
|
|
||||||
return oapi.GetTitle204Response{}, nil
|
|
||||||
}
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.GetTitle500Response{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
oapi_title, err = s.mapTitle(sqlc_title)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.GetTitle500Response{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return oapi.GetTitle200JSONResponse(oapi_title), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObject) (oapi.GetTitlesResponseObject, error) {
|
|
||||||
|
|
||||||
opai_titles := make([]oapi.Title, 0)
|
|
||||||
mqreq := rmq.RabbitRequest{
|
|
||||||
Timestamp: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
word := Word2Sqlc(request.Params.Word)
|
|
||||||
if word != nil {
|
|
||||||
mqreq.Name = *word
|
|
||||||
}
|
|
||||||
|
|
||||||
season, err := ReleaseSeason2sqlc(request.Params.ReleaseSeason)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.GetTitles400Response{}, err
|
|
||||||
}
|
|
||||||
if season != nil {
|
|
||||||
mqreq.Season = *request.Params.ReleaseSeason
|
|
||||||
}
|
|
||||||
|
|
||||||
title_statuses, err := TitleStatus2Sqlc(request.Params.Status)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.GetTitles400Response{}, err
|
|
||||||
}
|
|
||||||
if title_statuses != nil {
|
|
||||||
mqreq.Statuses = *request.Params.Status
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.Params.ExtSearch != nil && *request.Params.ExtSearch {
|
|
||||||
|
|
||||||
// Структура для ответа (должна совпадать с тем, что шлёт микросервис)
|
|
||||||
var reply struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Result string `json:"result"`
|
|
||||||
Preview string `json:"preview_url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Делаем RPC-вызов — и ЖДЁМ ответа
|
|
||||||
err := s.RPCclient.Call(
|
|
||||||
ctx,
|
|
||||||
mqreq,
|
|
||||||
&reply,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("RabitMQ: %v", err)
|
|
||||||
// return oapi.GetTitles500Response{}, err
|
|
||||||
}
|
|
||||||
// // Возвращаем результат
|
|
||||||
// return oapi.ProcessMedia200JSONResponse{
|
|
||||||
// Status: reply.Status,
|
|
||||||
// Result: reply.Result,
|
|
||||||
// Preview: reply.Preview,
|
|
||||||
// }, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
params := sqlc.SearchTitlesParams{
|
|
||||||
Word: word,
|
|
||||||
TitleStatuses: title_statuses,
|
|
||||||
Rating: request.Params.Rating,
|
|
||||||
ReleaseYear: request.Params.ReleaseYear,
|
|
||||||
ReleaseSeason: season,
|
|
||||||
Forward: true, // default
|
|
||||||
SortBy: "id", // default
|
|
||||||
Limit: request.Params.Limit,
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.Params.SortForward != nil {
|
|
||||||
params.Forward = *request.Params.SortForward
|
|
||||||
}
|
|
||||||
if request.Params.Sort != nil {
|
|
||||||
params.SortBy = string(*request.Params.Sort)
|
|
||||||
if request.Params.Cursor != nil {
|
|
||||||
// here we set CursorYear CursorID CursorRating fields
|
|
||||||
err := ParseCursorInto(string(*request.Params.Sort), string(*request.Params.Cursor), ¶ms)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.GetTitles400Response{}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// param = nil means it will not be used
|
|
||||||
titles, err := s.db.SearchTitles(ctx, params)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.GetTitles500Response{}, nil
|
|
||||||
}
|
|
||||||
if len(titles) == 0 {
|
|
||||||
return oapi.GetTitles204Response{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var new_cursor oapi.CursorObj
|
|
||||||
|
|
||||||
for _, title := range titles {
|
|
||||||
|
|
||||||
_title := sqlc.GetTitleByIDRow{
|
|
||||||
ID: title.ID,
|
|
||||||
// StudioID: title.StudioID,
|
|
||||||
PosterID: title.PosterID,
|
|
||||||
TitleStatus: title.TitleStatus,
|
|
||||||
Rating: title.Rating,
|
|
||||||
RatingCount: title.RatingCount,
|
|
||||||
ReleaseYear: title.ReleaseYear,
|
|
||||||
ReleaseSeason: title.ReleaseSeason,
|
|
||||||
Season: title.Season,
|
|
||||||
EpisodesAired: title.EpisodesAired,
|
|
||||||
EpisodesAll: title.EpisodesAll,
|
|
||||||
// EpisodesLen: title.EpisodesLen,
|
|
||||||
TitleStorageType: title.TitleStorageType,
|
|
||||||
TitleImagePath: title.TitleImagePath,
|
|
||||||
TitleNames: title.TitleNames,
|
|
||||||
TagNames: title.TagNames,
|
|
||||||
StudioName: title.StudioName,
|
|
||||||
// StudioIllustID: title.StudioIllustID,
|
|
||||||
// StudioDesc: title.StudioDesc,
|
|
||||||
// StudioStorageType: title.StudioStorageType,
|
|
||||||
// StudioImagePath: title.StudioImagePath,
|
|
||||||
}
|
|
||||||
|
|
||||||
// if title.TitleStorageType != nil {
|
|
||||||
// s := *title.TitleStorageType
|
|
||||||
// _title.TitleStorageType = string(s)
|
|
||||||
// }
|
|
||||||
|
|
||||||
t, err := s.mapTitle(_title)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.GetTitles500Response{}, nil
|
|
||||||
}
|
|
||||||
opai_titles = append(opai_titles, t)
|
|
||||||
|
|
||||||
new_cursor.Id = t.Id
|
|
||||||
if request.Params.Sort != nil {
|
|
||||||
switch string(*request.Params.Sort) {
|
|
||||||
case "year":
|
|
||||||
tmp := fmt.Sprint(*t.ReleaseYear)
|
|
||||||
new_cursor.Param = &tmp
|
|
||||||
case "rating":
|
|
||||||
tmp := strconv.FormatFloat(*t.Rating, 'f', -1, 64)
|
|
||||||
new_cursor.Param = &tmp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return oapi.GetTitles200JSONResponse{Cursor: new_cursor, Data: opai_titles}, nil
|
|
||||||
}
|
|
||||||
|
|
@ -2,534 +2,50 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
oapi "nyanimedb/api"
|
oapi "nyanimedb/api"
|
||||||
sqlc "nyanimedb/sql"
|
sqlc "nyanimedb/sql"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgconn"
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
"github.com/oapi-codegen/runtime/types"
|
"github.com/oapi-codegen/runtime/types"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
type Server struct {
|
||||||
pgErrDuplicateKey = "23505"
|
db *sqlc.Queries
|
||||||
)
|
|
||||||
|
|
||||||
func mapUser(u sqlc.GetUserByIDRow) (oapi.User, error) {
|
|
||||||
i := oapi.Image{
|
|
||||||
Id: u.AvatarID,
|
|
||||||
ImagePath: u.ImagePath,
|
|
||||||
}
|
|
||||||
s, err := sql2StorageType(u.StorageType)
|
|
||||||
if err != nil {
|
|
||||||
return oapi.User{}, fmt.Errorf("mapUser, storage type: %v", err)
|
|
||||||
}
|
|
||||||
i.StorageType = s
|
|
||||||
return oapi.User{
|
|
||||||
Image: &i,
|
|
||||||
CreationDate: &u.CreationDate,
|
|
||||||
DispName: u.DispName,
|
|
||||||
Id: &u.ID,
|
|
||||||
Mail: StringToEmail(u.Mail),
|
|
||||||
Nickname: u.Nickname,
|
|
||||||
UserDesc: u.UserDesc,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Server) GetUsersId(ctx context.Context, req oapi.GetUsersIdRequestObject) (oapi.GetUsersIdResponseObject, error) {
|
func NewServer(db *sqlc.Queries) Server {
|
||||||
|
return Server{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseInt64(s string) (int32, error) {
|
||||||
|
i, err := strconv.ParseInt(s, 10, 64)
|
||||||
|
return int32(i), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapUser(u sqlc.GetUserByIDRow) oapi.User {
|
||||||
|
return oapi.User{
|
||||||
|
AvatarId: u.AvatarID,
|
||||||
|
CreationDate: u.CreationDate,
|
||||||
|
DispName: u.DispName,
|
||||||
|
Id: &u.ID,
|
||||||
|
Mail: (*types.Email)(u.Mail),
|
||||||
|
Nickname: u.Nickname,
|
||||||
|
UserDesc: u.UserDesc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Server) GetUsersUserId(ctx context.Context, req oapi.GetUsersUserIdRequestObject) (oapi.GetUsersUserIdResponseObject, error) {
|
||||||
userID, err := parseInt64(req.UserId)
|
userID, err := parseInt64(req.UserId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return oapi.GetUsersId404Response{}, nil
|
return oapi.GetUsersUserId404Response{}, nil
|
||||||
}
|
}
|
||||||
_user, err := s.db.GetUserByID(context.TODO(), userID)
|
user, err := s.db.GetUserByID(context.TODO(), int64(userID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == pgx.ErrNoRows {
|
if err == pgx.ErrNoRows {
|
||||||
return oapi.GetUsersId404Response{}, nil
|
return oapi.GetUsersUserId404Response{}, nil
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
user, err := mapUser(_user)
|
return oapi.GetUsersUserId200JSONResponse(mapUser(user)), nil
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.GetUsersId500Response{}, err
|
|
||||||
}
|
|
||||||
return oapi.GetUsersId200JSONResponse(user), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func sqlDate2oapi(p_date pgtype.Timestamptz) *time.Time {
|
|
||||||
if p_date.Valid {
|
|
||||||
t := p_date.Time
|
|
||||||
return &t
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func oapiDate2sql(t *time.Time) pgtype.Timestamptz {
|
|
||||||
if t == nil {
|
|
||||||
return pgtype.Timestamptz{Valid: false}
|
|
||||||
}
|
|
||||||
return pgtype.Timestamptz{
|
|
||||||
Time: *t,
|
|
||||||
Valid: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// func UserTitleStatus2Sqlc(s *[]oapi.UserTitleStatus) (*SqlcUserStatus, error) {
|
|
||||||
// var sqlc_status SqlcUserStatus
|
|
||||||
// if s == nil {
|
|
||||||
// return &sqlc_status, nil
|
|
||||||
// }
|
|
||||||
// for _, t := range *s {
|
|
||||||
// switch t {
|
|
||||||
// case oapi.UserTitleStatusFinished:
|
|
||||||
// sqlc_status.finished = "finished"
|
|
||||||
// case oapi.UserTitleStatusDropped:
|
|
||||||
// sqlc_status.dropped = "dropped"
|
|
||||||
// case oapi.UserTitleStatusPlanned:
|
|
||||||
// sqlc_status.planned = "planned"
|
|
||||||
// case oapi.UserTitleStatusInProgress:
|
|
||||||
// sqlc_status.in_progress = "in-progress"
|
|
||||||
// default:
|
|
||||||
// return nil, fmt.Errorf("unexpected tittle status: %s", t)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return &sqlc_status, nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
func sql2usertitlestatus(s sqlc.UsertitleStatusT) (oapi.UserTitleStatus, error) {
|
|
||||||
var status oapi.UserTitleStatus
|
|
||||||
|
|
||||||
switch s {
|
|
||||||
case sqlc.UsertitleStatusTFinished:
|
|
||||||
status = oapi.UserTitleStatusFinished
|
|
||||||
case sqlc.UsertitleStatusTDropped:
|
|
||||||
status = oapi.UserTitleStatusDropped
|
|
||||||
case sqlc.UsertitleStatusTPlanned:
|
|
||||||
status = oapi.UserTitleStatusPlanned
|
|
||||||
case sqlc.UsertitleStatusTInProgress:
|
|
||||||
status = oapi.UserTitleStatusInProgress
|
|
||||||
default:
|
|
||||||
return status, fmt.Errorf("unexpected tittle status: %s", s)
|
|
||||||
}
|
|
||||||
|
|
||||||
return status, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func UserTitleStatus2Sqlc(s *[]oapi.UserTitleStatus) ([]sqlc.UsertitleStatusT, error) {
|
|
||||||
var sqlc_status []sqlc.UsertitleStatusT
|
|
||||||
if s == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
for _, t := range *s {
|
|
||||||
switch t {
|
|
||||||
case oapi.UserTitleStatusFinished:
|
|
||||||
sqlc_status = append(sqlc_status, sqlc.UsertitleStatusTFinished)
|
|
||||||
case oapi.UserTitleStatusInProgress:
|
|
||||||
sqlc_status = append(sqlc_status, sqlc.UsertitleStatusTInProgress)
|
|
||||||
case oapi.UserTitleStatusDropped:
|
|
||||||
sqlc_status = append(sqlc_status, sqlc.UsertitleStatusTDropped)
|
|
||||||
case oapi.UserTitleStatusPlanned:
|
|
||||||
sqlc_status = append(sqlc_status, sqlc.UsertitleStatusTPlanned)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unexpected tittle status: %s", t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sqlc_status, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func UserTitleStatus2Sqlc1(s *oapi.UserTitleStatus) (*sqlc.UsertitleStatusT, error) {
|
|
||||||
var sqlc_status sqlc.UsertitleStatusT = sqlc.UsertitleStatusTFinished
|
|
||||||
if s == nil {
|
|
||||||
return &sqlc_status, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch *s {
|
|
||||||
case oapi.UserTitleStatusFinished:
|
|
||||||
sqlc_status = sqlc.UsertitleStatusTFinished
|
|
||||||
case oapi.UserTitleStatusInProgress:
|
|
||||||
sqlc_status = sqlc.UsertitleStatusTInProgress
|
|
||||||
case oapi.UserTitleStatusDropped:
|
|
||||||
sqlc_status = sqlc.UsertitleStatusTDropped
|
|
||||||
case oapi.UserTitleStatusPlanned:
|
|
||||||
sqlc_status = sqlc.UsertitleStatusTPlanned
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unexpected tittle status: %s", *s)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &sqlc_status, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Server) mapUsertitle(ctx context.Context, t sqlc.SearchUserTitlesRow) (oapi.UserTitle, error) {
|
|
||||||
|
|
||||||
oapi_usertitle := oapi.UserTitle{
|
|
||||||
Ctime: &t.UserCtime,
|
|
||||||
Rate: t.UserRate,
|
|
||||||
ReviewId: t.ReviewID,
|
|
||||||
// Status: ,
|
|
||||||
// Title: ,
|
|
||||||
UserId: t.UserID,
|
|
||||||
}
|
|
||||||
|
|
||||||
status, err := sql2usertitlestatus(t.UsertitleStatus)
|
|
||||||
if err != nil {
|
|
||||||
return oapi_usertitle, fmt.Errorf("mapUsertitle: %v", err)
|
|
||||||
}
|
|
||||||
oapi_usertitle.Status = status
|
|
||||||
|
|
||||||
_title := sqlc.GetTitleByIDRow{
|
|
||||||
ID: t.ID,
|
|
||||||
// StudioID: title.StudioID,
|
|
||||||
PosterID: t.PosterID,
|
|
||||||
TitleStatus: t.TitleStatus,
|
|
||||||
Rating: t.Rating,
|
|
||||||
RatingCount: t.RatingCount,
|
|
||||||
ReleaseYear: t.ReleaseYear,
|
|
||||||
ReleaseSeason: t.ReleaseSeason,
|
|
||||||
Season: t.Season,
|
|
||||||
EpisodesAired: t.EpisodesAired,
|
|
||||||
EpisodesAll: t.EpisodesAll,
|
|
||||||
// EpisodesLen: title.EpisodesLen,
|
|
||||||
TitleStorageType: t.TitleStorageType,
|
|
||||||
TitleImagePath: t.TitleImagePath,
|
|
||||||
StudioName: t.StudioName,
|
|
||||||
TitleNames: t.TitleNames,
|
|
||||||
TagNames: t.TagNames,
|
|
||||||
// StudioIllustID: title.StudioIllustID,
|
|
||||||
// StudioDesc: title.StudioDesc,
|
|
||||||
// StudioStorageType: title.StudioStorageType,
|
|
||||||
// StudioImagePath: title.StudioImagePath,
|
|
||||||
}
|
|
||||||
|
|
||||||
oapi_title, err := s.mapTitle(_title)
|
|
||||||
if err != nil {
|
|
||||||
return oapi_usertitle, fmt.Errorf("mapUsertitle: %v", err)
|
|
||||||
}
|
|
||||||
oapi_usertitle.Title = &oapi_title
|
|
||||||
|
|
||||||
return oapi_usertitle, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Server) GetUserTitles(ctx context.Context, request oapi.GetUserTitlesRequestObject) (oapi.GetUserTitlesResponseObject, error) {
|
|
||||||
|
|
||||||
oapi_usertitles := make([]oapi.UserTitle, 0)
|
|
||||||
|
|
||||||
word := Word2Sqlc(request.Params.Word)
|
|
||||||
|
|
||||||
season, err := ReleaseSeason2sqlc(request.Params.ReleaseSeason)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.GetUserTitles400Response{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// var statuses_sort []string
|
|
||||||
// if request.Params.Status != nil {
|
|
||||||
// for _, s := range *request.Params.Status {
|
|
||||||
// ss := string(s) // s type is alias for string
|
|
||||||
// statuses_sort = append(statuses_sort, ss)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
watch_status, err := UserTitleStatus2Sqlc(request.Params.WatchStatus)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.GetUserTitles400Response{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
title_statuses, err := TitleStatus2Sqlc(request.Params.Status)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.GetUserTitles400Response{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
userID, err := parseInt64(request.UserId)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("get user titles: %v", err)
|
|
||||||
return oapi.GetUserTitles404Response{}, err
|
|
||||||
}
|
|
||||||
params := sqlc.SearchUserTitlesParams{
|
|
||||||
UserID: userID,
|
|
||||||
Word: word,
|
|
||||||
TitleStatuses: title_statuses,
|
|
||||||
UsertitleStatuses: watch_status,
|
|
||||||
Rating: request.Params.Rating,
|
|
||||||
Rate: request.Params.MyRate,
|
|
||||||
ReleaseYear: request.Params.ReleaseYear,
|
|
||||||
ReleaseSeason: season,
|
|
||||||
Forward: true, // default
|
|
||||||
SortBy: "id", // default
|
|
||||||
Limit: request.Params.Limit,
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.Params.SortForward != nil {
|
|
||||||
params.Forward = *request.Params.SortForward
|
|
||||||
}
|
|
||||||
if request.Params.Sort != nil {
|
|
||||||
params.SortBy = string(*request.Params.Sort)
|
|
||||||
if request.Params.Cursor != nil {
|
|
||||||
// here we set CursorYear CursorID CursorRating fields
|
|
||||||
err := ParseCursorInto(string(*request.Params.Sort), string(*request.Params.Cursor), ¶ms)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.GetUserTitles400Response{}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// param = nil means it will not be used
|
|
||||||
titles, err := s.db.SearchUserTitles(ctx, params)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.GetUserTitles500Response{}, nil
|
|
||||||
}
|
|
||||||
if len(titles) == 0 {
|
|
||||||
return oapi.GetUserTitles204Response{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var new_cursor oapi.CursorObj
|
|
||||||
|
|
||||||
for _, title := range titles {
|
|
||||||
|
|
||||||
t, err := s.mapUsertitle(ctx, title)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.GetUserTitles500Response{}, nil
|
|
||||||
}
|
|
||||||
oapi_usertitles = append(oapi_usertitles, t)
|
|
||||||
|
|
||||||
new_cursor.Id = t.Title.Id
|
|
||||||
if request.Params.Sort != nil {
|
|
||||||
switch string(*request.Params.Sort) {
|
|
||||||
case "year":
|
|
||||||
tmp := fmt.Sprint(*t.Title.ReleaseYear)
|
|
||||||
new_cursor.Param = &tmp
|
|
||||||
case "rating":
|
|
||||||
tmp := strconv.FormatFloat(*t.Title.Rating, 'f', -1, 64) // падает
|
|
||||||
new_cursor.Param = &tmp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return oapi.GetUserTitles200JSONResponse{Cursor: new_cursor, Data: oapi_usertitles}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func EmailToStringPtr(e *types.Email) *string {
|
|
||||||
if e == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
s := string(*e)
|
|
||||||
return &s
|
|
||||||
}
|
|
||||||
|
|
||||||
func StringToEmail(e *string) *types.Email {
|
|
||||||
if e == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
s := types.Email(*e)
|
|
||||||
return &s
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateUser implements oapi.StrictServerInterface.
|
|
||||||
func (s Server) UpdateUser(ctx context.Context, request oapi.UpdateUserRequestObject) (oapi.UpdateUserResponseObject, error) {
|
|
||||||
|
|
||||||
params := sqlc.UpdateUserParams{
|
|
||||||
AvatarID: request.Body.AvatarId,
|
|
||||||
DispName: request.Body.DispName,
|
|
||||||
UserDesc: request.Body.UserDesc,
|
|
||||||
Mail: EmailToStringPtr(request.Body.Mail),
|
|
||||||
UserID: request.UserId,
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := s.db.UpdateUser(ctx, params)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.UpdateUser500Response{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
oapi_user := oapi.User{ // maybe its possible to make one sqlc type and use one map func iinstead of this shit
|
|
||||||
// AvatarId: user.AvatarID,
|
|
||||||
CreationDate: &user.CreationDate,
|
|
||||||
DispName: user.DispName,
|
|
||||||
Id: &user.ID,
|
|
||||||
Mail: StringToEmail(user.Mail),
|
|
||||||
Nickname: user.Nickname,
|
|
||||||
UserDesc: user.UserDesc,
|
|
||||||
}
|
|
||||||
|
|
||||||
return oapi.UpdateUser200JSONResponse(oapi_user), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Server) AddUserTitle(ctx context.Context, request oapi.AddUserTitleRequestObject) (oapi.AddUserTitleResponseObject, error) {
|
|
||||||
//TODO: add review if exists
|
|
||||||
status, err := UserTitleStatus2Sqlc1(&request.Body.Status)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.AddUserTitle400Response{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
params := sqlc.InsertUserTitleParams{
|
|
||||||
UserID: request.UserId,
|
|
||||||
TitleID: request.Body.TitleId,
|
|
||||||
Status: *status,
|
|
||||||
Rate: request.Body.Rate,
|
|
||||||
Ftime: oapiDate2sql(request.Body.Ftime),
|
|
||||||
}
|
|
||||||
|
|
||||||
user_title, err := s.db.InsertUserTitle(ctx, params)
|
|
||||||
if err != nil {
|
|
||||||
var pgErr *pgconn.PgError
|
|
||||||
if errors.As(err, &pgErr) {
|
|
||||||
// fmt.Println(pgErr.Message) // => syntax error at end of input
|
|
||||||
// fmt.Println(pgErr.Code) // => 42601
|
|
||||||
if pgErr.Code == pgErrDuplicateKey { //duplicate key value
|
|
||||||
return oapi.AddUserTitle409Response{}, nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.AddUserTitle500Response{}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
oapi_status, err := sql2usertitlestatus(user_title.Status)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.AddUserTitle500Response{}, nil
|
|
||||||
}
|
|
||||||
oapi_usertitle := oapi.UserTitleMini{
|
|
||||||
Ctime: &user_title.Ctime,
|
|
||||||
Rate: user_title.Rate,
|
|
||||||
ReviewId: user_title.ReviewID,
|
|
||||||
Status: oapi_status,
|
|
||||||
TitleId: user_title.TitleID,
|
|
||||||
UserId: user_title.UserID,
|
|
||||||
}
|
|
||||||
|
|
||||||
return oapi.AddUserTitle200JSONResponse(oapi_usertitle), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteUserTitle implements oapi.StrictServerInterface.
|
|
||||||
func (s Server) DeleteUserTitle(ctx context.Context, request oapi.DeleteUserTitleRequestObject) (oapi.DeleteUserTitleResponseObject, error) {
|
|
||||||
params := sqlc.DeleteUserTitleParams{
|
|
||||||
UserID: request.UserId,
|
|
||||||
TitleID: request.TitleId,
|
|
||||||
}
|
|
||||||
_, err := s.db.DeleteUserTitle(ctx, params)
|
|
||||||
if err != nil {
|
|
||||||
if err == pgx.ErrNoRows {
|
|
||||||
return oapi.DeleteUserTitle404Response{}, nil
|
|
||||||
}
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.DeleteUserTitle500Response{}, nil
|
|
||||||
}
|
|
||||||
return oapi.DeleteUserTitle200Response{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateUserTitle implements oapi.StrictServerInterface.
|
|
||||||
func (s Server) UpdateUserTitle(ctx context.Context, request oapi.UpdateUserTitleRequestObject) (oapi.UpdateUserTitleResponseObject, error) {
|
|
||||||
|
|
||||||
status, err := UserTitleStatus2Sqlc1(request.Body.Status)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.UpdateUserTitle400Response{}, nil
|
|
||||||
}
|
|
||||||
params := sqlc.UpdateUserTitleParams{
|
|
||||||
Status: status,
|
|
||||||
Rate: request.Body.Rate,
|
|
||||||
UserID: request.UserId,
|
|
||||||
TitleID: request.TitleId,
|
|
||||||
Ftime: oapiDate2sql(request.Body.Ftime),
|
|
||||||
}
|
|
||||||
|
|
||||||
user_title, err := s.db.UpdateUserTitle(ctx, params)
|
|
||||||
if err != nil {
|
|
||||||
if err == pgx.ErrNoRows {
|
|
||||||
return oapi.UpdateUserTitle404Response{}, nil
|
|
||||||
}
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.UpdateUserTitle500Response{}, nil
|
|
||||||
}
|
|
||||||
oapi_status, err := sql2usertitlestatus(user_title.Status)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.UpdateUserTitle500Response{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
oapi_usertitle := oapi.UserTitleMini{
|
|
||||||
Ctime: &user_title.Ctime,
|
|
||||||
Rate: user_title.Rate,
|
|
||||||
ReviewId: user_title.ReviewID,
|
|
||||||
Status: oapi_status,
|
|
||||||
TitleId: user_title.TitleID,
|
|
||||||
UserId: user_title.UserID,
|
|
||||||
}
|
|
||||||
|
|
||||||
return oapi.UpdateUserTitle200JSONResponse(oapi_usertitle), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Server) GetUserTitle(ctx context.Context, request oapi.GetUserTitleRequestObject) (oapi.GetUserTitleResponseObject, error) {
|
|
||||||
user_title, err := s.db.GetUserTitleByID(ctx, sqlc.GetUserTitleByIDParams{
|
|
||||||
TitleID: request.TitleId,
|
|
||||||
UserID: request.UserId,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
if err == pgx.ErrNoRows {
|
|
||||||
return oapi.GetUserTitle404Response{}, nil
|
|
||||||
} else {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.GetUserTitle500Response{}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
oapi_status, err := sql2usertitlestatus(user_title.Status)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.GetUserTitle500Response{}, nil
|
|
||||||
}
|
|
||||||
oapi_usertitle := oapi.UserTitleMini{
|
|
||||||
Ctime: &user_title.Ctime,
|
|
||||||
Rate: user_title.Rate,
|
|
||||||
ReviewId: user_title.ReviewID,
|
|
||||||
Status: oapi_status,
|
|
||||||
TitleId: user_title.TitleID,
|
|
||||||
UserId: user_title.UserID,
|
|
||||||
}
|
|
||||||
|
|
||||||
return oapi.GetUserTitle200JSONResponse(oapi_usertitle), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUsers implements oapi.StrictServerInterface.
|
|
||||||
func (s *Server) GetUsers(ctx context.Context, request oapi.GetUsersRequestObject) (oapi.GetUsersResponseObject, error) {
|
|
||||||
params := sqlc.SearchUserParams{
|
|
||||||
Word: request.Params.Word,
|
|
||||||
Cursor: request.Params.CursorId,
|
|
||||||
Limit: request.Params.Limit,
|
|
||||||
}
|
|
||||||
_users, err := s.db.SearchUser(ctx, params)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.GetUsers500Response{}, nil
|
|
||||||
}
|
|
||||||
if len(_users) == 0 {
|
|
||||||
return oapi.GetUsers204Response{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var users []oapi.User
|
|
||||||
var cursor int64
|
|
||||||
for _, user := range _users {
|
|
||||||
oapi_user := oapi.User{ // maybe its possible to make one sqlc type and use one map func iinstead of this shit
|
|
||||||
// add image
|
|
||||||
CreationDate: &user.CreationDate,
|
|
||||||
DispName: user.DispName,
|
|
||||||
Id: &user.ID,
|
|
||||||
Mail: StringToEmail(user.Mail),
|
|
||||||
Nickname: user.Nickname,
|
|
||||||
UserDesc: user.UserDesc,
|
|
||||||
}
|
|
||||||
users = append(users, oapi_user)
|
|
||||||
|
|
||||||
cursor = user.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
return oapi.GetUsers200JSONResponse{Data: users, Cursor: cursor}, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package main
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
sqlc "nyanimedb/sql"
|
sqlc "nyanimedb/sql"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
@ -11,63 +10,46 @@ import (
|
||||||
|
|
||||||
oapi "nyanimedb/api"
|
oapi "nyanimedb/api"
|
||||||
handlers "nyanimedb/modules/backend/handlers"
|
handlers "nyanimedb/modules/backend/handlers"
|
||||||
middleware "nyanimedb/modules/backend/middlewares"
|
|
||||||
"nyanimedb/modules/backend/rmq"
|
|
||||||
|
|
||||||
"github.com/gin-contrib/cors"
|
"github.com/gin-contrib/cors"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/pelletier/go-toml/v2"
|
"github.com/pelletier/go-toml/v2"
|
||||||
"github.com/rabbitmq/amqp091-go"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var AppConfig Config
|
var AppConfig Config
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) != 2 {
|
// if len(os.Args) != 2 {
|
||||||
AppConfig.Mode = "env"
|
// AppConfig.Mode = "env"
|
||||||
} else {
|
// } else {
|
||||||
AppConfig.Mode = "argv"
|
// AppConfig.Mode = "argv"
|
||||||
}
|
// }
|
||||||
|
|
||||||
err := InitConfig()
|
// err := InitConfig()
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
log.Fatalf("Failed to init config: %v\n", err)
|
// log.Fatalf("Failed to init config: %v\n", err)
|
||||||
}
|
// }
|
||||||
|
|
||||||
pool, err := pgxpool.New(context.Background(), AppConfig.DdUrl)
|
conn, err := pgx.Connect(context.Background(), os.Getenv("DATABASE_URL"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer pool.Close()
|
defer conn.Close(context.Background())
|
||||||
|
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
if len(AppConfig.AuthEnabled) > 0 && AppConfig.AuthEnabled != "false" {
|
queries := sqlc.New(conn)
|
||||||
r.Use(middleware.CSRFMiddleware())
|
|
||||||
r.Use(middleware.JWTAuthMiddleware(AppConfig.JwtPrivateKey))
|
|
||||||
}
|
|
||||||
|
|
||||||
queries := sqlc.New(pool)
|
server := handlers.NewServer(queries)
|
||||||
|
// r.LoadHTMLGlob("templates/*")
|
||||||
rmqConn, err := amqp091.Dial(AppConfig.RmqURL)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to connect to RabbitMQ: %v", err)
|
|
||||||
}
|
|
||||||
defer rmqConn.Close()
|
|
||||||
|
|
||||||
rpcClient := rmq.NewRPCClient(rmqConn, 30*time.Second)
|
|
||||||
|
|
||||||
server := handlers.NewServer(queries, rpcClient)
|
|
||||||
|
|
||||||
r.Use(cors.New(cors.Config{
|
r.Use(cors.New(cors.Config{
|
||||||
AllowOrigins: []string{AppConfig.ServiceAddress},
|
AllowOrigins: []string{"*"}, // allow all origins, change to specific domains in production
|
||||||
// AllowOrigins: []string{"*"},
|
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
|
||||||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "PATCH"},
|
AllowHeaders: []string{"Origin", "Content-Type", "Accept"},
|
||||||
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "X-XSRF-TOKEN"},
|
|
||||||
ExposeHeaders: []string{"Content-Length"},
|
ExposeHeaders: []string{"Content-Length"},
|
||||||
AllowCredentials: true,
|
AllowCredentials: true,
|
||||||
MaxAge: 12 * time.Hour,
|
MaxAge: 12 * time.Hour,
|
||||||
|
|
@ -75,15 +57,27 @@ func main() {
|
||||||
|
|
||||||
oapi.RegisterHandlers(r, oapi.NewStrictHandler(
|
oapi.RegisterHandlers(r, oapi.NewStrictHandler(
|
||||||
server,
|
server,
|
||||||
|
// сюда можно добавить middlewares, если нужно
|
||||||
[]oapi.StrictMiddlewareFunc{},
|
[]oapi.StrictMiddlewareFunc{},
|
||||||
))
|
))
|
||||||
|
// r.GET("/", func(c *gin.Context) {
|
||||||
|
// c.HTML(http.StatusOK, "index.html", gin.H{
|
||||||
|
// "title": "Welcome Page",
|
||||||
|
// "message": "Hello, Gin with HTML templates!",
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
// Запуск
|
// r.GET("/api", func(c *gin.Context) {
|
||||||
log.Infof("Server starting on :8080")
|
// items := []Item{
|
||||||
if err := r.Run(":8080"); err != nil && err != http.ErrServerClosed {
|
// {ID: 1, Title: "First Item", Description: "This is the description of the first item."},
|
||||||
log.Fatalf("server failed: %v", err)
|
// {ID: 2, Title: "Second Item", Description: "This is the description of the second item."},
|
||||||
}
|
// {ID: 3, Title: "Third Item", Description: "This is the description of the third item."},
|
||||||
|
// }
|
||||||
|
|
||||||
|
// c.JSON(http.StatusOK, items)
|
||||||
|
// })
|
||||||
|
|
||||||
|
r.Run(":8080")
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitConfig() error {
|
func InitConfig() error {
|
||||||
|
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ctxKey — приватный тип для ключа контекста
|
|
||||||
type ctxKey struct{}
|
|
||||||
|
|
||||||
// ginContextKey — уникальный ключ для хранения *gin.Context
|
|
||||||
var ginContextKey = &ctxKey{}
|
|
||||||
|
|
||||||
// GinContextToContext сохраняет *gin.Context в context.Context запроса
|
|
||||||
func GinContextToContext(c *gin.Context) {
|
|
||||||
ctx := context.WithValue(c.Request.Context(), ginContextKey, c)
|
|
||||||
c.Request = c.Request.WithContext(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GinContextFromContext извлекает *gin.Context из context.Context
|
|
||||||
func GinContextFromContext(ctx context.Context) (*gin.Context, bool) {
|
|
||||||
ginCtx, ok := ctx.Value(ginContextKey).(*gin.Context)
|
|
||||||
return ginCtx, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func JWTAuthMiddleware(secret string) gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
// 1. Получаем access_token из cookie
|
|
||||||
tokenStr, err := c.Cookie("access_token")
|
|
||||||
if err != nil {
|
|
||||||
abortWithJSON(c, http.StatusUnauthorized, "missing access_token cookie")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Парсим токен с MapClaims
|
|
||||||
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
|
|
||||||
if t.Method != jwt.SigningMethodHS256 {
|
|
||||||
return nil, errors.New("unexpected signing method: " + t.Method.Alg())
|
|
||||||
}
|
|
||||||
return []byte(secret), nil // ← конвертируем string → []byte
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
abortWithJSON(c, http.StatusUnauthorized, "invalid token: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Проверяем валидность
|
|
||||||
if !token.Valid {
|
|
||||||
abortWithJSON(c, http.StatusUnauthorized, "token is invalid")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Извлекаем user_id из claims
|
|
||||||
claims, ok := token.Claims.(jwt.MapClaims)
|
|
||||||
if !ok {
|
|
||||||
abortWithJSON(c, http.StatusUnauthorized, "invalid claims format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userID, ok := claims["user_id"].(string)
|
|
||||||
if !ok || userID == "" {
|
|
||||||
abortWithJSON(c, http.StatusUnauthorized, "user_id claim missing or invalid")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Сохраняем в контексте
|
|
||||||
c.Set("user_id", userID)
|
|
||||||
|
|
||||||
// 6. Для oapi-codegen — кладём gin.Context в request context
|
|
||||||
GinContextToContext(c)
|
|
||||||
|
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Вспомогательные функции (без изменений)
|
|
||||||
func UserIDFromGin(c *gin.Context) (string, bool) {
|
|
||||||
id, exists := c.Get("user_id")
|
|
||||||
if !exists {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
if s, ok := id.(string); ok {
|
|
||||||
return s, true
|
|
||||||
}
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
func UserIDFromContext(ctx context.Context) (string, error) {
|
|
||||||
ginCtx, ok := GinContextFromContext(ctx)
|
|
||||||
if !ok {
|
|
||||||
return "", errors.New("gin context not found")
|
|
||||||
}
|
|
||||||
userID, ok := UserIDFromGin(ginCtx)
|
|
||||||
if !ok {
|
|
||||||
return "", errors.New("user_id not found in context")
|
|
||||||
}
|
|
||||||
return userID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func abortWithJSON(c *gin.Context, code int, message string) {
|
|
||||||
c.AbortWithStatusJSON(code, gin.H{
|
|
||||||
"error": "unauthorized",
|
|
||||||
"message": message,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/subtle"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CSRFMiddleware для Gin
|
|
||||||
func CSRFMiddleware() gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
// Пропускаем безопасные методы
|
|
||||||
if !isStateChangingMethod(c.Request.Method) {
|
|
||||||
c.Next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Получаем токен из заголовка
|
|
||||||
headerToken := c.GetHeader("X-XSRF-TOKEN")
|
|
||||||
if headerToken == "" {
|
|
||||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
|
||||||
"error": "missing X-XSRF-TOKEN header",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Получаем токен из cookie
|
|
||||||
cookie, err := c.Cookie("xsrf_token")
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
|
||||||
"error": "missing xsrf_token cookie",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Безопасное сравнение
|
|
||||||
if subtle.ConstantTimeCompare([]byte(headerToken), []byte(cookie)) != 1 {
|
|
||||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
|
||||||
"error": "CSRF token mismatch",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Опционально: сохраняем токен в контексте
|
|
||||||
c.Set("csrf_token", headerToken)
|
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isStateChangingMethod(method string) bool {
|
|
||||||
switch method {
|
|
||||||
case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CSRFTokenFromGin извлекает токен из Gin context
|
|
||||||
func CSRFTokenFromGin(c *gin.Context) (string, bool) {
|
|
||||||
token, exists := c.Get("xsrf_token")
|
|
||||||
if !exists {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
if s, ok := token.(string); ok {
|
|
||||||
return s, true
|
|
||||||
}
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
-- name: GetImageByID :one
|
-- name: GetImageByID :one
|
||||||
SELECT id, storage_type, image_path
|
SELECT id, storage_type, image_path
|
||||||
FROM images
|
FROM images
|
||||||
WHERE id = sqlc.arg('illust_id')::bigint;
|
WHERE id = $1;
|
||||||
|
|
||||||
-- name: CreateImage :one
|
-- name: CreateImage :one
|
||||||
INSERT INTO images (storage_type, image_path)
|
INSERT INTO images (storage_type, image_path)
|
||||||
|
|
@ -9,428 +9,134 @@ VALUES ($1, $2)
|
||||||
RETURNING id, storage_type, image_path;
|
RETURNING id, storage_type, image_path;
|
||||||
|
|
||||||
-- name: GetUserByID :one
|
-- name: GetUserByID :one
|
||||||
SELECT
|
SELECT id, avatar_id, mail, nickname, disp_name, user_desc, creation_date
|
||||||
t.id as id,
|
FROM users
|
||||||
t.avatar_id as avatar_id,
|
WHERE id = $1;
|
||||||
t.mail as mail,
|
|
||||||
t.nickname as nickname,
|
|
||||||
t.disp_name as disp_name,
|
|
||||||
t.user_desc as user_desc,
|
|
||||||
t.creation_date as creation_date,
|
|
||||||
i.storage_type as storage_type,
|
|
||||||
i.image_path as image_path
|
|
||||||
FROM users as t
|
|
||||||
LEFT JOIN images as i ON (t.avatar_id = i.id)
|
|
||||||
WHERE t.id = sqlc.arg('id')::bigint;
|
|
||||||
|
|
||||||
-- name: SearchUser :many
|
-- -- name: ListUsers :many
|
||||||
SELECT
|
-- SELECT user_id, avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date
|
||||||
u.id AS id,
|
-- FROM users
|
||||||
u.avatar_id AS avatar_id,
|
-- ORDER BY user_id
|
||||||
u.mail AS mail,
|
-- LIMIT $1 OFFSET $2;
|
||||||
u.nickname AS nickname,
|
|
||||||
u.disp_name AS disp_name,
|
|
||||||
u.user_desc AS user_desc,
|
|
||||||
u.creation_date AS creation_date,
|
|
||||||
i.storage_type AS storage_type,
|
|
||||||
i.image_path AS image_path
|
|
||||||
FROM users AS u
|
|
||||||
LEFT JOIN images AS i ON u.avatar_id = i.id
|
|
||||||
WHERE
|
|
||||||
(
|
|
||||||
sqlc.narg('word')::text IS NULL
|
|
||||||
OR (
|
|
||||||
SELECT bool_and(
|
|
||||||
u.nickname ILIKE ('%' || term || '%')
|
|
||||||
OR u.disp_name ILIKE ('%' || term || '%')
|
|
||||||
)
|
|
||||||
FROM unnest(string_to_array(trim(sqlc.narg('word')::text), ' ')) AS term
|
|
||||||
WHERE term <> ''
|
|
||||||
)
|
|
||||||
)
|
|
||||||
AND (
|
|
||||||
sqlc.narg('cursor')::int IS NULL
|
|
||||||
OR u.id > sqlc.narg('cursor')::int
|
|
||||||
)
|
|
||||||
ORDER BY u.id ASC
|
|
||||||
LIMIT COALESCE(sqlc.narg('limit')::int, 20);
|
|
||||||
|
|
||||||
-- name: GetStudioByID :one
|
-- -- name: CreateUser :one
|
||||||
SELECT *
|
-- INSERT INTO users (avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date)
|
||||||
FROM studios
|
-- VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
WHERE id = sqlc.arg('studio_id')::bigint;
|
-- RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date;
|
||||||
|
|
||||||
-- name: InsertStudio :one
|
-- -- name: UpdateUser :one
|
||||||
INSERT INTO studios (studio_name, illust_id, studio_desc)
|
-- UPDATE users
|
||||||
VALUES (
|
-- SET
|
||||||
sqlc.arg('studio_name')::text,
|
-- avatar_id = COALESCE(sqlc.narg('avatar_id'), avatar_id),
|
||||||
sqlc.narg('illust_id')::bigint,
|
-- disp_name = COALESCE(sqlc.narg('disp_name'), disp_name),
|
||||||
sqlc.narg('studio_desc')::text)
|
-- user_desc = COALESCE(sqlc.narg('user_desc'), user_desc),
|
||||||
RETURNING id, studio_name, illust_id, studio_desc;
|
-- passhash = COALESCE(sqlc.narg('passhash'), passhash)
|
||||||
|
-- WHERE user_id = sqlc.arg('user_id')
|
||||||
|
-- RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date;
|
||||||
|
|
||||||
-- name: GetTitleTags :many
|
-- -- name: DeleteUser :exec
|
||||||
SELECT
|
-- DELETE FROM users
|
||||||
tag_names
|
-- WHERE user_id = $1;
|
||||||
FROM tags as g
|
|
||||||
JOIN title_tags as t ON(t.tag_id = g.id)
|
|
||||||
WHERE t.title_id = sqlc.arg('title_id')::bigint;
|
|
||||||
|
|
||||||
-- name: InsertTitleTags :one
|
-- -- name: GetTitleByID :one
|
||||||
INSERT INTO title_tags (title_id, tag_id)
|
-- SELECT title_id, title_names, studio_id, poster_id, signal_ids,
|
||||||
VALUES (
|
-- title_status, rating, rating_count, release_year, release_season,
|
||||||
sqlc.arg('title_id')::bigint,
|
-- season, episodes_aired, episodes_all, episodes_len
|
||||||
sqlc.arg('tag_id')::bigint)
|
-- FROM titles
|
||||||
RETURNING title_id, tag_id;
|
-- WHERE title_id = $1;
|
||||||
|
|
||||||
-- name: InsertTag :one
|
-- -- name: ListTitles :many
|
||||||
INSERT INTO tags (tag_names)
|
-- SELECT title_id, title_names, studio_id, poster_id, signal_ids,
|
||||||
VALUES (
|
-- title_status, rating, rating_count, release_year, release_season,
|
||||||
sqlc.arg('tag_names')::jsonb)
|
-- season, episodes_aired, episodes_all, episodes_len
|
||||||
RETURNING id, tag_names;
|
-- FROM titles
|
||||||
|
-- ORDER BY title_id
|
||||||
|
-- LIMIT $1 OFFSET $2;
|
||||||
|
|
||||||
-- name: UpdateUser :one
|
-- -- name: UpdateTitle :one
|
||||||
UPDATE users
|
-- UPDATE titles
|
||||||
SET
|
-- SET
|
||||||
avatar_id = COALESCE(sqlc.narg('avatar_id'), avatar_id),
|
-- title_names = COALESCE(sqlc.narg('title_names'), title_names),
|
||||||
disp_name = COALESCE(sqlc.narg('disp_name'), disp_name),
|
-- studio_id = COALESCE(sqlc.narg('studio_id'), studio_id),
|
||||||
user_desc = COALESCE(sqlc.narg('user_desc'), user_desc),
|
-- poster_id = COALESCE(sqlc.narg('poster_id'), poster_id),
|
||||||
mail = COALESCE(sqlc.narg('mail'), mail)
|
-- signal_ids = COALESCE(sqlc.narg('signal_ids'), signal_ids),
|
||||||
WHERE id = sqlc.arg('user_id')
|
-- title_status = COALESCE(sqlc.narg('title_status'), title_status),
|
||||||
RETURNING id, avatar_id, nickname, disp_name, user_desc, creation_date, mail;
|
-- release_year = COALESCE(sqlc.narg('release_year'), release_year),
|
||||||
|
-- release_season = COALESCE(sqlc.narg('release_season'), release_season),
|
||||||
|
-- episodes_aired = COALESCE(sqlc.narg('episodes_aired'), episodes_aired),
|
||||||
|
-- episodes_all = COALESCE(sqlc.narg('episodes_all'), episodes_all),
|
||||||
|
-- episodes_len = COALESCE(sqlc.narg('episodes_len'), episodes_len)
|
||||||
|
-- WHERE title_id = sqlc.arg('title_id')
|
||||||
|
-- RETURNING *;
|
||||||
|
|
||||||
-- name: GetTitleByID :one
|
-- -- name: GetReviewByID :one
|
||||||
-- sqlc.struct: TitlesFull
|
-- SELECT review_id, user_id, title_id, image_ids, review_text, creation_date
|
||||||
SELECT
|
-- FROM reviews
|
||||||
t.*,
|
-- WHERE review_id = $1;
|
||||||
i.storage_type as title_storage_type,
|
|
||||||
i.image_path as title_image_path,
|
|
||||||
COALESCE(
|
|
||||||
jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL),
|
|
||||||
'[]'::jsonb
|
|
||||||
)::jsonb as tag_names,
|
|
||||||
s.studio_name as studio_name,
|
|
||||||
s.illust_id as studio_illust_id,
|
|
||||||
s.studio_desc as studio_desc,
|
|
||||||
si.storage_type as studio_storage_type,
|
|
||||||
si.image_path as studio_image_path
|
|
||||||
|
|
||||||
FROM titles as t
|
-- -- name: CreateReview :one
|
||||||
LEFT JOIN images as i ON (t.poster_id = i.id)
|
-- INSERT INTO reviews (user_id, title_id, image_ids, review_text, creation_date)
|
||||||
LEFT JOIN title_tags as tt ON (t.id = tt.title_id)
|
-- VALUES ($1, $2, $3, $4, $5)
|
||||||
LEFT JOIN tags as g ON (tt.tag_id = g.id)
|
-- RETURNING review_id, user_id, title_id, image_ids, review_text, creation_date;
|
||||||
LEFT JOIN studios as s ON (t.studio_id = s.id)
|
|
||||||
LEFT JOIN images as si ON (s.illust_id = si.id)
|
|
||||||
|
|
||||||
WHERE t.id = sqlc.arg('title_id')::bigint
|
-- -- name: UpdateReview :one
|
||||||
GROUP BY
|
-- UPDATE reviews
|
||||||
t.id, i.id, s.id, si.id;
|
-- SET
|
||||||
|
-- image_ids = COALESCE(sqlc.narg('image_ids'), image_ids),
|
||||||
|
-- review_text = COALESCE(sqlc.narg('review_text'), review_text)
|
||||||
|
-- WHERE review_id = sqlc.arg('review_id')
|
||||||
|
-- RETURNING *;
|
||||||
|
|
||||||
-- name: SearchTitles :many
|
-- -- name: DeleteReview :exec
|
||||||
SELECT
|
-- DELETE FROM reviews
|
||||||
t.id as id,
|
-- WHERE review_id = $1;
|
||||||
t.title_names as title_names,
|
|
||||||
t.poster_id as poster_id,
|
|
||||||
t.title_status as title_status,
|
|
||||||
t.rating as rating,
|
|
||||||
t.rating_count as rating_count,
|
|
||||||
t.release_year as release_year,
|
|
||||||
t.release_season as release_season,
|
|
||||||
t.season as season,
|
|
||||||
t.episodes_aired as episodes_aired,
|
|
||||||
t.episodes_all as episodes_all,
|
|
||||||
i.storage_type as title_storage_type,
|
|
||||||
i.image_path as title_image_path,
|
|
||||||
COALESCE(
|
|
||||||
jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL),
|
|
||||||
'[]'::jsonb
|
|
||||||
)::jsonb as tag_names,
|
|
||||||
s.studio_name as studio_name
|
|
||||||
|
|
||||||
FROM titles as t
|
-- -- name: ListReviewsByTitle :many
|
||||||
LEFT JOIN images as i ON (t.poster_id = i.id)
|
-- SELECT review_id, user_id, title_id, image_ids, review_text, creation_date
|
||||||
LEFT JOIN title_tags as tt ON (t.id = tt.title_id)
|
-- FROM reviews
|
||||||
LEFT JOIN tags as g ON (tt.tag_id = g.id)
|
-- WHERE title_id = $1
|
||||||
LEFT JOIN studios as s ON (t.studio_id = s.id)
|
-- ORDER BY creation_date DESC
|
||||||
|
-- LIMIT $2 OFFSET $3;
|
||||||
|
|
||||||
WHERE
|
-- -- name: ListReviewsByUser :many
|
||||||
CASE
|
-- SELECT review_id, user_id, title_id, image_ids, review_text, creation_date
|
||||||
WHEN sqlc.arg('forward')::boolean THEN
|
-- FROM reviews
|
||||||
-- forward: greater than cursor (next page)
|
-- WHERE user_id = $1
|
||||||
CASE sqlc.arg('sort_by')::text
|
-- ORDER BY creation_date DESC
|
||||||
WHEN 'year' THEN
|
-- LIMIT $2 OFFSET $3;
|
||||||
(sqlc.narg('cursor_year')::int IS NULL) OR
|
|
||||||
(t.release_year > sqlc.narg('cursor_year')::int) OR
|
|
||||||
(t.release_year = sqlc.narg('cursor_year')::int AND t.id > sqlc.narg('cursor_id')::bigint)
|
|
||||||
|
|
||||||
WHEN 'rating' THEN
|
-- -- name: GetUserTitle :one
|
||||||
(sqlc.narg('cursor_rating')::float IS NULL) OR
|
-- SELECT usertitle_id, user_id, title_id, status, rate, review_id
|
||||||
(t.rating > sqlc.narg('cursor_rating')::float) OR
|
-- FROM usertitles
|
||||||
(t.rating = sqlc.narg('cursor_rating')::float AND t.id > sqlc.narg('cursor_id')::bigint)
|
-- WHERE user_id = $1 AND title_id = $2;
|
||||||
|
|
||||||
WHEN 'id' THEN
|
-- -- name: ListUserTitles :many
|
||||||
(sqlc.narg('cursor_id')::bigint IS NULL) OR
|
-- SELECT usertitle_id, user_id, title_id, status, rate, review_id
|
||||||
(t.id > sqlc.narg('cursor_id')::bigint)
|
-- FROM usertitles
|
||||||
|
-- WHERE user_id = $1
|
||||||
|
-- ORDER BY usertitle_id
|
||||||
|
-- LIMIT $2 OFFSET $3;
|
||||||
|
|
||||||
ELSE true -- fallback
|
-- -- name: CreateUserTitle :one
|
||||||
END
|
-- INSERT INTO usertitles (user_id, title_id, status, rate, review_id)
|
||||||
|
-- VALUES ($1, $2, $3, $4, $5)
|
||||||
|
-- RETURNING usertitle_id, user_id, title_id, status, rate, review_id;
|
||||||
|
|
||||||
ELSE
|
-- -- name: UpdateUserTitle :one
|
||||||
-- backward: less than cursor (prev page)
|
-- UPDATE usertitles
|
||||||
CASE sqlc.arg('sort_by')::text
|
-- SET
|
||||||
WHEN 'year' THEN
|
-- status = COALESCE(sqlc.narg('status'), status),
|
||||||
(sqlc.narg('cursor_year')::int IS NULL) OR
|
-- rate = COALESCE(sqlc.narg('rate'), rate),
|
||||||
(t.release_year < sqlc.narg('cursor_year')::int) OR
|
-- review_id = COALESCE(sqlc.narg('review_id'), review_id)
|
||||||
(t.release_year = sqlc.narg('cursor_year')::int AND t.id < sqlc.narg('cursor_id')::bigint)
|
-- WHERE user_id = $1 AND title_id = $2
|
||||||
|
-- RETURNING *;
|
||||||
|
|
||||||
WHEN 'rating' THEN
|
-- -- name: DeleteUserTitle :exec
|
||||||
(sqlc.narg('cursor_rating')::float IS NULL) OR
|
-- DELETE FROM usertitles
|
||||||
(t.rating < sqlc.narg('cursor_rating')::float) OR
|
-- WHERE user_id = $1 AND ($2::int IS NULL OR title_id = $2);
|
||||||
(t.rating = sqlc.narg('cursor_rating')::float AND t.id < sqlc.narg('cursor_id')::bigint)
|
|
||||||
|
|
||||||
WHEN 'id' THEN
|
-- -- name: ListTags :many
|
||||||
(sqlc.narg('cursor_id')::bigint IS NULL) OR
|
-- SELECT tag_id, tag_names
|
||||||
(t.id < sqlc.narg('cursor_id')::bigint)
|
-- FROM tags
|
||||||
|
-- ORDER BY tag_id
|
||||||
ELSE true
|
-- LIMIT $1 OFFSET $2;
|
||||||
END
|
|
||||||
END
|
|
||||||
|
|
||||||
AND (
|
|
||||||
CASE
|
|
||||||
WHEN sqlc.narg('word')::text IS NOT NULL THEN
|
|
||||||
(
|
|
||||||
SELECT bool_and(
|
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM jsonb_each_text(t.title_names) AS t(key, val)
|
|
||||||
WHERE val ILIKE pattern
|
|
||||||
)
|
|
||||||
)
|
|
||||||
FROM unnest(
|
|
||||||
ARRAY(
|
|
||||||
SELECT '%' || trim(w) || '%'
|
|
||||||
FROM unnest(string_to_array(sqlc.narg('word')::text, ' ')) AS w
|
|
||||||
WHERE trim(w) <> ''
|
|
||||||
)
|
|
||||||
) AS pattern
|
|
||||||
)
|
|
||||||
ELSE true
|
|
||||||
END
|
|
||||||
)
|
|
||||||
|
|
||||||
AND (
|
|
||||||
sqlc.narg('title_statuses')::title_status_t[] IS NULL
|
|
||||||
OR array_length(sqlc.narg('title_statuses')::title_status_t[], 1) IS NULL
|
|
||||||
OR array_length(sqlc.narg('title_statuses')::title_status_t[], 1) = 0
|
|
||||||
OR t.title_status = ANY(sqlc.narg('title_statuses')::title_status_t[])
|
|
||||||
)
|
|
||||||
AND (sqlc.narg('rating')::float IS NULL OR t.rating >= sqlc.narg('rating')::float)
|
|
||||||
AND (sqlc.narg('release_year')::int IS NULL OR t.release_year = sqlc.narg('release_year')::int)
|
|
||||||
AND (sqlc.narg('release_season')::release_season_t IS NULL OR t.release_season = sqlc.narg('release_season')::release_season_t)
|
|
||||||
|
|
||||||
GROUP BY
|
|
||||||
t.id, i.id, s.id
|
|
||||||
|
|
||||||
ORDER BY
|
|
||||||
CASE WHEN sqlc.arg('forward')::boolean THEN
|
|
||||||
CASE
|
|
||||||
WHEN sqlc.arg('sort_by')::text = 'id' THEN t.id
|
|
||||||
WHEN sqlc.arg('sort_by')::text = 'year' THEN t.release_year
|
|
||||||
WHEN sqlc.arg('sort_by')::text = 'rating' THEN t.rating
|
|
||||||
END
|
|
||||||
END ASC,
|
|
||||||
CASE WHEN NOT sqlc.arg('forward')::boolean THEN
|
|
||||||
CASE
|
|
||||||
WHEN sqlc.arg('sort_by')::text = 'id' THEN t.id
|
|
||||||
WHEN sqlc.arg('sort_by')::text = 'year' THEN t.release_year
|
|
||||||
WHEN sqlc.arg('sort_by')::text = 'rating' THEN t.rating
|
|
||||||
END
|
|
||||||
END DESC,
|
|
||||||
|
|
||||||
CASE WHEN sqlc.arg('sort_by')::text <> 'id' THEN t.id END ASC
|
|
||||||
|
|
||||||
LIMIT COALESCE(sqlc.narg('limit')::int, 100); -- 100 is default limit
|
|
||||||
|
|
||||||
-- name: SearchUserTitles :many
|
|
||||||
SELECT
|
|
||||||
t.id as id,
|
|
||||||
t.title_names as title_names,
|
|
||||||
t.poster_id as poster_id,
|
|
||||||
t.title_status as title_status,
|
|
||||||
t.rating as rating,
|
|
||||||
t.rating_count as rating_count,
|
|
||||||
t.release_year as release_year,
|
|
||||||
t.release_season as release_season,
|
|
||||||
t.season as season,
|
|
||||||
t.episodes_aired as episodes_aired,
|
|
||||||
t.episodes_all as episodes_all,
|
|
||||||
u.user_id as user_id,
|
|
||||||
u.status as usertitle_status,
|
|
||||||
u.rate as user_rate,
|
|
||||||
u.review_id as review_id,
|
|
||||||
u.ctime as user_ctime,
|
|
||||||
i.storage_type as title_storage_type,
|
|
||||||
i.image_path as title_image_path,
|
|
||||||
COALESCE(
|
|
||||||
jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL),
|
|
||||||
'[]'::jsonb
|
|
||||||
)::jsonb as tag_names,
|
|
||||||
s.studio_name as studio_name
|
|
||||||
|
|
||||||
FROM usertitles as u
|
|
||||||
JOIN titles as t ON (u.title_id = t.id)
|
|
||||||
LEFT JOIN images as i ON (t.poster_id = i.id)
|
|
||||||
LEFT JOIN title_tags as tt ON (t.id = tt.title_id)
|
|
||||||
LEFT JOIN tags as g ON (tt.tag_id = g.id)
|
|
||||||
LEFT JOIN studios as s ON (t.studio_id = s.id)
|
|
||||||
|
|
||||||
WHERE
|
|
||||||
u.user_id = sqlc.arg('user_id')::bigint
|
|
||||||
AND
|
|
||||||
CASE
|
|
||||||
WHEN sqlc.arg('forward')::boolean THEN
|
|
||||||
-- forward: greater than cursor (next page)
|
|
||||||
CASE sqlc.arg('sort_by')::text
|
|
||||||
WHEN 'year' THEN
|
|
||||||
(sqlc.narg('cursor_year')::int IS NULL) OR
|
|
||||||
(t.release_year > sqlc.narg('cursor_year')::int) OR
|
|
||||||
(t.release_year = sqlc.narg('cursor_year')::int AND t.id > sqlc.narg('cursor_id')::bigint)
|
|
||||||
|
|
||||||
WHEN 'rating' THEN
|
|
||||||
(sqlc.narg('cursor_rating')::float IS NULL) OR
|
|
||||||
(t.rating > sqlc.narg('cursor_rating')::float) OR
|
|
||||||
(t.rating = sqlc.narg('cursor_rating')::float AND t.id > sqlc.narg('cursor_id')::bigint)
|
|
||||||
|
|
||||||
WHEN 'id' THEN
|
|
||||||
(sqlc.narg('cursor_id')::bigint IS NULL) OR
|
|
||||||
(t.id > sqlc.narg('cursor_id')::bigint)
|
|
||||||
|
|
||||||
ELSE true -- fallback
|
|
||||||
END
|
|
||||||
|
|
||||||
ELSE
|
|
||||||
-- backward: less than cursor (prev page)
|
|
||||||
CASE sqlc.arg('sort_by')::text
|
|
||||||
WHEN 'year' THEN
|
|
||||||
(sqlc.narg('cursor_year')::int IS NULL) OR
|
|
||||||
(t.release_year < sqlc.narg('cursor_year')::int) OR
|
|
||||||
(t.release_year = sqlc.narg('cursor_year')::int AND t.id < sqlc.narg('cursor_id')::bigint)
|
|
||||||
|
|
||||||
WHEN 'rating' THEN
|
|
||||||
(sqlc.narg('cursor_rating')::float IS NULL) OR
|
|
||||||
(t.rating < sqlc.narg('cursor_rating')::float) OR
|
|
||||||
(t.rating = sqlc.narg('cursor_rating')::float AND t.id < sqlc.narg('cursor_id')::bigint)
|
|
||||||
|
|
||||||
WHEN 'id' THEN
|
|
||||||
(sqlc.narg('cursor_id')::bigint IS NULL) OR
|
|
||||||
(t.id < sqlc.narg('cursor_id')::bigint)
|
|
||||||
|
|
||||||
ELSE true
|
|
||||||
END
|
|
||||||
END
|
|
||||||
|
|
||||||
AND (
|
|
||||||
CASE
|
|
||||||
WHEN sqlc.narg('word')::text IS NOT NULL THEN
|
|
||||||
(
|
|
||||||
SELECT bool_and(
|
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM jsonb_each_text(t.title_names) AS t(key, val)
|
|
||||||
WHERE val ILIKE pattern
|
|
||||||
)
|
|
||||||
)
|
|
||||||
FROM unnest(
|
|
||||||
ARRAY(
|
|
||||||
SELECT '%' || trim(w) || '%'
|
|
||||||
FROM unnest(string_to_array(sqlc.narg('word')::text, ' ')) AS w
|
|
||||||
WHERE trim(w) <> ''
|
|
||||||
)
|
|
||||||
) AS pattern
|
|
||||||
)
|
|
||||||
ELSE true
|
|
||||||
END
|
|
||||||
)
|
|
||||||
|
|
||||||
AND (
|
|
||||||
sqlc.narg('title_statuses')::title_status_t[] IS NULL
|
|
||||||
OR array_length(sqlc.narg('title_statuses')::title_status_t[], 1) IS NULL
|
|
||||||
OR array_length(sqlc.narg('title_statuses')::title_status_t[], 1) = 0
|
|
||||||
OR t.title_status = ANY(sqlc.narg('title_statuses')::title_status_t[])
|
|
||||||
)
|
|
||||||
AND (
|
|
||||||
sqlc.narg('usertitle_statuses')::usertitle_status_t[] IS NULL
|
|
||||||
OR array_length(sqlc.narg('usertitle_statuses')::usertitle_status_t[], 1) IS NULL
|
|
||||||
OR array_length(sqlc.narg('usertitle_statuses')::usertitle_status_t[], 1) = 0
|
|
||||||
OR u.status = ANY(sqlc.narg('usertitle_statuses')::usertitle_status_t[])
|
|
||||||
)
|
|
||||||
AND (sqlc.narg('rate')::int IS NULL OR u.rate >= sqlc.narg('rate')::int)
|
|
||||||
AND (sqlc.narg('rating')::float IS NULL OR t.rating >= sqlc.narg('rating')::float)
|
|
||||||
AND (sqlc.narg('release_year')::int IS NULL OR t.release_year = sqlc.narg('release_year')::int)
|
|
||||||
AND (sqlc.narg('release_season')::release_season_t IS NULL OR t.release_season = sqlc.narg('release_season')::release_season_t)
|
|
||||||
|
|
||||||
GROUP BY
|
|
||||||
t.id, u.user_id, u.status, u.rate, u.review_id, u.ctime, i.id, s.id
|
|
||||||
|
|
||||||
ORDER BY
|
|
||||||
CASE WHEN sqlc.arg('forward')::boolean THEN
|
|
||||||
CASE
|
|
||||||
WHEN sqlc.arg('sort_by')::text = 'id' THEN t.id
|
|
||||||
WHEN sqlc.arg('sort_by')::text = 'year' THEN t.release_year
|
|
||||||
WHEN sqlc.arg('sort_by')::text = 'rating' THEN t.rating
|
|
||||||
WHEN sqlc.arg('sort_by')::text = 'rate' THEN u.rate
|
|
||||||
END
|
|
||||||
END ASC,
|
|
||||||
CASE WHEN NOT sqlc.arg('forward')::boolean THEN
|
|
||||||
CASE
|
|
||||||
WHEN sqlc.arg('sort_by')::text = 'id' THEN t.id
|
|
||||||
WHEN sqlc.arg('sort_by')::text = 'year' THEN t.release_year
|
|
||||||
WHEN sqlc.arg('sort_by')::text = 'rating' THEN t.rating
|
|
||||||
WHEN sqlc.arg('sort_by')::text = 'rate' THEN u.rate
|
|
||||||
END
|
|
||||||
END DESC,
|
|
||||||
|
|
||||||
CASE WHEN sqlc.arg('sort_by')::text <> 'id' THEN t.id END ASC
|
|
||||||
|
|
||||||
LIMIT COALESCE(sqlc.narg('limit')::int, 100); -- 100 is default limit
|
|
||||||
|
|
||||||
-- name: GetReviewByID :one
|
|
||||||
SELECT *
|
|
||||||
FROM reviews
|
|
||||||
WHERE review_id = sqlc.arg('review_id')::bigint;
|
|
||||||
|
|
||||||
-- name: InsertUserTitle :one
|
|
||||||
INSERT INTO usertitles (user_id, title_id, status, rate, review_id, ctime)
|
|
||||||
VALUES (
|
|
||||||
sqlc.arg('user_id')::bigint,
|
|
||||||
sqlc.arg('title_id')::bigint,
|
|
||||||
sqlc.arg('status')::usertitle_status_t,
|
|
||||||
sqlc.narg('rate')::int,
|
|
||||||
sqlc.narg('review_id')::bigint,
|
|
||||||
sqlc.narg('ftime')::timestamptz
|
|
||||||
)
|
|
||||||
RETURNING user_id, title_id, status, rate, review_id, ctime;
|
|
||||||
|
|
||||||
-- name: UpdateUserTitle :one
|
|
||||||
-- Fails with sql.ErrNoRows if (user_id, title_id) not found
|
|
||||||
UPDATE usertitles
|
|
||||||
SET
|
|
||||||
status = COALESCE(sqlc.narg('status')::usertitle_status_t, status),
|
|
||||||
rate = COALESCE(sqlc.narg('rate')::int, rate),
|
|
||||||
ctime = COALESCE(sqlc.narg('ftime')::timestamptz, ctime)
|
|
||||||
WHERE
|
|
||||||
user_id = sqlc.arg('user_id')
|
|
||||||
AND title_id = sqlc.arg('title_id')
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: DeleteUserTitle :one
|
|
||||||
DELETE FROM usertitles
|
|
||||||
WHERE user_id = sqlc.arg('user_id')
|
|
||||||
AND title_id = sqlc.arg('title_id')
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: GetUserTitleByID :one
|
|
||||||
SELECT
|
|
||||||
ut.*
|
|
||||||
FROM usertitles as ut
|
|
||||||
WHERE ut.title_id = sqlc.arg('title_id')::bigint AND ut.user_id = sqlc.arg('user_id')::bigint;
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
package rmq
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
oapi "nyanimedb/api"
|
|
||||||
|
|
||||||
amqp "github.com/rabbitmq/amqp091-go"
|
|
||||||
)
|
|
||||||
|
|
||||||
const RPCQueueName = "anime_import_rpc"
|
|
||||||
|
|
||||||
// RabbitRequest не меняем
|
|
||||||
type RabbitRequest struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Statuses []oapi.TitleStatus `json:"statuses,omitempty"`
|
|
||||||
Rating float64 `json:"rating,omitempty"`
|
|
||||||
Year int32 `json:"year,omitempty"`
|
|
||||||
Season oapi.ReleaseSeason `json:"season,omitempty"`
|
|
||||||
Timestamp time.Time `json:"timestamp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RPCClient struct {
|
|
||||||
conn *amqp.Connection
|
|
||||||
timeout time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRPCClient(conn *amqp.Connection, timeout time.Duration) *RPCClient {
|
|
||||||
return &RPCClient{conn: conn, timeout: timeout}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *RPCClient) Call(
|
|
||||||
ctx context.Context,
|
|
||||||
request RabbitRequest,
|
|
||||||
replyPayload any,
|
|
||||||
) error {
|
|
||||||
|
|
||||||
// 1. Канал для запроса и ответа
|
|
||||||
ch, err := c.conn.Channel()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("channel: %w", err)
|
|
||||||
}
|
|
||||||
defer ch.Close()
|
|
||||||
|
|
||||||
// 2. Декларируем фиксированную очередь RPC (идемпотентно)
|
|
||||||
_, err = ch.QueueDeclare(
|
|
||||||
RPCQueueName,
|
|
||||||
true, // durable
|
|
||||||
false, // auto-delete
|
|
||||||
false, // exclusive
|
|
||||||
false, // no-wait
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("declare rpc queue: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Создаём временную очередь ДЛЯ ОТВЕТА
|
|
||||||
replyQueue, err := ch.QueueDeclare(
|
|
||||||
"",
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("declare reply queue: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Подписываемся на очередь ответов
|
|
||||||
msgs, err := ch.Consume(
|
|
||||||
replyQueue.Name,
|
|
||||||
"",
|
|
||||||
true, // auto-ack
|
|
||||||
true, // exclusive
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("consume reply: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// correlation ID
|
|
||||||
corrID := fmt.Sprintf("%d", time.Now().UnixNano())
|
|
||||||
|
|
||||||
// 5. сериализация запроса
|
|
||||||
body, err := json.Marshal(request)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("marshal request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Публикация RPC-запроса
|
|
||||||
err = ch.Publish(
|
|
||||||
"",
|
|
||||||
RPCQueueName, // ← фиксированная очередь!
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
amqp.Publishing{
|
|
||||||
ContentType: "application/json",
|
|
||||||
CorrelationId: corrID,
|
|
||||||
ReplyTo: replyQueue.Name,
|
|
||||||
Timestamp: time.Now(),
|
|
||||||
Body: body,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("publish: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. Ждём ответ с таймаутом
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, c.timeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case msg := <-msgs:
|
|
||||||
if msg.CorrelationId == corrID {
|
|
||||||
return json.Unmarshal(msg.Body, replyPayload)
|
|
||||||
}
|
|
||||||
case <-timeoutCtx.Done():
|
|
||||||
return fmt.Errorf("rpc timeout: %w", timeoutCtx.Err())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,10 +2,11 @@ package main
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Mode string
|
Mode string
|
||||||
ServiceAddress string `toml:"ServiceAddress" env:"SERVICE_ADDRESS"`
|
|
||||||
DdUrl string `toml:"DbUrl" env:"DATABASE_URL"`
|
|
||||||
JwtPrivateKey string `toml:"JwtPrivateKey" env:"JWT_PRIVATE_KEY"`
|
|
||||||
LogLevel string `toml:"LogLevel" env:"LOG_LEVEL"`
|
LogLevel string `toml:"LogLevel" env:"LOG_LEVEL"`
|
||||||
RmqURL string `toml:"RabbitMQUrl" env:"RABBITMQ_URL"`
|
}
|
||||||
AuthEnabled string `toml:"AuthEnabled" env:"AUTH_ENABLED"`
|
|
||||||
|
type Item struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,15 +19,6 @@ server {
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_cache_bypass $http_upgrade;
|
proxy_cache_bypass $http_upgrade;
|
||||||
}
|
}
|
||||||
location /auth/ {
|
|
||||||
rewrite ^/auth/(.*)$ /$1 break;
|
|
||||||
proxy_pass http://nyanimedb-auth:8082/;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
}
|
|
||||||
#error_page 404 /404.html;
|
#error_page 404 /404.html;
|
||||||
|
|
||||||
error_page 500 502 503 504 /50x.html;
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
|
|
||||||
944
modules/frontend/package-lock.json
generated
944
modules/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -10,15 +10,10 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.9",
|
|
||||||
"@heroicons/react": "^2.2.0",
|
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-cookie": "^8.0.1",
|
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-router-dom": "^7.9.4",
|
"react-router-dom": "^7.9.4"
|
||||||
"tailwindcss": "^4.1.17"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.36.0",
|
"@eslint/js": "^9.36.0",
|
||||||
|
|
@ -34,8 +29,5 @@
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.45.0",
|
"typescript-eslint": "^8.45.0",
|
||||||
"vite": "^7.1.7"
|
"vite": "^7.1.7"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "20.x"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
#root {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
|
}
|
||||||
|
.logo.react:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
a:nth-of-type(2) .logo {
|
||||||
|
animation: logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
@ -1,43 +1,15 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||||
import UserPage from "./pages/UserPage/UserPage";
|
import UserPage from "./components/UserPage/UserPage";
|
||||||
import TitlesPage from "./pages/TitlesPage/TitlesPage";
|
|
||||||
import TitlePage from "./pages/TitlePage/TitlePage";
|
|
||||||
import { LoginPage } from "./pages/LoginPage/LoginPage";
|
|
||||||
import { Header } from "./components/Header/Header";
|
|
||||||
|
|
||||||
// import { OpenAPI } from "./api";
|
|
||||||
|
|
||||||
// OpenAPI.WITH_CREDENTIALS = true
|
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const username = localStorage.getItem("username") || undefined;
|
|
||||||
const userId = localStorage.getItem("userId");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<Header username={username} />
|
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* auth */}
|
|
||||||
<Route path="/login" element={<LoginPage />} />
|
|
||||||
<Route path="/signup" element={<LoginPage />} />
|
|
||||||
{/*<Route path="/signup" element={<LoginPage />} />*/}
|
|
||||||
|
|
||||||
{/* users */}
|
|
||||||
{/*<Route path="/users" element={<UsersPage />} />*/}
|
|
||||||
<Route path="/users/:id" element={<UserPage />} />
|
<Route path="/users/:id" element={<UserPage />} />
|
||||||
<Route
|
|
||||||
path="/profile"
|
|
||||||
element={userId ? <UserPage userId={userId} /> : <LoginPage />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* titles */}
|
|
||||||
<Route path="/titles" element={<TitlesPage />} />
|
|
||||||
<Route path="/titles/:id" element={<TitlePage />} />
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
import { type ClientOptions, type Config, createClient, createConfig } from './client';
|
|
||||||
import type { ClientOptions as ClientOptions2 } from './types.gen';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `createClientConfig()` function will be called on client initialization
|
|
||||||
* and the returned object will become the client's initial configuration.
|
|
||||||
*
|
|
||||||
* You may want to initialize your client this way instead of calling
|
|
||||||
* `setConfig()`. This is useful for example if you're using Next.js
|
|
||||||
* to ensure your client always has the correct values.
|
|
||||||
*/
|
|
||||||
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>;
|
|
||||||
|
|
||||||
export const client = createClient(createConfig<ClientOptions2>({ baseUrl: '/api/v1' }));
|
|
||||||
|
|
@ -1,301 +0,0 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
import { createSseClient } from '../core/serverSentEvents.gen';
|
|
||||||
import type { HttpMethod } from '../core/types.gen';
|
|
||||||
import { getValidRequestBody } from '../core/utils.gen';
|
|
||||||
import type {
|
|
||||||
Client,
|
|
||||||
Config,
|
|
||||||
RequestOptions,
|
|
||||||
ResolvedRequestOptions,
|
|
||||||
} from './types.gen';
|
|
||||||
import {
|
|
||||||
buildUrl,
|
|
||||||
createConfig,
|
|
||||||
createInterceptors,
|
|
||||||
getParseAs,
|
|
||||||
mergeConfigs,
|
|
||||||
mergeHeaders,
|
|
||||||
setAuthParams,
|
|
||||||
} from './utils.gen';
|
|
||||||
|
|
||||||
type ReqInit = Omit<RequestInit, 'body' | 'headers'> & {
|
|
||||||
body?: any;
|
|
||||||
headers: ReturnType<typeof mergeHeaders>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createClient = (config: Config = {}): Client => {
|
|
||||||
let _config = mergeConfigs(createConfig(), config);
|
|
||||||
|
|
||||||
const getConfig = (): Config => ({ ..._config });
|
|
||||||
|
|
||||||
const setConfig = (config: Config): Config => {
|
|
||||||
_config = mergeConfigs(_config, config);
|
|
||||||
return getConfig();
|
|
||||||
};
|
|
||||||
|
|
||||||
const interceptors = createInterceptors<
|
|
||||||
Request,
|
|
||||||
Response,
|
|
||||||
unknown,
|
|
||||||
ResolvedRequestOptions
|
|
||||||
>();
|
|
||||||
|
|
||||||
const beforeRequest = async (options: RequestOptions) => {
|
|
||||||
const opts = {
|
|
||||||
..._config,
|
|
||||||
...options,
|
|
||||||
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
|
|
||||||
headers: mergeHeaders(_config.headers, options.headers),
|
|
||||||
serializedBody: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (opts.security) {
|
|
||||||
await setAuthParams({
|
|
||||||
...opts,
|
|
||||||
security: opts.security,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.requestValidator) {
|
|
||||||
await opts.requestValidator(opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.body !== undefined && opts.bodySerializer) {
|
|
||||||
opts.serializedBody = opts.bodySerializer(opts.body);
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove Content-Type header if body is empty to avoid sending invalid requests
|
|
||||||
if (opts.body === undefined || opts.serializedBody === '') {
|
|
||||||
opts.headers.delete('Content-Type');
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = buildUrl(opts);
|
|
||||||
|
|
||||||
return { opts, url };
|
|
||||||
};
|
|
||||||
|
|
||||||
const request: Client['request'] = async (options) => {
|
|
||||||
// @ts-expect-error
|
|
||||||
const { opts, url } = await beforeRequest(options);
|
|
||||||
const requestInit: ReqInit = {
|
|
||||||
redirect: 'follow',
|
|
||||||
...opts,
|
|
||||||
body: getValidRequestBody(opts),
|
|
||||||
};
|
|
||||||
|
|
||||||
let request = new Request(url, requestInit);
|
|
||||||
|
|
||||||
for (const fn of interceptors.request.fns) {
|
|
||||||
if (fn) {
|
|
||||||
request = await fn(request, opts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch must be assigned here, otherwise it would throw the error:
|
|
||||||
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
|
|
||||||
const _fetch = opts.fetch!;
|
|
||||||
let response: Response;
|
|
||||||
|
|
||||||
try {
|
|
||||||
response = await _fetch(request);
|
|
||||||
} catch (error) {
|
|
||||||
// Handle fetch exceptions (AbortError, network errors, etc.)
|
|
||||||
let finalError = error;
|
|
||||||
|
|
||||||
for (const fn of interceptors.error.fns) {
|
|
||||||
if (fn) {
|
|
||||||
finalError = (await fn(
|
|
||||||
error,
|
|
||||||
undefined as any,
|
|
||||||
request,
|
|
||||||
opts,
|
|
||||||
)) as unknown;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finalError = finalError || ({} as unknown);
|
|
||||||
|
|
||||||
if (opts.throwOnError) {
|
|
||||||
throw finalError;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return error response
|
|
||||||
return opts.responseStyle === 'data'
|
|
||||||
? undefined
|
|
||||||
: {
|
|
||||||
error: finalError,
|
|
||||||
request,
|
|
||||||
response: undefined as any,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const fn of interceptors.response.fns) {
|
|
||||||
if (fn) {
|
|
||||||
response = await fn(response, request, opts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = {
|
|
||||||
request,
|
|
||||||
response,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const parseAs =
|
|
||||||
(opts.parseAs === 'auto'
|
|
||||||
? getParseAs(response.headers.get('Content-Type'))
|
|
||||||
: opts.parseAs) ?? 'json';
|
|
||||||
|
|
||||||
if (
|
|
||||||
response.status === 204 ||
|
|
||||||
response.headers.get('Content-Length') === '0'
|
|
||||||
) {
|
|
||||||
let emptyData: any;
|
|
||||||
switch (parseAs) {
|
|
||||||
case 'arrayBuffer':
|
|
||||||
case 'blob':
|
|
||||||
case 'text':
|
|
||||||
emptyData = await response[parseAs]();
|
|
||||||
break;
|
|
||||||
case 'formData':
|
|
||||||
emptyData = new FormData();
|
|
||||||
break;
|
|
||||||
case 'stream':
|
|
||||||
emptyData = response.body;
|
|
||||||
break;
|
|
||||||
case 'json':
|
|
||||||
default:
|
|
||||||
emptyData = {};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return opts.responseStyle === 'data'
|
|
||||||
? emptyData
|
|
||||||
: {
|
|
||||||
data: emptyData,
|
|
||||||
...result,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let data: any;
|
|
||||||
switch (parseAs) {
|
|
||||||
case 'arrayBuffer':
|
|
||||||
case 'blob':
|
|
||||||
case 'formData':
|
|
||||||
case 'json':
|
|
||||||
case 'text':
|
|
||||||
data = await response[parseAs]();
|
|
||||||
break;
|
|
||||||
case 'stream':
|
|
||||||
return opts.responseStyle === 'data'
|
|
||||||
? response.body
|
|
||||||
: {
|
|
||||||
data: response.body,
|
|
||||||
...result,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parseAs === 'json') {
|
|
||||||
if (opts.responseValidator) {
|
|
||||||
await opts.responseValidator(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.responseTransformer) {
|
|
||||||
data = await opts.responseTransformer(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return opts.responseStyle === 'data'
|
|
||||||
? data
|
|
||||||
: {
|
|
||||||
data,
|
|
||||||
...result,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const textError = await response.text();
|
|
||||||
let jsonError: unknown;
|
|
||||||
|
|
||||||
try {
|
|
||||||
jsonError = JSON.parse(textError);
|
|
||||||
} catch {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
|
|
||||||
const error = jsonError ?? textError;
|
|
||||||
let finalError = error;
|
|
||||||
|
|
||||||
for (const fn of interceptors.error.fns) {
|
|
||||||
if (fn) {
|
|
||||||
finalError = (await fn(error, response, request, opts)) as string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finalError = finalError || ({} as string);
|
|
||||||
|
|
||||||
if (opts.throwOnError) {
|
|
||||||
throw finalError;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: we probably want to return error and improve types
|
|
||||||
return opts.responseStyle === 'data'
|
|
||||||
? undefined
|
|
||||||
: {
|
|
||||||
error: finalError,
|
|
||||||
...result,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeMethodFn =
|
|
||||||
(method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
|
|
||||||
request({ ...options, method });
|
|
||||||
|
|
||||||
const makeSseFn =
|
|
||||||
(method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
|
|
||||||
const { opts, url } = await beforeRequest(options);
|
|
||||||
return createSseClient({
|
|
||||||
...opts,
|
|
||||||
body: opts.body as BodyInit | null | undefined,
|
|
||||||
headers: opts.headers as unknown as Record<string, string>,
|
|
||||||
method,
|
|
||||||
onRequest: async (url, init) => {
|
|
||||||
let request = new Request(url, init);
|
|
||||||
for (const fn of interceptors.request.fns) {
|
|
||||||
if (fn) {
|
|
||||||
request = await fn(request, opts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return request;
|
|
||||||
},
|
|
||||||
url,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
buildUrl,
|
|
||||||
connect: makeMethodFn('CONNECT'),
|
|
||||||
delete: makeMethodFn('DELETE'),
|
|
||||||
get: makeMethodFn('GET'),
|
|
||||||
getConfig,
|
|
||||||
head: makeMethodFn('HEAD'),
|
|
||||||
interceptors,
|
|
||||||
options: makeMethodFn('OPTIONS'),
|
|
||||||
patch: makeMethodFn('PATCH'),
|
|
||||||
post: makeMethodFn('POST'),
|
|
||||||
put: makeMethodFn('PUT'),
|
|
||||||
request,
|
|
||||||
setConfig,
|
|
||||||
sse: {
|
|
||||||
connect: makeSseFn('CONNECT'),
|
|
||||||
delete: makeSseFn('DELETE'),
|
|
||||||
get: makeSseFn('GET'),
|
|
||||||
head: makeSseFn('HEAD'),
|
|
||||||
options: makeSseFn('OPTIONS'),
|
|
||||||
patch: makeSseFn('PATCH'),
|
|
||||||
post: makeSseFn('POST'),
|
|
||||||
put: makeSseFn('PUT'),
|
|
||||||
trace: makeSseFn('TRACE'),
|
|
||||||
},
|
|
||||||
trace: makeMethodFn('TRACE'),
|
|
||||||
} as Client;
|
|
||||||
};
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
export type { Auth } from '../core/auth.gen';
|
|
||||||
export type { QuerySerializerOptions } from '../core/bodySerializer.gen';
|
|
||||||
export {
|
|
||||||
formDataBodySerializer,
|
|
||||||
jsonBodySerializer,
|
|
||||||
urlSearchParamsBodySerializer,
|
|
||||||
} from '../core/bodySerializer.gen';
|
|
||||||
export { buildClientParams } from '../core/params.gen';
|
|
||||||
export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
|
|
||||||
export { createClient } from './client.gen';
|
|
||||||
export type {
|
|
||||||
Client,
|
|
||||||
ClientOptions,
|
|
||||||
Config,
|
|
||||||
CreateClientConfig,
|
|
||||||
Options,
|
|
||||||
RequestOptions,
|
|
||||||
RequestResult,
|
|
||||||
ResolvedRequestOptions,
|
|
||||||
ResponseStyle,
|
|
||||||
TDataShape,
|
|
||||||
} from './types.gen';
|
|
||||||
export { createConfig, mergeHeaders } from './utils.gen';
|
|
||||||
|
|
@ -1,241 +0,0 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
import type { Auth } from '../core/auth.gen';
|
|
||||||
import type {
|
|
||||||
ServerSentEventsOptions,
|
|
||||||
ServerSentEventsResult,
|
|
||||||
} from '../core/serverSentEvents.gen';
|
|
||||||
import type {
|
|
||||||
Client as CoreClient,
|
|
||||||
Config as CoreConfig,
|
|
||||||
} from '../core/types.gen';
|
|
||||||
import type { Middleware } from './utils.gen';
|
|
||||||
|
|
||||||
export type ResponseStyle = 'data' | 'fields';
|
|
||||||
|
|
||||||
export interface Config<T extends ClientOptions = ClientOptions>
|
|
||||||
extends Omit<RequestInit, 'body' | 'headers' | 'method'>,
|
|
||||||
CoreConfig {
|
|
||||||
/**
|
|
||||||
* Base URL for all requests made by this client.
|
|
||||||
*/
|
|
||||||
baseUrl?: T['baseUrl'];
|
|
||||||
/**
|
|
||||||
* Fetch API implementation. You can use this option to provide a custom
|
|
||||||
* fetch instance.
|
|
||||||
*
|
|
||||||
* @default globalThis.fetch
|
|
||||||
*/
|
|
||||||
fetch?: typeof fetch;
|
|
||||||
/**
|
|
||||||
* Please don't use the Fetch client for Next.js applications. The `next`
|
|
||||||
* options won't have any effect.
|
|
||||||
*
|
|
||||||
* Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead.
|
|
||||||
*/
|
|
||||||
next?: never;
|
|
||||||
/**
|
|
||||||
* Return the response data parsed in a specified format. By default, `auto`
|
|
||||||
* will infer the appropriate method from the `Content-Type` response header.
|
|
||||||
* You can override this behavior with any of the {@link Body} methods.
|
|
||||||
* Select `stream` if you don't want to parse response data at all.
|
|
||||||
*
|
|
||||||
* @default 'auto'
|
|
||||||
*/
|
|
||||||
parseAs?:
|
|
||||||
| 'arrayBuffer'
|
|
||||||
| 'auto'
|
|
||||||
| 'blob'
|
|
||||||
| 'formData'
|
|
||||||
| 'json'
|
|
||||||
| 'stream'
|
|
||||||
| 'text';
|
|
||||||
/**
|
|
||||||
* Should we return only data or multiple fields (data, error, response, etc.)?
|
|
||||||
*
|
|
||||||
* @default 'fields'
|
|
||||||
*/
|
|
||||||
responseStyle?: ResponseStyle;
|
|
||||||
/**
|
|
||||||
* Throw an error instead of returning it in the response?
|
|
||||||
*
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
throwOnError?: T['throwOnError'];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RequestOptions<
|
|
||||||
TData = unknown,
|
|
||||||
TResponseStyle extends ResponseStyle = 'fields',
|
|
||||||
ThrowOnError extends boolean = boolean,
|
|
||||||
Url extends string = string,
|
|
||||||
> extends Config<{
|
|
||||||
responseStyle: TResponseStyle;
|
|
||||||
throwOnError: ThrowOnError;
|
|
||||||
}>,
|
|
||||||
Pick<
|
|
||||||
ServerSentEventsOptions<TData>,
|
|
||||||
| 'onSseError'
|
|
||||||
| 'onSseEvent'
|
|
||||||
| 'sseDefaultRetryDelay'
|
|
||||||
| 'sseMaxRetryAttempts'
|
|
||||||
| 'sseMaxRetryDelay'
|
|
||||||
> {
|
|
||||||
/**
|
|
||||||
* Any body that you want to add to your request.
|
|
||||||
*
|
|
||||||
* {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
|
|
||||||
*/
|
|
||||||
body?: unknown;
|
|
||||||
path?: Record<string, unknown>;
|
|
||||||
query?: Record<string, unknown>;
|
|
||||||
/**
|
|
||||||
* Security mechanism(s) to use for the request.
|
|
||||||
*/
|
|
||||||
security?: ReadonlyArray<Auth>;
|
|
||||||
url: Url;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ResolvedRequestOptions<
|
|
||||||
TResponseStyle extends ResponseStyle = 'fields',
|
|
||||||
ThrowOnError extends boolean = boolean,
|
|
||||||
Url extends string = string,
|
|
||||||
> extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
|
|
||||||
serializedBody?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RequestResult<
|
|
||||||
TData = unknown,
|
|
||||||
TError = unknown,
|
|
||||||
ThrowOnError extends boolean = boolean,
|
|
||||||
TResponseStyle extends ResponseStyle = 'fields',
|
|
||||||
> = ThrowOnError extends true
|
|
||||||
? Promise<
|
|
||||||
TResponseStyle extends 'data'
|
|
||||||
? TData extends Record<string, unknown>
|
|
||||||
? TData[keyof TData]
|
|
||||||
: TData
|
|
||||||
: {
|
|
||||||
data: TData extends Record<string, unknown>
|
|
||||||
? TData[keyof TData]
|
|
||||||
: TData;
|
|
||||||
request: Request;
|
|
||||||
response: Response;
|
|
||||||
}
|
|
||||||
>
|
|
||||||
: Promise<
|
|
||||||
TResponseStyle extends 'data'
|
|
||||||
?
|
|
||||||
| (TData extends Record<string, unknown>
|
|
||||||
? TData[keyof TData]
|
|
||||||
: TData)
|
|
||||||
| undefined
|
|
||||||
: (
|
|
||||||
| {
|
|
||||||
data: TData extends Record<string, unknown>
|
|
||||||
? TData[keyof TData]
|
|
||||||
: TData;
|
|
||||||
error: undefined;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
data: undefined;
|
|
||||||
error: TError extends Record<string, unknown>
|
|
||||||
? TError[keyof TError]
|
|
||||||
: TError;
|
|
||||||
}
|
|
||||||
) & {
|
|
||||||
request: Request;
|
|
||||||
response: Response;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
export interface ClientOptions {
|
|
||||||
baseUrl?: string;
|
|
||||||
responseStyle?: ResponseStyle;
|
|
||||||
throwOnError?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
type MethodFn = <
|
|
||||||
TData = unknown,
|
|
||||||
TError = unknown,
|
|
||||||
ThrowOnError extends boolean = false,
|
|
||||||
TResponseStyle extends ResponseStyle = 'fields',
|
|
||||||
>(
|
|
||||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
|
|
||||||
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
|
|
||||||
|
|
||||||
type SseFn = <
|
|
||||||
TData = unknown,
|
|
||||||
TError = unknown,
|
|
||||||
ThrowOnError extends boolean = false,
|
|
||||||
TResponseStyle extends ResponseStyle = 'fields',
|
|
||||||
>(
|
|
||||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
|
|
||||||
) => Promise<ServerSentEventsResult<TData, TError>>;
|
|
||||||
|
|
||||||
type RequestFn = <
|
|
||||||
TData = unknown,
|
|
||||||
TError = unknown,
|
|
||||||
ThrowOnError extends boolean = false,
|
|
||||||
TResponseStyle extends ResponseStyle = 'fields',
|
|
||||||
>(
|
|
||||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'> &
|
|
||||||
Pick<
|
|
||||||
Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>,
|
|
||||||
'method'
|
|
||||||
>,
|
|
||||||
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
|
|
||||||
|
|
||||||
type BuildUrlFn = <
|
|
||||||
TData extends {
|
|
||||||
body?: unknown;
|
|
||||||
path?: Record<string, unknown>;
|
|
||||||
query?: Record<string, unknown>;
|
|
||||||
url: string;
|
|
||||||
},
|
|
||||||
>(
|
|
||||||
options: TData & Options<TData>,
|
|
||||||
) => string;
|
|
||||||
|
|
||||||
export type Client = CoreClient<
|
|
||||||
RequestFn,
|
|
||||||
Config,
|
|
||||||
MethodFn,
|
|
||||||
BuildUrlFn,
|
|
||||||
SseFn
|
|
||||||
> & {
|
|
||||||
interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `createClientConfig()` function will be called on client initialization
|
|
||||||
* and the returned object will become the client's initial configuration.
|
|
||||||
*
|
|
||||||
* You may want to initialize your client this way instead of calling
|
|
||||||
* `setConfig()`. This is useful for example if you're using Next.js
|
|
||||||
* to ensure your client always has the correct values.
|
|
||||||
*/
|
|
||||||
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
|
|
||||||
override?: Config<ClientOptions & T>,
|
|
||||||
) => Config<Required<ClientOptions> & T>;
|
|
||||||
|
|
||||||
export interface TDataShape {
|
|
||||||
body?: unknown;
|
|
||||||
headers?: unknown;
|
|
||||||
path?: unknown;
|
|
||||||
query?: unknown;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
|
|
||||||
|
|
||||||
export type Options<
|
|
||||||
TData extends TDataShape = TDataShape,
|
|
||||||
ThrowOnError extends boolean = boolean,
|
|
||||||
TResponse = unknown,
|
|
||||||
TResponseStyle extends ResponseStyle = 'fields',
|
|
||||||
> = OmitKeys<
|
|
||||||
RequestOptions<TResponse, TResponseStyle, ThrowOnError>,
|
|
||||||
'body' | 'path' | 'query' | 'url'
|
|
||||||
> &
|
|
||||||
([TData] extends [never] ? unknown : Omit<TData, 'url'>);
|
|
||||||
|
|
@ -1,332 +0,0 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
import { getAuthToken } from '../core/auth.gen';
|
|
||||||
import type { QuerySerializerOptions } from '../core/bodySerializer.gen';
|
|
||||||
import { jsonBodySerializer } from '../core/bodySerializer.gen';
|
|
||||||
import {
|
|
||||||
serializeArrayParam,
|
|
||||||
serializeObjectParam,
|
|
||||||
serializePrimitiveParam,
|
|
||||||
} from '../core/pathSerializer.gen';
|
|
||||||
import { getUrl } from '../core/utils.gen';
|
|
||||||
import type { Client, ClientOptions, Config, RequestOptions } from './types.gen';
|
|
||||||
|
|
||||||
export const createQuerySerializer = <T = unknown>({
|
|
||||||
parameters = {},
|
|
||||||
...args
|
|
||||||
}: QuerySerializerOptions = {}) => {
|
|
||||||
const querySerializer = (queryParams: T) => {
|
|
||||||
const search: string[] = [];
|
|
||||||
if (queryParams && typeof queryParams === 'object') {
|
|
||||||
for (const name in queryParams) {
|
|
||||||
const value = queryParams[name];
|
|
||||||
|
|
||||||
if (value === undefined || value === null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = parameters[name] || args;
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
const serializedArray = serializeArrayParam({
|
|
||||||
allowReserved: options.allowReserved,
|
|
||||||
explode: true,
|
|
||||||
name,
|
|
||||||
style: 'form',
|
|
||||||
value,
|
|
||||||
...options.array,
|
|
||||||
});
|
|
||||||
if (serializedArray) search.push(serializedArray);
|
|
||||||
} else if (typeof value === 'object') {
|
|
||||||
const serializedObject = serializeObjectParam({
|
|
||||||
allowReserved: options.allowReserved,
|
|
||||||
explode: true,
|
|
||||||
name,
|
|
||||||
style: 'deepObject',
|
|
||||||
value: value as Record<string, unknown>,
|
|
||||||
...options.object,
|
|
||||||
});
|
|
||||||
if (serializedObject) search.push(serializedObject);
|
|
||||||
} else {
|
|
||||||
const serializedPrimitive = serializePrimitiveParam({
|
|
||||||
allowReserved: options.allowReserved,
|
|
||||||
name,
|
|
||||||
value: value as string,
|
|
||||||
});
|
|
||||||
if (serializedPrimitive) search.push(serializedPrimitive);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return search.join('&');
|
|
||||||
};
|
|
||||||
return querySerializer;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Infers parseAs value from provided Content-Type header.
|
|
||||||
*/
|
|
||||||
export const getParseAs = (
|
|
||||||
contentType: string | null,
|
|
||||||
): Exclude<Config['parseAs'], 'auto'> => {
|
|
||||||
if (!contentType) {
|
|
||||||
// If no Content-Type header is provided, the best we can do is return the raw response body,
|
|
||||||
// which is effectively the same as the 'stream' option.
|
|
||||||
return 'stream';
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleanContent = contentType.split(';')[0]?.trim();
|
|
||||||
|
|
||||||
if (!cleanContent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
cleanContent.startsWith('application/json') ||
|
|
||||||
cleanContent.endsWith('+json')
|
|
||||||
) {
|
|
||||||
return 'json';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleanContent === 'multipart/form-data') {
|
|
||||||
return 'formData';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
['application/', 'audio/', 'image/', 'video/'].some((type) =>
|
|
||||||
cleanContent.startsWith(type),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return 'blob';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleanContent.startsWith('text/')) {
|
|
||||||
return 'text';
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkForExistence = (
|
|
||||||
options: Pick<RequestOptions, 'auth' | 'query'> & {
|
|
||||||
headers: Headers;
|
|
||||||
},
|
|
||||||
name?: string,
|
|
||||||
): boolean => {
|
|
||||||
if (!name) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
options.headers.has(name) ||
|
|
||||||
options.query?.[name] ||
|
|
||||||
options.headers.get('Cookie')?.includes(`${name}=`)
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setAuthParams = async ({
|
|
||||||
security,
|
|
||||||
...options
|
|
||||||
}: Pick<Required<RequestOptions>, 'security'> &
|
|
||||||
Pick<RequestOptions, 'auth' | 'query'> & {
|
|
||||||
headers: Headers;
|
|
||||||
}) => {
|
|
||||||
for (const auth of security) {
|
|
||||||
if (checkForExistence(options, auth.name)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = await getAuthToken(auth, options.auth);
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = auth.name ?? 'Authorization';
|
|
||||||
|
|
||||||
switch (auth.in) {
|
|
||||||
case 'query':
|
|
||||||
if (!options.query) {
|
|
||||||
options.query = {};
|
|
||||||
}
|
|
||||||
options.query[name] = token;
|
|
||||||
break;
|
|
||||||
case 'cookie':
|
|
||||||
options.headers.append('Cookie', `${name}=${token}`);
|
|
||||||
break;
|
|
||||||
case 'header':
|
|
||||||
default:
|
|
||||||
options.headers.set(name, token);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const buildUrl: Client['buildUrl'] = (options) =>
|
|
||||||
getUrl({
|
|
||||||
baseUrl: options.baseUrl as string,
|
|
||||||
path: options.path,
|
|
||||||
query: options.query,
|
|
||||||
querySerializer:
|
|
||||||
typeof options.querySerializer === 'function'
|
|
||||||
? options.querySerializer
|
|
||||||
: createQuerySerializer(options.querySerializer),
|
|
||||||
url: options.url,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const mergeConfigs = (a: Config, b: Config): Config => {
|
|
||||||
const config = { ...a, ...b };
|
|
||||||
if (config.baseUrl?.endsWith('/')) {
|
|
||||||
config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
|
|
||||||
}
|
|
||||||
config.headers = mergeHeaders(a.headers, b.headers);
|
|
||||||
return config;
|
|
||||||
};
|
|
||||||
|
|
||||||
const headersEntries = (headers: Headers): Array<[string, string]> => {
|
|
||||||
const entries: Array<[string, string]> = [];
|
|
||||||
headers.forEach((value, key) => {
|
|
||||||
entries.push([key, value]);
|
|
||||||
});
|
|
||||||
return entries;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mergeHeaders = (
|
|
||||||
...headers: Array<Required<Config>['headers'] | undefined>
|
|
||||||
): Headers => {
|
|
||||||
const mergedHeaders = new Headers();
|
|
||||||
for (const header of headers) {
|
|
||||||
if (!header) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const iterator =
|
|
||||||
header instanceof Headers
|
|
||||||
? headersEntries(header)
|
|
||||||
: Object.entries(header);
|
|
||||||
|
|
||||||
for (const [key, value] of iterator) {
|
|
||||||
if (value === null) {
|
|
||||||
mergedHeaders.delete(key);
|
|
||||||
} else if (Array.isArray(value)) {
|
|
||||||
for (const v of value) {
|
|
||||||
mergedHeaders.append(key, v as string);
|
|
||||||
}
|
|
||||||
} else if (value !== undefined) {
|
|
||||||
// assume object headers are meant to be JSON stringified, i.e. their
|
|
||||||
// content value in OpenAPI specification is 'application/json'
|
|
||||||
mergedHeaders.set(
|
|
||||||
key,
|
|
||||||
typeof value === 'object' ? JSON.stringify(value) : (value as string),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mergedHeaders;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ErrInterceptor<Err, Res, Req, Options> = (
|
|
||||||
error: Err,
|
|
||||||
response: Res,
|
|
||||||
request: Req,
|
|
||||||
options: Options,
|
|
||||||
) => Err | Promise<Err>;
|
|
||||||
|
|
||||||
type ReqInterceptor<Req, Options> = (
|
|
||||||
request: Req,
|
|
||||||
options: Options,
|
|
||||||
) => Req | Promise<Req>;
|
|
||||||
|
|
||||||
type ResInterceptor<Res, Req, Options> = (
|
|
||||||
response: Res,
|
|
||||||
request: Req,
|
|
||||||
options: Options,
|
|
||||||
) => Res | Promise<Res>;
|
|
||||||
|
|
||||||
class Interceptors<Interceptor> {
|
|
||||||
fns: Array<Interceptor | null> = [];
|
|
||||||
|
|
||||||
clear(): void {
|
|
||||||
this.fns = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
eject(id: number | Interceptor): void {
|
|
||||||
const index = this.getInterceptorIndex(id);
|
|
||||||
if (this.fns[index]) {
|
|
||||||
this.fns[index] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exists(id: number | Interceptor): boolean {
|
|
||||||
const index = this.getInterceptorIndex(id);
|
|
||||||
return Boolean(this.fns[index]);
|
|
||||||
}
|
|
||||||
|
|
||||||
getInterceptorIndex(id: number | Interceptor): number {
|
|
||||||
if (typeof id === 'number') {
|
|
||||||
return this.fns[id] ? id : -1;
|
|
||||||
}
|
|
||||||
return this.fns.indexOf(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
update(
|
|
||||||
id: number | Interceptor,
|
|
||||||
fn: Interceptor,
|
|
||||||
): number | Interceptor | false {
|
|
||||||
const index = this.getInterceptorIndex(id);
|
|
||||||
if (this.fns[index]) {
|
|
||||||
this.fns[index] = fn;
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
use(fn: Interceptor): number {
|
|
||||||
this.fns.push(fn);
|
|
||||||
return this.fns.length - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Middleware<Req, Res, Err, Options> {
|
|
||||||
error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>;
|
|
||||||
request: Interceptors<ReqInterceptor<Req, Options>>;
|
|
||||||
response: Interceptors<ResInterceptor<Res, Req, Options>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createInterceptors = <Req, Res, Err, Options>(): Middleware<
|
|
||||||
Req,
|
|
||||||
Res,
|
|
||||||
Err,
|
|
||||||
Options
|
|
||||||
> => ({
|
|
||||||
error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
|
|
||||||
request: new Interceptors<ReqInterceptor<Req, Options>>(),
|
|
||||||
response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const defaultQuerySerializer = createQuerySerializer({
|
|
||||||
allowReserved: false,
|
|
||||||
array: {
|
|
||||||
explode: true,
|
|
||||||
style: 'form',
|
|
||||||
},
|
|
||||||
object: {
|
|
||||||
explode: true,
|
|
||||||
style: 'deepObject',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const defaultHeaders = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createConfig = <T extends ClientOptions = ClientOptions>(
|
|
||||||
override: Config<Omit<ClientOptions, keyof T> & T> = {},
|
|
||||||
): Config<Omit<ClientOptions, keyof T> & T> => ({
|
|
||||||
...jsonBodySerializer,
|
|
||||||
headers: defaultHeaders,
|
|
||||||
parseAs: 'auto',
|
|
||||||
querySerializer: defaultQuerySerializer,
|
|
||||||
...override,
|
|
||||||
});
|
|
||||||
|
|
@ -20,7 +20,7 @@ export type OpenAPIConfig = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OpenAPI: OpenAPIConfig = {
|
export const OpenAPI: OpenAPIConfig = {
|
||||||
BASE: 'http://10.1.0.65:8081/auth',
|
BASE: '/api/v1',
|
||||||
VERSION: '1.0.0',
|
VERSION: '1.0.0',
|
||||||
WITH_CREDENTIALS: false,
|
WITH_CREDENTIALS: false,
|
||||||
CREDENTIALS: 'include',
|
CREDENTIALS: 'include',
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
export type AuthToken = string | undefined;
|
|
||||||
|
|
||||||
export interface Auth {
|
|
||||||
/**
|
|
||||||
* Which part of the request do we use to send the auth?
|
|
||||||
*
|
|
||||||
* @default 'header'
|
|
||||||
*/
|
|
||||||
in?: 'header' | 'query' | 'cookie';
|
|
||||||
/**
|
|
||||||
* Header or query parameter name.
|
|
||||||
*
|
|
||||||
* @default 'Authorization'
|
|
||||||
*/
|
|
||||||
name?: string;
|
|
||||||
scheme?: 'basic' | 'bearer';
|
|
||||||
type: 'apiKey' | 'http';
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getAuthToken = async (
|
|
||||||
auth: Auth,
|
|
||||||
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
|
|
||||||
): Promise<string | undefined> => {
|
|
||||||
const token =
|
|
||||||
typeof callback === 'function' ? await callback(auth) : callback;
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auth.scheme === 'bearer') {
|
|
||||||
return `Bearer ${token}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auth.scheme === 'basic') {
|
|
||||||
return `Basic ${btoa(token)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return token;
|
|
||||||
};
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
import type {
|
|
||||||
ArrayStyle,
|
|
||||||
ObjectStyle,
|
|
||||||
SerializerOptions,
|
|
||||||
} from './pathSerializer.gen';
|
|
||||||
|
|
||||||
export type QuerySerializer = (query: Record<string, unknown>) => string;
|
|
||||||
|
|
||||||
export type BodySerializer = (body: any) => any;
|
|
||||||
|
|
||||||
type QuerySerializerOptionsObject = {
|
|
||||||
allowReserved?: boolean;
|
|
||||||
array?: Partial<SerializerOptions<ArrayStyle>>;
|
|
||||||
object?: Partial<SerializerOptions<ObjectStyle>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QuerySerializerOptions = QuerySerializerOptionsObject & {
|
|
||||||
/**
|
|
||||||
* Per-parameter serialization overrides. When provided, these settings
|
|
||||||
* override the global array/object settings for specific parameter names.
|
|
||||||
*/
|
|
||||||
parameters?: Record<string, QuerySerializerOptionsObject>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const serializeFormDataPair = (
|
|
||||||
data: FormData,
|
|
||||||
key: string,
|
|
||||||
value: unknown,
|
|
||||||
): void => {
|
|
||||||
if (typeof value === 'string' || value instanceof Blob) {
|
|
||||||
data.append(key, value);
|
|
||||||
} else if (value instanceof Date) {
|
|
||||||
data.append(key, value.toISOString());
|
|
||||||
} else {
|
|
||||||
data.append(key, JSON.stringify(value));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const serializeUrlSearchParamsPair = (
|
|
||||||
data: URLSearchParams,
|
|
||||||
key: string,
|
|
||||||
value: unknown,
|
|
||||||
): void => {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
data.append(key, value);
|
|
||||||
} else {
|
|
||||||
data.append(key, JSON.stringify(value));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const formDataBodySerializer = {
|
|
||||||
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
|
|
||||||
body: T,
|
|
||||||
): FormData => {
|
|
||||||
const data = new FormData();
|
|
||||||
|
|
||||||
Object.entries(body).forEach(([key, value]) => {
|
|
||||||
if (value === undefined || value === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
value.forEach((v) => serializeFormDataPair(data, key, v));
|
|
||||||
} else {
|
|
||||||
serializeFormDataPair(data, key, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const jsonBodySerializer = {
|
|
||||||
bodySerializer: <T>(body: T): string =>
|
|
||||||
JSON.stringify(body, (_key, value) =>
|
|
||||||
typeof value === 'bigint' ? value.toString() : value,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const urlSearchParamsBodySerializer = {
|
|
||||||
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
|
|
||||||
body: T,
|
|
||||||
): string => {
|
|
||||||
const data = new URLSearchParams();
|
|
||||||
|
|
||||||
Object.entries(body).forEach(([key, value]) => {
|
|
||||||
if (value === undefined || value === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v));
|
|
||||||
} else {
|
|
||||||
serializeUrlSearchParamsPair(data, key, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return data.toString();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,176 +0,0 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
type Slot = 'body' | 'headers' | 'path' | 'query';
|
|
||||||
|
|
||||||
export type Field =
|
|
||||||
| {
|
|
||||||
in: Exclude<Slot, 'body'>;
|
|
||||||
/**
|
|
||||||
* Field name. This is the name we want the user to see and use.
|
|
||||||
*/
|
|
||||||
key: string;
|
|
||||||
/**
|
|
||||||
* Field mapped name. This is the name we want to use in the request.
|
|
||||||
* If omitted, we use the same value as `key`.
|
|
||||||
*/
|
|
||||||
map?: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
in: Extract<Slot, 'body'>;
|
|
||||||
/**
|
|
||||||
* Key isn't required for bodies.
|
|
||||||
*/
|
|
||||||
key?: string;
|
|
||||||
map?: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
/**
|
|
||||||
* Field name. This is the name we want the user to see and use.
|
|
||||||
*/
|
|
||||||
key: string;
|
|
||||||
/**
|
|
||||||
* Field mapped name. This is the name we want to use in the request.
|
|
||||||
* If `in` is omitted, `map` aliases `key` to the transport layer.
|
|
||||||
*/
|
|
||||||
map: Slot;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface Fields {
|
|
||||||
allowExtra?: Partial<Record<Slot, boolean>>;
|
|
||||||
args?: ReadonlyArray<Field>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FieldsConfig = ReadonlyArray<Field | Fields>;
|
|
||||||
|
|
||||||
const extraPrefixesMap: Record<string, Slot> = {
|
|
||||||
$body_: 'body',
|
|
||||||
$headers_: 'headers',
|
|
||||||
$path_: 'path',
|
|
||||||
$query_: 'query',
|
|
||||||
};
|
|
||||||
const extraPrefixes = Object.entries(extraPrefixesMap);
|
|
||||||
|
|
||||||
type KeyMap = Map<
|
|
||||||
string,
|
|
||||||
| {
|
|
||||||
in: Slot;
|
|
||||||
map?: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
in?: never;
|
|
||||||
map: Slot;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
|
|
||||||
if (!map) {
|
|
||||||
map = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const config of fields) {
|
|
||||||
if ('in' in config) {
|
|
||||||
if (config.key) {
|
|
||||||
map.set(config.key, {
|
|
||||||
in: config.in,
|
|
||||||
map: config.map,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if ('key' in config) {
|
|
||||||
map.set(config.key, {
|
|
||||||
map: config.map,
|
|
||||||
});
|
|
||||||
} else if (config.args) {
|
|
||||||
buildKeyMap(config.args, map);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return map;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Params {
|
|
||||||
body: unknown;
|
|
||||||
headers: Record<string, unknown>;
|
|
||||||
path: Record<string, unknown>;
|
|
||||||
query: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stripEmptySlots = (params: Params) => {
|
|
||||||
for (const [slot, value] of Object.entries(params)) {
|
|
||||||
if (value && typeof value === 'object' && !Object.keys(value).length) {
|
|
||||||
delete params[slot as Slot];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const buildClientParams = (
|
|
||||||
args: ReadonlyArray<unknown>,
|
|
||||||
fields: FieldsConfig,
|
|
||||||
) => {
|
|
||||||
const params: Params = {
|
|
||||||
body: {},
|
|
||||||
headers: {},
|
|
||||||
path: {},
|
|
||||||
query: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const map = buildKeyMap(fields);
|
|
||||||
|
|
||||||
let config: FieldsConfig[number] | undefined;
|
|
||||||
|
|
||||||
for (const [index, arg] of args.entries()) {
|
|
||||||
if (fields[index]) {
|
|
||||||
config = fields[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('in' in config) {
|
|
||||||
if (config.key) {
|
|
||||||
const field = map.get(config.key)!;
|
|
||||||
const name = field.map || config.key;
|
|
||||||
if (field.in) {
|
|
||||||
(params[field.in] as Record<string, unknown>)[name] = arg;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
params.body = arg;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (const [key, value] of Object.entries(arg ?? {})) {
|
|
||||||
const field = map.get(key);
|
|
||||||
|
|
||||||
if (field) {
|
|
||||||
if (field.in) {
|
|
||||||
const name = field.map || key;
|
|
||||||
(params[field.in] as Record<string, unknown>)[name] = value;
|
|
||||||
} else {
|
|
||||||
params[field.map] = value;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const extra = extraPrefixes.find(([prefix]) =>
|
|
||||||
key.startsWith(prefix),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (extra) {
|
|
||||||
const [prefix, slot] = extra;
|
|
||||||
(params[slot] as Record<string, unknown>)[
|
|
||||||
key.slice(prefix.length)
|
|
||||||
] = value;
|
|
||||||
} else if ('allowExtra' in config && config.allowExtra) {
|
|
||||||
for (const [slot, allowed] of Object.entries(config.allowExtra)) {
|
|
||||||
if (allowed) {
|
|
||||||
(params[slot as Slot] as Record<string, unknown>)[key] = value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stripEmptySlots(params);
|
|
||||||
|
|
||||||
return params;
|
|
||||||
};
|
|
||||||
|
|
@ -1,181 +0,0 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
interface SerializeOptions<T>
|
|
||||||
extends SerializePrimitiveOptions,
|
|
||||||
SerializerOptions<T> {}
|
|
||||||
|
|
||||||
interface SerializePrimitiveOptions {
|
|
||||||
allowReserved?: boolean;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SerializerOptions<T> {
|
|
||||||
/**
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
explode: boolean;
|
|
||||||
style: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
|
|
||||||
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
|
|
||||||
type MatrixStyle = 'label' | 'matrix' | 'simple';
|
|
||||||
export type ObjectStyle = 'form' | 'deepObject';
|
|
||||||
type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
|
|
||||||
|
|
||||||
interface SerializePrimitiveParam extends SerializePrimitiveOptions {
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
|
|
||||||
switch (style) {
|
|
||||||
case 'label':
|
|
||||||
return '.';
|
|
||||||
case 'matrix':
|
|
||||||
return ';';
|
|
||||||
case 'simple':
|
|
||||||
return ',';
|
|
||||||
default:
|
|
||||||
return '&';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
|
|
||||||
switch (style) {
|
|
||||||
case 'form':
|
|
||||||
return ',';
|
|
||||||
case 'pipeDelimited':
|
|
||||||
return '|';
|
|
||||||
case 'spaceDelimited':
|
|
||||||
return '%20';
|
|
||||||
default:
|
|
||||||
return ',';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
|
|
||||||
switch (style) {
|
|
||||||
case 'label':
|
|
||||||
return '.';
|
|
||||||
case 'matrix':
|
|
||||||
return ';';
|
|
||||||
case 'simple':
|
|
||||||
return ',';
|
|
||||||
default:
|
|
||||||
return '&';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const serializeArrayParam = ({
|
|
||||||
allowReserved,
|
|
||||||
explode,
|
|
||||||
name,
|
|
||||||
style,
|
|
||||||
value,
|
|
||||||
}: SerializeOptions<ArraySeparatorStyle> & {
|
|
||||||
value: unknown[];
|
|
||||||
}) => {
|
|
||||||
if (!explode) {
|
|
||||||
const joinedValues = (
|
|
||||||
allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
|
|
||||||
).join(separatorArrayNoExplode(style));
|
|
||||||
switch (style) {
|
|
||||||
case 'label':
|
|
||||||
return `.${joinedValues}`;
|
|
||||||
case 'matrix':
|
|
||||||
return `;${name}=${joinedValues}`;
|
|
||||||
case 'simple':
|
|
||||||
return joinedValues;
|
|
||||||
default:
|
|
||||||
return `${name}=${joinedValues}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const separator = separatorArrayExplode(style);
|
|
||||||
const joinedValues = value
|
|
||||||
.map((v) => {
|
|
||||||
if (style === 'label' || style === 'simple') {
|
|
||||||
return allowReserved ? v : encodeURIComponent(v as string);
|
|
||||||
}
|
|
||||||
|
|
||||||
return serializePrimitiveParam({
|
|
||||||
allowReserved,
|
|
||||||
name,
|
|
||||||
value: v as string,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.join(separator);
|
|
||||||
return style === 'label' || style === 'matrix'
|
|
||||||
? separator + joinedValues
|
|
||||||
: joinedValues;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const serializePrimitiveParam = ({
|
|
||||||
allowReserved,
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
}: SerializePrimitiveParam) => {
|
|
||||||
if (value === undefined || value === null) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
throw new Error(
|
|
||||||
'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const serializeObjectParam = ({
|
|
||||||
allowReserved,
|
|
||||||
explode,
|
|
||||||
name,
|
|
||||||
style,
|
|
||||||
value,
|
|
||||||
valueOnly,
|
|
||||||
}: SerializeOptions<ObjectSeparatorStyle> & {
|
|
||||||
value: Record<string, unknown> | Date;
|
|
||||||
valueOnly?: boolean;
|
|
||||||
}) => {
|
|
||||||
if (value instanceof Date) {
|
|
||||||
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (style !== 'deepObject' && !explode) {
|
|
||||||
let values: string[] = [];
|
|
||||||
Object.entries(value).forEach(([key, v]) => {
|
|
||||||
values = [
|
|
||||||
...values,
|
|
||||||
key,
|
|
||||||
allowReserved ? (v as string) : encodeURIComponent(v as string),
|
|
||||||
];
|
|
||||||
});
|
|
||||||
const joinedValues = values.join(',');
|
|
||||||
switch (style) {
|
|
||||||
case 'form':
|
|
||||||
return `${name}=${joinedValues}`;
|
|
||||||
case 'label':
|
|
||||||
return `.${joinedValues}`;
|
|
||||||
case 'matrix':
|
|
||||||
return `;${name}=${joinedValues}`;
|
|
||||||
default:
|
|
||||||
return joinedValues;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const separator = separatorObjectExplode(style);
|
|
||||||
const joinedValues = Object.entries(value)
|
|
||||||
.map(([key, v]) =>
|
|
||||||
serializePrimitiveParam({
|
|
||||||
allowReserved,
|
|
||||||
name: style === 'deepObject' ? `${name}[${key}]` : key,
|
|
||||||
value: v as string,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.join(separator);
|
|
||||||
return style === 'label' || style === 'matrix'
|
|
||||||
? separator + joinedValues
|
|
||||||
: joinedValues;
|
|
||||||
};
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON-friendly union that mirrors what Pinia Colada can hash.
|
|
||||||
*/
|
|
||||||
export type JsonValue =
|
|
||||||
| null
|
|
||||||
| string
|
|
||||||
| number
|
|
||||||
| boolean
|
|
||||||
| JsonValue[]
|
|
||||||
| { [key: string]: JsonValue };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
|
|
||||||
*/
|
|
||||||
export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
|
|
||||||
if (
|
|
||||||
value === undefined ||
|
|
||||||
typeof value === 'function' ||
|
|
||||||
typeof value === 'symbol'
|
|
||||||
) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if (typeof value === 'bigint') {
|
|
||||||
return value.toString();
|
|
||||||
}
|
|
||||||
if (value instanceof Date) {
|
|
||||||
return value.toISOString();
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Safely stringifies a value and parses it back into a JsonValue.
|
|
||||||
*/
|
|
||||||
export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
|
|
||||||
try {
|
|
||||||
const json = JSON.stringify(input, queryKeyJsonReplacer);
|
|
||||||
if (json === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return JSON.parse(json) as JsonValue;
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detects plain objects (including objects with a null prototype).
|
|
||||||
*/
|
|
||||||
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
|
||||||
if (value === null || typeof value !== 'object') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const prototype = Object.getPrototypeOf(value as object);
|
|
||||||
return prototype === Object.prototype || prototype === null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Turns URLSearchParams into a sorted JSON object for deterministic keys.
|
|
||||||
*/
|
|
||||||
const serializeSearchParams = (params: URLSearchParams): JsonValue => {
|
|
||||||
const entries = Array.from(params.entries()).sort(([a], [b]) =>
|
|
||||||
a.localeCompare(b),
|
|
||||||
);
|
|
||||||
const result: Record<string, JsonValue> = {};
|
|
||||||
|
|
||||||
for (const [key, value] of entries) {
|
|
||||||
const existing = result[key];
|
|
||||||
if (existing === undefined) {
|
|
||||||
result[key] = value;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(existing)) {
|
|
||||||
(existing as string[]).push(value);
|
|
||||||
} else {
|
|
||||||
result[key] = [existing, value];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalizes any accepted value into a JSON-friendly shape for query keys.
|
|
||||||
*/
|
|
||||||
export const serializeQueryKeyValue = (
|
|
||||||
value: unknown,
|
|
||||||
): JsonValue | undefined => {
|
|
||||||
if (value === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof value === 'string' ||
|
|
||||||
typeof value === 'number' ||
|
|
||||||
typeof value === 'boolean'
|
|
||||||
) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
value === undefined ||
|
|
||||||
typeof value === 'function' ||
|
|
||||||
typeof value === 'symbol'
|
|
||||||
) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'bigint') {
|
|
||||||
return value.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value instanceof Date) {
|
|
||||||
return value.toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return stringifyToJsonValue(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof URLSearchParams !== 'undefined' &&
|
|
||||||
value instanceof URLSearchParams
|
|
||||||
) {
|
|
||||||
return serializeSearchParams(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPlainObject(value)) {
|
|
||||||
return stringifyToJsonValue(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
@ -1,264 +0,0 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
import type { Config } from './types.gen';
|
|
||||||
|
|
||||||
export type ServerSentEventsOptions<TData = unknown> = Omit<
|
|
||||||
RequestInit,
|
|
||||||
'method'
|
|
||||||
> &
|
|
||||||
Pick<Config, 'method' | 'responseTransformer' | 'responseValidator'> & {
|
|
||||||
/**
|
|
||||||
* Fetch API implementation. You can use this option to provide a custom
|
|
||||||
* fetch instance.
|
|
||||||
*
|
|
||||||
* @default globalThis.fetch
|
|
||||||
*/
|
|
||||||
fetch?: typeof fetch;
|
|
||||||
/**
|
|
||||||
* Implementing clients can call request interceptors inside this hook.
|
|
||||||
*/
|
|
||||||
onRequest?: (url: string, init: RequestInit) => Promise<Request>;
|
|
||||||
/**
|
|
||||||
* Callback invoked when a network or parsing error occurs during streaming.
|
|
||||||
*
|
|
||||||
* This option applies only if the endpoint returns a stream of events.
|
|
||||||
*
|
|
||||||
* @param error The error that occurred.
|
|
||||||
*/
|
|
||||||
onSseError?: (error: unknown) => void;
|
|
||||||
/**
|
|
||||||
* Callback invoked when an event is streamed from the server.
|
|
||||||
*
|
|
||||||
* This option applies only if the endpoint returns a stream of events.
|
|
||||||
*
|
|
||||||
* @param event Event streamed from the server.
|
|
||||||
* @returns Nothing (void).
|
|
||||||
*/
|
|
||||||
onSseEvent?: (event: StreamEvent<TData>) => void;
|
|
||||||
serializedBody?: RequestInit['body'];
|
|
||||||
/**
|
|
||||||
* Default retry delay in milliseconds.
|
|
||||||
*
|
|
||||||
* This option applies only if the endpoint returns a stream of events.
|
|
||||||
*
|
|
||||||
* @default 3000
|
|
||||||
*/
|
|
||||||
sseDefaultRetryDelay?: number;
|
|
||||||
/**
|
|
||||||
* Maximum number of retry attempts before giving up.
|
|
||||||
*/
|
|
||||||
sseMaxRetryAttempts?: number;
|
|
||||||
/**
|
|
||||||
* Maximum retry delay in milliseconds.
|
|
||||||
*
|
|
||||||
* Applies only when exponential backoff is used.
|
|
||||||
*
|
|
||||||
* This option applies only if the endpoint returns a stream of events.
|
|
||||||
*
|
|
||||||
* @default 30000
|
|
||||||
*/
|
|
||||||
sseMaxRetryDelay?: number;
|
|
||||||
/**
|
|
||||||
* Optional sleep function for retry backoff.
|
|
||||||
*
|
|
||||||
* Defaults to using `setTimeout`.
|
|
||||||
*/
|
|
||||||
sseSleepFn?: (ms: number) => Promise<void>;
|
|
||||||
url: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface StreamEvent<TData = unknown> {
|
|
||||||
data: TData;
|
|
||||||
event?: string;
|
|
||||||
id?: string;
|
|
||||||
retry?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ServerSentEventsResult<
|
|
||||||
TData = unknown,
|
|
||||||
TReturn = void,
|
|
||||||
TNext = unknown,
|
|
||||||
> = {
|
|
||||||
stream: AsyncGenerator<
|
|
||||||
TData extends Record<string, unknown> ? TData[keyof TData] : TData,
|
|
||||||
TReturn,
|
|
||||||
TNext
|
|
||||||
>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createSseClient = <TData = unknown>({
|
|
||||||
onRequest,
|
|
||||||
onSseError,
|
|
||||||
onSseEvent,
|
|
||||||
responseTransformer,
|
|
||||||
responseValidator,
|
|
||||||
sseDefaultRetryDelay,
|
|
||||||
sseMaxRetryAttempts,
|
|
||||||
sseMaxRetryDelay,
|
|
||||||
sseSleepFn,
|
|
||||||
url,
|
|
||||||
...options
|
|
||||||
}: ServerSentEventsOptions): ServerSentEventsResult<TData> => {
|
|
||||||
let lastEventId: string | undefined;
|
|
||||||
|
|
||||||
const sleep =
|
|
||||||
sseSleepFn ??
|
|
||||||
((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
||||||
|
|
||||||
const createStream = async function* () {
|
|
||||||
let retryDelay: number = sseDefaultRetryDelay ?? 3000;
|
|
||||||
let attempt = 0;
|
|
||||||
const signal = options.signal ?? new AbortController().signal;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
if (signal.aborted) break;
|
|
||||||
|
|
||||||
attempt++;
|
|
||||||
|
|
||||||
const headers =
|
|
||||||
options.headers instanceof Headers
|
|
||||||
? options.headers
|
|
||||||
: new Headers(options.headers as Record<string, string> | undefined);
|
|
||||||
|
|
||||||
if (lastEventId !== undefined) {
|
|
||||||
headers.set('Last-Event-ID', lastEventId);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const requestInit: RequestInit = {
|
|
||||||
redirect: 'follow',
|
|
||||||
...options,
|
|
||||||
body: options.serializedBody,
|
|
||||||
headers,
|
|
||||||
signal,
|
|
||||||
};
|
|
||||||
let request = new Request(url, requestInit);
|
|
||||||
if (onRequest) {
|
|
||||||
request = await onRequest(url, requestInit);
|
|
||||||
}
|
|
||||||
// fetch must be assigned here, otherwise it would throw the error:
|
|
||||||
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
|
|
||||||
const _fetch = options.fetch ?? globalThis.fetch;
|
|
||||||
const response = await _fetch(request);
|
|
||||||
|
|
||||||
if (!response.ok)
|
|
||||||
throw new Error(
|
|
||||||
`SSE failed: ${response.status} ${response.statusText}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.body) throw new Error('No body in SSE response');
|
|
||||||
|
|
||||||
const reader = response.body
|
|
||||||
.pipeThrough(new TextDecoderStream())
|
|
||||||
.getReader();
|
|
||||||
|
|
||||||
let buffer = '';
|
|
||||||
|
|
||||||
const abortHandler = () => {
|
|
||||||
try {
|
|
||||||
reader.cancel();
|
|
||||||
} catch {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
signal.addEventListener('abort', abortHandler);
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
buffer += value;
|
|
||||||
|
|
||||||
const chunks = buffer.split('\n\n');
|
|
||||||
buffer = chunks.pop() ?? '';
|
|
||||||
|
|
||||||
for (const chunk of chunks) {
|
|
||||||
const lines = chunk.split('\n');
|
|
||||||
const dataLines: Array<string> = [];
|
|
||||||
let eventName: string | undefined;
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.startsWith('data:')) {
|
|
||||||
dataLines.push(line.replace(/^data:\s*/, ''));
|
|
||||||
} else if (line.startsWith('event:')) {
|
|
||||||
eventName = line.replace(/^event:\s*/, '');
|
|
||||||
} else if (line.startsWith('id:')) {
|
|
||||||
lastEventId = line.replace(/^id:\s*/, '');
|
|
||||||
} else if (line.startsWith('retry:')) {
|
|
||||||
const parsed = Number.parseInt(
|
|
||||||
line.replace(/^retry:\s*/, ''),
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
if (!Number.isNaN(parsed)) {
|
|
||||||
retryDelay = parsed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let data: unknown;
|
|
||||||
let parsedJson = false;
|
|
||||||
|
|
||||||
if (dataLines.length) {
|
|
||||||
const rawData = dataLines.join('\n');
|
|
||||||
try {
|
|
||||||
data = JSON.parse(rawData);
|
|
||||||
parsedJson = true;
|
|
||||||
} catch {
|
|
||||||
data = rawData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedJson) {
|
|
||||||
if (responseValidator) {
|
|
||||||
await responseValidator(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (responseTransformer) {
|
|
||||||
data = await responseTransformer(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onSseEvent?.({
|
|
||||||
data,
|
|
||||||
event: eventName,
|
|
||||||
id: lastEventId,
|
|
||||||
retry: retryDelay,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (dataLines.length) {
|
|
||||||
yield data as any;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
signal.removeEventListener('abort', abortHandler);
|
|
||||||
reader.releaseLock();
|
|
||||||
}
|
|
||||||
|
|
||||||
break; // exit loop on normal completion
|
|
||||||
} catch (error) {
|
|
||||||
// connection failed or aborted; retry after delay
|
|
||||||
onSseError?.(error);
|
|
||||||
|
|
||||||
if (
|
|
||||||
sseMaxRetryAttempts !== undefined &&
|
|
||||||
attempt >= sseMaxRetryAttempts
|
|
||||||
) {
|
|
||||||
break; // stop after firing error
|
|
||||||
}
|
|
||||||
|
|
||||||
// exponential backoff: double retry each attempt, cap at 30s
|
|
||||||
const backoff = Math.min(
|
|
||||||
retryDelay * 2 ** (attempt - 1),
|
|
||||||
sseMaxRetryDelay ?? 30000,
|
|
||||||
);
|
|
||||||
await sleep(backoff);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const stream = createStream();
|
|
||||||
|
|
||||||
return { stream };
|
|
||||||
};
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
import type { Auth, AuthToken } from './auth.gen';
|
|
||||||
import type {
|
|
||||||
BodySerializer,
|
|
||||||
QuerySerializer,
|
|
||||||
QuerySerializerOptions,
|
|
||||||
} from './bodySerializer.gen';
|
|
||||||
|
|
||||||
export type HttpMethod =
|
|
||||||
| 'connect'
|
|
||||||
| 'delete'
|
|
||||||
| 'get'
|
|
||||||
| 'head'
|
|
||||||
| 'options'
|
|
||||||
| 'patch'
|
|
||||||
| 'post'
|
|
||||||
| 'put'
|
|
||||||
| 'trace';
|
|
||||||
|
|
||||||
export type Client<
|
|
||||||
RequestFn = never,
|
|
||||||
Config = unknown,
|
|
||||||
MethodFn = never,
|
|
||||||
BuildUrlFn = never,
|
|
||||||
SseFn = never,
|
|
||||||
> = {
|
|
||||||
/**
|
|
||||||
* Returns the final request URL.
|
|
||||||
*/
|
|
||||||
buildUrl: BuildUrlFn;
|
|
||||||
getConfig: () => Config;
|
|
||||||
request: RequestFn;
|
|
||||||
setConfig: (config: Config) => Config;
|
|
||||||
} & {
|
|
||||||
[K in HttpMethod]: MethodFn;
|
|
||||||
} & ([SseFn] extends [never]
|
|
||||||
? { sse?: never }
|
|
||||||
: { sse: { [K in HttpMethod]: SseFn } });
|
|
||||||
|
|
||||||
export interface Config {
|
|
||||||
/**
|
|
||||||
* Auth token or a function returning auth token. The resolved value will be
|
|
||||||
* added to the request payload as defined by its `security` array.
|
|
||||||
*/
|
|
||||||
auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken;
|
|
||||||
/**
|
|
||||||
* A function for serializing request body parameter. By default,
|
|
||||||
* {@link JSON.stringify()} will be used.
|
|
||||||
*/
|
|
||||||
bodySerializer?: BodySerializer | null;
|
|
||||||
/**
|
|
||||||
* An object containing any HTTP headers that you want to pre-populate your
|
|
||||||
* `Headers` object with.
|
|
||||||
*
|
|
||||||
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
|
|
||||||
*/
|
|
||||||
headers?:
|
|
||||||
| RequestInit['headers']
|
|
||||||
| Record<
|
|
||||||
string,
|
|
||||||
| string
|
|
||||||
| number
|
|
||||||
| boolean
|
|
||||||
| (string | number | boolean)[]
|
|
||||||
| null
|
|
||||||
| undefined
|
|
||||||
| unknown
|
|
||||||
>;
|
|
||||||
/**
|
|
||||||
* The request method.
|
|
||||||
*
|
|
||||||
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
|
|
||||||
*/
|
|
||||||
method?: Uppercase<HttpMethod>;
|
|
||||||
/**
|
|
||||||
* A function for serializing request query parameters. By default, arrays
|
|
||||||
* will be exploded in form style, objects will be exploded in deepObject
|
|
||||||
* style, and reserved characters are percent-encoded.
|
|
||||||
*
|
|
||||||
* This method will have no effect if the native `paramsSerializer()` Axios
|
|
||||||
* API function is used.
|
|
||||||
*
|
|
||||||
* {@link https://swagger.io/docs/specification/serialization/#query View examples}
|
|
||||||
*/
|
|
||||||
querySerializer?: QuerySerializer | QuerySerializerOptions;
|
|
||||||
/**
|
|
||||||
* A function validating request data. This is useful if you want to ensure
|
|
||||||
* the request conforms to the desired shape, so it can be safely sent to
|
|
||||||
* the server.
|
|
||||||
*/
|
|
||||||
requestValidator?: (data: unknown) => Promise<unknown>;
|
|
||||||
/**
|
|
||||||
* A function transforming response data before it's returned. This is useful
|
|
||||||
* for post-processing data, e.g. converting ISO strings into Date objects.
|
|
||||||
*/
|
|
||||||
responseTransformer?: (data: unknown) => Promise<unknown>;
|
|
||||||
/**
|
|
||||||
* A function validating response data. This is useful if you want to ensure
|
|
||||||
* the response conforms to the desired shape, so it can be safely passed to
|
|
||||||
* the transformers and returned to the user.
|
|
||||||
*/
|
|
||||||
responseValidator?: (data: unknown) => Promise<unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
|
|
||||||
? true
|
|
||||||
: [T] extends [never | undefined]
|
|
||||||
? [undefined] extends [T]
|
|
||||||
? false
|
|
||||||
: true
|
|
||||||
: false;
|
|
||||||
|
|
||||||
export type OmitNever<T extends Record<string, unknown>> = {
|
|
||||||
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true
|
|
||||||
? never
|
|
||||||
: K]: T[K];
|
|
||||||
};
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
import type { BodySerializer, QuerySerializer } from './bodySerializer.gen';
|
|
||||||
import {
|
|
||||||
type ArraySeparatorStyle,
|
|
||||||
serializeArrayParam,
|
|
||||||
serializeObjectParam,
|
|
||||||
serializePrimitiveParam,
|
|
||||||
} from './pathSerializer.gen';
|
|
||||||
|
|
||||||
export interface PathSerializer {
|
|
||||||
path: Record<string, unknown>;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PATH_PARAM_RE = /\{[^{}]+\}/g;
|
|
||||||
|
|
||||||
export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
|
|
||||||
let url = _url;
|
|
||||||
const matches = _url.match(PATH_PARAM_RE);
|
|
||||||
if (matches) {
|
|
||||||
for (const match of matches) {
|
|
||||||
let explode = false;
|
|
||||||
let name = match.substring(1, match.length - 1);
|
|
||||||
let style: ArraySeparatorStyle = 'simple';
|
|
||||||
|
|
||||||
if (name.endsWith('*')) {
|
|
||||||
explode = true;
|
|
||||||
name = name.substring(0, name.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.startsWith('.')) {
|
|
||||||
name = name.substring(1);
|
|
||||||
style = 'label';
|
|
||||||
} else if (name.startsWith(';')) {
|
|
||||||
name = name.substring(1);
|
|
||||||
style = 'matrix';
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = path[name];
|
|
||||||
|
|
||||||
if (value === undefined || value === null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
url = url.replace(
|
|
||||||
match,
|
|
||||||
serializeArrayParam({ explode, name, style, value }),
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
url = url.replace(
|
|
||||||
match,
|
|
||||||
serializeObjectParam({
|
|
||||||
explode,
|
|
||||||
name,
|
|
||||||
style,
|
|
||||||
value: value as Record<string, unknown>,
|
|
||||||
valueOnly: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (style === 'matrix') {
|
|
||||||
url = url.replace(
|
|
||||||
match,
|
|
||||||
`;${serializePrimitiveParam({
|
|
||||||
name,
|
|
||||||
value: value as string,
|
|
||||||
})}`,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const replaceValue = encodeURIComponent(
|
|
||||||
style === 'label' ? `.${value as string}` : (value as string),
|
|
||||||
);
|
|
||||||
url = url.replace(match, replaceValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUrl = ({
|
|
||||||
baseUrl,
|
|
||||||
path,
|
|
||||||
query,
|
|
||||||
querySerializer,
|
|
||||||
url: _url,
|
|
||||||
}: {
|
|
||||||
baseUrl?: string;
|
|
||||||
path?: Record<string, unknown>;
|
|
||||||
query?: Record<string, unknown>;
|
|
||||||
querySerializer: QuerySerializer;
|
|
||||||
url: string;
|
|
||||||
}) => {
|
|
||||||
const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
|
|
||||||
let url = (baseUrl ?? '') + pathUrl;
|
|
||||||
if (path) {
|
|
||||||
url = defaultPathSerializer({ path, url });
|
|
||||||
}
|
|
||||||
let search = query ? querySerializer(query) : '';
|
|
||||||
if (search.startsWith('?')) {
|
|
||||||
search = search.substring(1);
|
|
||||||
}
|
|
||||||
if (search) {
|
|
||||||
url += `?${search}`;
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getValidRequestBody(options: {
|
|
||||||
body?: unknown;
|
|
||||||
bodySerializer?: BodySerializer | null;
|
|
||||||
serializedBody?: unknown;
|
|
||||||
}) {
|
|
||||||
const hasBody = options.body !== undefined;
|
|
||||||
const isSerializedBody = hasBody && options.bodySerializer;
|
|
||||||
|
|
||||||
if (isSerializedBody) {
|
|
||||||
if ('serializedBody' in options) {
|
|
||||||
const hasSerializedBody =
|
|
||||||
options.serializedBody !== undefined && options.serializedBody !== '';
|
|
||||||
|
|
||||||
return hasSerializedBody ? options.serializedBody : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// not all clients implement a serializedBody property (i.e. client-axios)
|
|
||||||
return options.body !== '' ? options.body : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// plain/text body
|
|
||||||
if (hasBody) {
|
|
||||||
return options.body;
|
|
||||||
}
|
|
||||||
|
|
||||||
// no body was provided
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,16 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export { ApiError } from './core/ApiError';
|
||||||
|
export { CancelablePromise, CancelError } from './core/CancelablePromise';
|
||||||
|
export { OpenAPI } from './core/OpenAPI';
|
||||||
|
export type { OpenAPIConfig } from './core/OpenAPI';
|
||||||
|
|
||||||
export type * from './types.gen';
|
export type { Review } from './models/Review';
|
||||||
export * from './sdk.gen';
|
export type { Tag } from './models/Tag';
|
||||||
|
export type { Title } from './models/Title';
|
||||||
|
export type { User } from './models/User';
|
||||||
|
export type { UserTitle } from './models/UserTitle';
|
||||||
|
|
||||||
|
export { DefaultService } from './services/DefaultService';
|
||||||
|
|
|
||||||
5
modules/frontend/src/api/models/Review.ts
Normal file
5
modules/frontend/src/api/models/Review.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type Review = Record<string, any>;
|
||||||
5
modules/frontend/src/api/models/Tag.ts
Normal file
5
modules/frontend/src/api/models/Tag.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type Tag = Record<string, any>;
|
||||||
5
modules/frontend/src/api/models/Title.ts
Normal file
5
modules/frontend/src/api/models/Title.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type Title = Record<string, any>;
|
||||||
35
modules/frontend/src/api/models/User.ts
Normal file
35
modules/frontend/src/api/models/User.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type User = {
|
||||||
|
/**
|
||||||
|
* Unique user ID (primary key)
|
||||||
|
*/
|
||||||
|
id?: number;
|
||||||
|
/**
|
||||||
|
* ID of the user avatar (references images table)
|
||||||
|
*/
|
||||||
|
avatar_id?: number | null;
|
||||||
|
/**
|
||||||
|
* User email
|
||||||
|
*/
|
||||||
|
mail?: string;
|
||||||
|
/**
|
||||||
|
* Username (alphanumeric + _ or -)
|
||||||
|
*/
|
||||||
|
nickname: string;
|
||||||
|
/**
|
||||||
|
* Display name
|
||||||
|
*/
|
||||||
|
disp_name?: string;
|
||||||
|
/**
|
||||||
|
* User description
|
||||||
|
*/
|
||||||
|
user_desc?: string;
|
||||||
|
/**
|
||||||
|
* Timestamp when the user was created
|
||||||
|
*/
|
||||||
|
creation_date: string;
|
||||||
|
};
|
||||||
|
|
||||||
5
modules/frontend/src/api/models/UserTitle.ts
Normal file
5
modules/frontend/src/api/models/UserTitle.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type UserTitle = Record<string, any>;
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
import type { Client, Options as Options2, TDataShape } from './client';
|
|
||||||
import { client } from './client.gen';
|
|
||||||
import type { AddUserTitleData, AddUserTitleErrors, AddUserTitleResponses, DeleteUserTitleData, DeleteUserTitleErrors, DeleteUserTitleResponses, GetTitleData, GetTitleErrors, GetTitleResponses, GetTitlesData, GetTitlesErrors, GetTitlesResponses, GetUsersData, GetUsersErrors, GetUsersIdData, GetUsersIdErrors, GetUsersIdResponses, GetUsersResponses, GetUserTitleData, GetUserTitleErrors, GetUserTitleResponses, GetUserTitlesData, GetUserTitlesErrors, GetUserTitlesResponses, UpdateUserData, UpdateUserErrors, UpdateUserResponses, UpdateUserTitleData, UpdateUserTitleErrors, UpdateUserTitleResponses } from './types.gen';
|
|
||||||
|
|
||||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
|
|
||||||
/**
|
|
||||||
* You can provide a client instance returned by `createClient()` instead of
|
|
||||||
* individual options. This might be also useful if you want to implement a
|
|
||||||
* custom client.
|
|
||||||
*/
|
|
||||||
client?: Client;
|
|
||||||
/**
|
|
||||||
* You can pass arbitrary values through the `meta` object. This can be
|
|
||||||
* used to access values that aren't defined as part of the SDK function.
|
|
||||||
*/
|
|
||||||
meta?: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get titles
|
|
||||||
*/
|
|
||||||
export const getTitles = <ThrowOnError extends boolean = false>(options?: Options<GetTitlesData, ThrowOnError>) => (options?.client ?? client).get<GetTitlesResponses, GetTitlesErrors, ThrowOnError>({
|
|
||||||
querySerializer: { parameters: { status: { array: { explode: false } } } },
|
|
||||||
url: '/titles',
|
|
||||||
...options
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get title description
|
|
||||||
*/
|
|
||||||
export const getTitle = <ThrowOnError extends boolean = false>(options: Options<GetTitleData, ThrowOnError>) => (options.client ?? client).get<GetTitleResponses, GetTitleErrors, ThrowOnError>({ url: '/titles/{title_id}', ...options });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search user by nickname or dispname (both in one param), response is always sorted by id
|
|
||||||
*/
|
|
||||||
export const getUsers = <ThrowOnError extends boolean = false>(options?: Options<GetUsersData, ThrowOnError>) => (options?.client ?? client).get<GetUsersResponses, GetUsersErrors, ThrowOnError>({ url: '/users/', ...options });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get user info
|
|
||||||
*/
|
|
||||||
export const getUsersId = <ThrowOnError extends boolean = false>(options: Options<GetUsersIdData, ThrowOnError>) => (options.client ?? client).get<GetUsersIdResponses, GetUsersIdErrors, ThrowOnError>({ url: '/users/{user_id}', ...options });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Partially update a user account
|
|
||||||
*
|
|
||||||
* Update selected user profile fields (excluding password).
|
|
||||||
* Password updates must be done via the dedicated auth-service (`/auth/`).
|
|
||||||
* Fields not provided in the request body remain unchanged.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const updateUser = <ThrowOnError extends boolean = false>(options: Options<UpdateUserData, ThrowOnError>) => (options.client ?? client).patch<UpdateUserResponses, UpdateUserErrors, ThrowOnError>({
|
|
||||||
security: [{ name: 'X-XSRF-TOKEN', type: 'apiKey' }],
|
|
||||||
url: '/users/{user_id}',
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options.headers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get user titles
|
|
||||||
*/
|
|
||||||
export const getUserTitles = <ThrowOnError extends boolean = false>(options: Options<GetUserTitlesData, ThrowOnError>) => (options.client ?? client).get<GetUserTitlesResponses, GetUserTitlesErrors, ThrowOnError>({
|
|
||||||
querySerializer: { parameters: { status: { array: { explode: false } }, watch_status: { array: { explode: false } } } },
|
|
||||||
url: '/users/{user_id}/titles',
|
|
||||||
...options
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a title to a user
|
|
||||||
*
|
|
||||||
* User adding title to list af watched, status required
|
|
||||||
*/
|
|
||||||
export const addUserTitle = <ThrowOnError extends boolean = false>(options: Options<AddUserTitleData, ThrowOnError>) => (options.client ?? client).post<AddUserTitleResponses, AddUserTitleErrors, ThrowOnError>({
|
|
||||||
url: '/users/{user_id}/titles',
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options.headers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a usertitle
|
|
||||||
*
|
|
||||||
* User deleting title from list of watched
|
|
||||||
*/
|
|
||||||
export const deleteUserTitle = <ThrowOnError extends boolean = false>(options: Options<DeleteUserTitleData, ThrowOnError>) => (options.client ?? client).delete<DeleteUserTitleResponses, DeleteUserTitleErrors, ThrowOnError>({
|
|
||||||
security: [{ name: 'X-XSRF-TOKEN', type: 'apiKey' }],
|
|
||||||
url: '/users/{user_id}/titles/{title_id}',
|
|
||||||
...options
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get user title
|
|
||||||
*/
|
|
||||||
export const getUserTitle = <ThrowOnError extends boolean = false>(options: Options<GetUserTitleData, ThrowOnError>) => (options.client ?? client).get<GetUserTitleResponses, GetUserTitleErrors, ThrowOnError>({ url: '/users/{user_id}/titles/{title_id}', ...options });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a usertitle
|
|
||||||
*
|
|
||||||
* User updating title list of watched
|
|
||||||
*/
|
|
||||||
export const updateUserTitle = <ThrowOnError extends boolean = false>(options: Options<UpdateUserTitleData, ThrowOnError>) => (options.client ?? client).patch<UpdateUserTitleResponses, UpdateUserTitleErrors, ThrowOnError>({
|
|
||||||
security: [{ name: 'X-XSRF-TOKEN', type: 'apiKey' }],
|
|
||||||
url: '/users/{user_id}/titles/{title_id}',
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options.headers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
35
modules/frontend/src/api/services/DefaultService.ts
Normal file
35
modules/frontend/src/api/services/DefaultService.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { User } from '../models/User';
|
||||||
|
import type { CancelablePromise } from '../core/CancelablePromise';
|
||||||
|
import { OpenAPI } from '../core/OpenAPI';
|
||||||
|
import { request as __request } from '../core/request';
|
||||||
|
export class DefaultService {
|
||||||
|
/**
|
||||||
|
* Get user info
|
||||||
|
* @param userId
|
||||||
|
* @param fields
|
||||||
|
* @returns User User info
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public static getUsers(
|
||||||
|
userId: string,
|
||||||
|
fields: string = 'all',
|
||||||
|
): CancelablePromise<User> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: 'GET',
|
||||||
|
url: '/users/{user_id}',
|
||||||
|
path: {
|
||||||
|
'user_id': userId,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
'fields': fields,
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
404: `User not found`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,620 +0,0 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
export type ClientOptions = {
|
|
||||||
baseUrl: `${string}://${string}/api/v1` | (string & {});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Title sort order
|
|
||||||
*/
|
|
||||||
export type TitleSort = 'id' | 'year' | 'rating' | 'views';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Title status
|
|
||||||
*/
|
|
||||||
export type TitleStatus = 'finished' | 'ongoing' | 'planned';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Title release season
|
|
||||||
*/
|
|
||||||
export type ReleaseSeason = 'winter' | 'spring' | 'summer' | 'fall';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Image storage type
|
|
||||||
*/
|
|
||||||
export type StorageType = 's3' | 'local';
|
|
||||||
|
|
||||||
export type Image = {
|
|
||||||
id?: number;
|
|
||||||
storage_type?: StorageType;
|
|
||||||
image_path?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Studio = {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
poster?: Image;
|
|
||||||
description?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A localized tag: keys are language codes (ISO 639-1), values are tag names
|
|
||||||
*/
|
|
||||||
export type Tag = {
|
|
||||||
[key: string]: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Array of localized tags
|
|
||||||
*/
|
|
||||||
export type Tags = Array<Tag>;
|
|
||||||
|
|
||||||
export type Title = {
|
|
||||||
/**
|
|
||||||
* Unique title ID (primary key)
|
|
||||||
*/
|
|
||||||
id: number;
|
|
||||||
/**
|
|
||||||
* Localized titles. Key = language (ISO 639-1), value = list of names
|
|
||||||
*/
|
|
||||||
title_names: {
|
|
||||||
[key: string]: Array<string>;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Localized description. Key = language (ISO 639-1), value = description.
|
|
||||||
*/
|
|
||||||
title_desc?: {
|
|
||||||
[key: string]: string;
|
|
||||||
};
|
|
||||||
studio?: Studio;
|
|
||||||
tags: Tags;
|
|
||||||
poster?: Image;
|
|
||||||
title_status?: TitleStatus;
|
|
||||||
rating?: number;
|
|
||||||
rating_count?: number;
|
|
||||||
release_year?: number;
|
|
||||||
release_season?: ReleaseSeason;
|
|
||||||
episodes_aired?: number;
|
|
||||||
episodes_all?: number;
|
|
||||||
episodes_len?: {
|
|
||||||
[key: string]: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CursorObj = {
|
|
||||||
id: number;
|
|
||||||
param?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type User = {
|
|
||||||
/**
|
|
||||||
* Unique user ID (primary key)
|
|
||||||
*/
|
|
||||||
id?: number;
|
|
||||||
image?: Image;
|
|
||||||
/**
|
|
||||||
* User email
|
|
||||||
*/
|
|
||||||
mail?: string;
|
|
||||||
/**
|
|
||||||
* Username (alphanumeric + _ or -)
|
|
||||||
*/
|
|
||||||
nickname: string;
|
|
||||||
/**
|
|
||||||
* Display name
|
|
||||||
*/
|
|
||||||
disp_name?: string;
|
|
||||||
/**
|
|
||||||
* User description
|
|
||||||
*/
|
|
||||||
user_desc?: string;
|
|
||||||
/**
|
|
||||||
* Timestamp when the user was created
|
|
||||||
*/
|
|
||||||
creation_date?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User's title status
|
|
||||||
*/
|
|
||||||
export type UserTitleStatus = 'finished' | 'planned' | 'dropped' | 'in-progress';
|
|
||||||
|
|
||||||
export type UserTitle = {
|
|
||||||
user_id: number;
|
|
||||||
title?: Title;
|
|
||||||
status: UserTitleStatus;
|
|
||||||
rate?: number;
|
|
||||||
review_id?: number;
|
|
||||||
ctime?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UserTitleMini = {
|
|
||||||
user_id: number;
|
|
||||||
title_id: number;
|
|
||||||
status: UserTitleStatus;
|
|
||||||
rate?: number;
|
|
||||||
review_id?: number;
|
|
||||||
ctime?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Review = {
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Cursor = string;
|
|
||||||
|
|
||||||
export type TitleSort2 = TitleSort;
|
|
||||||
|
|
||||||
export type GetTitlesData = {
|
|
||||||
body?: never;
|
|
||||||
path?: never;
|
|
||||||
query?: {
|
|
||||||
cursor?: string;
|
|
||||||
sort?: TitleSort;
|
|
||||||
sort_forward?: boolean;
|
|
||||||
ext_search?: boolean;
|
|
||||||
word?: string;
|
|
||||||
/**
|
|
||||||
* List of title statuses to filter
|
|
||||||
*/
|
|
||||||
status?: Array<TitleStatus>;
|
|
||||||
rating?: number;
|
|
||||||
release_year?: number;
|
|
||||||
release_season?: ReleaseSeason;
|
|
||||||
limit?: number;
|
|
||||||
offset?: number;
|
|
||||||
fields?: string;
|
|
||||||
};
|
|
||||||
url: '/titles';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetTitlesErrors = {
|
|
||||||
/**
|
|
||||||
* Request params are not correct
|
|
||||||
*/
|
|
||||||
400: unknown;
|
|
||||||
/**
|
|
||||||
* Unknown server error
|
|
||||||
*/
|
|
||||||
500: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetTitlesResponses = {
|
|
||||||
/**
|
|
||||||
* List of titles with cursor
|
|
||||||
*/
|
|
||||||
200: {
|
|
||||||
/**
|
|
||||||
* List of titles
|
|
||||||
*/
|
|
||||||
data: Array<Title>;
|
|
||||||
cursor: CursorObj;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* No titles found
|
|
||||||
*/
|
|
||||||
204: void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetTitlesResponse = GetTitlesResponses[keyof GetTitlesResponses];
|
|
||||||
|
|
||||||
export type GetTitleData = {
|
|
||||||
body?: never;
|
|
||||||
path: {
|
|
||||||
title_id: number;
|
|
||||||
};
|
|
||||||
query?: {
|
|
||||||
fields?: string;
|
|
||||||
};
|
|
||||||
url: '/titles/{title_id}';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetTitleErrors = {
|
|
||||||
/**
|
|
||||||
* Request params are not correct
|
|
||||||
*/
|
|
||||||
400: unknown;
|
|
||||||
/**
|
|
||||||
* Title not found
|
|
||||||
*/
|
|
||||||
404: unknown;
|
|
||||||
/**
|
|
||||||
* Unknown server error
|
|
||||||
*/
|
|
||||||
500: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetTitleResponses = {
|
|
||||||
/**
|
|
||||||
* Title description
|
|
||||||
*/
|
|
||||||
200: Title;
|
|
||||||
/**
|
|
||||||
* No title found
|
|
||||||
*/
|
|
||||||
204: void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetTitleResponse = GetTitleResponses[keyof GetTitleResponses];
|
|
||||||
|
|
||||||
export type GetUsersData = {
|
|
||||||
body?: never;
|
|
||||||
path?: never;
|
|
||||||
query?: {
|
|
||||||
word?: string;
|
|
||||||
limit?: number;
|
|
||||||
/**
|
|
||||||
* pass cursor naked
|
|
||||||
*/
|
|
||||||
cursor_id?: number;
|
|
||||||
};
|
|
||||||
url: '/users/';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetUsersErrors = {
|
|
||||||
/**
|
|
||||||
* Request params are not correct
|
|
||||||
*/
|
|
||||||
400: unknown;
|
|
||||||
/**
|
|
||||||
* Unknown server error
|
|
||||||
*/
|
|
||||||
500: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetUsersResponses = {
|
|
||||||
/**
|
|
||||||
* List of users with cursor
|
|
||||||
*/
|
|
||||||
200: {
|
|
||||||
/**
|
|
||||||
* List of users
|
|
||||||
*/
|
|
||||||
data: Array<User>;
|
|
||||||
cursor: number;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* No users found
|
|
||||||
*/
|
|
||||||
204: void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetUsersResponse = GetUsersResponses[keyof GetUsersResponses];
|
|
||||||
|
|
||||||
export type GetUsersIdData = {
|
|
||||||
body?: never;
|
|
||||||
path: {
|
|
||||||
user_id: string;
|
|
||||||
};
|
|
||||||
query?: {
|
|
||||||
fields?: string;
|
|
||||||
};
|
|
||||||
url: '/users/{user_id}';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetUsersIdErrors = {
|
|
||||||
/**
|
|
||||||
* Request params are not correct
|
|
||||||
*/
|
|
||||||
400: unknown;
|
|
||||||
/**
|
|
||||||
* User not found
|
|
||||||
*/
|
|
||||||
404: unknown;
|
|
||||||
/**
|
|
||||||
* Unknown server error
|
|
||||||
*/
|
|
||||||
500: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetUsersIdResponses = {
|
|
||||||
/**
|
|
||||||
* User info
|
|
||||||
*/
|
|
||||||
200: User;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetUsersIdResponse = GetUsersIdResponses[keyof GetUsersIdResponses];
|
|
||||||
|
|
||||||
export type UpdateUserData = {
|
|
||||||
/**
|
|
||||||
* Only provided fields are updated. Omitted fields remain unchanged.
|
|
||||||
*/
|
|
||||||
body: {
|
|
||||||
/**
|
|
||||||
* ID of the user avatar (references `images.id`); set to `null` to remove avatar
|
|
||||||
*/
|
|
||||||
avatar_id?: number | null;
|
|
||||||
/**
|
|
||||||
* User email (must be unique and valid)
|
|
||||||
*/
|
|
||||||
mail?: string;
|
|
||||||
/**
|
|
||||||
* Username (alphanumeric + `_` or `-`, 3–16 chars)
|
|
||||||
*/
|
|
||||||
nickname?: string;
|
|
||||||
/**
|
|
||||||
* Display name
|
|
||||||
*/
|
|
||||||
disp_name?: string;
|
|
||||||
/**
|
|
||||||
* User description / bio
|
|
||||||
*/
|
|
||||||
user_desc?: string;
|
|
||||||
};
|
|
||||||
path: {
|
|
||||||
/**
|
|
||||||
* User ID (primary key)
|
|
||||||
*/
|
|
||||||
user_id: number;
|
|
||||||
};
|
|
||||||
query?: never;
|
|
||||||
url: '/users/{user_id}';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UpdateUserErrors = {
|
|
||||||
/**
|
|
||||||
* Invalid input (e.g., validation failed, nickname/email conflict, malformed JSON)
|
|
||||||
*/
|
|
||||||
400: unknown;
|
|
||||||
/**
|
|
||||||
* Unauthorized — missing or invalid authentication token
|
|
||||||
*/
|
|
||||||
401: unknown;
|
|
||||||
/**
|
|
||||||
* Forbidden — user is not allowed to modify this resource (e.g., not own profile & no admin rights)
|
|
||||||
*/
|
|
||||||
403: unknown;
|
|
||||||
/**
|
|
||||||
* User not found
|
|
||||||
*/
|
|
||||||
404: unknown;
|
|
||||||
/**
|
|
||||||
* Conflict — e.g., requested `nickname` or `mail` already taken by another user
|
|
||||||
*/
|
|
||||||
409: unknown;
|
|
||||||
/**
|
|
||||||
* Unprocessable Entity — semantic errors not caught by schema (e.g., invalid `avatar_id`)
|
|
||||||
*/
|
|
||||||
422: unknown;
|
|
||||||
/**
|
|
||||||
* Unknown server error
|
|
||||||
*/
|
|
||||||
500: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UpdateUserResponses = {
|
|
||||||
/**
|
|
||||||
* User updated successfully. Returns updated user representation (excluding sensitive fields).
|
|
||||||
*/
|
|
||||||
200: User;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UpdateUserResponse = UpdateUserResponses[keyof UpdateUserResponses];
|
|
||||||
|
|
||||||
export type GetUserTitlesData = {
|
|
||||||
body?: never;
|
|
||||||
path: {
|
|
||||||
user_id: string;
|
|
||||||
};
|
|
||||||
query?: {
|
|
||||||
cursor?: string;
|
|
||||||
sort?: TitleSort;
|
|
||||||
sort_forward?: boolean;
|
|
||||||
word?: string;
|
|
||||||
/**
|
|
||||||
* List of title statuses to filter
|
|
||||||
*/
|
|
||||||
status?: Array<TitleStatus>;
|
|
||||||
watch_status?: Array<UserTitleStatus>;
|
|
||||||
rating?: number;
|
|
||||||
my_rate?: number;
|
|
||||||
release_year?: number;
|
|
||||||
release_season?: ReleaseSeason;
|
|
||||||
limit?: number;
|
|
||||||
fields?: string;
|
|
||||||
};
|
|
||||||
url: '/users/{user_id}/titles';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetUserTitlesErrors = {
|
|
||||||
/**
|
|
||||||
* Request params are not correct
|
|
||||||
*/
|
|
||||||
400: unknown;
|
|
||||||
/**
|
|
||||||
* User not found
|
|
||||||
*/
|
|
||||||
404: unknown;
|
|
||||||
/**
|
|
||||||
* Unknown server error
|
|
||||||
*/
|
|
||||||
500: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetUserTitlesResponses = {
|
|
||||||
/**
|
|
||||||
* List of user titles
|
|
||||||
*/
|
|
||||||
200: {
|
|
||||||
data: Array<UserTitle>;
|
|
||||||
cursor: CursorObj;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* No titles found
|
|
||||||
*/
|
|
||||||
204: void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetUserTitlesResponse = GetUserTitlesResponses[keyof GetUserTitlesResponses];
|
|
||||||
|
|
||||||
export type AddUserTitleData = {
|
|
||||||
body: {
|
|
||||||
title_id: number;
|
|
||||||
status: UserTitleStatus;
|
|
||||||
rate?: number;
|
|
||||||
};
|
|
||||||
path: {
|
|
||||||
/**
|
|
||||||
* ID of the user to assign the title to
|
|
||||||
*/
|
|
||||||
user_id: number;
|
|
||||||
};
|
|
||||||
query?: never;
|
|
||||||
url: '/users/{user_id}/titles';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AddUserTitleErrors = {
|
|
||||||
/**
|
|
||||||
* Invalid request body (missing fields, invalid types, etc.)
|
|
||||||
*/
|
|
||||||
400: unknown;
|
|
||||||
/**
|
|
||||||
* Unauthorized — missing or invalid auth token
|
|
||||||
*/
|
|
||||||
401: unknown;
|
|
||||||
/**
|
|
||||||
* Forbidden — user not allowed to assign titles to this user
|
|
||||||
*/
|
|
||||||
403: unknown;
|
|
||||||
/**
|
|
||||||
* User or Title not found
|
|
||||||
*/
|
|
||||||
404: unknown;
|
|
||||||
/**
|
|
||||||
* Conflict — title already assigned to user (if applicable)
|
|
||||||
*/
|
|
||||||
409: unknown;
|
|
||||||
/**
|
|
||||||
* Internal server error
|
|
||||||
*/
|
|
||||||
500: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AddUserTitleResponses = {
|
|
||||||
/**
|
|
||||||
* Title successfully added to user
|
|
||||||
*/
|
|
||||||
200: UserTitleMini;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AddUserTitleResponse = AddUserTitleResponses[keyof AddUserTitleResponses];
|
|
||||||
|
|
||||||
export type DeleteUserTitleData = {
|
|
||||||
body?: never;
|
|
||||||
path: {
|
|
||||||
user_id: number;
|
|
||||||
title_id: number;
|
|
||||||
};
|
|
||||||
query?: never;
|
|
||||||
url: '/users/{user_id}/titles/{title_id}';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DeleteUserTitleErrors = {
|
|
||||||
/**
|
|
||||||
* Unauthorized — missing or invalid auth token
|
|
||||||
*/
|
|
||||||
401: unknown;
|
|
||||||
/**
|
|
||||||
* Forbidden — user not allowed to delete title
|
|
||||||
*/
|
|
||||||
403: unknown;
|
|
||||||
/**
|
|
||||||
* User or Title not found
|
|
||||||
*/
|
|
||||||
404: unknown;
|
|
||||||
/**
|
|
||||||
* Internal server error
|
|
||||||
*/
|
|
||||||
500: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DeleteUserTitleResponses = {
|
|
||||||
/**
|
|
||||||
* Title successfully deleted
|
|
||||||
*/
|
|
||||||
200: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetUserTitleData = {
|
|
||||||
body?: never;
|
|
||||||
path: {
|
|
||||||
user_id: number;
|
|
||||||
title_id: number;
|
|
||||||
};
|
|
||||||
query?: never;
|
|
||||||
url: '/users/{user_id}/titles/{title_id}';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetUserTitleErrors = {
|
|
||||||
/**
|
|
||||||
* Request params are not correct
|
|
||||||
*/
|
|
||||||
400: unknown;
|
|
||||||
/**
|
|
||||||
* User or title not found
|
|
||||||
*/
|
|
||||||
404: unknown;
|
|
||||||
/**
|
|
||||||
* Unknown server error
|
|
||||||
*/
|
|
||||||
500: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetUserTitleResponses = {
|
|
||||||
/**
|
|
||||||
* User titles
|
|
||||||
*/
|
|
||||||
200: UserTitleMini;
|
|
||||||
/**
|
|
||||||
* No user title found
|
|
||||||
*/
|
|
||||||
204: void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetUserTitleResponse = GetUserTitleResponses[keyof GetUserTitleResponses];
|
|
||||||
|
|
||||||
export type UpdateUserTitleData = {
|
|
||||||
body: {
|
|
||||||
status?: UserTitleStatus;
|
|
||||||
rate?: number;
|
|
||||||
};
|
|
||||||
path: {
|
|
||||||
user_id: number;
|
|
||||||
title_id: number;
|
|
||||||
};
|
|
||||||
query?: never;
|
|
||||||
url: '/users/{user_id}/titles/{title_id}';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UpdateUserTitleErrors = {
|
|
||||||
/**
|
|
||||||
* Invalid request body (missing fields, invalid types, etc.)
|
|
||||||
*/
|
|
||||||
400: unknown;
|
|
||||||
/**
|
|
||||||
* Unauthorized — missing or invalid auth token
|
|
||||||
*/
|
|
||||||
401: unknown;
|
|
||||||
/**
|
|
||||||
* Forbidden — user not allowed to update title
|
|
||||||
*/
|
|
||||||
403: unknown;
|
|
||||||
/**
|
|
||||||
* User or Title not found
|
|
||||||
*/
|
|
||||||
404: unknown;
|
|
||||||
/**
|
|
||||||
* Internal server error
|
|
||||||
*/
|
|
||||||
500: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UpdateUserTitleResponses = {
|
|
||||||
/**
|
|
||||||
* Title successfully updated
|
|
||||||
*/
|
|
||||||
200: UserTitleMini;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UpdateUserTitleResponse = UpdateUserTitleResponses[keyof UpdateUserTitleResponses];
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
/* generated using openapi-typescript-codegen -- do not edit */
|
|
||||||
/* istanbul ignore file */
|
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
export { ApiError } from './core/ApiError';
|
|
||||||
export { CancelablePromise, CancelError } from './core/CancelablePromise';
|
|
||||||
export { OpenAPI } from './core/OpenAPI';
|
|
||||||
export type { OpenAPIConfig } from './core/OpenAPI';
|
|
||||||
|
|
||||||
export { AuthService } from './services/AuthService';
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
/* generated using openapi-typescript-codegen -- do not edit */
|
|
||||||
/* istanbul ignore file */
|
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
import type { CancelablePromise } from '../core/CancelablePromise';
|
|
||||||
import { OpenAPI } from '../core/OpenAPI';
|
|
||||||
import { request as __request } from '../core/request';
|
|
||||||
export class AuthService {
|
|
||||||
/**
|
|
||||||
* Sign up a new user
|
|
||||||
* @param requestBody
|
|
||||||
* @returns any Sign-up result
|
|
||||||
* @throws ApiError
|
|
||||||
*/
|
|
||||||
public static postSignUp(
|
|
||||||
requestBody: {
|
|
||||||
nickname: string;
|
|
||||||
pass: string;
|
|
||||||
},
|
|
||||||
): CancelablePromise<{
|
|
||||||
user_id: number;
|
|
||||||
}> {
|
|
||||||
return __request(OpenAPI, {
|
|
||||||
method: 'POST',
|
|
||||||
url: '/sign-up',
|
|
||||||
body: requestBody,
|
|
||||||
mediaType: 'application/json',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Sign in a user and return JWT
|
|
||||||
* @param requestBody
|
|
||||||
* @returns any Sign-in result with JWT
|
|
||||||
* @throws ApiError
|
|
||||||
*/
|
|
||||||
public static postSignIn(
|
|
||||||
requestBody: {
|
|
||||||
nickname: string;
|
|
||||||
pass: string;
|
|
||||||
},
|
|
||||||
): CancelablePromise<{
|
|
||||||
user_id: number;
|
|
||||||
user_name: string;
|
|
||||||
}> {
|
|
||||||
return __request(OpenAPI, {
|
|
||||||
method: 'POST',
|
|
||||||
url: '/sign-in',
|
|
||||||
body: requestBody,
|
|
||||||
mediaType: 'application/json',
|
|
||||||
errors: {
|
|
||||||
401: `Access denied due to invalid credentials`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
import React, { useState } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/solid";
|
|
||||||
|
|
||||||
type HeaderProps = {
|
|
||||||
username?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Header: React.FC<HeaderProps> = ({ username }) => {
|
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
|
||||||
|
|
||||||
const toggleMenu = () => setMenuOpen(!menuOpen);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header className="w-full bg-white shadow-md sticky top-0 left-0 z-50">
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="flex justify-between h-16 items-center">
|
|
||||||
|
|
||||||
{/* Левый блок — логотип / название */}
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<Link to="/" className="text-xl font-bold text-gray-800 hover:text-blue-600">
|
|
||||||
NyanimeDB
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Центр — ссылки на разделы (desktop) */}
|
|
||||||
<nav className="hidden md:flex space-x-4">
|
|
||||||
<Link to="/titles" className="text-gray-700 hover:text-blue-600">
|
|
||||||
Titles
|
|
||||||
</Link>
|
|
||||||
<Link to="/users" className="text-gray-700 hover:text-blue-600">
|
|
||||||
Users
|
|
||||||
</Link>
|
|
||||||
<Link to="/about" className="text-gray-700 hover:text-blue-600">
|
|
||||||
About
|
|
||||||
</Link>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{/* Правый блок — профиль */}
|
|
||||||
<div className="hidden md:flex items-center space-x-4">
|
|
||||||
{username ? (
|
|
||||||
<Link to="/profile" className="text-gray-700 hover:text-blue-600 font-medium">
|
|
||||||
{username}
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<Link to="/login" className="text-gray-700 hover:text-blue-600 font-medium">
|
|
||||||
Login
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Бургер для мобильного */}
|
|
||||||
<div className="md:hidden flex items-center">
|
|
||||||
<button
|
|
||||||
onClick={toggleMenu}
|
|
||||||
className="p-2 rounded-md hover:bg-gray-200 transition"
|
|
||||||
>
|
|
||||||
{menuOpen ? (
|
|
||||||
<XMarkIcon className="w-6 h-6 text-gray-800" />
|
|
||||||
) : (
|
|
||||||
<Bars3Icon className="w-6 h-6 text-gray-800" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Мобильное меню */}
|
|
||||||
{menuOpen && (
|
|
||||||
<div className="md:hidden bg-white border-t border-gray-200 shadow-md">
|
|
||||||
<nav className="flex flex-col p-4 space-y-2">
|
|
||||||
<Link to="/titles" className="text-gray-700 hover:text-blue-600" onClick={() => setMenuOpen(false)}>Titles</Link>
|
|
||||||
<Link to="/users" className="text-gray-700 hover:text-blue-600" onClick={() => setMenuOpen(false)}>Users</Link>
|
|
||||||
<Link to="/about" className="text-gray-700 hover:text-blue-600" onClick={() => setMenuOpen(false)}>About</Link>
|
|
||||||
{username ? (
|
|
||||||
<Link to="/profile" className="text-gray-700 hover:text-blue-600 font-medium" onClick={() => setMenuOpen(false)}>
|
|
||||||
{username}
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<Link to="/login" className="text-gray-700 hover:text-blue-600 font-medium" onClick={() => setMenuOpen(false)}>
|
|
||||||
Login
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { Squares2X2Icon, Bars3Icon } from "@heroicons/react/24/solid";
|
|
||||||
|
|
||||||
export type LayoutSwitchProps = {
|
|
||||||
layout: "square" | "horizontal"
|
|
||||||
setLayout: (value: React.SetStateAction<"square" | "horizontal">) => void
|
|
||||||
};
|
|
||||||
|
|
||||||
export function LayoutSwitch({
|
|
||||||
layout,
|
|
||||||
setLayout
|
|
||||||
}: LayoutSwitchProps) {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<button
|
|
||||||
className="p-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
|
|
||||||
onClick={() =>
|
|
||||||
setLayout(prev => (prev === "square" ? "horizontal" : "square"))
|
|
||||||
}>
|
|
||||||
{layout === "square"
|
|
||||||
? <Squares2X2Icon className="w-6 h-6" />
|
|
||||||
: <Bars3Icon className="w-6 h-6" />
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export type ListViewProps<T> = {
|
|
||||||
items: T[];
|
|
||||||
layout: "square" | "horizontal";
|
|
||||||
renderItem: (item: T, layout: "square" | "horizontal") => React.ReactNode;
|
|
||||||
onLoadMore: () => void;
|
|
||||||
hasMore: boolean;
|
|
||||||
loadingMore: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ListView<T>({
|
|
||||||
items,
|
|
||||||
layout,
|
|
||||||
renderItem,
|
|
||||||
onLoadMore,
|
|
||||||
hasMore,
|
|
||||||
loadingMore
|
|
||||||
}: ListViewProps<T>) {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full flex flex-col items-center">
|
|
||||||
{/* Items */}
|
|
||||||
<div
|
|
||||||
className={`w-full sm:w-4/5 grid gap-6 ${
|
|
||||||
layout === "square"
|
|
||||||
? "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4"
|
|
||||||
: "grid-cols-1"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{items.map(item => renderItem(item, layout))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Load More */}
|
|
||||||
{hasMore && (
|
|
||||||
<div className="mt-8">
|
|
||||||
<button
|
|
||||||
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition disabled:opacity-50"
|
|
||||||
disabled={loadingMore}
|
|
||||||
onClick={onLoadMore}
|
|
||||||
>
|
|
||||||
{loadingMore ? "Loading..." : "Load More"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
type SearchBarProps = {
|
|
||||||
placeholder?: string;
|
|
||||||
search: string;
|
|
||||||
setSearch: (value: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function SearchBar({
|
|
||||||
placeholder = "Search...",
|
|
||||||
search,
|
|
||||||
setSearch,
|
|
||||||
}: SearchBarProps) {
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={search}
|
|
||||||
placeholder={placeholder}
|
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
|
||||||
className="
|
|
||||||
w-full
|
|
||||||
px-4
|
|
||||||
py-2
|
|
||||||
border
|
|
||||||
border-gray-300
|
|
||||||
rounded-lg
|
|
||||||
focus:outline-none
|
|
||||||
focus:ring-2
|
|
||||||
focus:ring-blue-500
|
|
||||||
text-black
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
// import { DefaultService } from "../../api";
|
|
||||||
import { addUserTitle, deleteUserTitle, getUserTitle, updateUserTitle } from "../../api";
|
|
||||||
import type { UserTitleStatus } from "../../api";
|
|
||||||
import { useCookies } from 'react-cookie';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ClockIcon,
|
|
||||||
CheckCircleIcon,
|
|
||||||
PlayCircleIcon,
|
|
||||||
XCircleIcon,
|
|
||||||
} from "@heroicons/react/24/solid";
|
|
||||||
// import { stat } from "fs";
|
|
||||||
|
|
||||||
// Статусы с иконками и подписью
|
|
||||||
const STATUS_BUTTONS: { status: UserTitleStatus; icon: React.ReactNode; label: string }[] = [
|
|
||||||
{ status: "planned", icon: <ClockIcon className="w-5 h-5" />, label: "Planned" },
|
|
||||||
{ status: "finished", icon: <CheckCircleIcon className="w-5 h-5" />, label: "Finished" },
|
|
||||||
{ status: "in-progress", icon: <PlayCircleIcon className="w-5 h-5" />, label: "In Progress" },
|
|
||||||
{ status: "dropped", icon: <XCircleIcon className="w-5 h-5" />, label: "Dropped" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function TitleStatusControls({ titleId }: { titleId: number }) {
|
|
||||||
const [cookies] = useCookies(['xsrf_token']);
|
|
||||||
const xsrfToken = cookies['xsrf_token'] || null;
|
|
||||||
|
|
||||||
const [currentStatus, setCurrentStatus] = useState<UserTitleStatus | null>(null);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const userIdStr = localStorage.getItem("userId");
|
|
||||||
const userId = userIdStr ? Number(userIdStr) : null;
|
|
||||||
|
|
||||||
// --- Load initial status ---
|
|
||||||
useEffect(() => {
|
|
||||||
if (!userId) return;
|
|
||||||
getUserTitle({ path: { user_id: userId, title_id: titleId } })
|
|
||||||
.then(res => setCurrentStatus(res.data?.status ?? null))
|
|
||||||
.catch(() => setCurrentStatus(null)); // 404 = not assigned
|
|
||||||
|
|
||||||
// DefaultService.getUserTitle(userId, titleId)
|
|
||||||
// .then((res) => setCurrentStatus(res.status))
|
|
||||||
// .catch(() => setCurrentStatus(null)); // 404 = user title does not exist
|
|
||||||
}, [titleId, userId]);
|
|
||||||
|
|
||||||
// --- Handle click ---
|
|
||||||
const handleStatusClick = async (status: UserTitleStatus) => {
|
|
||||||
if (!userId || loading) return;
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1) Если кликнули на текущий статус — DELETE
|
|
||||||
if (currentStatus === status) {
|
|
||||||
// await DefaultService.deleteUserTitle(userId, titleId);
|
|
||||||
await deleteUserTitle({path: {
|
|
||||||
user_id: userId,
|
|
||||||
title_id: titleId,
|
|
||||||
},
|
|
||||||
headers: { "X-XSRF-TOKEN": xsrfToken },
|
|
||||||
})
|
|
||||||
setCurrentStatus(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Если другой статус — POST или PATCH
|
|
||||||
if (!currentStatus) {
|
|
||||||
// ещё нет записи — POST
|
|
||||||
// const added = await DefaultService.addUserTitle(userId, {
|
|
||||||
// title_id: titleId,
|
|
||||||
// status,
|
|
||||||
// });
|
|
||||||
const added = await addUserTitle({
|
|
||||||
body: {
|
|
||||||
title_id: titleId,
|
|
||||||
status: status,
|
|
||||||
},
|
|
||||||
path: {user_id: userId},
|
|
||||||
headers: { "X-XSRF-TOKEN": xsrfToken },
|
|
||||||
});
|
|
||||||
|
|
||||||
setCurrentStatus(added.data?.status ?? null);
|
|
||||||
} else {
|
|
||||||
// уже есть запись — PATCH
|
|
||||||
//const updated = await DefaultService.updateUserTitle(userId, titleId, { status });
|
|
||||||
const updated = await updateUserTitle({
|
|
||||||
path: { user_id: userId, title_id: titleId },
|
|
||||||
body: { status },
|
|
||||||
headers: { "X-XSRF-TOKEN": xsrfToken },
|
|
||||||
});
|
|
||||||
setCurrentStatus(updated.data?.status ?? null);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex gap-2 flex-wrap justify-center mt-2">
|
|
||||||
{STATUS_BUTTONS.map(btn => (
|
|
||||||
<button
|
|
||||||
key={btn.status}
|
|
||||||
onClick={() => handleStatusClick(btn.status)}
|
|
||||||
disabled={loading}
|
|
||||||
className={`
|
|
||||||
px-3 py-1 rounded-md border flex items-center gap-1 transition
|
|
||||||
${currentStatus === btn.status
|
|
||||||
? "bg-blue-600 text-white border-blue-700"
|
|
||||||
: "bg-gray-200 text-black border-gray-300 hover:bg-gray-300"}
|
|
||||||
`}
|
|
||||||
title={btn.label}
|
|
||||||
>
|
|
||||||
{btn.icon}
|
|
||||||
<span>{btn.label}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
import { useState } from "react";
|
|
||||||
import type { TitleStatus, ReleaseSeason } from "../../api";
|
|
||||||
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/solid";
|
|
||||||
|
|
||||||
export type TitlesFilter = {
|
|
||||||
extSearch: boolean;
|
|
||||||
status: TitleStatus | "";
|
|
||||||
rating: number | "";
|
|
||||||
releaseYear: number | "";
|
|
||||||
releaseSeason: ReleaseSeason | "";
|
|
||||||
};
|
|
||||||
|
|
||||||
type TitlesFilterPanelProps = {
|
|
||||||
filters: TitlesFilter;
|
|
||||||
setFilters: (filters: TitlesFilter) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const STATUS_OPTIONS: (TitleStatus | "")[] = ["", "planned", "finished", "ongoing"];
|
|
||||||
const SEASON_OPTIONS: (ReleaseSeason | "")[] = ["", "winter", "spring", "summer", "fall"];
|
|
||||||
const RATING_OPTIONS = ["", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
|
||||||
|
|
||||||
export function TitlesFilterPanel({ filters, setFilters }: TitlesFilterPanelProps) {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const handleChange = (field: keyof TitlesFilter, value: any) => {
|
|
||||||
setFilters({ ...filters, [field]: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full flex justify-center my-4">
|
|
||||||
<div className="bg-white shadow rounded-lg w-full max-w-3xl p-4">
|
|
||||||
{/* Заголовок панели */}
|
|
||||||
<div
|
|
||||||
className="flex justify-between items-center cursor-pointer"
|
|
||||||
onClick={() => setOpen((prev) => !prev)}
|
|
||||||
>
|
|
||||||
<h3 className="text-lg font-medium">Filters</h3>
|
|
||||||
{open ? <ChevronUpIcon className="w-5 h-5" /> : <ChevronDownIcon className="w-5 h-5" />}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Контент панели */}
|
|
||||||
{open && (
|
|
||||||
<div className="mt-4 grid grid-cols-2 sm:grid-cols-3 gap-4">
|
|
||||||
{/* Extended Search */}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="extSearch"
|
|
||||||
checked={filters.extSearch}
|
|
||||||
onChange={(e) => handleChange("extSearch", e.target.checked)}
|
|
||||||
className="w-4 h-4"
|
|
||||||
/>
|
|
||||||
<label htmlFor="extSearch" className="text-sm">
|
|
||||||
Extended Search
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Status */}
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<label htmlFor="status" className="text-sm mb-1">Status</label>
|
|
||||||
<select
|
|
||||||
id="status"
|
|
||||||
value={filters.status}
|
|
||||||
onChange={(e) => handleChange("status", e.target.value || "")}
|
|
||||||
className="border rounded px-2 py-1"
|
|
||||||
>
|
|
||||||
{STATUS_OPTIONS.map((s) => (
|
|
||||||
<option key={s || "all"} value={s}>{s || "All"}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Rating */}
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<label htmlFor="rating" className="text-sm mb-1">Rating</label>
|
|
||||||
<select
|
|
||||||
id="rating"
|
|
||||||
value={filters.rating}
|
|
||||||
onChange={(e) => handleChange("rating", e.target.value ? Number(e.target.value) : "")}
|
|
||||||
className="border rounded px-2 py-1"
|
|
||||||
>
|
|
||||||
{RATING_OPTIONS.map((r) => (
|
|
||||||
<option key={r} value={r}>{r || "All"}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Release Year */}
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<label htmlFor="releaseYear" className="text-sm mb-1">Release Year</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="releaseYear"
|
|
||||||
value={filters.releaseYear || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleChange("releaseYear", e.target.value ? Number(e.target.value) : "")
|
|
||||||
}
|
|
||||||
className="border rounded px-2 py-1"
|
|
||||||
placeholder="Any"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Release Season */}
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<label htmlFor="releaseSeason" className="text-sm mb-1">Release Season</label>
|
|
||||||
<select
|
|
||||||
id="releaseSeason"
|
|
||||||
value={filters.releaseSeason}
|
|
||||||
onChange={(e) => handleChange("releaseSeason", e.target.value || "")}
|
|
||||||
className="border rounded px-2 py-1"
|
|
||||||
>
|
|
||||||
{SEASON_OPTIONS.map((s) => (
|
|
||||||
<option key={s || "all"} value={s}>{s || "All"}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
import { useState } from "react";
|
|
||||||
import type { TitleSort } from "../../api";
|
|
||||||
import { ChevronDownIcon, ArrowUpIcon, ArrowDownIcon } from "@heroicons/react/24/solid";
|
|
||||||
|
|
||||||
type TitlesSortBoxProps = {
|
|
||||||
sort: TitleSort;
|
|
||||||
setSort: (value: TitleSort) => void;
|
|
||||||
sortForward: boolean;
|
|
||||||
setSortForward: (value: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SORT_OPTIONS: TitleSort[] = ["id", "rating", "year", "views"];
|
|
||||||
|
|
||||||
export function TitlesSortBox({
|
|
||||||
sort,
|
|
||||||
setSort,
|
|
||||||
sortForward,
|
|
||||||
setSortForward,
|
|
||||||
}: TitlesSortBoxProps) {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const toggleSortDirection = () => setSortForward(!sortForward);
|
|
||||||
const handleSortSelect = (newSort: TitleSort) => {
|
|
||||||
setSort(newSort);
|
|
||||||
setOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="inline-flex relative z-50">
|
|
||||||
{/* Левая часть — смена направления */}
|
|
||||||
<button
|
|
||||||
onClick={toggleSortDirection}
|
|
||||||
className="px-4 py-2 flex items-center justify-center bg-gray-100 hover:bg-gray-200 border border-gray-300 rounded-l-lg transition"
|
|
||||||
>
|
|
||||||
{sortForward ? <ArrowUpIcon className="w-4 h-4 mr-1" /> : <ArrowDownIcon className="w-4 h-4 mr-1" />}
|
|
||||||
<span className="text-sm font-medium">Order</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Правая часть — выбор параметра */}
|
|
||||||
<button
|
|
||||||
onClick={() => setOpen(!open)}
|
|
||||||
className="px-4 py-2 flex items-center justify-center bg-gray-100 hover:bg-gray-200 border border-gray-300 border-l-0 rounded-r-lg transition"
|
|
||||||
>
|
|
||||||
<span className="text-sm font-medium">{sort}</span>
|
|
||||||
<ChevronDownIcon className="w-4 h-4 ml-1" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Dropdown */}
|
|
||||||
{open && (
|
|
||||||
<ul className="absolute top-full left-0 mt-1 w-40 bg-white border border-gray-300 rounded-md shadow-lg z-[1000]">
|
|
||||||
{SORT_OPTIONS.map(option => (
|
|
||||||
<li key={option}>
|
|
||||||
<button
|
|
||||||
className={`w-full text-left px-4 py-2 hover:bg-gray-100 transition ${
|
|
||||||
option === sort ? "font-bold bg-gray-100" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => handleSortSelect(option)}
|
|
||||||
>
|
|
||||||
{option}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
97
modules/frontend/src/components/UserPage/UserPage.module.css
Normal file
97
modules/frontend/src/components/UserPage/UserPage.module.css
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 3rem 1rem;
|
||||||
|
background-color: #f5f6fa;
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatarImg {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 3px solid #4a90e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatarPlaceholder {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #dcdde1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 3rem;
|
||||||
|
color: #4a4a4a;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 3px solid #4a90e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
color: #2f3640;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #718093;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #353b48;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.created {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #7f8fa6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 80vh;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #4a90e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 80vh;
|
||||||
|
color: #e84118;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue