Compare commits
2 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80c72e1d5f | ||
|
|
5f29ef23a2 |
72 changed files with 1089 additions and 3923 deletions
|
|
@ -1,49 +0,0 @@
|
||||||
name: Build (backend build only)
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- dev-ars
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
# Build backend
|
|
||||||
- uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version: '^1.25'
|
|
||||||
|
|
||||||
- name: Build backend
|
|
||||||
run: |
|
|
||||||
cd modules/backend
|
|
||||||
go build -o nyanimedb .
|
|
||||||
tar -czvf nyanimedb-backend.tar.gz nyanimedb
|
|
||||||
|
|
||||||
- name: Upload built backend to artifactory
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: nyanimedb-backend.tar.gz
|
|
||||||
path: modules/backend/nyanimedb-backend.tar.gz
|
|
||||||
|
|
||||||
# Build Docker images
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ${{ vars.REGISTRY }}
|
|
||||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
|
||||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build and push backend image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: Dockerfiles/Dockerfile_backend
|
|
||||||
push: true
|
|
||||||
tags: meowgit.nekoea.red/nihonium/nyanimedb-backend:latest
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
name: Build (frontend build only)
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- front
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
# Build frontend
|
|
||||||
- uses: actions/setup-node@v5
|
|
||||||
with:
|
|
||||||
node-version-file: modules/frontend/package.json
|
|
||||||
cache: npm
|
|
||||||
cache-dependency-path: modules/frontend/package-lock.json
|
|
||||||
|
|
||||||
- name: Build frontend
|
|
||||||
env:
|
|
||||||
VITE_BACKEND_API_BASE_URL: ${{ vars.BACKEND_API_BASE_URL }}
|
|
||||||
run: |
|
|
||||||
cd modules/frontend
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
tar -czvf nyanimedb-frontend.tar.gz dist/
|
|
||||||
|
|
||||||
- name: Upload built frontend to artifactory
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: nyanimedb-frontend.tar.gz
|
|
||||||
path: modules/frontend/nyanimedb-frontend.tar.gz
|
|
||||||
|
|
||||||
# Build Docker images
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ${{ vars.REGISTRY }}
|
|
||||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
|
||||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build and push frontend image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: Dockerfiles/Dockerfile_frontend
|
|
||||||
push: true
|
|
||||||
tags: meowgit.nekoea.red/nihonium/nyanimedb-frontend:latest
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
FROM python:3.12-slim
|
|
||||||
|
|
||||||
WORKDIR /app/modules/anime_etl
|
|
||||||
COPY modules/anime_etl/pyproject.toml modules/anime_etl/uv.lock ./
|
|
||||||
|
|
||||||
RUN pip install --no-cache-dir uv \
|
|
||||||
&& uv sync --frozen --no-dev
|
|
||||||
|
|
||||||
COPY modules/anime_etl ./
|
|
||||||
|
|
||||||
ENV NYANIMEDB_MEDIA_ROOT=/media
|
|
||||||
|
|
||||||
CMD ["uv", "run", "python", "-m", "rabbit_worker"]
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
FROM python:3.12-slim
|
|
||||||
|
|
||||||
WORKDIR /app/modules/image_storage
|
|
||||||
COPY modules/image_storage/pyproject.toml modules/image_storage/uv.lock ./
|
|
||||||
|
|
||||||
RUN pip install --no-cache-dir uv \
|
|
||||||
&& uv sync --frozen --no-dev
|
|
||||||
|
|
||||||
COPY modules/image_storage ./
|
|
||||||
|
|
||||||
ENV NYANIMEDB_MEDIA_ROOT=/media
|
|
||||||
|
|
||||||
EXPOSE 8000
|
|
||||||
|
|
||||||
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
||||||
|
|
@ -395,9 +395,6 @@ paths:
|
||||||
rate:
|
rate:
|
||||||
type: integer
|
type: integer
|
||||||
format: int32
|
format: int32
|
||||||
ftime:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
required:
|
required:
|
||||||
- title_id
|
- title_id
|
||||||
- status
|
- status
|
||||||
|
|
@ -481,9 +478,6 @@ paths:
|
||||||
rate:
|
rate:
|
||||||
type: integer
|
type: integer
|
||||||
format: int32
|
format: int32
|
||||||
ftime:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Title successfully updated
|
description: Title successfully updated
|
||||||
|
|
@ -533,42 +527,6 @@ paths:
|
||||||
description: Internal server error
|
description: Internal server error
|
||||||
security:
|
security:
|
||||||
- XsrfAuthHeader: []
|
- 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:
|
components:
|
||||||
parameters:
|
parameters:
|
||||||
cursor:
|
cursor:
|
||||||
|
|
@ -689,11 +647,6 @@ components:
|
||||||
example:
|
example:
|
||||||
- Attack on Titan
|
- Attack on Titan
|
||||||
- AoT
|
- AoT
|
||||||
title_desc:
|
|
||||||
description: 'Localized description. Key = language (ISO 639-1), value = description.'
|
|
||||||
type: object
|
|
||||||
additionalProperties:
|
|
||||||
type: string
|
|
||||||
studio:
|
studio:
|
||||||
$ref: '#/components/schemas/Studio'
|
$ref: '#/components/schemas/Studio'
|
||||||
tags:
|
tags:
|
||||||
|
|
@ -800,7 +753,7 @@ components:
|
||||||
review_id:
|
review_id:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
ftime:
|
ctime:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
required:
|
required:
|
||||||
|
|
@ -824,7 +777,7 @@ components:
|
||||||
review_id:
|
review_id:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
ftime:
|
ctime:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
required:
|
required:
|
||||||
|
|
|
||||||
118
api/api.gen.go
118
api/api.gen.go
|
|
@ -7,10 +7,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
@ -116,9 +113,6 @@ type Title struct {
|
||||||
// Tags Array of localized tags
|
// Tags Array of localized tags
|
||||||
Tags Tags `json:"tags"`
|
Tags Tags `json:"tags"`
|
||||||
|
|
||||||
// TitleDesc Localized description. Key = language (ISO 639-1), value = description.
|
|
||||||
TitleDesc *map[string]string `json:"title_desc,omitempty"`
|
|
||||||
|
|
||||||
// TitleNames Localized titles. Key = language (ISO 639-1), value = list of names
|
// TitleNames Localized titles. Key = language (ISO 639-1), value = list of names
|
||||||
TitleNames map[string][]string `json:"title_names"`
|
TitleNames map[string][]string `json:"title_names"`
|
||||||
|
|
||||||
|
|
@ -156,7 +150,7 @@ type User struct {
|
||||||
|
|
||||||
// UserTitle defines model for UserTitle.
|
// UserTitle defines model for UserTitle.
|
||||||
type UserTitle struct {
|
type UserTitle struct {
|
||||||
Ftime *time.Time `json:"ftime,omitempty"`
|
Ctime *time.Time `json:"ctime,omitempty"`
|
||||||
Rate *int32 `json:"rate,omitempty"`
|
Rate *int32 `json:"rate,omitempty"`
|
||||||
ReviewId *int64 `json:"review_id,omitempty"`
|
ReviewId *int64 `json:"review_id,omitempty"`
|
||||||
|
|
||||||
|
|
@ -168,7 +162,7 @@ type UserTitle struct {
|
||||||
|
|
||||||
// UserTitleMini defines model for UserTitleMini.
|
// UserTitleMini defines model for UserTitleMini.
|
||||||
type UserTitleMini struct {
|
type UserTitleMini struct {
|
||||||
Ftime *time.Time `json:"ftime,omitempty"`
|
Ctime *time.Time `json:"ctime,omitempty"`
|
||||||
Rate *int32 `json:"rate,omitempty"`
|
Rate *int32 `json:"rate,omitempty"`
|
||||||
ReviewId *int64 `json:"review_id,omitempty"`
|
ReviewId *int64 `json:"review_id,omitempty"`
|
||||||
|
|
||||||
|
|
@ -184,9 +178,6 @@ type UserTitleStatus string
|
||||||
// Cursor defines model for cursor.
|
// Cursor defines model for cursor.
|
||||||
type Cursor = string
|
type Cursor = string
|
||||||
|
|
||||||
// PostMediaUploadMultipartBody defines parameters for PostMediaUpload.
|
|
||||||
type PostMediaUploadMultipartBody = interface{}
|
|
||||||
|
|
||||||
// GetTitlesParams defines parameters for GetTitles.
|
// GetTitlesParams defines parameters for GetTitles.
|
||||||
type GetTitlesParams struct {
|
type GetTitlesParams struct {
|
||||||
Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"`
|
Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"`
|
||||||
|
|
@ -262,7 +253,6 @@ type GetUserTitlesParams struct {
|
||||||
|
|
||||||
// AddUserTitleJSONBody defines parameters for AddUserTitle.
|
// AddUserTitleJSONBody defines parameters for AddUserTitle.
|
||||||
type AddUserTitleJSONBody struct {
|
type AddUserTitleJSONBody struct {
|
||||||
Ftime *time.Time `json:"ftime,omitempty"`
|
|
||||||
Rate *int32 `json:"rate,omitempty"`
|
Rate *int32 `json:"rate,omitempty"`
|
||||||
|
|
||||||
// Status User's title status
|
// Status User's title status
|
||||||
|
|
@ -272,16 +262,12 @@ type AddUserTitleJSONBody struct {
|
||||||
|
|
||||||
// UpdateUserTitleJSONBody defines parameters for UpdateUserTitle.
|
// UpdateUserTitleJSONBody defines parameters for UpdateUserTitle.
|
||||||
type UpdateUserTitleJSONBody struct {
|
type UpdateUserTitleJSONBody struct {
|
||||||
Ftime *time.Time `json:"ftime,omitempty"`
|
|
||||||
Rate *int32 `json:"rate,omitempty"`
|
Rate *int32 `json:"rate,omitempty"`
|
||||||
|
|
||||||
// Status User's title status
|
// Status User's title status
|
||||||
Status *UserTitleStatus `json:"status,omitempty"`
|
Status *UserTitleStatus `json:"status,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostMediaUploadMultipartRequestBody defines body for PostMediaUpload for multipart/form-data ContentType.
|
|
||||||
type PostMediaUploadMultipartRequestBody = PostMediaUploadMultipartBody
|
|
||||||
|
|
||||||
// UpdateUserJSONRequestBody defines body for UpdateUser for application/json ContentType.
|
// UpdateUserJSONRequestBody defines body for UpdateUser for application/json ContentType.
|
||||||
type UpdateUserJSONRequestBody UpdateUserJSONBody
|
type UpdateUserJSONRequestBody UpdateUserJSONBody
|
||||||
|
|
||||||
|
|
@ -293,9 +279,6 @@ type UpdateUserTitleJSONRequestBody UpdateUserTitleJSONBody
|
||||||
|
|
||||||
// ServerInterface represents all server handlers.
|
// ServerInterface represents all server handlers.
|
||||||
type ServerInterface interface {
|
type ServerInterface interface {
|
||||||
// Upload an image (PNG, JPEG, or WebP)
|
|
||||||
// (POST /media/upload)
|
|
||||||
PostMediaUpload(c *gin.Context)
|
|
||||||
// Get titles
|
// Get titles
|
||||||
// (GET /titles)
|
// (GET /titles)
|
||||||
GetTitles(c *gin.Context, params GetTitlesParams)
|
GetTitles(c *gin.Context, params GetTitlesParams)
|
||||||
|
|
@ -337,19 +320,6 @@ type ServerInterfaceWrapper struct {
|
||||||
|
|
||||||
type MiddlewareFunc func(c *gin.Context)
|
type MiddlewareFunc func(c *gin.Context)
|
||||||
|
|
||||||
// PostMediaUpload operation middleware
|
|
||||||
func (siw *ServerInterfaceWrapper) PostMediaUpload(c *gin.Context) {
|
|
||||||
|
|
||||||
for _, middleware := range siw.HandlerMiddlewares {
|
|
||||||
middleware(c)
|
|
||||||
if c.IsAborted() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
siw.Handler.PostMediaUpload(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTitles operation middleware
|
// GetTitles operation middleware
|
||||||
func (siw *ServerInterfaceWrapper) GetTitles(c *gin.Context) {
|
func (siw *ServerInterfaceWrapper) GetTitles(c *gin.Context) {
|
||||||
|
|
||||||
|
|
@ -881,7 +851,6 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options
|
||||||
ErrorHandler: errorHandler,
|
ErrorHandler: errorHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
router.POST(options.BaseURL+"/media/upload", wrapper.PostMediaUpload)
|
|
||||||
router.GET(options.BaseURL+"/titles", wrapper.GetTitles)
|
router.GET(options.BaseURL+"/titles", wrapper.GetTitles)
|
||||||
router.GET(options.BaseURL+"/titles/:title_id", wrapper.GetTitle)
|
router.GET(options.BaseURL+"/titles/:title_id", wrapper.GetTitle)
|
||||||
router.GET(options.BaseURL+"/users/", wrapper.GetUsers)
|
router.GET(options.BaseURL+"/users/", wrapper.GetUsers)
|
||||||
|
|
@ -894,49 +863,6 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options
|
||||||
router.PATCH(options.BaseURL+"/users/:user_id/titles/:title_id", wrapper.UpdateUserTitle)
|
router.PATCH(options.BaseURL+"/users/:user_id/titles/:title_id", wrapper.UpdateUserTitle)
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostMediaUploadRequestObject struct {
|
|
||||||
Body io.Reader
|
|
||||||
MultipartBody *multipart.Reader
|
|
||||||
}
|
|
||||||
|
|
||||||
type PostMediaUploadResponseObject interface {
|
|
||||||
VisitPostMediaUploadResponse(w http.ResponseWriter) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type PostMediaUpload200JSONResponse Image
|
|
||||||
|
|
||||||
func (response PostMediaUpload200JSONResponse) VisitPostMediaUploadResponse(w http.ResponseWriter) error {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(200)
|
|
||||||
|
|
||||||
return json.NewEncoder(w).Encode(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
type PostMediaUpload400JSONResponse string
|
|
||||||
|
|
||||||
func (response PostMediaUpload400JSONResponse) VisitPostMediaUploadResponse(w http.ResponseWriter) error {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(400)
|
|
||||||
|
|
||||||
return json.NewEncoder(w).Encode(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
type PostMediaUpload415Response struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (response PostMediaUpload415Response) VisitPostMediaUploadResponse(w http.ResponseWriter) error {
|
|
||||||
w.WriteHeader(415)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type PostMediaUpload500Response struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (response PostMediaUpload500Response) VisitPostMediaUploadResponse(w http.ResponseWriter) error {
|
|
||||||
w.WriteHeader(500)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetTitlesRequestObject struct {
|
type GetTitlesRequestObject struct {
|
||||||
Params GetTitlesParams
|
Params GetTitlesParams
|
||||||
}
|
}
|
||||||
|
|
@ -1474,9 +1400,6 @@ func (response UpdateUserTitle500Response) VisitUpdateUserTitleResponse(w http.R
|
||||||
|
|
||||||
// StrictServerInterface represents all server handlers.
|
// StrictServerInterface represents all server handlers.
|
||||||
type StrictServerInterface interface {
|
type StrictServerInterface interface {
|
||||||
// Upload an image (PNG, JPEG, or WebP)
|
|
||||||
// (POST /media/upload)
|
|
||||||
PostMediaUpload(ctx context.Context, request PostMediaUploadRequestObject) (PostMediaUploadResponseObject, error)
|
|
||||||
// Get titles
|
// Get titles
|
||||||
// (GET /titles)
|
// (GET /titles)
|
||||||
GetTitles(ctx context.Context, request GetTitlesRequestObject) (GetTitlesResponseObject, error)
|
GetTitles(ctx context.Context, request GetTitlesRequestObject) (GetTitlesResponseObject, error)
|
||||||
|
|
@ -1521,43 +1444,6 @@ type strictHandler struct {
|
||||||
middlewares []StrictMiddlewareFunc
|
middlewares []StrictMiddlewareFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostMediaUpload operation middleware
|
|
||||||
func (sh *strictHandler) PostMediaUpload(ctx *gin.Context) {
|
|
||||||
var request PostMediaUploadRequestObject
|
|
||||||
|
|
||||||
if strings.HasPrefix(ctx.GetHeader("Content-Type"), "encoding") {
|
|
||||||
request.Body = ctx.Request.Body
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(ctx.GetHeader("Content-Type"), "multipart/form-data") {
|
|
||||||
if reader, err := ctx.Request.MultipartReader(); err == nil {
|
|
||||||
request.MultipartBody = reader
|
|
||||||
} else {
|
|
||||||
ctx.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
|
|
||||||
return sh.ssi.PostMediaUpload(ctx, request.(PostMediaUploadRequestObject))
|
|
||||||
}
|
|
||||||
for _, middleware := range sh.middlewares {
|
|
||||||
handler = middleware(handler, "PostMediaUpload")
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := handler(ctx, request)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
ctx.Error(err)
|
|
||||||
ctx.Status(http.StatusInternalServerError)
|
|
||||||
} else if validResponse, ok := response.(PostMediaUploadResponseObject); ok {
|
|
||||||
if err := validResponse.VisitPostMediaUploadResponse(ctx.Writer); err != nil {
|
|
||||||
ctx.Error(err)
|
|
||||||
}
|
|
||||||
} else if response != nil {
|
|
||||||
ctx.Error(fmt.Errorf("unexpected response type: %T", response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTitles operation middleware
|
// GetTitles operation middleware
|
||||||
func (sh *strictHandler) GetTitles(ctx *gin.Context, params GetTitlesParams) {
|
func (sh *strictHandler) GetTitles(ctx *gin.Context, params GetTitlesParams) {
|
||||||
var request GetTitlesRequestObject
|
var request GetTitlesRequestObject
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,6 @@ paths:
|
||||||
$ref: "./paths/users-id-titles.yaml"
|
$ref: "./paths/users-id-titles.yaml"
|
||||||
/users/{user_id}/titles/{title_id}:
|
/users/{user_id}/titles/{title_id}:
|
||||||
$ref: "./paths/users-id-titles-id.yaml"
|
$ref: "./paths/users-id-titles-id.yaml"
|
||||||
/media/upload:
|
|
||||||
$ref: "./paths/media_upload.yaml"
|
|
||||||
|
|
||||||
components:
|
components:
|
||||||
parameters:
|
parameters:
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -61,9 +61,6 @@ patch:
|
||||||
rate:
|
rate:
|
||||||
type: integer
|
type: integer
|
||||||
format: int32
|
format: int32
|
||||||
ftime:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Title successfully updated
|
description: Title successfully updated
|
||||||
|
|
|
||||||
|
|
@ -122,9 +122,6 @@ post:
|
||||||
rate:
|
rate:
|
||||||
type: integer
|
type: integer
|
||||||
format: int32
|
format: int32
|
||||||
ftime:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Title successfully added to user
|
description: Title successfully added to user
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,6 @@ properties:
|
||||||
- Титаны
|
- Титаны
|
||||||
ja:
|
ja:
|
||||||
- 進撃の巨人
|
- 進撃の巨人
|
||||||
title_desc:
|
|
||||||
type: object
|
|
||||||
description: Localized description. Key = language (ISO 639-1), value = description.
|
|
||||||
additionalProperties:
|
|
||||||
type: string
|
|
||||||
studio:
|
studio:
|
||||||
$ref: ./Studio.yaml
|
$ref: ./Studio.yaml
|
||||||
tags:
|
tags:
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,6 @@ properties:
|
||||||
review_id:
|
review_id:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
ftime:
|
ctime:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,6 @@ properties:
|
||||||
review_id:
|
review_id:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
ftime:
|
ctime:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
|
|
||||||
269
auth/auth.gen.go
269
auth/auth.gen.go
|
|
@ -13,23 +13,6 @@ import (
|
||||||
strictgin "github.com/oapi-codegen/runtime/strictmiddleware/gin"
|
strictgin "github.com/oapi-codegen/runtime/strictmiddleware/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
BearerAuthScopes = "bearerAuth.Scopes"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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.
|
// PostSignInJSONBody defines parameters for PostSignIn.
|
||||||
type PostSignInJSONBody struct {
|
type PostSignInJSONBody struct {
|
||||||
Nickname string `json:"nickname"`
|
Nickname string `json:"nickname"`
|
||||||
|
|
@ -42,9 +25,6 @@ type PostSignUpJSONBody struct {
|
||||||
Pass string `json:"pass"`
|
Pass string `json:"pass"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetImpersonationTokenJSONRequestBody defines body for GetImpersonationToken for application/json ContentType.
|
|
||||||
type GetImpersonationTokenJSONRequestBody GetImpersonationTokenJSONBody
|
|
||||||
|
|
||||||
// PostSignInJSONRequestBody defines body for PostSignIn for application/json ContentType.
|
// PostSignInJSONRequestBody defines body for PostSignIn for application/json ContentType.
|
||||||
type PostSignInJSONRequestBody PostSignInJSONBody
|
type PostSignInJSONRequestBody PostSignInJSONBody
|
||||||
|
|
||||||
|
|
@ -53,15 +33,6 @@ type PostSignUpJSONRequestBody PostSignUpJSONBody
|
||||||
|
|
||||||
// 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)
|
|
||||||
// Logs out the user
|
|
||||||
// (POST /logout)
|
|
||||||
Logout(c *gin.Context)
|
|
||||||
// Refreshes access_token and refresh_token
|
|
||||||
// (GET /refresh-tokens)
|
|
||||||
RefreshTokens(c *gin.Context)
|
|
||||||
// Sign in a user and return JWT
|
// Sign in a user and return JWT
|
||||||
// (POST /sign-in)
|
// (POST /sign-in)
|
||||||
PostSignIn(c *gin.Context)
|
PostSignIn(c *gin.Context)
|
||||||
|
|
@ -79,47 +50,6 @@ type ServerInterfaceWrapper struct {
|
||||||
|
|
||||||
type MiddlewareFunc func(c *gin.Context)
|
type MiddlewareFunc func(c *gin.Context)
|
||||||
|
|
||||||
// GetImpersonationToken operation middleware
|
|
||||||
func (siw *ServerInterfaceWrapper) GetImpersonationToken(c *gin.Context) {
|
|
||||||
|
|
||||||
c.Set(BearerAuthScopes, []string{})
|
|
||||||
|
|
||||||
for _, middleware := range siw.HandlerMiddlewares {
|
|
||||||
middleware(c)
|
|
||||||
if c.IsAborted() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
siw.Handler.GetImpersonationToken(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout operation middleware
|
|
||||||
func (siw *ServerInterfaceWrapper) Logout(c *gin.Context) {
|
|
||||||
|
|
||||||
for _, middleware := range siw.HandlerMiddlewares {
|
|
||||||
middleware(c)
|
|
||||||
if c.IsAborted() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
siw.Handler.Logout(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RefreshTokens operation middleware
|
|
||||||
func (siw *ServerInterfaceWrapper) RefreshTokens(c *gin.Context) {
|
|
||||||
|
|
||||||
for _, middleware := range siw.HandlerMiddlewares {
|
|
||||||
middleware(c)
|
|
||||||
if c.IsAborted() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
siw.Handler.RefreshTokens(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PostSignIn operation middleware
|
// PostSignIn operation middleware
|
||||||
func (siw *ServerInterfaceWrapper) PostSignIn(c *gin.Context) {
|
func (siw *ServerInterfaceWrapper) PostSignIn(c *gin.Context) {
|
||||||
|
|
||||||
|
|
@ -173,107 +103,10 @@ 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+"/logout", wrapper.Logout)
|
|
||||||
router.GET(options.BaseURL+"/refresh-tokens", wrapper.RefreshTokens)
|
|
||||||
router.POST(options.BaseURL+"/sign-in", wrapper.PostSignIn)
|
router.POST(options.BaseURL+"/sign-in", wrapper.PostSignIn)
|
||||||
router.POST(options.BaseURL+"/sign-up", wrapper.PostSignUp)
|
router.POST(options.BaseURL+"/sign-up", wrapper.PostSignUp)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientErrorResponse struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServerErrorResponse struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
type UnauthorizedErrorResponse struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetImpersonationTokenRequestObject struct {
|
|
||||||
Body *GetImpersonationTokenJSONRequestBody
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetImpersonationTokenResponseObject interface {
|
|
||||||
VisitGetImpersonationTokenResponse(w http.ResponseWriter) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetImpersonationToken200JSONResponse struct {
|
|
||||||
// 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.WriteHeader(200)
|
|
||||||
|
|
||||||
return json.NewEncoder(w).Encode(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetImpersonationToken401Response = UnauthorizedErrorResponse
|
|
||||||
|
|
||||||
func (response GetImpersonationToken401Response) VisitGetImpersonationTokenResponse(w http.ResponseWriter) error {
|
|
||||||
w.WriteHeader(401)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type LogoutRequestObject struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
type LogoutResponseObject interface {
|
|
||||||
VisitLogoutResponse(w http.ResponseWriter) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type Logout200Response struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (response Logout200Response) VisitLogoutResponse(w http.ResponseWriter) error {
|
|
||||||
w.WriteHeader(200)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Logout500Response = ServerErrorResponse
|
|
||||||
|
|
||||||
func (response Logout500Response) VisitLogoutResponse(w http.ResponseWriter) error {
|
|
||||||
w.WriteHeader(500)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type RefreshTokensRequestObject struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
type RefreshTokensResponseObject interface {
|
|
||||||
VisitRefreshTokensResponse(w http.ResponseWriter) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type RefreshTokens200Response struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (response RefreshTokens200Response) VisitRefreshTokensResponse(w http.ResponseWriter) error {
|
|
||||||
w.WriteHeader(200)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type RefreshTokens400Response = ClientErrorResponse
|
|
||||||
|
|
||||||
func (response RefreshTokens400Response) VisitRefreshTokensResponse(w http.ResponseWriter) error {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type RefreshTokens401Response = UnauthorizedErrorResponse
|
|
||||||
|
|
||||||
func (response RefreshTokens401Response) VisitRefreshTokensResponse(w http.ResponseWriter) error {
|
|
||||||
w.WriteHeader(401)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type RefreshTokens500Response = ServerErrorResponse
|
|
||||||
|
|
||||||
func (response RefreshTokens500Response) VisitRefreshTokensResponse(w http.ResponseWriter) error {
|
|
||||||
w.WriteHeader(500)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type PostSignInRequestObject struct {
|
type PostSignInRequestObject struct {
|
||||||
Body *PostSignInJSONRequestBody
|
Body *PostSignInJSONRequestBody
|
||||||
}
|
}
|
||||||
|
|
@ -294,11 +127,15 @@ func (response PostSignIn200JSONResponse) VisitPostSignInResponse(w http.Respons
|
||||||
return json.NewEncoder(w).Encode(response)
|
return json.NewEncoder(w).Encode(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostSignIn401Response = UnauthorizedErrorResponse
|
type PostSignIn401JSONResponse struct {
|
||||||
|
Error *string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
func (response PostSignIn401Response) VisitPostSignInResponse(w http.ResponseWriter) error {
|
func (response PostSignIn401JSONResponse) VisitPostSignInResponse(w http.ResponseWriter) error {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(401)
|
w.WriteHeader(401)
|
||||||
return nil
|
|
||||||
|
return json.NewEncoder(w).Encode(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostSignUpRequestObject struct {
|
type PostSignUpRequestObject struct {
|
||||||
|
|
@ -322,15 +159,6 @@ 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)
|
|
||||||
// Logs out the user
|
|
||||||
// (POST /logout)
|
|
||||||
Logout(ctx context.Context, request LogoutRequestObject) (LogoutResponseObject, error)
|
|
||||||
// Refreshes access_token and refresh_token
|
|
||||||
// (GET /refresh-tokens)
|
|
||||||
RefreshTokens(ctx context.Context, request RefreshTokensRequestObject) (RefreshTokensResponseObject, error)
|
|
||||||
// Sign in a user and return JWT
|
// Sign in a user and return JWT
|
||||||
// (POST /sign-in)
|
// (POST /sign-in)
|
||||||
PostSignIn(ctx context.Context, request PostSignInRequestObject) (PostSignInResponseObject, error)
|
PostSignIn(ctx context.Context, request PostSignInRequestObject) (PostSignInResponseObject, error)
|
||||||
|
|
@ -351,89 +179,6 @@ type strictHandler struct {
|
||||||
middlewares []StrictMiddlewareFunc
|
middlewares []StrictMiddlewareFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetImpersonationToken operation middleware
|
|
||||||
func (sh *strictHandler) GetImpersonationToken(ctx *gin.Context) {
|
|
||||||
var request GetImpersonationTokenRequestObject
|
|
||||||
|
|
||||||
var body GetImpersonationTokenJSONRequestBody
|
|
||||||
if err := ctx.ShouldBindJSON(&body); err != nil {
|
|
||||||
ctx.Status(http.StatusBadRequest)
|
|
||||||
ctx.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
request.Body = &body
|
|
||||||
|
|
||||||
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
|
|
||||||
return sh.ssi.GetImpersonationToken(ctx, request.(GetImpersonationTokenRequestObject))
|
|
||||||
}
|
|
||||||
for _, middleware := range sh.middlewares {
|
|
||||||
handler = middleware(handler, "GetImpersonationToken")
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := handler(ctx, request)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
ctx.Error(err)
|
|
||||||
ctx.Status(http.StatusInternalServerError)
|
|
||||||
} else if validResponse, ok := response.(GetImpersonationTokenResponseObject); ok {
|
|
||||||
if err := validResponse.VisitGetImpersonationTokenResponse(ctx.Writer); err != nil {
|
|
||||||
ctx.Error(err)
|
|
||||||
}
|
|
||||||
} else if response != nil {
|
|
||||||
ctx.Error(fmt.Errorf("unexpected response type: %T", response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout operation middleware
|
|
||||||
func (sh *strictHandler) Logout(ctx *gin.Context) {
|
|
||||||
var request LogoutRequestObject
|
|
||||||
|
|
||||||
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
|
|
||||||
return sh.ssi.Logout(ctx, request.(LogoutRequestObject))
|
|
||||||
}
|
|
||||||
for _, middleware := range sh.middlewares {
|
|
||||||
handler = middleware(handler, "Logout")
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := handler(ctx, request)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
ctx.Error(err)
|
|
||||||
ctx.Status(http.StatusInternalServerError)
|
|
||||||
} else if validResponse, ok := response.(LogoutResponseObject); ok {
|
|
||||||
if err := validResponse.VisitLogoutResponse(ctx.Writer); err != nil {
|
|
||||||
ctx.Error(err)
|
|
||||||
}
|
|
||||||
} else if response != nil {
|
|
||||||
ctx.Error(fmt.Errorf("unexpected response type: %T", response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RefreshTokens operation middleware
|
|
||||||
func (sh *strictHandler) RefreshTokens(ctx *gin.Context) {
|
|
||||||
var request RefreshTokensRequestObject
|
|
||||||
|
|
||||||
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
|
|
||||||
return sh.ssi.RefreshTokens(ctx, request.(RefreshTokensRequestObject))
|
|
||||||
}
|
|
||||||
for _, middleware := range sh.middlewares {
|
|
||||||
handler = middleware(handler, "RefreshTokens")
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := handler(ctx, request)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
ctx.Error(err)
|
|
||||||
ctx.Status(http.StatusInternalServerError)
|
|
||||||
} else if validResponse, ok := response.(RefreshTokensResponseObject); ok {
|
|
||||||
if err := validResponse.VisitRefreshTokensResponse(ctx.Writer); err != nil {
|
|
||||||
ctx.Error(err)
|
|
||||||
}
|
|
||||||
} else if response != nil {
|
|
||||||
ctx.Error(fmt.Errorf("unexpected response type: %T", response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PostSignIn operation middleware
|
// PostSignIn operation middleware
|
||||||
func (sh *strictHandler) PostSignIn(ctx *gin.Context) {
|
func (sh *strictHandler) PostSignIn(ctx *gin.Context) {
|
||||||
var request PostSignInRequestObject
|
var request PostSignInRequestObject
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package auth
|
|
||||||
|
|
||||||
import "github.com/golang-jwt/jwt/v5"
|
|
||||||
|
|
||||||
type TokenClaims struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
ImpID *string `json:"imp_id,omitempty"`
|
|
||||||
jwt.RegisteredClaims
|
|
||||||
}
|
|
||||||
|
|
@ -10,7 +10,6 @@ paths:
|
||||||
/sign-up:
|
/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
|
||||||
|
|
@ -42,7 +41,6 @@ paths:
|
||||||
/sign-in:
|
/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
|
||||||
|
|
@ -75,83 +73,88 @@ paths:
|
||||||
user_name:
|
user_name:
|
||||||
type: string
|
type: string
|
||||||
"401":
|
"401":
|
||||||
$ref: '#/components/responses/UnauthorizedError'
|
description: Access denied due to invalid credentials
|
||||||
|
|
||||||
/get-impersonation-token:
|
|
||||||
post:
|
|
||||||
summary: Get service impersontaion token
|
|
||||||
operationId: getImpersonationToken
|
|
||||||
tags: [Auth]
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
user_id:
|
error:
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
external_id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
oneOf:
|
|
||||||
- required: ["user_id"]
|
|
||||||
- required: ["external_id"]
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: Generated impersonation access token
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- access_token
|
|
||||||
properties:
|
|
||||||
access_token:
|
|
||||||
type: string
|
type: string
|
||||||
description: JWT access token
|
example: "Access denied"
|
||||||
"401":
|
# /auth/verify-token:
|
||||||
$ref: '#/components/responses/UnauthorizedError'
|
# post:
|
||||||
|
# summary: Verify JWT validity
|
||||||
/refresh-tokens:
|
# tags: [Auth]
|
||||||
get:
|
# requestBody:
|
||||||
summary: Refreshes access_token and refresh_token
|
# required: true
|
||||||
operationId: refreshTokens
|
# content:
|
||||||
tags: [Auth]
|
# application/json:
|
||||||
responses:
|
# schema:
|
||||||
# This one sets two cookies: access_token and refresh_token
|
# type: object
|
||||||
"200":
|
# required: [token]
|
||||||
description: Refresh success
|
# properties:
|
||||||
"400":
|
# token:
|
||||||
$ref: '#/components/responses/ClientError'
|
# type: string
|
||||||
"401":
|
# description: JWT token to validate
|
||||||
$ref: '#/components/responses/UnauthorizedError'
|
# responses:
|
||||||
"500":
|
# "200":
|
||||||
$ref: '#/components/responses/ServerError'
|
# description: Token validation result
|
||||||
|
# content:
|
||||||
/logout:
|
# application/json:
|
||||||
post:
|
# schema:
|
||||||
summary: Logs out the user
|
# type: object
|
||||||
operationId: logout
|
# properties:
|
||||||
tags: [Auth]
|
# valid:
|
||||||
responses:
|
# type: boolean
|
||||||
"200":
|
# description: True if token is valid
|
||||||
description: Logout success
|
# user_id:
|
||||||
"500":
|
# type: string
|
||||||
$ref: '#/components/responses/ServerError'
|
# nullable: true
|
||||||
|
# description: User ID extracted from token if valid
|
||||||
components:
|
# error:
|
||||||
securitySchemes:
|
# type: string
|
||||||
bearerAuth:
|
# nullable: true
|
||||||
type: http
|
# description: Error message if token is invalid
|
||||||
scheme: bearer
|
# /auth/refresh-token:
|
||||||
responses:
|
# post:
|
||||||
UnauthorizedError:
|
# summary: Refresh JWT using a refresh token
|
||||||
description: Access token is missing or invalid
|
# tags: [Auth]
|
||||||
ServerError:
|
# requestBody:
|
||||||
description: ServerError
|
# required: true
|
||||||
ClientError:
|
# content:
|
||||||
description: ClientError
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# type: object
|
||||||
|
# required: [refresh_token]
|
||||||
|
# properties:
|
||||||
|
# refresh_token:
|
||||||
|
# type: string
|
||||||
|
# description: JWT refresh token obtained from sign-in
|
||||||
|
# responses:
|
||||||
|
# "200":
|
||||||
|
# description: New access (and optionally refresh) token
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# type: object
|
||||||
|
# properties:
|
||||||
|
# valid:
|
||||||
|
# type: boolean
|
||||||
|
# description: True if refresh token was valid
|
||||||
|
# user_id:
|
||||||
|
# type: string
|
||||||
|
# nullable: true
|
||||||
|
# description: User ID extracted from refresh token
|
||||||
|
# access_token:
|
||||||
|
# type: string
|
||||||
|
# description: New access token
|
||||||
|
# nullable: true
|
||||||
|
# refresh_token:
|
||||||
|
# type: string
|
||||||
|
# description: New refresh token (optional)
|
||||||
|
# nullable: true
|
||||||
|
# error:
|
||||||
|
# type: string
|
||||||
|
# nullable: true
|
||||||
|
# description: Error message if refresh token is invalid
|
||||||
|
|
|
||||||
|
|
@ -40,22 +40,6 @@ services:
|
||||||
retries: 5
|
retries: 5
|
||||||
start_period: 10s
|
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
|
||||||
container_name: nyanimedb-backend
|
container_name: nyanimedb-backend
|
||||||
|
|
@ -70,8 +54,6 @@ services:
|
||||||
IMAGES_BASE_URL: http://nyanimedb-images:8000
|
IMAGES_BASE_URL: http://nyanimedb-images:8000
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
# ports:
|
|
||||||
# - "8080:8080"
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres
|
- postgres
|
||||||
- rabbitmq
|
- rabbitmq
|
||||||
|
|
@ -87,8 +69,8 @@ services:
|
||||||
DATABASE_URL: ${DATABASE_URL}
|
DATABASE_URL: ${DATABASE_URL}
|
||||||
SERVICE_ADDRESS: ${SERVICE_ADDRESS}
|
SERVICE_ADDRESS: ${SERVICE_ADDRESS}
|
||||||
JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY}
|
JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY}
|
||||||
# ports:
|
ports:
|
||||||
# - "8082:8082"
|
- "8082:8082"
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres
|
- postgres
|
||||||
networks:
|
networks:
|
||||||
|
|
@ -142,7 +124,6 @@ volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
rabbitmq_data:
|
rabbitmq_data:
|
||||||
media_data:
|
media_data:
|
||||||
redis_data:
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
nyanimedb-network:
|
nyanimedb-network:
|
||||||
|
|
|
||||||
16
go.mod
16
go.mod
|
|
@ -18,7 +18,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
|
||||||
|
|
@ -43,13 +42,12 @@ require (
|
||||||
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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
20
go.sum
20
go.sum
|
|
@ -13,8 +13,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=
|
||||||
|
|
@ -105,18 +103,10 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
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.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.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-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-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.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
|
@ -124,15 +114,11 @@ 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.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-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.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.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-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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|
@ -145,8 +131,6 @@ 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.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-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.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
|
@ -160,16 +144,12 @@ 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.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-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.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.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.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=
|
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=
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ async def get_or_create_image(
|
||||||
VALUES (%s, %s)
|
VALUES (%s, %s)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
""",
|
""",
|
||||||
("local", rel_path),
|
("image-service", rel_path),
|
||||||
)
|
)
|
||||||
row = await cur.fetchone()
|
row = await cur.fetchone()
|
||||||
return row["id"]
|
return row["id"]
|
||||||
|
|
|
||||||
|
|
@ -47,35 +47,10 @@ func CheckPassword(password, hash string) (bool, error) {
|
||||||
return argon2id.ComparePasswordAndHash(password, hash)
|
return argon2id.ComparePasswordAndHash(password, hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) generateImpersonationToken(userID string, impersonatedBy string) (string, error) {
|
func (s Server) generateTokens(userID string) (accessToken string, refreshToken string, csrfToken string, err error) {
|
||||||
now := time.Now()
|
accessClaims := jwt.MapClaims{
|
||||||
claims := auth.TokenClaims{
|
"user_id": userID,
|
||||||
ImpID: &impersonatedBy,
|
"exp": time.Now().Add(15 * time.Minute).Unix(),
|
||||||
Type: "access",
|
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
|
||||||
Subject: userID,
|
|
||||||
IssuedAt: jwt.NewNumericDate(now),
|
|
||||||
ExpiresAt: jwt.NewNumericDate(now.Add(15 * time.Minute)),
|
|
||||||
ID: generateJTI(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
||||||
return token.SignedString([]byte(s.JwtPrivateKey))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) generateTokens(userID string) (accessToken string, refreshToken string, csrfToken string, err error) {
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
// Access token (15 мин)
|
|
||||||
accessClaims := auth.TokenClaims{
|
|
||||||
Type: "access",
|
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
|
||||||
Subject: userID,
|
|
||||||
IssuedAt: jwt.NewNumericDate(now),
|
|
||||||
ExpiresAt: jwt.NewNumericDate(now.Add(15 * time.Minute)),
|
|
||||||
ID: generateJTI(),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
at := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
|
at := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
|
||||||
accessToken, err = at.SignedString([]byte(s.JwtPrivateKey))
|
accessToken, err = at.SignedString([]byte(s.JwtPrivateKey))
|
||||||
|
|
@ -83,15 +58,9 @@ func (s *Server) generateTokens(userID string) (accessToken string, refreshToken
|
||||||
return "", "", "", err
|
return "", "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh token (7 дней)
|
refreshClaims := jwt.MapClaims{
|
||||||
refreshClaims := auth.TokenClaims{
|
"user_id": userID,
|
||||||
Type: "refresh",
|
"exp": time.Now().Add(7 * 24 * time.Hour).Unix(),
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
|
||||||
Subject: userID,
|
|
||||||
IssuedAt: jwt.NewNumericDate(now),
|
|
||||||
ExpiresAt: jwt.NewNumericDate(now.Add(7 * 24 * time.Hour)),
|
|
||||||
ID: generateJTI(),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
rt := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
|
rt := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
|
||||||
refreshToken, err = rt.SignedString([]byte(s.JwtPrivateKey))
|
refreshToken, err = rt.SignedString([]byte(s.JwtPrivateKey))
|
||||||
|
|
@ -99,7 +68,6 @@ func (s *Server) generateTokens(userID string) (accessToken string, refreshToken
|
||||||
return "", "", "", err
|
return "", "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// CSRF token
|
|
||||||
csrfBytes := make([]byte, 32)
|
csrfBytes := make([]byte, 32)
|
||||||
_, err = rand.Read(csrfBytes)
|
_, err = rand.Read(csrfBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -151,10 +119,13 @@ func (s Server) PostSignIn(ctx context.Context, req auth.PostSignInRequestObject
|
||||||
// TODO: return 500
|
// TODO: return 500
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
return auth.PostSignIn401Response{}, nil
|
err_msg := "invalid credentials"
|
||||||
|
return auth.PostSignIn401JSONResponse{
|
||||||
|
Error: &err_msg,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
accessToken, refreshToken, csrfToken, err := s.generateTokens(fmt.Sprintf("%d", user.ID))
|
accessToken, refreshToken, csrfToken, err := s.generateTokens(req.Body.Nickname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to generate tokens for user %s: %v", req.Body.Nickname, err)
|
log.Errorf("failed to generate tokens for user %s: %v", req.Body.Nickname, err)
|
||||||
// TODO: return 500
|
// TODO: return 500
|
||||||
|
|
@ -173,144 +144,95 @@ func (s Server) PostSignIn(ctx context.Context, req auth.PostSignInRequestObject
|
||||||
return result, nil
|
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 := ExtractBearerToken(ginCtx.Request.Header.Get("Authorization"))
|
// token, err := jwt.Parse(req.Body.Token, func(t *jwt.Token) (interface{}, error) {
|
||||||
if err != nil {
|
// if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
// TODO: return 500
|
// return nil, fmt.Errorf("unexpected signing method")
|
||||||
log.Errorf("failed to extract bearer token: %v", err)
|
// }
|
||||||
return auth.GetImpersonationToken401Response{}, err
|
// return accessSecret, nil
|
||||||
}
|
// })
|
||||||
log.Printf("got auth token: %s", token)
|
|
||||||
|
|
||||||
ext_service, err := s.db.GetExternalServiceByToken(context.Background(), &token)
|
// if err != nil {
|
||||||
if err != nil {
|
// e := err.Error()
|
||||||
log.Errorf("failed to get external service by token: %v", err)
|
// errStr = &e
|
||||||
return auth.GetImpersonationToken401Response{}, err
|
// return auth.PostAuthVerifyToken200JSONResponse{
|
||||||
// TODO: check err and retyrn 400/500
|
// Valid: &valid,
|
||||||
}
|
// UserId: userID,
|
||||||
|
// Error: errStr,
|
||||||
|
// }, nil
|
||||||
|
// }
|
||||||
|
|
||||||
var user_id string = ""
|
// if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||||
|
// if uid, ok := claims["user_id"].(string); ok {
|
||||||
|
// valid = true
|
||||||
|
// userID = &uid
|
||||||
|
// } else {
|
||||||
|
// e := "user_id not found in token"
|
||||||
|
// errStr = &e
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// e := "invalid token claims"
|
||||||
|
// errStr = &e
|
||||||
|
// }
|
||||||
|
|
||||||
if req.Body.ExternalId != nil {
|
// return auth.PostAuthVerifyToken200JSONResponse{
|
||||||
user, err := s.db.GetUserByExternalServiceId(context.Background(), sqlc.GetUserByExternalServiceIdParams{
|
// Valid: &valid,
|
||||||
ExternalID: fmt.Sprintf("%d", *req.Body.ExternalId),
|
// UserId: userID,
|
||||||
ServiceID: ext_service.ID,
|
// Error: errStr,
|
||||||
})
|
// }, nil
|
||||||
if err != nil {
|
// }
|
||||||
log.Errorf("failed to get user by external user id: %v", err)
|
|
||||||
return auth.GetImpersonationToken401Response{}, err
|
|
||||||
// TODO: check err and retyrn 400/500
|
|
||||||
}
|
|
||||||
|
|
||||||
user_id = fmt.Sprintf("%d", user.ID)
|
// func (s Server) PostAuthRefreshToken(ctx context.Context, req auth.PostAuthRefreshTokenRequestObject) (auth.PostAuthRefreshTokenResponseObject, error) {
|
||||||
}
|
// valid := false
|
||||||
|
// var userID *string
|
||||||
|
// var errStr *string
|
||||||
|
|
||||||
if req.Body.UserId != nil {
|
// token, err := jwt.Parse(req.Body.Token, func(t *jwt.Token) (interface{}, error) {
|
||||||
// TODO: check user existence
|
// if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
if user_id != "" && user_id != fmt.Sprintf("%d", *req.Body.UserId) {
|
// return nil, fmt.Errorf("unexpected signing method")
|
||||||
log.Error("user_id and external_d are incorrect")
|
// }
|
||||||
// TODO: 405
|
// return refreshSecret, nil
|
||||||
return auth.GetImpersonationToken401Response{}, nil
|
// })
|
||||||
} else {
|
|
||||||
user_id = fmt.Sprintf("%d", *req.Body.UserId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
accessToken, err := s.generateImpersonationToken(user_id, fmt.Sprintf("%d", ext_service.ID))
|
// if err != nil {
|
||||||
if err != nil {
|
// e := err.Error()
|
||||||
log.Errorf("failed to generate impersonation token: %v", err)
|
// errStr = &e
|
||||||
return auth.GetImpersonationToken401Response{}, err
|
// return auth.PostAuthVerifyToken200JSONResponse{
|
||||||
// TODO: check err and retyrn 400/500
|
// Valid: &valid,
|
||||||
}
|
// UserId: userID,
|
||||||
|
// Error: errStr,
|
||||||
|
// }, nil
|
||||||
|
// }
|
||||||
|
|
||||||
return auth.GetImpersonationToken200JSONResponse{AccessToken: accessToken}, 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
|
||||||
|
// }
|
||||||
|
|
||||||
func (s Server) RefreshTokens(ctx context.Context, req auth.RefreshTokensRequestObject) (auth.RefreshTokensResponseObject, error) {
|
// return auth.PostAuthVerifyToken200JSONResponse{
|
||||||
ginCtx, ok := ctx.Value(gin.ContextKey).(*gin.Context)
|
// Valid: &valid,
|
||||||
if !ok {
|
// UserId: userID,
|
||||||
log.Print("failed to get gin context")
|
// Error: errStr,
|
||||||
return auth.RefreshTokens500Response{}, fmt.Errorf("failed to get gin.Context from context.Context")
|
// }, nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
rtCookie, err := ginCtx.Request.Cookie("refresh_token")
|
|
||||||
if err != nil {
|
|
||||||
log.Print("failed to get refresh_token cookie")
|
|
||||||
return auth.RefreshTokens400Response{}, fmt.Errorf("failed to get refresh_token cookie")
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshToken := rtCookie.Value
|
|
||||||
|
|
||||||
token, err := jwt.ParseWithClaims(refreshToken, &auth.TokenClaims{}, func(t *jwt.Token) (interface{}, error) {
|
|
||||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
||||||
return nil, fmt.Errorf("unexpected signing method")
|
|
||||||
}
|
|
||||||
return []byte(s.JwtPrivateKey), nil
|
|
||||||
})
|
|
||||||
if err != nil || !token.Valid {
|
|
||||||
log.Print("invalid refresh token")
|
|
||||||
return auth.RefreshTokens401Response{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
claims, ok := token.Claims.(*auth.TokenClaims)
|
|
||||||
if !ok || claims.Subject == "" {
|
|
||||||
log.Print("invalid refresh token claims")
|
|
||||||
return auth.RefreshTokens401Response{}, nil
|
|
||||||
}
|
|
||||||
if claims.Type != "refresh" {
|
|
||||||
log.Errorf("token is not a refresh token")
|
|
||||||
return auth.RefreshTokens401Response{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
accessToken, refreshToken, csrfToken, err := s.generateTokens(claims.Subject)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to generate tokens for user %s: %v", claims.Subject, err)
|
|
||||||
return auth.RefreshTokens500Response{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
|
|
||||||
return auth.RefreshTokens200Response{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Server) Logout(ctx context.Context, req auth.LogoutRequestObject) (auth.LogoutResponseObject, error) {
|
|
||||||
// TODO: get current tokens and add them to block list
|
|
||||||
ginCtx, ok := ctx.Value(gin.ContextKey).(*gin.Context)
|
|
||||||
if !ok {
|
|
||||||
log.Print("failed to get gin context")
|
|
||||||
return auth.Logout500Response{}, fmt.Errorf("failed to get gin.Context from context.Context")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete cookies by setting MaxAge negative
|
|
||||||
// TODO: change secure to true
|
|
||||||
ginCtx.SetCookie("access_token", "", -1, "/api", "", false, true)
|
|
||||||
ginCtx.SetCookie("refresh_token", "", -1, "/auth", "", false, true)
|
|
||||||
ginCtx.SetCookie("xsrf_token", "", -1, "/", "", false, false)
|
|
||||||
|
|
||||||
return auth.Logout200Response{}, 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
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateJTI() string {
|
|
||||||
b := make([]byte, 16)
|
|
||||||
_, _ = rand.Read(b)
|
|
||||||
return base64.RawURLEncoding.EncodeToString(b)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ func main() {
|
||||||
|
|
||||||
log.Info("allow origins:", AppConfig.ServiceAddress)
|
log.Info("allow origins:", AppConfig.ServiceAddress)
|
||||||
r.Use(cors.New(cors.Config{
|
r.Use(cors.New(cors.Config{
|
||||||
AllowOrigins: []string{AppConfig.ServiceAddress},
|
AllowOrigins: []string{"*"},
|
||||||
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"},
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,3 @@ INTO users (passhash, nickname)
|
||||||
VALUES (sqlc.arg(passhash), sqlc.arg(nickname))
|
VALUES (sqlc.arg(passhash), sqlc.arg(nickname))
|
||||||
RETURNING id;
|
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');
|
|
||||||
|
|
@ -73,14 +73,6 @@ func (s Server) mapTitle(title sqlc.GetTitleByIDRow) (oapi.Title, error) {
|
||||||
}
|
}
|
||||||
oapi_title.TitleNames = title_names
|
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 {
|
if len(title.EpisodesLen) > 0 {
|
||||||
episodes_lens := make(map[string]float64, 0)
|
episodes_lens := make(map[string]float64, 0)
|
||||||
err = json.Unmarshal(title.EpisodesLen, &episodes_lens)
|
err = json.Unmarshal(title.EpisodesLen, &episodes_lens)
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -69,19 +69,6 @@ func sqlDate2oapi(p_date pgtype.Timestamptz) *time.Time {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func oapiDate2sql(t *time.Time) pgtype.Timestamptz {
|
|
||||||
if t == nil {
|
|
||||||
return pgtype.Timestamptz{
|
|
||||||
Time: time.Now(),
|
|
||||||
Valid: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pgtype.Timestamptz{
|
|
||||||
Time: *t,
|
|
||||||
Valid: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// func UserTitleStatus2Sqlc(s *[]oapi.UserTitleStatus) (*SqlcUserStatus, error) {
|
// func UserTitleStatus2Sqlc(s *[]oapi.UserTitleStatus) (*SqlcUserStatus, error) {
|
||||||
// var sqlc_status SqlcUserStatus
|
// var sqlc_status SqlcUserStatus
|
||||||
// if s == nil {
|
// if s == nil {
|
||||||
|
|
@ -170,7 +157,7 @@ func UserTitleStatus2Sqlc1(s *oapi.UserTitleStatus) (*sqlc.UsertitleStatusT, err
|
||||||
func (s Server) mapUsertitle(ctx context.Context, t sqlc.SearchUserTitlesRow) (oapi.UserTitle, error) {
|
func (s Server) mapUsertitle(ctx context.Context, t sqlc.SearchUserTitlesRow) (oapi.UserTitle, error) {
|
||||||
|
|
||||||
oapi_usertitle := oapi.UserTitle{
|
oapi_usertitle := oapi.UserTitle{
|
||||||
Ftime: &t.UserFtime,
|
Ctime: &t.UserCtime,
|
||||||
Rate: t.UserRate,
|
Rate: t.UserRate,
|
||||||
ReviewId: t.ReviewID,
|
ReviewId: t.ReviewID,
|
||||||
// Status: ,
|
// Status: ,
|
||||||
|
|
@ -378,7 +365,6 @@ func (s Server) AddUserTitle(ctx context.Context, request oapi.AddUserTitleReque
|
||||||
TitleID: request.Body.TitleId,
|
TitleID: request.Body.TitleId,
|
||||||
Status: *status,
|
Status: *status,
|
||||||
Rate: request.Body.Rate,
|
Rate: request.Body.Rate,
|
||||||
Ftime: oapiDate2sql(request.Body.Ftime),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user_title, err := s.db.InsertUserTitle(ctx, params)
|
user_title, err := s.db.InsertUserTitle(ctx, params)
|
||||||
|
|
@ -389,9 +375,6 @@ func (s Server) AddUserTitle(ctx context.Context, request oapi.AddUserTitleReque
|
||||||
// fmt.Println(pgErr.Code) // => 42601
|
// fmt.Println(pgErr.Code) // => 42601
|
||||||
if pgErr.Code == pgErrDuplicateKey { //duplicate key value
|
if pgErr.Code == pgErrDuplicateKey { //duplicate key value
|
||||||
return oapi.AddUserTitle409Response{}, nil
|
return oapi.AddUserTitle409Response{}, nil
|
||||||
} else {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return oapi.AddUserTitle500Response{}, nil
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Errorf("%v", err)
|
log.Errorf("%v", err)
|
||||||
|
|
@ -404,7 +387,7 @@ func (s Server) AddUserTitle(ctx context.Context, request oapi.AddUserTitleReque
|
||||||
return oapi.AddUserTitle500Response{}, nil
|
return oapi.AddUserTitle500Response{}, nil
|
||||||
}
|
}
|
||||||
oapi_usertitle := oapi.UserTitleMini{
|
oapi_usertitle := oapi.UserTitleMini{
|
||||||
Ftime: &user_title.Ftime,
|
Ctime: &user_title.Ctime,
|
||||||
Rate: user_title.Rate,
|
Rate: user_title.Rate,
|
||||||
ReviewId: user_title.ReviewID,
|
ReviewId: user_title.ReviewID,
|
||||||
Status: oapi_status,
|
Status: oapi_status,
|
||||||
|
|
@ -445,7 +428,6 @@ func (s Server) UpdateUserTitle(ctx context.Context, request oapi.UpdateUserTitl
|
||||||
Rate: request.Body.Rate,
|
Rate: request.Body.Rate,
|
||||||
UserID: request.UserId,
|
UserID: request.UserId,
|
||||||
TitleID: request.TitleId,
|
TitleID: request.TitleId,
|
||||||
Ftime: oapiDate2sql(request.Body.Ftime),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user_title, err := s.db.UpdateUserTitle(ctx, params)
|
user_title, err := s.db.UpdateUserTitle(ctx, params)
|
||||||
|
|
@ -463,7 +445,7 @@ func (s Server) UpdateUserTitle(ctx context.Context, request oapi.UpdateUserTitl
|
||||||
}
|
}
|
||||||
|
|
||||||
oapi_usertitle := oapi.UserTitleMini{
|
oapi_usertitle := oapi.UserTitleMini{
|
||||||
Ftime: &user_title.Ftime,
|
Ctime: &user_title.Ctime,
|
||||||
Rate: user_title.Rate,
|
Rate: user_title.Rate,
|
||||||
ReviewId: user_title.ReviewID,
|
ReviewId: user_title.ReviewID,
|
||||||
Status: oapi_status,
|
Status: oapi_status,
|
||||||
|
|
@ -493,7 +475,7 @@ func (s Server) GetUserTitle(ctx context.Context, request oapi.GetUserTitleReque
|
||||||
return oapi.GetUserTitle500Response{}, nil
|
return oapi.GetUserTitle500Response{}, nil
|
||||||
}
|
}
|
||||||
oapi_usertitle := oapi.UserTitleMini{
|
oapi_usertitle := oapi.UserTitleMini{
|
||||||
Ftime: &user_title.Ftime,
|
Ctime: &user_title.Ctime,
|
||||||
Rate: user_title.Rate,
|
Rate: user_title.Rate,
|
||||||
ReviewId: user_title.ReviewID,
|
ReviewId: user_title.ReviewID,
|
||||||
Status: oapi_status,
|
Status: oapi_status,
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,8 @@ package middleware
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"nyanimedb/auth"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
@ -40,18 +37,12 @@ func JWTAuthMiddleware(secret string) gin.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Парсим токен с MapClaims
|
// 2. Парсим токен с MapClaims
|
||||||
token, err := jwt.ParseWithClaims(tokenStr, &auth.TokenClaims{}, func(t *jwt.Token) (interface{}, error) {
|
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
|
||||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
if t.Method != jwt.SigningMethodHS256 {
|
||||||
return nil, fmt.Errorf("unexpected signing method")
|
return nil, errors.New("unexpected signing method: " + t.Method.Alg())
|
||||||
}
|
}
|
||||||
return []byte(secret), nil
|
return []byte(secret), nil // ← конвертируем string → []byte
|
||||||
})
|
})
|
||||||
// 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 {
|
if err != nil {
|
||||||
abortWithJSON(c, http.StatusUnauthorized, "invalid token: "+err.Error())
|
abortWithJSON(c, http.StatusUnauthorized, "invalid token: "+err.Error())
|
||||||
return
|
return
|
||||||
|
|
@ -64,23 +55,20 @@ func JWTAuthMiddleware(secret string) gin.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Извлекаем user_id из claims
|
// 4. Извлекаем user_id из claims
|
||||||
claims, ok := token.Claims.(*auth.TokenClaims)
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
if !ok {
|
if !ok {
|
||||||
abortWithJSON(c, http.StatusUnauthorized, "invalid claims format")
|
abortWithJSON(c, http.StatusUnauthorized, "invalid claims format")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if claims.Subject == "" {
|
userID, ok := claims["user_id"].(string)
|
||||||
|
if !ok || userID == "" {
|
||||||
abortWithJSON(c, http.StatusUnauthorized, "user_id claim missing or invalid")
|
abortWithJSON(c, http.StatusUnauthorized, "user_id claim missing or invalid")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if claims.Type != "access" {
|
|
||||||
abortWithJSON(c, http.StatusUnauthorized, "token type is not access")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Сохраняем в контексте
|
// 5. Сохраняем в контексте
|
||||||
c.Set("user_id", claims.Subject)
|
c.Set("user_id", userID)
|
||||||
|
|
||||||
// 6. Для oapi-codegen — кладём gin.Context в request context
|
// 6. Для oapi-codegen — кладём gin.Context в request context
|
||||||
GinContextToContext(c)
|
GinContextToContext(c)
|
||||||
|
|
|
||||||
|
|
@ -268,7 +268,7 @@ SELECT
|
||||||
u.status as usertitle_status,
|
u.status as usertitle_status,
|
||||||
u.rate as user_rate,
|
u.rate as user_rate,
|
||||||
u.review_id as review_id,
|
u.review_id as review_id,
|
||||||
u.ftime as user_ftime,
|
u.ctime as user_ctime,
|
||||||
i.storage_type as title_storage_type,
|
i.storage_type as title_storage_type,
|
||||||
i.image_path as title_image_path,
|
i.image_path as title_image_path,
|
||||||
COALESCE(
|
COALESCE(
|
||||||
|
|
@ -370,7 +370,7 @@ WHERE
|
||||||
AND (sqlc.narg('release_season')::release_season_t IS NULL OR t.release_season = sqlc.narg('release_season')::release_season_t)
|
AND (sqlc.narg('release_season')::release_season_t IS NULL OR t.release_season = sqlc.narg('release_season')::release_season_t)
|
||||||
|
|
||||||
GROUP BY
|
GROUP BY
|
||||||
t.id, u.user_id, u.status, u.rate, u.review_id, u.ftime, i.id, s.id
|
t.id, u.user_id, u.status, u.rate, u.review_id, u.ctime, i.id, s.id
|
||||||
|
|
||||||
ORDER BY
|
ORDER BY
|
||||||
CASE WHEN sqlc.arg('forward')::boolean THEN
|
CASE WHEN sqlc.arg('forward')::boolean THEN
|
||||||
|
|
@ -400,24 +400,22 @@ FROM reviews
|
||||||
WHERE review_id = sqlc.arg('review_id')::bigint;
|
WHERE review_id = sqlc.arg('review_id')::bigint;
|
||||||
|
|
||||||
-- name: InsertUserTitle :one
|
-- name: InsertUserTitle :one
|
||||||
INSERT INTO usertitles (user_id, title_id, status, rate, review_id, ftime)
|
INSERT INTO usertitles (user_id, title_id, status, rate, review_id)
|
||||||
VALUES (
|
VALUES (
|
||||||
sqlc.arg('user_id')::bigint,
|
sqlc.arg('user_id')::bigint,
|
||||||
sqlc.arg('title_id')::bigint,
|
sqlc.arg('title_id')::bigint,
|
||||||
sqlc.arg('status')::usertitle_status_t,
|
sqlc.arg('status')::usertitle_status_t,
|
||||||
sqlc.narg('rate')::int,
|
sqlc.narg('rate')::int,
|
||||||
sqlc.narg('review_id')::bigint,
|
sqlc.narg('review_id')::bigint
|
||||||
sqlc.narg('ftime')::timestamptz
|
|
||||||
)
|
)
|
||||||
RETURNING user_id, title_id, status, rate, review_id, ftime;
|
RETURNING user_id, title_id, status, rate, review_id, ctime;
|
||||||
|
|
||||||
-- name: UpdateUserTitle :one
|
-- name: UpdateUserTitle :one
|
||||||
-- Fails with sql.ErrNoRows if (user_id, title_id) not found
|
-- Fails with sql.ErrNoRows if (user_id, title_id) not found
|
||||||
UPDATE usertitles
|
UPDATE usertitles
|
||||||
SET
|
SET
|
||||||
status = COALESCE(sqlc.narg('status')::usertitle_status_t, status),
|
status = COALESCE(sqlc.narg('status')::usertitle_status_t, status),
|
||||||
rate = COALESCE(sqlc.narg('rate')::int, rate),
|
rate = COALESCE(sqlc.narg('rate')::int, rate)
|
||||||
ftime = COALESCE(sqlc.narg('ftime')::timestamptz, ftime)
|
|
||||||
WHERE
|
WHERE
|
||||||
user_id = sqlc.arg('user_id')
|
user_id = sqlc.arg('user_id')
|
||||||
AND title_id = sqlc.arg('title_id')
|
AND title_id = sqlc.arg('title_id')
|
||||||
|
|
|
||||||
|
|
@ -28,16 +28,6 @@ server {
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_cache_bypass $http_upgrade;
|
proxy_cache_bypass $http_upgrade;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /media/ {
|
|
||||||
rewrite ^/media/(.*)$ /$1 break;
|
|
||||||
proxy_pass http://nyanimedb-images:8000/;
|
|
||||||
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;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useState, useEffect } 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 "./pages/UserPage/UserPage";
|
||||||
import TitlesPage from "./pages/TitlesPage/TitlesPage";
|
import TitlesPage from "./pages/TitlesPage/TitlesPage";
|
||||||
|
|
@ -12,26 +11,12 @@ import { Header } from "./components/Header/Header";
|
||||||
// OpenAPI.WITH_CREDENTIALS = true
|
// OpenAPI.WITH_CREDENTIALS = true
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const [userId, setUserId] = useState<string | null>(localStorage.getItem("user_id"));
|
const username = localStorage.getItem("username") || undefined;
|
||||||
|
const userId = localStorage.getItem("userId");
|
||||||
// 2. Listen for the same event the Header uses
|
|
||||||
useEffect(() => {
|
|
||||||
const handleAuthChange = () => {
|
|
||||||
setUserId(localStorage.getItem("user_id"));
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("storage", handleAuthChange);
|
|
||||||
window.addEventListener("local-storage-update", handleAuthChange);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("storage", handleAuthChange);
|
|
||||||
window.removeEventListener("local-storage-update", handleAuthChange);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<Header />
|
<Header username={username} />
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* auth */}
|
{/* auth */}
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,4 @@ import type { ClientOptions as ClientOptions2 } from './types.gen';
|
||||||
*/
|
*/
|
||||||
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>;
|
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>;
|
||||||
|
|
||||||
export const client = createClient(createConfig<ClientOptions2>({ baseUrl: '/api/v1' }));
|
export const client = createClient(createConfig<ClientOptions2>({ baseUrl: 'http://10.1.0.65:8081/api/v1' }));
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import type { Client, Options as Options2, TDataShape } from './client';
|
import type { Client, Options as Options2, TDataShape } from './client';
|
||||||
import { client } from './client.gen';
|
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, PostMediaUploadData, PostMediaUploadErrors, PostMediaUploadResponses, UpdateUserData, UpdateUserErrors, UpdateUserResponses, UpdateUserTitleData, UpdateUserTitleErrors, UpdateUserTitleResponses } from './types.gen';
|
import type { AddUserTitleData, AddUserTitleErrors, AddUserTitleResponses, DeleteUserTitleData, DeleteUserTitleErrors, DeleteUserTitleResponses, GetTitleData, GetTitleErrors, GetTitleResponses, GetTitlesData, GetTitlesErrors, GetTitlesResponses, GetUsersIdData, GetUsersIdErrors, GetUsersIdResponses, 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> & {
|
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
|
||||||
/**
|
/**
|
||||||
|
|
@ -32,11 +32,6 @@ export const getTitles = <ThrowOnError extends boolean = false>(options?: Option
|
||||||
*/
|
*/
|
||||||
export const getTitle = <ThrowOnError extends boolean = false>(options: Options<GetTitleData, ThrowOnError>) => (options.client ?? client).get<GetTitleResponses, GetTitleErrors, ThrowOnError>({ url: '/titles/{title_id}', ...options });
|
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
|
* Get user info
|
||||||
*/
|
*/
|
||||||
|
|
@ -113,18 +108,3 @@ export const updateUserTitle = <ThrowOnError extends boolean = false>(options: O
|
||||||
...options.headers
|
...options.headers
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload an image (PNG, JPEG, or WebP)
|
|
||||||
*
|
|
||||||
* Uploads a single image file. Supported formats: **PNG**, **JPEG/JPG**, **WebP**.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const postMediaUpload = <ThrowOnError extends boolean = false>(options: Options<PostMediaUploadData, ThrowOnError>) => (options.client ?? client).post<PostMediaUploadResponses, PostMediaUploadErrors, ThrowOnError>({
|
|
||||||
url: '/media/upload',
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'encoding',
|
|
||||||
...options.headers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -60,12 +60,6 @@ export type Title = {
|
||||||
title_names: {
|
title_names: {
|
||||||
[key: string]: Array<string>;
|
[key: string]: Array<string>;
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* Localized description. Key = language (ISO 639-1), value = description.
|
|
||||||
*/
|
|
||||||
title_desc?: {
|
|
||||||
[key: string]: string;
|
|
||||||
};
|
|
||||||
studio?: Studio;
|
studio?: Studio;
|
||||||
tags: Tags;
|
tags: Tags;
|
||||||
poster?: Image;
|
poster?: Image;
|
||||||
|
|
@ -125,7 +119,7 @@ export type UserTitle = {
|
||||||
status: UserTitleStatus;
|
status: UserTitleStatus;
|
||||||
rate?: number;
|
rate?: number;
|
||||||
review_id?: number;
|
review_id?: number;
|
||||||
ftime?: string;
|
ctime?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserTitleMini = {
|
export type UserTitleMini = {
|
||||||
|
|
@ -134,7 +128,7 @@ export type UserTitleMini = {
|
||||||
status: UserTitleStatus;
|
status: UserTitleStatus;
|
||||||
rate?: number;
|
rate?: number;
|
||||||
review_id?: number;
|
review_id?: number;
|
||||||
ftime?: string;
|
ctime?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Review = {
|
export type Review = {
|
||||||
|
|
@ -237,50 +231,6 @@ export type GetTitleResponses = {
|
||||||
|
|
||||||
export type GetTitleResponse = GetTitleResponses[keyof GetTitleResponses];
|
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 = {
|
export type GetUsersIdData = {
|
||||||
body?: never;
|
body?: never;
|
||||||
path: {
|
path: {
|
||||||
|
|
@ -453,7 +403,6 @@ export type AddUserTitleData = {
|
||||||
title_id: number;
|
title_id: number;
|
||||||
status: UserTitleStatus;
|
status: UserTitleStatus;
|
||||||
rate?: number;
|
rate?: number;
|
||||||
ftime?: string;
|
|
||||||
};
|
};
|
||||||
path: {
|
path: {
|
||||||
/**
|
/**
|
||||||
|
|
@ -579,7 +528,6 @@ export type UpdateUserTitleData = {
|
||||||
body: {
|
body: {
|
||||||
status?: UserTitleStatus;
|
status?: UserTitleStatus;
|
||||||
rate?: number;
|
rate?: number;
|
||||||
ftime?: string;
|
|
||||||
};
|
};
|
||||||
path: {
|
path: {
|
||||||
user_id: number;
|
user_id: number;
|
||||||
|
|
@ -620,38 +568,3 @@ export type UpdateUserTitleResponses = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateUserTitleResponse = UpdateUserTitleResponses[keyof UpdateUserTitleResponses];
|
export type UpdateUserTitleResponse = UpdateUserTitleResponses[keyof UpdateUserTitleResponses];
|
||||||
|
|
||||||
export type PostMediaUploadData = {
|
|
||||||
body: unknown;
|
|
||||||
path?: never;
|
|
||||||
query?: never;
|
|
||||||
url: '/media/upload';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PostMediaUploadErrors = {
|
|
||||||
/**
|
|
||||||
* Bad request — e.g., invalid/malformed image, empty file
|
|
||||||
*/
|
|
||||||
400: string;
|
|
||||||
/**
|
|
||||||
* 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)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
415: unknown;
|
|
||||||
/**
|
|
||||||
* Internal server error
|
|
||||||
*/
|
|
||||||
500: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PostMediaUploadError = PostMediaUploadErrors[keyof PostMediaUploadErrors];
|
|
||||||
|
|
||||||
export type PostMediaUploadResponses = {
|
|
||||||
/**
|
|
||||||
* Image uploaded successfully
|
|
||||||
*/
|
|
||||||
200: Image;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PostMediaUploadResponse = PostMediaUploadResponses[keyof PostMediaUploadResponses];
|
|
||||||
|
|
|
||||||
|
|
@ -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: '/auth' }));
|
|
||||||
|
|
@ -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,
|
|
||||||
});
|
|
||||||
25
modules/frontend/src/auth/core/ApiError.ts
Normal file
25
modules/frontend/src/auth/core/ApiError.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||||
|
import type { ApiResult } from './ApiResult';
|
||||||
|
|
||||||
|
export class ApiError extends Error {
|
||||||
|
public readonly url: string;
|
||||||
|
public readonly status: number;
|
||||||
|
public readonly statusText: string;
|
||||||
|
public readonly body: any;
|
||||||
|
public readonly request: ApiRequestOptions;
|
||||||
|
|
||||||
|
constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
|
||||||
|
super(message);
|
||||||
|
|
||||||
|
this.name = 'ApiError';
|
||||||
|
this.url = response.url;
|
||||||
|
this.status = response.status;
|
||||||
|
this.statusText = response.statusText;
|
||||||
|
this.body = response.body;
|
||||||
|
this.request = request;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
modules/frontend/src/auth/core/ApiRequestOptions.ts
Normal file
17
modules/frontend/src/auth/core/ApiRequestOptions.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type ApiRequestOptions = {
|
||||||
|
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
|
||||||
|
readonly url: string;
|
||||||
|
readonly path?: Record<string, any>;
|
||||||
|
readonly cookies?: Record<string, any>;
|
||||||
|
readonly headers?: Record<string, any>;
|
||||||
|
readonly query?: Record<string, any>;
|
||||||
|
readonly formData?: Record<string, any>;
|
||||||
|
readonly body?: any;
|
||||||
|
readonly mediaType?: string;
|
||||||
|
readonly responseHeader?: string;
|
||||||
|
readonly errors?: Record<number, string>;
|
||||||
|
};
|
||||||
11
modules/frontend/src/auth/core/ApiResult.ts
Normal file
11
modules/frontend/src/auth/core/ApiResult.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type ApiResult = {
|
||||||
|
readonly url: string;
|
||||||
|
readonly ok: boolean;
|
||||||
|
readonly status: number;
|
||||||
|
readonly statusText: string;
|
||||||
|
readonly body: any;
|
||||||
|
};
|
||||||
131
modules/frontend/src/auth/core/CancelablePromise.ts
Normal file
131
modules/frontend/src/auth/core/CancelablePromise.ts
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export class CancelError extends Error {
|
||||||
|
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'CancelError';
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isCancelled(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OnCancel {
|
||||||
|
readonly isResolved: boolean;
|
||||||
|
readonly isRejected: boolean;
|
||||||
|
readonly isCancelled: boolean;
|
||||||
|
|
||||||
|
(cancelHandler: () => void): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CancelablePromise<T> implements Promise<T> {
|
||||||
|
#isResolved: boolean;
|
||||||
|
#isRejected: boolean;
|
||||||
|
#isCancelled: boolean;
|
||||||
|
readonly #cancelHandlers: (() => void)[];
|
||||||
|
readonly #promise: Promise<T>;
|
||||||
|
#resolve?: (value: T | PromiseLike<T>) => void;
|
||||||
|
#reject?: (reason?: any) => void;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
executor: (
|
||||||
|
resolve: (value: T | PromiseLike<T>) => void,
|
||||||
|
reject: (reason?: any) => void,
|
||||||
|
onCancel: OnCancel
|
||||||
|
) => void
|
||||||
|
) {
|
||||||
|
this.#isResolved = false;
|
||||||
|
this.#isRejected = false;
|
||||||
|
this.#isCancelled = false;
|
||||||
|
this.#cancelHandlers = [];
|
||||||
|
this.#promise = new Promise<T>((resolve, reject) => {
|
||||||
|
this.#resolve = resolve;
|
||||||
|
this.#reject = reject;
|
||||||
|
|
||||||
|
const onResolve = (value: T | PromiseLike<T>): void => {
|
||||||
|
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#isResolved = true;
|
||||||
|
if (this.#resolve) this.#resolve(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onReject = (reason?: any): void => {
|
||||||
|
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#isRejected = true;
|
||||||
|
if (this.#reject) this.#reject(reason);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = (cancelHandler: () => void): void => {
|
||||||
|
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#cancelHandlers.push(cancelHandler);
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.defineProperty(onCancel, 'isResolved', {
|
||||||
|
get: (): boolean => this.#isResolved,
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(onCancel, 'isRejected', {
|
||||||
|
get: (): boolean => this.#isRejected,
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(onCancel, 'isCancelled', {
|
||||||
|
get: (): boolean => this.#isCancelled,
|
||||||
|
});
|
||||||
|
|
||||||
|
return executor(onResolve, onReject, onCancel as OnCancel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get [Symbol.toStringTag]() {
|
||||||
|
return "Cancellable Promise";
|
||||||
|
}
|
||||||
|
|
||||||
|
public then<TResult1 = T, TResult2 = never>(
|
||||||
|
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
||||||
|
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
|
||||||
|
): Promise<TResult1 | TResult2> {
|
||||||
|
return this.#promise.then(onFulfilled, onRejected);
|
||||||
|
}
|
||||||
|
|
||||||
|
public catch<TResult = never>(
|
||||||
|
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
|
||||||
|
): Promise<T | TResult> {
|
||||||
|
return this.#promise.catch(onRejected);
|
||||||
|
}
|
||||||
|
|
||||||
|
public finally(onFinally?: (() => void) | null): Promise<T> {
|
||||||
|
return this.#promise.finally(onFinally);
|
||||||
|
}
|
||||||
|
|
||||||
|
public cancel(): void {
|
||||||
|
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#isCancelled = true;
|
||||||
|
if (this.#cancelHandlers.length) {
|
||||||
|
try {
|
||||||
|
for (const cancelHandler of this.#cancelHandlers) {
|
||||||
|
cancelHandler();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Cancellation threw an error', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.#cancelHandlers.length = 0;
|
||||||
|
if (this.#reject) this.#reject(new CancelError('Request aborted'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isCancelled(): boolean {
|
||||||
|
return this.#isCancelled;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
modules/frontend/src/auth/core/OpenAPI.ts
Normal file
32
modules/frontend/src/auth/core/OpenAPI.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||||
|
|
||||||
|
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
|
||||||
|
type Headers = Record<string, string>;
|
||||||
|
|
||||||
|
export type OpenAPIConfig = {
|
||||||
|
BASE: string;
|
||||||
|
VERSION: string;
|
||||||
|
WITH_CREDENTIALS: boolean;
|
||||||
|
CREDENTIALS: 'include' | 'omit' | 'same-origin';
|
||||||
|
TOKEN?: string | Resolver<string> | undefined;
|
||||||
|
USERNAME?: string | Resolver<string> | undefined;
|
||||||
|
PASSWORD?: string | Resolver<string> | undefined;
|
||||||
|
HEADERS?: Headers | Resolver<Headers> | undefined;
|
||||||
|
ENCODE_PATH?: ((path: string) => string) | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OpenAPI: OpenAPIConfig = {
|
||||||
|
BASE: 'http://10.1.0.65:8081/auth',
|
||||||
|
VERSION: '1.0.0',
|
||||||
|
WITH_CREDENTIALS: false,
|
||||||
|
CREDENTIALS: 'include',
|
||||||
|
TOKEN: undefined,
|
||||||
|
USERNAME: undefined,
|
||||||
|
PASSWORD: undefined,
|
||||||
|
HEADERS: undefined,
|
||||||
|
ENCODE_PATH: undefined,
|
||||||
|
};
|
||||||
|
|
@ -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;
|
|
||||||
};
|
|
||||||
323
modules/frontend/src/auth/core/request.ts
Normal file
323
modules/frontend/src/auth/core/request.ts
Normal file
|
|
@ -0,0 +1,323 @@
|
||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import axios from 'axios';
|
||||||
|
import type { AxiosError, AxiosRequestConfig, AxiosResponse, AxiosInstance } from 'axios';
|
||||||
|
import FormData from 'form-data';
|
||||||
|
|
||||||
|
import { ApiError } from './ApiError';
|
||||||
|
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||||
|
import type { ApiResult } from './ApiResult';
|
||||||
|
import { CancelablePromise } from './CancelablePromise';
|
||||||
|
import type { OnCancel } from './CancelablePromise';
|
||||||
|
import type { OpenAPIConfig } from './OpenAPI';
|
||||||
|
|
||||||
|
export const isDefined = <T>(value: T | null | undefined): value is Exclude<T, null | undefined> => {
|
||||||
|
return value !== undefined && value !== null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isString = (value: any): value is string => {
|
||||||
|
return typeof value === 'string';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isStringWithValue = (value: any): value is string => {
|
||||||
|
return isString(value) && value !== '';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isBlob = (value: any): value is Blob => {
|
||||||
|
return (
|
||||||
|
typeof value === 'object' &&
|
||||||
|
typeof value.type === 'string' &&
|
||||||
|
typeof value.stream === 'function' &&
|
||||||
|
typeof value.arrayBuffer === 'function' &&
|
||||||
|
typeof value.constructor === 'function' &&
|
||||||
|
typeof value.constructor.name === 'string' &&
|
||||||
|
/^(Blob|File)$/.test(value.constructor.name) &&
|
||||||
|
/^(Blob|File)$/.test(value[Symbol.toStringTag])
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isFormData = (value: any): value is FormData => {
|
||||||
|
return value instanceof FormData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isSuccess = (status: number): boolean => {
|
||||||
|
return status >= 200 && status < 300;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const base64 = (str: string): string => {
|
||||||
|
try {
|
||||||
|
return btoa(str);
|
||||||
|
} catch (err) {
|
||||||
|
// @ts-ignore
|
||||||
|
return Buffer.from(str).toString('base64');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getQueryString = (params: Record<string, any>): string => {
|
||||||
|
const qs: string[] = [];
|
||||||
|
|
||||||
|
const append = (key: string, value: any) => {
|
||||||
|
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const process = (key: string, value: any) => {
|
||||||
|
if (isDefined(value)) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach(v => {
|
||||||
|
process(key, v);
|
||||||
|
});
|
||||||
|
} else if (typeof value === 'object') {
|
||||||
|
Object.entries(value).forEach(([k, v]) => {
|
||||||
|
process(`${key}[${k}]`, v);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
append(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
process(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (qs.length > 0) {
|
||||||
|
return `?${qs.join('&')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
|
||||||
|
const encoder = config.ENCODE_PATH || encodeURI;
|
||||||
|
|
||||||
|
const path = options.url
|
||||||
|
.replace('{api-version}', config.VERSION)
|
||||||
|
.replace(/{(.*?)}/g, (substring: string, group: string) => {
|
||||||
|
if (options.path?.hasOwnProperty(group)) {
|
||||||
|
return encoder(String(options.path[group]));
|
||||||
|
}
|
||||||
|
return substring;
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = `${config.BASE}${path}`;
|
||||||
|
if (options.query) {
|
||||||
|
return `${url}${getQueryString(options.query)}`;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFormData = (options: ApiRequestOptions): FormData | undefined => {
|
||||||
|
if (options.formData) {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
const process = (key: string, value: any) => {
|
||||||
|
if (isString(value) || isBlob(value)) {
|
||||||
|
formData.append(key, value);
|
||||||
|
} else {
|
||||||
|
formData.append(key, JSON.stringify(value));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(options.formData)
|
||||||
|
.filter(([_, value]) => isDefined(value))
|
||||||
|
.forEach(([key, value]) => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach(v => process(key, v));
|
||||||
|
} else {
|
||||||
|
process(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return formData;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
|
||||||
|
|
||||||
|
export const resolve = async <T>(options: ApiRequestOptions, resolver?: T | Resolver<T>): Promise<T | undefined> => {
|
||||||
|
if (typeof resolver === 'function') {
|
||||||
|
return (resolver as Resolver<T>)(options);
|
||||||
|
}
|
||||||
|
return resolver;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions, formData?: FormData): Promise<Record<string, string>> => {
|
||||||
|
const [token, username, password, additionalHeaders] = await Promise.all([
|
||||||
|
resolve(options, config.TOKEN),
|
||||||
|
resolve(options, config.USERNAME),
|
||||||
|
resolve(options, config.PASSWORD),
|
||||||
|
resolve(options, config.HEADERS),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const formHeaders = typeof formData?.getHeaders === 'function' && formData?.getHeaders() || {}
|
||||||
|
|
||||||
|
const headers = Object.entries({
|
||||||
|
Accept: 'application/json',
|
||||||
|
...additionalHeaders,
|
||||||
|
...options.headers,
|
||||||
|
...formHeaders,
|
||||||
|
})
|
||||||
|
.filter(([_, value]) => isDefined(value))
|
||||||
|
.reduce((headers, [key, value]) => ({
|
||||||
|
...headers,
|
||||||
|
[key]: String(value),
|
||||||
|
}), {} as Record<string, string>);
|
||||||
|
|
||||||
|
if (isStringWithValue(token)) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStringWithValue(username) && isStringWithValue(password)) {
|
||||||
|
const credentials = base64(`${username}:${password}`);
|
||||||
|
headers['Authorization'] = `Basic ${credentials}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.body !== undefined) {
|
||||||
|
if (options.mediaType) {
|
||||||
|
headers['Content-Type'] = options.mediaType;
|
||||||
|
} else if (isBlob(options.body)) {
|
||||||
|
headers['Content-Type'] = options.body.type || 'application/octet-stream';
|
||||||
|
} else if (isString(options.body)) {
|
||||||
|
headers['Content-Type'] = 'text/plain';
|
||||||
|
} else if (!isFormData(options.body)) {
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRequestBody = (options: ApiRequestOptions): any => {
|
||||||
|
if (options.body) {
|
||||||
|
return options.body;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendRequest = async <T>(
|
||||||
|
config: OpenAPIConfig,
|
||||||
|
options: ApiRequestOptions,
|
||||||
|
url: string,
|
||||||
|
body: any,
|
||||||
|
formData: FormData | undefined,
|
||||||
|
headers: Record<string, string>,
|
||||||
|
onCancel: OnCancel,
|
||||||
|
axiosClient: AxiosInstance
|
||||||
|
): Promise<AxiosResponse<T>> => {
|
||||||
|
const source = axios.CancelToken.source();
|
||||||
|
|
||||||
|
const requestConfig: AxiosRequestConfig = {
|
||||||
|
url,
|
||||||
|
headers,
|
||||||
|
data: body ?? formData,
|
||||||
|
method: options.method,
|
||||||
|
withCredentials: config.WITH_CREDENTIALS,
|
||||||
|
withXSRFToken: config.CREDENTIALS === 'include' ? config.WITH_CREDENTIALS : false,
|
||||||
|
cancelToken: source.token,
|
||||||
|
};
|
||||||
|
|
||||||
|
onCancel(() => source.cancel('The user aborted a request.'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await axiosClient.request(requestConfig);
|
||||||
|
} catch (error) {
|
||||||
|
const axiosError = error as AxiosError<T>;
|
||||||
|
if (axiosError.response) {
|
||||||
|
return axiosError.response;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getResponseHeader = (response: AxiosResponse<any>, responseHeader?: string): string | undefined => {
|
||||||
|
if (responseHeader) {
|
||||||
|
const content = response.headers[responseHeader];
|
||||||
|
if (isString(content)) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getResponseBody = (response: AxiosResponse<any>): any => {
|
||||||
|
if (response.status !== 204) {
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
|
||||||
|
const errors: Record<number, string> = {
|
||||||
|
400: 'Bad Request',
|
||||||
|
401: 'Unauthorized',
|
||||||
|
403: 'Forbidden',
|
||||||
|
404: 'Not Found',
|
||||||
|
500: 'Internal Server Error',
|
||||||
|
502: 'Bad Gateway',
|
||||||
|
503: 'Service Unavailable',
|
||||||
|
...options.errors,
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = errors[result.status];
|
||||||
|
if (error) {
|
||||||
|
throw new ApiError(options, result, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
const errorStatus = result.status ?? 'unknown';
|
||||||
|
const errorStatusText = result.statusText ?? 'unknown';
|
||||||
|
const errorBody = (() => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(result.body, null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
throw new ApiError(options, result,
|
||||||
|
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request method
|
||||||
|
* @param config The OpenAPI configuration object
|
||||||
|
* @param options The request options from the service
|
||||||
|
* @param axiosClient The axios client instance to use
|
||||||
|
* @returns CancelablePromise<T>
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions, axiosClient: AxiosInstance = axios): CancelablePromise<T> => {
|
||||||
|
return new CancelablePromise(async (resolve, reject, onCancel) => {
|
||||||
|
try {
|
||||||
|
const url = getUrl(config, options);
|
||||||
|
const formData = getFormData(options);
|
||||||
|
const body = getRequestBody(options);
|
||||||
|
const headers = await getHeaders(config, options, formData);
|
||||||
|
|
||||||
|
if (!onCancel.isCancelled) {
|
||||||
|
const response = await sendRequest<T>(config, options, url, body, formData, headers, onCancel, axiosClient);
|
||||||
|
const responseBody = getResponseBody(response);
|
||||||
|
const responseHeader = getResponseHeader(response, options.responseHeader);
|
||||||
|
|
||||||
|
const result: ApiResult = {
|
||||||
|
url,
|
||||||
|
ok: isSuccess(response.status),
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
body: responseHeader ?? responseBody,
|
||||||
|
};
|
||||||
|
|
||||||
|
catchErrorCodes(options, result);
|
||||||
|
|
||||||
|
resolve(result.body);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -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,10 @@
|
||||||
// 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 { AuthService } from './services/AuthService';
|
||||||
export * from './sdk.gen';
|
|
||||||
|
|
|
||||||
|
|
@ -1,66 +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 { GetImpersonationTokenData, GetImpersonationTokenErrors, GetImpersonationTokenResponses, LogoutData, LogoutErrors, LogoutResponses, PostSignInData, PostSignInErrors, PostSignInResponses, PostSignUpData, PostSignUpResponses, RefreshTokensData, RefreshTokensErrors, RefreshTokensResponses } 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>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sign up a new user
|
|
||||||
*/
|
|
||||||
export const postSignUp = <ThrowOnError extends boolean = false>(options: Options<PostSignUpData, ThrowOnError>) => (options.client ?? client).post<PostSignUpResponses, unknown, ThrowOnError>({
|
|
||||||
url: '/sign-up',
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options.headers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sign in a user and return JWT
|
|
||||||
*/
|
|
||||||
export const postSignIn = <ThrowOnError extends boolean = false>(options: Options<PostSignInData, ThrowOnError>) => (options.client ?? client).post<PostSignInResponses, PostSignInErrors, ThrowOnError>({
|
|
||||||
url: '/sign-in',
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options.headers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get service impersontaion token
|
|
||||||
*/
|
|
||||||
export const getImpersonationToken = <ThrowOnError extends boolean = false>(options: Options<GetImpersonationTokenData, ThrowOnError>) => (options.client ?? client).post<GetImpersonationTokenResponses, GetImpersonationTokenErrors, ThrowOnError>({
|
|
||||||
security: [{ scheme: 'bearer', type: 'http' }],
|
|
||||||
url: '/get-impersonation-token',
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options.headers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refreshes access_token and refresh_token
|
|
||||||
*/
|
|
||||||
export const refreshTokens = <ThrowOnError extends boolean = false>(options?: Options<RefreshTokensData, ThrowOnError>) => (options?.client ?? client).get<RefreshTokensResponses, RefreshTokensErrors, ThrowOnError>({ url: '/refresh-tokens', ...options });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs out the user
|
|
||||||
*/
|
|
||||||
export const logout = <ThrowOnError extends boolean = false>(options?: Options<LogoutData, ThrowOnError>) => (options?.client ?? client).post<LogoutResponses, LogoutErrors, ThrowOnError>({ url: '/logout', ...options });
|
|
||||||
55
modules/frontend/src/auth/services/AuthService.ts
Normal file
55
modules/frontend/src/auth/services/AuthService.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
/* 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,136 +0,0 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
export type ClientOptions = {
|
|
||||||
baseUrl: `${string}://${string}/auth` | (string & {});
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PostSignUpData = {
|
|
||||||
body: {
|
|
||||||
nickname: string;
|
|
||||||
pass: string;
|
|
||||||
};
|
|
||||||
path?: never;
|
|
||||||
query?: never;
|
|
||||||
url: '/sign-up';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PostSignUpResponses = {
|
|
||||||
/**
|
|
||||||
* Sign-up result
|
|
||||||
*/
|
|
||||||
200: {
|
|
||||||
user_id: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PostSignUpResponse = PostSignUpResponses[keyof PostSignUpResponses];
|
|
||||||
|
|
||||||
export type PostSignInData = {
|
|
||||||
body: {
|
|
||||||
nickname: string;
|
|
||||||
pass: string;
|
|
||||||
};
|
|
||||||
path?: never;
|
|
||||||
query?: never;
|
|
||||||
url: '/sign-in';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PostSignInErrors = {
|
|
||||||
/**
|
|
||||||
* Access token is missing or invalid
|
|
||||||
*/
|
|
||||||
401: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PostSignInResponses = {
|
|
||||||
/**
|
|
||||||
* Sign-in result with JWT
|
|
||||||
*/
|
|
||||||
200: {
|
|
||||||
user_id: number;
|
|
||||||
user_name: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PostSignInResponse = PostSignInResponses[keyof PostSignInResponses];
|
|
||||||
|
|
||||||
export type GetImpersonationTokenData = {
|
|
||||||
body: unknown & {
|
|
||||||
user_id?: number;
|
|
||||||
external_id?: number;
|
|
||||||
};
|
|
||||||
path?: never;
|
|
||||||
query?: never;
|
|
||||||
url: '/get-impersonation-token';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetImpersonationTokenErrors = {
|
|
||||||
/**
|
|
||||||
* Access token is missing or invalid
|
|
||||||
*/
|
|
||||||
401: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetImpersonationTokenResponses = {
|
|
||||||
/**
|
|
||||||
* Generated impersonation access token
|
|
||||||
*/
|
|
||||||
200: {
|
|
||||||
/**
|
|
||||||
* JWT access token
|
|
||||||
*/
|
|
||||||
access_token: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetImpersonationTokenResponse = GetImpersonationTokenResponses[keyof GetImpersonationTokenResponses];
|
|
||||||
|
|
||||||
export type RefreshTokensData = {
|
|
||||||
body?: never;
|
|
||||||
path?: never;
|
|
||||||
query?: never;
|
|
||||||
url: '/refresh-tokens';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RefreshTokensErrors = {
|
|
||||||
/**
|
|
||||||
* ClientError
|
|
||||||
*/
|
|
||||||
400: unknown;
|
|
||||||
/**
|
|
||||||
* Access token is missing or invalid
|
|
||||||
*/
|
|
||||||
401: unknown;
|
|
||||||
/**
|
|
||||||
* ServerError
|
|
||||||
*/
|
|
||||||
500: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RefreshTokensResponses = {
|
|
||||||
/**
|
|
||||||
* Refresh success
|
|
||||||
*/
|
|
||||||
200: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LogoutData = {
|
|
||||||
body?: never;
|
|
||||||
path?: never;
|
|
||||||
query?: never;
|
|
||||||
url: '/logout';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LogoutErrors = {
|
|
||||||
/**
|
|
||||||
* ServerError
|
|
||||||
*/
|
|
||||||
500: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LogoutResponses = {
|
|
||||||
/**
|
|
||||||
* Logout success
|
|
||||||
*/
|
|
||||||
200: unknown;
|
|
||||||
};
|
|
||||||
|
|
@ -1,106 +1,72 @@
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/solid";
|
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/solid";
|
||||||
import { logout } from "../../auth";
|
|
||||||
|
|
||||||
export const Header: React.FC = () => {
|
type HeaderProps = {
|
||||||
const navigate = useNavigate();
|
username?: string;
|
||||||
const [username, setUsername] = useState<string | null>(localStorage.getItem("user_name"));
|
};
|
||||||
|
|
||||||
|
export const Header: React.FC<HeaderProps> = ({ username }) => {
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
||||||
|
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const toggleMenu = () => setMenuOpen(!menuOpen);
|
||||||
|
|
||||||
// Listen for localStorage changes to update username dynamically
|
|
||||||
useEffect(() => {
|
|
||||||
const handleStorage = () => setUsername(localStorage.getItem("user_name"));
|
|
||||||
// This catches changes from OTHER tabs
|
|
||||||
window.addEventListener("storage", handleStorage);
|
|
||||||
|
|
||||||
// This catches changes in the CURRENT tab if you use dispatchEvent
|
|
||||||
window.addEventListener("local-storage-update", handleStorage);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("storage", handleStorage);
|
|
||||||
window.removeEventListener("local-storage-update", handleStorage);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
try {
|
|
||||||
await logout();
|
|
||||||
localStorage.removeItem("user_id");
|
|
||||||
localStorage.removeItem("user_name");
|
|
||||||
setUsername(null);
|
|
||||||
navigate("/login");
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Close dropdown on click outside
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
||||||
setDropdownOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="w-full bg-white shadow-md sticky top-0 left-0 z-50">
|
<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="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 justify-between h-16 items-center">
|
||||||
|
|
||||||
{/* Logo */}
|
{/* Левый блок — логотип / название */}
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<Link to="/" className="text-xl font-bold text-gray-800 hover:text-blue-600">
|
<Link to="/" className="text-xl font-bold text-gray-800 hover:text-blue-600">
|
||||||
NyanimeDB
|
NyanimeDB
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation (desktop) */}
|
{/* Центр — ссылки на разделы (desktop) */}
|
||||||
<nav className="hidden md:flex space-x-4">
|
<nav className="hidden md:flex space-x-4">
|
||||||
<Link to="/titles" className="text-gray-700 hover:text-blue-600">Titles</Link>
|
<Link to="/titles" className="text-gray-700 hover:text-blue-600">
|
||||||
<Link to="/users" className="text-gray-700 hover:text-blue-600">Users</Link>
|
Titles
|
||||||
<Link to="/about" className="text-gray-700 hover:text-blue-600">About</Link>
|
</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>
|
</nav>
|
||||||
|
|
||||||
{/* Profile / login */}
|
{/* Правый блок — профиль */}
|
||||||
<div className="hidden md:flex items-center space-x-4" ref={dropdownRef}>
|
<div className="hidden md:flex items-center space-x-4">
|
||||||
{username ? (
|
{username ? (
|
||||||
<div className="relative">
|
<Link to="/profile" className="text-gray-700 hover:text-blue-600 font-medium">
|
||||||
<button
|
|
||||||
onClick={() => setDropdownOpen(!dropdownOpen)}
|
|
||||||
className="text-gray-700 hover:text-blue-600 font-medium"
|
|
||||||
>
|
|
||||||
{username}
|
{username}
|
||||||
</button>
|
</Link>
|
||||||
{dropdownOpen && (
|
|
||||||
<div className="absolute right-0 mt-2 w-40 bg-white border rounded shadow-md z-50">
|
|
||||||
<Link to="/profile" className="block px-4 py-2 hover:bg-gray-100" onClick={() => setDropdownOpen(false)}>Profile</Link>
|
|
||||||
<button onClick={handleLogout} className="w-full text-left px-4 py-2 hover:bg-gray-100">Logout</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<Link to="/login" className="text-gray-700 hover:text-blue-600 font-medium">Login</Link>
|
<Link to="/login" className="text-gray-700 hover:text-blue-600 font-medium">
|
||||||
|
Login
|
||||||
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile burger */}
|
{/* Бургер для мобильного */}
|
||||||
<div className="md:hidden flex items-center">
|
<div className="md:hidden flex items-center">
|
||||||
<button onClick={() => setMenuOpen(!menuOpen)} className="p-2 rounded-md hover:bg-gray-200 transition">
|
<button
|
||||||
{menuOpen ? <XMarkIcon className="w-6 h-6 text-gray-800" /> : <Bars3Icon className="w-6 h-6 text-gray-800" />}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile menu */}
|
{/* Мобильное меню */}
|
||||||
{menuOpen && (
|
{menuOpen && (
|
||||||
<div className="md:hidden bg-white border-t border-gray-200 shadow-md">
|
<div className="md:hidden bg-white border-t border-gray-200 shadow-md">
|
||||||
<nav className="flex flex-col p-4 space-y-2">
|
<nav className="flex flex-col p-4 space-y-2">
|
||||||
|
|
@ -108,12 +74,13 @@ export const Header: React.FC = () => {
|
||||||
<Link to="/users" className="text-gray-700 hover:text-blue-600" onClick={() => setMenuOpen(false)}>Users</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>
|
<Link to="/about" className="text-gray-700 hover:text-blue-600" onClick={() => setMenuOpen(false)}>About</Link>
|
||||||
{username ? (
|
{username ? (
|
||||||
<>
|
<Link to="/profile" className="text-gray-700 hover:text-blue-600 font-medium" onClick={() => setMenuOpen(false)}>
|
||||||
<Link to="/profile" className="text-gray-700 hover:text-blue-600 font-medium" onClick={() => setMenuOpen(false)}>Profile</Link>
|
{username}
|
||||||
<button onClick={handleLogout} className="text-gray-700 hover:text-blue-600 font-medium text-left">Logout</button>
|
</Link>
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<Link to="/login" className="text-gray-700 hover:text-blue-600 font-medium" onClick={() => setMenuOpen(false)}>Login</Link>
|
<Link to="/login" className="text-gray-700 hover:text-blue-600 font-medium" onClick={() => setMenuOpen(false)}>
|
||||||
|
Login
|
||||||
|
</Link>
|
||||||
)}
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export function TitleStatusControls({ titleId }: { titleId: number }) {
|
||||||
const [currentStatus, setCurrentStatus] = useState<UserTitleStatus | null>(null);
|
const [currentStatus, setCurrentStatus] = useState<UserTitleStatus | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const userIdStr = localStorage.getItem("user_id");
|
const userIdStr = localStorage.getItem("userId");
|
||||||
const userId = userIdStr ? Number(userIdStr) : null;
|
const userId = userIdStr ? Number(userIdStr) : null;
|
||||||
|
|
||||||
// --- Load initial status ---
|
// --- Load initial status ---
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export function TitleCardHorizontal({ title }: { title: Title }) {
|
||||||
borderRadius: 8
|
borderRadius: 8
|
||||||
}}>
|
}}>
|
||||||
{title.poster?.image_path && (
|
{title.poster?.image_path && (
|
||||||
<img src={"media/" + title.poster.image_path} width={80} />
|
<img src={title.poster.image_path} width={80} />
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h3>{title.title_names["en"]}</h3>
|
<h3>{title.title_names["en"]}</h3>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export function TitleCardSquare({ title }: { title: Title }) {
|
||||||
textAlign: "center"
|
textAlign: "center"
|
||||||
}}>
|
}}>
|
||||||
{title.poster?.image_path && (
|
{title.poster?.image_path && (
|
||||||
<img src={"media/" + title.poster.image_path} width={140} />
|
<img src={title.poster.image_path} width={140} />
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h4>{title.title_names["en"]}</h4>
|
<h4>{title.title_names["en"]}</h4>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export function UserTitleCardHorizontal({ title }: { title: UserTitle }) {
|
||||||
borderRadius: 8
|
borderRadius: 8
|
||||||
}}>
|
}}>
|
||||||
{title.title?.poster?.image_path && (
|
{title.title?.poster?.image_path && (
|
||||||
<img src={"media/" + title.title?.poster.image_path} width={80} />
|
<img src={title.title?.poster.image_path} width={80} />
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h3>{title.title?.title_names["en"]}</h3>
|
<h3>{title.title?.title_names["en"]}</h3>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export function UserTitleCardSquare({ title }: { title: UserTitle }) {
|
||||||
textAlign: "center"
|
textAlign: "center"
|
||||||
}}>
|
}}>
|
||||||
{title.title?.poster?.image_path && (
|
{title.title?.poster?.image_path && (
|
||||||
<img src={"media/" + title.title?.poster.image_path} width={140} />
|
<img src={title.title?.poster.image_path} width={140} />
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h4>{title.title?.title_names["en"]}</h4>
|
<h4>{title.title?.title_names["en"]}</h4>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { postSignIn, postSignUp } from "../../auth";
|
import { AuthService } from "../../auth/services/AuthService";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export const LoginPage: React.FC = () => {
|
export const LoginPage: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isLogin, setIsLogin] = useState(true);
|
const [isLogin, setIsLogin] = useState(true); // true = login, false = signup
|
||||||
const [nickname, setNickname] = useState("");
|
const [nickname, setNickname] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
@ -17,32 +17,27 @@ export const LoginPage: React.FC = () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isLogin) {
|
if (isLogin) {
|
||||||
const res = await postSignIn({ body: { nickname, pass: password } });
|
const res = await AuthService.postSignIn({ nickname, pass: password });
|
||||||
if (res.data?.user_id && res.data?.user_name) {
|
if (res.user_id && res.user_name) {
|
||||||
localStorage.setItem("user_id", res.data.user_id.toString());
|
// Сохраняем user_id и username в localStorage
|
||||||
localStorage.setItem("user_name", res.data.user_name);
|
localStorage.setItem("userId", res.user_id.toString());
|
||||||
window.dispatchEvent(new Event("storage"));
|
localStorage.setItem("username", res.user_name);
|
||||||
navigate("/profile");
|
|
||||||
|
navigate("/profile"); // редирект на профиль
|
||||||
} else {
|
} else {
|
||||||
setError("Login failed");
|
setError("Login failed");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Sign up
|
// SignUp оставляем без сохранения данных
|
||||||
const res = await postSignUp({ body: { nickname, pass: password } });
|
const res = await AuthService.postSignUp({ nickname, pass: password });
|
||||||
if (res.data?.user_id) {
|
if (res.user_id) {
|
||||||
// Auto-login after sign-up
|
setIsLogin(true); // переключаемся на login после регистрации
|
||||||
const loginRes = await postSignIn({ body: { nickname, pass: password } });
|
|
||||||
if (loginRes.data?.user_id && loginRes.data?.user_name) {
|
|
||||||
localStorage.setItem("user_id", loginRes.data.user_id.toString());
|
|
||||||
localStorage.setItem("user_name", loginRes.data.user_name);
|
|
||||||
window.dispatchEvent(new Event("storage"));
|
|
||||||
navigate("/profile");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
setError("Sign up failed");
|
setError("Sign up failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
console.error(err);
|
||||||
setError(err?.message || "Something went wrong");
|
setError(err?.message || "Something went wrong");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -52,26 +47,39 @@ export const LoginPage: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 px-4">
|
<div className="min-h-screen flex items-center justify-center bg-gray-50 px-4">
|
||||||
<div className="max-w-md w-full bg-white shadow-md rounded-lg p-8">
|
<div className="max-w-md w-full bg-white shadow-md rounded-lg p-8">
|
||||||
<h2 className="text-2xl font-bold mb-6 text-center">{isLogin ? "Login" : "Sign Up"}</h2>
|
<h2 className="text-2xl font-bold mb-6 text-center">
|
||||||
|
{isLogin ? "Login" : "Sign Up"}
|
||||||
|
</h2>
|
||||||
|
|
||||||
{error && <div className="text-red-600 mb-4">{error}</div>}
|
{error && <div className="text-red-600 mb-4">{error}</div>}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Nickname
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={nickname}
|
value={nickname}
|
||||||
onChange={(e) => setNickname(e.target.value)}
|
onChange={(e) => setNickname(e.target.value)}
|
||||||
placeholder="Nickname"
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder="Password"
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
|
@ -83,9 +91,25 @@ export const LoginPage: React.FC = () => {
|
||||||
|
|
||||||
<div className="mt-4 text-center text-sm text-gray-600">
|
<div className="mt-4 text-center text-sm text-gray-600">
|
||||||
{isLogin ? (
|
{isLogin ? (
|
||||||
<>Don't have an account? <button onClick={() => setIsLogin(false)} className="text-blue-600 hover:underline">Sign Up</button></>
|
<>
|
||||||
|
Don't have an account?{" "}
|
||||||
|
<button
|
||||||
|
onClick={() => setIsLogin(false)}
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
Sign Up
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>Already have an account? <button onClick={() => setIsLogin(true)} className="text-blue-600 hover:underline">Login</button></>
|
<>
|
||||||
|
Already have an account?{" "}
|
||||||
|
<button
|
||||||
|
onClick={() => setIsLogin(true)}
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,154 +0,0 @@
|
||||||
// import React, { useEffect, useState } from "react";
|
|
||||||
// import { updateUser, getUsersId } from "../../api";
|
|
||||||
// import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
// export const SettingsPage: React.FC = () => {
|
|
||||||
// const navigate = useNavigate();
|
|
||||||
|
|
||||||
// const userId = Number(localStorage.getItem("user_id"));
|
|
||||||
// const initialNickname = localStorage.getItem("user_name") || "";
|
|
||||||
// const [mail, setMail] = useState("");
|
|
||||||
// const [nickname, setNickname] = useState(initialNickname);
|
|
||||||
// const [dispName, setDispName] = useState("");
|
|
||||||
// const [userDesc, setUserDesc] = useState("");
|
|
||||||
// const [avatarId, setAvatarId] = useState<number | null>(null);
|
|
||||||
|
|
||||||
// const [loading, setLoading] = useState(false);
|
|
||||||
// const [success, setSuccess] = useState<string | null>(null);
|
|
||||||
// const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// const fetch = async () => {
|
|
||||||
// const res = await getUsersId({
|
|
||||||
// path: { user_id: String(userId) },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// setProfile(res.data);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// fetch();
|
|
||||||
// }, [userId]);
|
|
||||||
|
|
||||||
// const saveSettings = async (e: React.FormEvent) => {
|
|
||||||
// e.preventDefault();
|
|
||||||
// setLoading(true);
|
|
||||||
// setSuccess(null);
|
|
||||||
// setError(null);
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// const res = await updateUser({
|
|
||||||
// path: { user_id: userId },
|
|
||||||
// body: {
|
|
||||||
// ...(mail ? { mail } : {}),
|
|
||||||
// ...(nickname ? { nickname } : {}),
|
|
||||||
// ...(dispName ? { disp_name: dispName } : {}),
|
|
||||||
// ...(userDesc ? { user_desc: userDesc } : {}),
|
|
||||||
// ...(avatarId !== undefined ? { avatar_id: avatarId } : {}),
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // Обновляем локальное отображение username
|
|
||||||
// if (nickname) {
|
|
||||||
// localStorage.setItem("user_name", nickname);
|
|
||||||
// window.dispatchEvent(new Event("storage")); // чтобы Header обновился
|
|
||||||
// }
|
|
||||||
|
|
||||||
// setSuccess("Settings updated!");
|
|
||||||
// setTimeout(() => navigate("/profile"), 800);
|
|
||||||
|
|
||||||
// } catch (err: any) {
|
|
||||||
// console.error(err);
|
|
||||||
// setError(err?.message || "Failed to update settings");
|
|
||||||
// } finally {
|
|
||||||
// setLoading(false);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <div className="max-w-2xl mx-auto p-6">
|
|
||||||
// <h1 className="text-3xl font-bold mb-6">User Settings</h1>
|
|
||||||
|
|
||||||
// {success && <div className="text-green-600 mb-4">{success}</div>}
|
|
||||||
// {error && <div className="text-red-600 mb-4">{error}</div>}
|
|
||||||
|
|
||||||
// <form className="space-y-6" onSubmit={saveSettings}>
|
|
||||||
// {/* Email */}
|
|
||||||
// <div>
|
|
||||||
// <label className="block text-sm font-medium mb-1">Email</label>
|
|
||||||
// <input
|
|
||||||
// type="email"
|
|
||||||
// value={mail}
|
|
||||||
// onChange={(e) => setMail(e.target.value)}
|
|
||||||
// placeholder="example@mail.com"
|
|
||||||
// className="w-full px-4 py-2 border rounded-lg"
|
|
||||||
// />
|
|
||||||
// </div>
|
|
||||||
|
|
||||||
// {/* Nickname */}
|
|
||||||
// <div>
|
|
||||||
// <label className="block text-sm font-medium mb-1">Nickname</label>
|
|
||||||
// <input
|
|
||||||
// type="text"
|
|
||||||
// value={nickname}
|
|
||||||
// onChange={(e) => setNickname(e.target.value)}
|
|
||||||
// className="w-full px-4 py-2 border rounded-lg"
|
|
||||||
// />
|
|
||||||
// </div>
|
|
||||||
|
|
||||||
// {/* Display name */}
|
|
||||||
// <div>
|
|
||||||
// <label className="block text-sm font-medium mb-1">Display name</label>
|
|
||||||
// <input
|
|
||||||
// type="text"
|
|
||||||
// value={dispName}
|
|
||||||
// onChange={(e) => setDispName(e.target.value)}
|
|
||||||
// placeholder="Shown name"
|
|
||||||
// className="w-full px-4 py-2 border rounded-lg"
|
|
||||||
// />
|
|
||||||
// </div>
|
|
||||||
|
|
||||||
// {/* Bio */}
|
|
||||||
// <div>
|
|
||||||
// <label className="block text-sm font-medium mb-1">Bio</label>
|
|
||||||
// <textarea
|
|
||||||
// value={userDesc}
|
|
||||||
// onChange={(e) => setUserDesc(e.target.value)}
|
|
||||||
// rows={4}
|
|
||||||
// className="w-full px-4 py-2 border rounded-lg resize-none"
|
|
||||||
// />
|
|
||||||
// </div>
|
|
||||||
|
|
||||||
// {/* Avatar ID */}
|
|
||||||
// <div>
|
|
||||||
// <label className="block text-sm font-medium mb-1">Avatar ID</label>
|
|
||||||
// <input
|
|
||||||
// type="number"
|
|
||||||
// value={avatarId ?? ""}
|
|
||||||
// onChange={(e) => {
|
|
||||||
// const v = e.target.value;
|
|
||||||
// setAvatarId(v === "" ? null : Number(v));
|
|
||||||
// }}
|
|
||||||
// placeholder="Image ID"
|
|
||||||
// className="w-full px-4 py-2 border rounded-lg"
|
|
||||||
// />
|
|
||||||
|
|
||||||
// <button
|
|
||||||
// type="button"
|
|
||||||
// onClick={() => setAvatarId(null)}
|
|
||||||
// className="text-sm text-blue-600 mt-1 hover:underline"
|
|
||||||
// >
|
|
||||||
// Remove avatar
|
|
||||||
// </button>
|
|
||||||
// </div>
|
|
||||||
|
|
||||||
// <button
|
|
||||||
// type="submit"
|
|
||||||
// disabled={loading}
|
|
||||||
// className="w-full bg-blue-600 text-white py-2 rounded-lg font-semibold hover:bg-blue-700 disabled:opacity-50"
|
|
||||||
// >
|
|
||||||
// {loading ? "Saving..." : "Save changes"}
|
|
||||||
// </button>
|
|
||||||
// </form>
|
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
// };
|
|
||||||
|
|
@ -46,7 +46,7 @@ export default function TitlePage() {
|
||||||
{/* Poster + status buttons */}
|
{/* Poster + status buttons */}
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<img
|
<img
|
||||||
src={"media/" + title.poster?.image_path || "/default-poster.png"}
|
src={title.poster?.image_path || "/default-poster.png"}
|
||||||
alt={title.title_names?.en?.[0] || "Title poster"}
|
alt={title.title_names?.en?.[0] || "Title poster"}
|
||||||
className="w-48 h-72 object-cover rounded-lg mb-4"
|
className="w-48 h-72 object-cover rounded-lg mb-4"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -127,16 +127,7 @@ const handleLoadMore = async () => {
|
||||||
</div>
|
</div>
|
||||||
<TitlesFilterPanel filters={filters} setFilters={setFilters} />
|
<TitlesFilterPanel filters={filters} setFilters={setFilters} />
|
||||||
|
|
||||||
{loading && (
|
{loading && <div className="mt-20 font-medium text-black">Loading...</div>}
|
||||||
<div className="mt-20 flex flex-col items-center justify-center space-y-4 font-medium text-black">
|
|
||||||
<span>Loading...</span>
|
|
||||||
<img
|
|
||||||
src="https://images.steamusercontent.com/ugc/920301026407341369/69CBEF69DED504CD8CC7838D370061089F4D81BD/"
|
|
||||||
alt="Loading animation"
|
|
||||||
className="size-100 object-contain"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!loading && titles.length === 0 && (
|
{!loading && titles.length === 0 && (
|
||||||
<div className="mt-20 font-medium text-black">No titles found.</div>
|
<div className="mt-20 font-medium text-black">No titles found.</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Image Storage Service — API
|
||||||
|
|
||||||
|
### POST `/upload`
|
||||||
|
|
||||||
|
Загрузка изображения как файла.
|
||||||
|
|
||||||
|
**Параметры запроса:**
|
||||||
|
|
||||||
|
* `file` — файл изображения (multipart/form-data, обязательный)
|
||||||
|
* `subdir` — подкаталог хранения (query, необязательный, по умолчанию `posters`)
|
||||||
|
|
||||||
|
**Ответ:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"path": "posters/ab/cd/<sha1>.jpg"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -F "file=@poster.jpg" "http://localhost:8000/upload?subdir=posters"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST `/download-by-url`
|
||||||
|
|
||||||
|
Загружает изображение из внешнего URL в файловое хранилище сервиса.
|
||||||
|
Клиенту файл не возвращается — сохраняется только на стороне сервиса.
|
||||||
|
|
||||||
|
**Тело запроса (JSON):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"url": "https://example.com/image.jpg",
|
||||||
|
"subdir": "posters"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`subdir` — необязательный, по умолчанию `posters`.
|
||||||
|
|
||||||
|
**Ответ:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"path": "posters/ab/cd/<sha1>.jpg"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:8000/download-by-url" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"url":"https://example.com/image.jpg"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GET `/{path:path}`
|
||||||
|
|
||||||
|
Получение сохранённого файла по относительному пути.
|
||||||
|
|
||||||
|
**Параметры:**
|
||||||
|
|
||||||
|
* `path` — относительный путь к файлу
|
||||||
|
(например: `posters/ab/cd/<sha1>.jpg`)
|
||||||
|
|
||||||
|
**Ответы:**
|
||||||
|
|
||||||
|
* `200 OK` — файл
|
||||||
|
* `404 Not Found` — файл не найден
|
||||||
|
|
||||||
|
**Пример:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/posters/ab/cd/<sha1>.jpg --output image.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Формат ответа
|
||||||
|
|
||||||
|
Все write-эндпоинты (`POST`) возвращают объект:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"path": "<relative_path>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Этот `path` используется для последующего доступа через `GET /{path}`.
|
||||||
|
|
@ -99,7 +99,7 @@ async def download_by_url(payload: DownloadByUrlRequest):
|
||||||
return {"path": rel}
|
return {"path": rel}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/{path:path}")
|
@app.get("/media/{path:path}")
|
||||||
async def get_image(path: str):
|
async def get_image(path: str):
|
||||||
"""
|
"""
|
||||||
Отдаёт файл по относительному пути (например, posters/ab/cd/hash.jpg).
|
Отдаёт файл по относительному пути (например, posters/ab/cd/hash.jpg).
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ CREATE TABLE users (
|
||||||
last_login timestamptz
|
last_login timestamptz
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE studios (
|
CREATE TABLE studios (
|
||||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
studio_name text NOT NULL UNIQUE,
|
studio_name text NOT NULL UNIQUE,
|
||||||
|
|
@ -86,7 +88,7 @@ CREATE TABLE usertitles (
|
||||||
status usertitle_status_t NOT NULL,
|
status usertitle_status_t NOT NULL,
|
||||||
rate int CHECK (rate > 0 AND rate <= 10),
|
rate int CHECK (rate > 0 AND rate <= 10),
|
||||||
review_id bigint REFERENCES reviews (id) ON DELETE SET NULL,
|
review_id bigint REFERENCES reviews (id) ON DELETE SET NULL,
|
||||||
ftime timestamptz NOT NULL DEFAULT now()
|
ctime timestamptz NOT NULL DEFAULT now()
|
||||||
-- // TODO: series status
|
-- // TODO: series status
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -106,13 +108,12 @@ CREATE TABLE signals (
|
||||||
|
|
||||||
CREATE TABLE external_services (
|
CREATE TABLE external_services (
|
||||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
name text UNIQUE NOT NULL,
|
name text UNIQUE NOT NULL
|
||||||
auth_token text
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE external_ids (
|
CREATE TABLE external_ids (
|
||||||
user_id bigint NOT NULL REFERENCES users (id),
|
user_id bigint NOT NULL REFERENCES users (id),
|
||||||
service_id bigint NOT NULL REFERENCES external_services (id),
|
service_id bigint REFERENCES external_services (id),
|
||||||
external_id text NOT NULL
|
external_id text NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -170,3 +171,16 @@ CREATE TRIGGER trg_notify_new_signal
|
||||||
AFTER INSERT ON signals
|
AFTER INSERT ON signals
|
||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
EXECUTE FUNCTION notify_new_signal();
|
EXECUTE FUNCTION notify_new_signal();
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION set_ctime()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.ctime = now();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER set_ctime_on_update
|
||||||
|
BEFORE UPDATE ON usertitles
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION set_ctime();
|
||||||
|
|
@ -188,14 +188,13 @@ func (ns NullUsertitleStatusT) Value() (driver.Value, error) {
|
||||||
|
|
||||||
type ExternalID struct {
|
type ExternalID struct {
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
ServiceID int64 `json:"service_id"`
|
ServiceID *int64 `json:"service_id"`
|
||||||
ExternalID string `json:"external_id"`
|
ExternalID string `json:"external_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExternalService struct {
|
type ExternalService struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
AuthToken *string `json:"auth_token"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Image struct {
|
type Image struct {
|
||||||
|
|
@ -284,5 +283,5 @@ type Usertitle struct {
|
||||||
Status UsertitleStatusT `json:"status"`
|
Status UsertitleStatusT `json:"status"`
|
||||||
Rate *int32 `json:"rate"`
|
Rate *int32 `json:"rate"`
|
||||||
ReviewID *int64 `json:"review_id"`
|
ReviewID *int64 `json:"review_id"`
|
||||||
Ftime time.Time `json:"ftime"`
|
Ctime time.Time `json:"ctime"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const createImage = `-- name: CreateImage :one
|
const createImage = `-- name: CreateImage :one
|
||||||
|
|
@ -54,7 +52,7 @@ const deleteUserTitle = `-- name: DeleteUserTitle :one
|
||||||
DELETE FROM usertitles
|
DELETE FROM usertitles
|
||||||
WHERE user_id = $1
|
WHERE user_id = $1
|
||||||
AND title_id = $2
|
AND title_id = $2
|
||||||
RETURNING user_id, title_id, status, rate, review_id, ftime
|
RETURNING user_id, title_id, status, rate, review_id, ctime
|
||||||
`
|
`
|
||||||
|
|
||||||
type DeleteUserTitleParams struct {
|
type DeleteUserTitleParams struct {
|
||||||
|
|
@ -71,24 +69,11 @@ func (q *Queries) DeleteUserTitle(ctx context.Context, arg DeleteUserTitleParams
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.Rate,
|
&i.Rate,
|
||||||
&i.ReviewID,
|
&i.ReviewID,
|
||||||
&i.Ftime,
|
&i.Ctime,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getExternalServiceByToken = `-- name: GetExternalServiceByToken :one
|
|
||||||
SELECT id, name, auth_token
|
|
||||||
FROM external_services
|
|
||||||
WHERE auth_token = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetExternalServiceByToken(ctx context.Context, authToken *string) (ExternalService, error) {
|
|
||||||
row := q.db.QueryRow(ctx, getExternalServiceByToken, authToken)
|
|
||||||
var i ExternalService
|
|
||||||
err := row.Scan(&i.ID, &i.Name, &i.AuthToken)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getImageByID = `-- name: GetImageByID :one
|
const getImageByID = `-- name: GetImageByID :one
|
||||||
SELECT id, storage_type, image_path
|
SELECT id, storage_type, image_path
|
||||||
FROM images
|
FROM images
|
||||||
|
|
@ -253,35 +238,6 @@ func (q *Queries) GetTitleTags(ctx context.Context, titleID int64) ([]json.RawMe
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUserByExternalServiceId = `-- name: GetUserByExternalServiceId :one
|
|
||||||
SELECT u.id, u.avatar_id, u.passhash, u.mail, u.nickname, u.disp_name, u.user_desc, u.creation_date, u.last_login
|
|
||||||
FROM users u
|
|
||||||
LEFT JOIN external_ids ei ON eu.user_id = u.id
|
|
||||||
WHERE ei.external_id = $1 AND ei.service_id = $2
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetUserByExternalServiceIdParams struct {
|
|
||||||
ExternalID string `json:"external_id"`
|
|
||||||
ServiceID int64 `json:"service_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetUserByExternalServiceId(ctx context.Context, arg GetUserByExternalServiceIdParams) (User, error) {
|
|
||||||
row := q.db.QueryRow(ctx, getUserByExternalServiceId, arg.ExternalID, arg.ServiceID)
|
|
||||||
var i User
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.AvatarID,
|
|
||||||
&i.Passhash,
|
|
||||||
&i.Mail,
|
|
||||||
&i.Nickname,
|
|
||||||
&i.DispName,
|
|
||||||
&i.UserDesc,
|
|
||||||
&i.CreationDate,
|
|
||||||
&i.LastLogin,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getUserByID = `-- name: GetUserByID :one
|
const getUserByID = `-- name: GetUserByID :one
|
||||||
SELECT
|
SELECT
|
||||||
t.id as id,
|
t.id as id,
|
||||||
|
|
@ -352,7 +308,7 @@ func (q *Queries) GetUserByNickname(ctx context.Context, nickname string) (User,
|
||||||
|
|
||||||
const getUserTitleByID = `-- name: GetUserTitleByID :one
|
const getUserTitleByID = `-- name: GetUserTitleByID :one
|
||||||
SELECT
|
SELECT
|
||||||
ut.user_id, ut.title_id, ut.status, ut.rate, ut.review_id, ut.ftime
|
ut.user_id, ut.title_id, ut.status, ut.rate, ut.review_id, ut.ctime
|
||||||
FROM usertitles as ut
|
FROM usertitles as ut
|
||||||
WHERE ut.title_id = $1::bigint AND ut.user_id = $2::bigint
|
WHERE ut.title_id = $1::bigint AND ut.user_id = $2::bigint
|
||||||
`
|
`
|
||||||
|
|
@ -371,7 +327,7 @@ func (q *Queries) GetUserTitleByID(ctx context.Context, arg GetUserTitleByIDPara
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.Rate,
|
&i.Rate,
|
||||||
&i.ReviewID,
|
&i.ReviewID,
|
||||||
&i.Ftime,
|
&i.Ctime,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -438,16 +394,15 @@ func (q *Queries) InsertTitleTags(ctx context.Context, arg InsertTitleTagsParams
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertUserTitle = `-- name: InsertUserTitle :one
|
const insertUserTitle = `-- name: InsertUserTitle :one
|
||||||
INSERT INTO usertitles (user_id, title_id, status, rate, review_id, ftime)
|
INSERT INTO usertitles (user_id, title_id, status, rate, review_id)
|
||||||
VALUES (
|
VALUES (
|
||||||
$1::bigint,
|
$1::bigint,
|
||||||
$2::bigint,
|
$2::bigint,
|
||||||
$3::usertitle_status_t,
|
$3::usertitle_status_t,
|
||||||
$4::int,
|
$4::int,
|
||||||
$5::bigint,
|
$5::bigint
|
||||||
$6::timestamptz
|
|
||||||
)
|
)
|
||||||
RETURNING user_id, title_id, status, rate, review_id, ftime
|
RETURNING user_id, title_id, status, rate, review_id, ctime
|
||||||
`
|
`
|
||||||
|
|
||||||
type InsertUserTitleParams struct {
|
type InsertUserTitleParams struct {
|
||||||
|
|
@ -456,7 +411,6 @@ type InsertUserTitleParams struct {
|
||||||
Status UsertitleStatusT `json:"status"`
|
Status UsertitleStatusT `json:"status"`
|
||||||
Rate *int32 `json:"rate"`
|
Rate *int32 `json:"rate"`
|
||||||
ReviewID *int64 `json:"review_id"`
|
ReviewID *int64 `json:"review_id"`
|
||||||
Ftime pgtype.Timestamptz `json:"ftime"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) InsertUserTitle(ctx context.Context, arg InsertUserTitleParams) (Usertitle, error) {
|
func (q *Queries) InsertUserTitle(ctx context.Context, arg InsertUserTitleParams) (Usertitle, error) {
|
||||||
|
|
@ -466,7 +420,6 @@ func (q *Queries) InsertUserTitle(ctx context.Context, arg InsertUserTitleParams
|
||||||
arg.Status,
|
arg.Status,
|
||||||
arg.Rate,
|
arg.Rate,
|
||||||
arg.ReviewID,
|
arg.ReviewID,
|
||||||
arg.Ftime,
|
|
||||||
)
|
)
|
||||||
var i Usertitle
|
var i Usertitle
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
|
|
@ -475,7 +428,7 @@ func (q *Queries) InsertUserTitle(ctx context.Context, arg InsertUserTitleParams
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.Rate,
|
&i.Rate,
|
||||||
&i.ReviewID,
|
&i.ReviewID,
|
||||||
&i.Ftime,
|
&i.Ctime,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -786,7 +739,7 @@ SELECT
|
||||||
u.status as usertitle_status,
|
u.status as usertitle_status,
|
||||||
u.rate as user_rate,
|
u.rate as user_rate,
|
||||||
u.review_id as review_id,
|
u.review_id as review_id,
|
||||||
u.ftime as user_ftime,
|
u.ctime as user_ctime,
|
||||||
i.storage_type as title_storage_type,
|
i.storage_type as title_storage_type,
|
||||||
i.image_path as title_image_path,
|
i.image_path as title_image_path,
|
||||||
COALESCE(
|
COALESCE(
|
||||||
|
|
@ -888,7 +841,7 @@ WHERE
|
||||||
AND ($13::release_season_t IS NULL OR t.release_season = $13::release_season_t)
|
AND ($13::release_season_t IS NULL OR t.release_season = $13::release_season_t)
|
||||||
|
|
||||||
GROUP BY
|
GROUP BY
|
||||||
t.id, u.user_id, u.status, u.rate, u.review_id, u.ftime, i.id, s.id
|
t.id, u.user_id, u.status, u.rate, u.review_id, u.ctime, i.id, s.id
|
||||||
|
|
||||||
ORDER BY
|
ORDER BY
|
||||||
CASE WHEN $2::boolean THEN
|
CASE WHEN $2::boolean THEN
|
||||||
|
|
@ -946,7 +899,7 @@ type SearchUserTitlesRow struct {
|
||||||
UsertitleStatus UsertitleStatusT `json:"usertitle_status"`
|
UsertitleStatus UsertitleStatusT `json:"usertitle_status"`
|
||||||
UserRate *int32 `json:"user_rate"`
|
UserRate *int32 `json:"user_rate"`
|
||||||
ReviewID *int64 `json:"review_id"`
|
ReviewID *int64 `json:"review_id"`
|
||||||
UserFtime time.Time `json:"user_ftime"`
|
UserCtime time.Time `json:"user_ctime"`
|
||||||
TitleStorageType *StorageTypeT `json:"title_storage_type"`
|
TitleStorageType *StorageTypeT `json:"title_storage_type"`
|
||||||
TitleImagePath *string `json:"title_image_path"`
|
TitleImagePath *string `json:"title_image_path"`
|
||||||
TagNames json.RawMessage `json:"tag_names"`
|
TagNames json.RawMessage `json:"tag_names"`
|
||||||
|
|
@ -994,7 +947,7 @@ func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesPara
|
||||||
&i.UsertitleStatus,
|
&i.UsertitleStatus,
|
||||||
&i.UserRate,
|
&i.UserRate,
|
||||||
&i.ReviewID,
|
&i.ReviewID,
|
||||||
&i.UserFtime,
|
&i.UserCtime,
|
||||||
&i.TitleStorageType,
|
&i.TitleStorageType,
|
||||||
&i.TitleImagePath,
|
&i.TitleImagePath,
|
||||||
&i.TagNames,
|
&i.TagNames,
|
||||||
|
|
@ -1064,18 +1017,16 @@ const updateUserTitle = `-- name: UpdateUserTitle :one
|
||||||
UPDATE usertitles
|
UPDATE usertitles
|
||||||
SET
|
SET
|
||||||
status = COALESCE($1::usertitle_status_t, status),
|
status = COALESCE($1::usertitle_status_t, status),
|
||||||
rate = COALESCE($2::int, rate),
|
rate = COALESCE($2::int, rate)
|
||||||
ftime = COALESCE($3::timestamptz, ftime)
|
|
||||||
WHERE
|
WHERE
|
||||||
user_id = $4
|
user_id = $3
|
||||||
AND title_id = $5
|
AND title_id = $4
|
||||||
RETURNING user_id, title_id, status, rate, review_id, ftime
|
RETURNING user_id, title_id, status, rate, review_id, ctime
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateUserTitleParams struct {
|
type UpdateUserTitleParams struct {
|
||||||
Status *UsertitleStatusT `json:"status"`
|
Status *UsertitleStatusT `json:"status"`
|
||||||
Rate *int32 `json:"rate"`
|
Rate *int32 `json:"rate"`
|
||||||
Ftime pgtype.Timestamptz `json:"ftime"`
|
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
TitleID int64 `json:"title_id"`
|
TitleID int64 `json:"title_id"`
|
||||||
}
|
}
|
||||||
|
|
@ -1085,7 +1036,6 @@ func (q *Queries) UpdateUserTitle(ctx context.Context, arg UpdateUserTitleParams
|
||||||
row := q.db.QueryRow(ctx, updateUserTitle,
|
row := q.db.QueryRow(ctx, updateUserTitle,
|
||||||
arg.Status,
|
arg.Status,
|
||||||
arg.Rate,
|
arg.Rate,
|
||||||
arg.Ftime,
|
|
||||||
arg.UserID,
|
arg.UserID,
|
||||||
arg.TitleID,
|
arg.TitleID,
|
||||||
)
|
)
|
||||||
|
|
@ -1096,7 +1046,7 @@ func (q *Queries) UpdateUserTitle(ctx context.Context, arg UpdateUserTitleParams
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.Rate,
|
&i.Rate,
|
||||||
&i.ReviewID,
|
&i.ReviewID,
|
||||||
&i.Ftime,
|
&i.Ctime,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue