Compare commits
23 commits
dc4c430231
...
3bbd2c2818
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bbd2c2818 | ||
| 7956a8a961 | |||
| 6955216568 | |||
| e67c9a77ce | |||
| 00894f4526 | |||
| 54c45ac3bc | |||
| 8bd515c33f | |||
| 003a477f9e | |||
| afb1db17bd | |||
| 5acc53ec9d | |||
| 184868b142 | |||
| 3afd4e2e86 | |||
| e67f0d7e5a | |||
| 7623adf2a7 | |||
| 90d7de51f3 | |||
| 9cb3f94e27 | |||
| 6d14ac365b | |||
| 57956f1f6e | |||
| 62e0633e69 | |||
| 6a5994e33e | |||
| fe18c0d865 | |||
| 40e341c05a | |||
| 169bb482ce |
30 changed files with 1247 additions and 370 deletions
|
|
@ -122,6 +122,53 @@ paths:
|
||||||
description: Unknown server error
|
description: Unknown server error
|
||||||
security:
|
security:
|
||||||
- JwtAuthCookies: []
|
- JwtAuthCookies: []
|
||||||
|
/users/:
|
||||||
|
get:
|
||||||
|
summary: 'Search user by nickname or dispname (both in one param), response is always sorted by id'
|
||||||
|
parameters:
|
||||||
|
- name: word
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: limit
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
default: 10
|
||||||
|
- name: cursor_id
|
||||||
|
in: query
|
||||||
|
description: pass cursor naked
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
default: 1
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: List of users with cursor
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
description: List of users
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/User'
|
||||||
|
cursor:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
default: 1
|
||||||
|
required:
|
||||||
|
- data
|
||||||
|
- cursor
|
||||||
|
'204':
|
||||||
|
description: No users found
|
||||||
|
'400':
|
||||||
|
description: Request params are not correct
|
||||||
|
'500':
|
||||||
|
description: Unknown server error
|
||||||
'/users/{user_id}':
|
'/users/{user_id}':
|
||||||
get:
|
get:
|
||||||
operationId: getUsersId
|
operationId: getUsersId
|
||||||
|
|
@ -348,6 +395,9 @@ 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
|
||||||
|
|
@ -431,6 +481,9 @@ 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
|
||||||
|
|
@ -480,6 +533,42 @@ 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:
|
||||||
|
|
@ -593,6 +682,11 @@ 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:
|
||||||
|
|
|
||||||
249
api/api.gen.go
249
api/api.gen.go
|
|
@ -7,7 +7,10 @@ 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"
|
||||||
|
|
@ -113,6 +116,9 @@ 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"`
|
||||||
|
|
||||||
|
|
@ -178,6 +184,9 @@ 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"`
|
||||||
|
|
@ -201,6 +210,15 @@ type GetTitleParams struct {
|
||||||
Fields *string `form:"fields,omitempty" json:"fields,omitempty"`
|
Fields *string `form:"fields,omitempty" json:"fields,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUsersParams defines parameters for GetUsers.
|
||||||
|
type GetUsersParams struct {
|
||||||
|
Word *string `form:"word,omitempty" json:"word,omitempty"`
|
||||||
|
Limit *int32 `form:"limit,omitempty" json:"limit,omitempty"`
|
||||||
|
|
||||||
|
// CursorId pass cursor naked
|
||||||
|
CursorId *int32 `form:"cursor_id,omitempty" json:"cursor_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// GetUsersIdParams defines parameters for GetUsersId.
|
// GetUsersIdParams defines parameters for GetUsersId.
|
||||||
type GetUsersIdParams struct {
|
type GetUsersIdParams struct {
|
||||||
Fields *string `form:"fields,omitempty" json:"fields,omitempty"`
|
Fields *string `form:"fields,omitempty" json:"fields,omitempty"`
|
||||||
|
|
@ -244,7 +262,8 @@ type GetUserTitlesParams struct {
|
||||||
|
|
||||||
// AddUserTitleJSONBody defines parameters for AddUserTitle.
|
// AddUserTitleJSONBody defines parameters for AddUserTitle.
|
||||||
type AddUserTitleJSONBody struct {
|
type AddUserTitleJSONBody struct {
|
||||||
Rate *int32 `json:"rate,omitempty"`
|
Ftime *time.Time `json:"ftime,omitempty"`
|
||||||
|
Rate *int32 `json:"rate,omitempty"`
|
||||||
|
|
||||||
// Status User's title status
|
// Status User's title status
|
||||||
Status UserTitleStatus `json:"status"`
|
Status UserTitleStatus `json:"status"`
|
||||||
|
|
@ -253,12 +272,16 @@ type AddUserTitleJSONBody struct {
|
||||||
|
|
||||||
// UpdateUserTitleJSONBody defines parameters for UpdateUserTitle.
|
// UpdateUserTitleJSONBody defines parameters for UpdateUserTitle.
|
||||||
type UpdateUserTitleJSONBody struct {
|
type UpdateUserTitleJSONBody struct {
|
||||||
Rate *int32 `json:"rate,omitempty"`
|
Ftime *time.Time `json:"ftime,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
|
||||||
|
|
||||||
|
|
@ -270,12 +293,18 @@ 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)
|
||||||
// Get title description
|
// Get title description
|
||||||
// (GET /titles/{title_id})
|
// (GET /titles/{title_id})
|
||||||
GetTitle(c *gin.Context, titleId int64, params GetTitleParams)
|
GetTitle(c *gin.Context, titleId int64, params GetTitleParams)
|
||||||
|
// Search user by nickname or dispname (both in one param), response is always sorted by id
|
||||||
|
// (GET /users/)
|
||||||
|
GetUsers(c *gin.Context, params GetUsersParams)
|
||||||
// Get user info
|
// Get user info
|
||||||
// (GET /users/{user_id})
|
// (GET /users/{user_id})
|
||||||
GetUsersId(c *gin.Context, userId string, params GetUsersIdParams)
|
GetUsersId(c *gin.Context, userId string, params GetUsersIdParams)
|
||||||
|
|
@ -308,6 +337,19 @@ 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) {
|
||||||
|
|
||||||
|
|
@ -459,6 +501,48 @@ func (siw *ServerInterfaceWrapper) GetTitle(c *gin.Context) {
|
||||||
siw.Handler.GetTitle(c, titleId, params)
|
siw.Handler.GetTitle(c, titleId, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUsers operation middleware
|
||||||
|
func (siw *ServerInterfaceWrapper) GetUsers(c *gin.Context) {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Parameter object where we will unmarshal all parameters from the context
|
||||||
|
var params GetUsersParams
|
||||||
|
|
||||||
|
// ------------- Optional query parameter "word" -------------
|
||||||
|
|
||||||
|
err = runtime.BindQueryParameter("form", true, false, "word", c.Request.URL.Query(), ¶ms.Word)
|
||||||
|
if err != nil {
|
||||||
|
siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter word: %w", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------- Optional query parameter "limit" -------------
|
||||||
|
|
||||||
|
err = runtime.BindQueryParameter("form", true, false, "limit", c.Request.URL.Query(), ¶ms.Limit)
|
||||||
|
if err != nil {
|
||||||
|
siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter limit: %w", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------- Optional query parameter "cursor_id" -------------
|
||||||
|
|
||||||
|
err = runtime.BindQueryParameter("form", true, false, "cursor_id", c.Request.URL.Query(), ¶ms.CursorId)
|
||||||
|
if err != nil {
|
||||||
|
siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter cursor_id: %w", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, middleware := range siw.HandlerMiddlewares {
|
||||||
|
middleware(c)
|
||||||
|
if c.IsAborted() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
siw.Handler.GetUsers(c, params)
|
||||||
|
}
|
||||||
|
|
||||||
// GetUsersId operation middleware
|
// GetUsersId operation middleware
|
||||||
func (siw *ServerInterfaceWrapper) GetUsersId(c *gin.Context) {
|
func (siw *ServerInterfaceWrapper) GetUsersId(c *gin.Context) {
|
||||||
|
|
||||||
|
|
@ -797,8 +881,10 @@ 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/:user_id", wrapper.GetUsersId)
|
router.GET(options.BaseURL+"/users/:user_id", wrapper.GetUsersId)
|
||||||
router.PATCH(options.BaseURL+"/users/:user_id", wrapper.UpdateUser)
|
router.PATCH(options.BaseURL+"/users/:user_id", wrapper.UpdateUser)
|
||||||
router.GET(options.BaseURL+"/users/:user_id/titles", wrapper.GetUserTitles)
|
router.GET(options.BaseURL+"/users/:user_id/titles", wrapper.GetUserTitles)
|
||||||
|
|
@ -808,6 +894,49 @@ 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
|
||||||
}
|
}
|
||||||
|
|
@ -904,6 +1033,52 @@ func (response GetTitle500Response) VisitGetTitleResponse(w http.ResponseWriter)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GetUsersRequestObject struct {
|
||||||
|
Params GetUsersParams
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetUsersResponseObject interface {
|
||||||
|
VisitGetUsersResponse(w http.ResponseWriter) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetUsers200JSONResponse struct {
|
||||||
|
Cursor int64 `json:"cursor"`
|
||||||
|
|
||||||
|
// Data List of users
|
||||||
|
Data []User `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (response GetUsers200JSONResponse) VisitGetUsersResponse(w http.ResponseWriter) error {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
|
||||||
|
return json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetUsers204Response struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (response GetUsers204Response) VisitGetUsersResponse(w http.ResponseWriter) error {
|
||||||
|
w.WriteHeader(204)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetUsers400Response struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (response GetUsers400Response) VisitGetUsersResponse(w http.ResponseWriter) error {
|
||||||
|
w.WriteHeader(400)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetUsers500Response struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (response GetUsers500Response) VisitGetUsersResponse(w http.ResponseWriter) error {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type GetUsersIdRequestObject struct {
|
type GetUsersIdRequestObject struct {
|
||||||
UserId string `json:"user_id"`
|
UserId string `json:"user_id"`
|
||||||
Params GetUsersIdParams
|
Params GetUsersIdParams
|
||||||
|
|
@ -1299,12 +1474,18 @@ 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)
|
||||||
// Get title description
|
// Get title description
|
||||||
// (GET /titles/{title_id})
|
// (GET /titles/{title_id})
|
||||||
GetTitle(ctx context.Context, request GetTitleRequestObject) (GetTitleResponseObject, error)
|
GetTitle(ctx context.Context, request GetTitleRequestObject) (GetTitleResponseObject, error)
|
||||||
|
// Search user by nickname or dispname (both in one param), response is always sorted by id
|
||||||
|
// (GET /users/)
|
||||||
|
GetUsers(ctx context.Context, request GetUsersRequestObject) (GetUsersResponseObject, error)
|
||||||
// Get user info
|
// Get user info
|
||||||
// (GET /users/{user_id})
|
// (GET /users/{user_id})
|
||||||
GetUsersId(ctx context.Context, request GetUsersIdRequestObject) (GetUsersIdResponseObject, error)
|
GetUsersId(ctx context.Context, request GetUsersIdRequestObject) (GetUsersIdResponseObject, error)
|
||||||
|
|
@ -1340,6 +1521,43 @@ 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
|
||||||
|
|
@ -1395,6 +1613,33 @@ func (sh *strictHandler) GetTitle(ctx *gin.Context, titleId int64, params GetTit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUsers operation middleware
|
||||||
|
func (sh *strictHandler) GetUsers(ctx *gin.Context, params GetUsersParams) {
|
||||||
|
var request GetUsersRequestObject
|
||||||
|
|
||||||
|
request.Params = params
|
||||||
|
|
||||||
|
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
|
||||||
|
return sh.ssi.GetUsers(ctx, request.(GetUsersRequestObject))
|
||||||
|
}
|
||||||
|
for _, middleware := range sh.middlewares {
|
||||||
|
handler = middleware(handler, "GetUsers")
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := handler(ctx, request)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
ctx.Status(http.StatusInternalServerError)
|
||||||
|
} else if validResponse, ok := response.(GetUsersResponseObject); ok {
|
||||||
|
if err := validResponse.VisitGetUsersResponse(ctx.Writer); err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
}
|
||||||
|
} else if response != nil {
|
||||||
|
ctx.Error(fmt.Errorf("unexpected response type: %T", response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GetUsersId operation middleware
|
// GetUsersId operation middleware
|
||||||
func (sh *strictHandler) GetUsersId(ctx *gin.Context, userId string, params GetUsersIdParams) {
|
func (sh *strictHandler) GetUsersId(ctx *gin.Context, userId string, params GetUsersIdParams) {
|
||||||
var request GetUsersIdRequestObject
|
var request GetUsersIdRequestObject
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,17 @@ paths:
|
||||||
$ref: "./paths/titles.yaml"
|
$ref: "./paths/titles.yaml"
|
||||||
/titles/{title_id}:
|
/titles/{title_id}:
|
||||||
$ref: "./paths/titles-id.yaml"
|
$ref: "./paths/titles-id.yaml"
|
||||||
|
/users/:
|
||||||
|
$ref: "./paths/users.yaml"
|
||||||
/users/{user_id}:
|
/users/{user_id}:
|
||||||
$ref: "./paths/users-id.yaml"
|
$ref: "./paths/users-id.yaml"
|
||||||
/users/{user_id}/titles:
|
/users/{user_id}/titles:
|
||||||
$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:
|
||||||
$ref: "./parameters/_index.yaml"
|
$ref: "./parameters/_index.yaml"
|
||||||
|
|
|
||||||
37
api/paths/media_upload.yaml
Normal file
37
api/paths/media_upload.yaml
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
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,6 +61,9 @@ 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,6 +122,9 @@ 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
|
||||||
|
|
|
||||||
46
api/paths/users.yaml
Normal file
46
api/paths/users.yaml
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
get:
|
||||||
|
summary: Search user by nickname or dispname (both in one param), response is always sorted by id
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: word
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- in: query
|
||||||
|
name: limit
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
default: 10
|
||||||
|
- in: query
|
||||||
|
name: cursor_id
|
||||||
|
description: pass cursor naked
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
default: 1
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: List of users with cursor
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '../schemas/User.yaml'
|
||||||
|
description: List of users
|
||||||
|
cursor:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
default: 1
|
||||||
|
required:
|
||||||
|
- data
|
||||||
|
- cursor
|
||||||
|
'204':
|
||||||
|
description: No users found
|
||||||
|
'400':
|
||||||
|
description: Request params are not correct
|
||||||
|
'500':
|
||||||
|
description: Unknown server error
|
||||||
|
|
@ -30,6 +30,11 @@ 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:
|
||||||
|
|
|
||||||
115
auth/auth.gen.go
115
auth/auth.gen.go
|
|
@ -13,6 +13,23 @@ 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"`
|
||||||
|
|
@ -25,6 +42,9 @@ 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
|
||||||
|
|
||||||
|
|
@ -33,6 +53,9 @@ 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)
|
||||||
// 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)
|
||||||
|
|
@ -50,6 +73,21 @@ 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)
|
||||||
|
}
|
||||||
|
|
||||||
// PostSignIn operation middleware
|
// PostSignIn operation middleware
|
||||||
func (siw *ServerInterfaceWrapper) PostSignIn(c *gin.Context) {
|
func (siw *ServerInterfaceWrapper) PostSignIn(c *gin.Context) {
|
||||||
|
|
||||||
|
|
@ -103,10 +141,41 @@ 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+"/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 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 PostSignInRequestObject struct {
|
type PostSignInRequestObject struct {
|
||||||
Body *PostSignInJSONRequestBody
|
Body *PostSignInJSONRequestBody
|
||||||
}
|
}
|
||||||
|
|
@ -127,15 +196,11 @@ func (response PostSignIn200JSONResponse) VisitPostSignInResponse(w http.Respons
|
||||||
return json.NewEncoder(w).Encode(response)
|
return json.NewEncoder(w).Encode(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostSignIn401JSONResponse struct {
|
type PostSignIn401Response = UnauthorizedErrorResponse
|
||||||
Error *string `json:"error,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (response PostSignIn401JSONResponse) VisitPostSignInResponse(w http.ResponseWriter) error {
|
func (response PostSignIn401Response) 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 {
|
||||||
|
|
@ -159,6 +224,9 @@ func (response PostSignUp200JSONResponse) VisitPostSignUpResponse(w http.Respons
|
||||||
|
|
||||||
// StrictServerInterface represents all server handlers.
|
// StrictServerInterface represents all server handlers.
|
||||||
type StrictServerInterface interface {
|
type StrictServerInterface interface {
|
||||||
|
// Get service impersontaion token
|
||||||
|
// (POST /get-impersonation-token)
|
||||||
|
GetImpersonationToken(ctx context.Context, request GetImpersonationTokenRequestObject) (GetImpersonationTokenResponseObject, error)
|
||||||
// Sign in a user and return JWT
|
// Sign in a user and return JWT
|
||||||
// (POST /sign-in)
|
// (POST /sign-in)
|
||||||
PostSignIn(ctx context.Context, request PostSignInRequestObject) (PostSignInResponseObject, error)
|
PostSignIn(ctx context.Context, request PostSignInRequestObject) (PostSignInResponseObject, error)
|
||||||
|
|
@ -179,6 +247,39 @@ 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ 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
|
||||||
|
|
@ -41,6 +42,7 @@ 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
|
||||||
|
|
@ -73,88 +75,52 @@ paths:
|
||||||
user_name:
|
user_name:
|
||||||
type: string
|
type: string
|
||||||
"401":
|
"401":
|
||||||
description: Access denied due to invalid credentials
|
$ref: '#/components/responses/UnauthorizedError'
|
||||||
|
|
||||||
|
/get-impersonation-token:
|
||||||
|
post:
|
||||||
|
summary: Get service impersontaion token
|
||||||
|
operationId: getImpersonationToken
|
||||||
|
tags: [Auth]
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
user_id:
|
||||||
|
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:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
|
required:
|
||||||
|
- access_token
|
||||||
properties:
|
properties:
|
||||||
error:
|
access_token:
|
||||||
type: string
|
type: string
|
||||||
example: "Access denied"
|
description: JWT access token
|
||||||
# /auth/verify-token:
|
"401":
|
||||||
# post:
|
$ref: '#/components/responses/UnauthorizedError'
|
||||||
# summary: Verify JWT validity
|
|
||||||
# tags: [Auth]
|
components:
|
||||||
# requestBody:
|
securitySchemes:
|
||||||
# required: true
|
bearerAuth:
|
||||||
# content:
|
type: http
|
||||||
# application/json:
|
scheme: bearer
|
||||||
# schema:
|
responses:
|
||||||
# type: object
|
UnauthorizedError:
|
||||||
# required: [token]
|
description: Access token is missing or invalid
|
||||||
# properties:
|
|
||||||
# token:
|
|
||||||
# type: string
|
|
||||||
# description: JWT token to validate
|
|
||||||
# responses:
|
|
||||||
# "200":
|
|
||||||
# description: Token validation result
|
|
||||||
# content:
|
|
||||||
# application/json:
|
|
||||||
# schema:
|
|
||||||
# type: object
|
|
||||||
# properties:
|
|
||||||
# valid:
|
|
||||||
# type: boolean
|
|
||||||
# description: True if token is valid
|
|
||||||
# user_id:
|
|
||||||
# type: string
|
|
||||||
# nullable: true
|
|
||||||
# description: User ID extracted from token if valid
|
|
||||||
# error:
|
|
||||||
# type: string
|
|
||||||
# nullable: true
|
|
||||||
# description: Error message if token is invalid
|
|
||||||
# /auth/refresh-token:
|
|
||||||
# post:
|
|
||||||
# summary: Refresh JWT using a refresh token
|
|
||||||
# tags: [Auth]
|
|
||||||
# requestBody:
|
|
||||||
# required: true
|
|
||||||
# content:
|
|
||||||
# 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,6 +40,22 @@ 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
|
||||||
|
|
@ -51,8 +67,8 @@ services:
|
||||||
RABBITMQ_URL: ${RABBITMQ_URL}
|
RABBITMQ_URL: ${RABBITMQ_URL}
|
||||||
JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY}
|
JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY}
|
||||||
AUTH_ENABLED: ${AUTH_ENABLED}
|
AUTH_ENABLED: ${AUTH_ENABLED}
|
||||||
ports:
|
# ports:
|
||||||
- "8080:8080"
|
# - "8080:8080"
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres
|
- postgres
|
||||||
- rabbitmq
|
- rabbitmq
|
||||||
|
|
@ -68,8 +84,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:
|
||||||
|
|
@ -89,6 +105,7 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
rabbitmq_data:
|
rabbitmq_data:
|
||||||
|
redis_data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
nyanimedb-network:
|
nyanimedb-network:
|
||||||
|
|
|
||||||
16
go.mod
16
go.mod
|
|
@ -18,6 +18,7 @@ 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
|
||||||
|
|
@ -42,12 +43,13 @@ 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.40.0 // indirect
|
golang.org/x/crypto v0.43.0 // indirect
|
||||||
golang.org/x/mod v0.25.0 // indirect
|
golang.org/x/image v0.33.0 // indirect
|
||||||
golang.org/x/net v0.42.0 // indirect
|
golang.org/x/mod v0.29.0 // indirect
|
||||||
golang.org/x/sync v0.16.0 // indirect
|
golang.org/x/net v0.46.0 // indirect
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
golang.org/x/sync v0.18.0 // indirect
|
||||||
golang.org/x/text v0.27.0 // indirect
|
golang.org/x/sys v0.37.0 // indirect
|
||||||
golang.org/x/tools v0.34.0 // indirect
|
golang.org/x/text v0.31.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,6 +13,8 @@ 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=
|
||||||
|
|
@ -103,10 +105,18 @@ 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=
|
||||||
|
|
@ -114,11 +124,15 @@ 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=
|
||||||
|
|
@ -131,6 +145,8 @@ 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=
|
||||||
|
|
@ -144,12 +160,16 @@ 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=
|
||||||
|
|
|
||||||
|
|
@ -47,10 +47,28 @@ func CheckPassword(password, hash string) (bool, error) {
|
||||||
return argon2id.ComparePasswordAndHash(password, hash)
|
return argon2id.ComparePasswordAndHash(password, hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s Server) generateImpersonationToken(userID string, impersonated_by string) (accessToken string, err error) {
|
||||||
|
accessClaims := jwt.MapClaims{
|
||||||
|
"user_id": userID,
|
||||||
|
"exp": time.Now().Add(15 * time.Minute).Unix(),
|
||||||
|
"imp_id": impersonated_by,
|
||||||
|
}
|
||||||
|
|
||||||
|
at := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
|
||||||
|
|
||||||
|
accessToken, err = at.SignedString([]byte(s.JwtPrivateKey))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s Server) generateTokens(userID string) (accessToken string, refreshToken string, csrfToken string, err error) {
|
func (s Server) generateTokens(userID string) (accessToken string, refreshToken string, csrfToken string, err error) {
|
||||||
accessClaims := jwt.MapClaims{
|
accessClaims := jwt.MapClaims{
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
"exp": time.Now().Add(15 * time.Minute).Unix(),
|
"exp": time.Now().Add(15 * time.Minute).Unix(),
|
||||||
|
//TODO: add created_at
|
||||||
}
|
}
|
||||||
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))
|
||||||
|
|
@ -119,10 +137,7 @@ func (s Server) PostSignIn(ctx context.Context, req auth.PostSignInRequestObject
|
||||||
// TODO: return 500
|
// TODO: return 500
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
err_msg := "invalid credentials"
|
return auth.PostSignIn401Response{}, nil
|
||||||
return auth.PostSignIn401JSONResponse{
|
|
||||||
Error: &err_msg,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
accessToken, refreshToken, csrfToken, err := s.generateTokens(req.Body.Nickname)
|
accessToken, refreshToken, csrfToken, err := s.generateTokens(req.Body.Nickname)
|
||||||
|
|
@ -144,47 +159,65 @@ func (s Server) PostSignIn(ctx context.Context, req auth.PostSignInRequestObject
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// func (s Server) PostAuthVerifyToken(ctx context.Context, req auth.PostAuthVerifyTokenRequestObject) (auth.PostAuthVerifyTokenResponseObject, error) {
|
func (s Server) GetImpersonationToken(ctx context.Context, req auth.GetImpersonationTokenRequestObject) (auth.GetImpersonationTokenResponseObject, error) {
|
||||||
// valid := false
|
ginCtx, ok := ctx.Value(gin.ContextKey).(*gin.Context)
|
||||||
// var userID *string
|
if !ok {
|
||||||
// var errStr *string
|
log.Print("failed to get gin context")
|
||||||
|
// TODO: change to 500
|
||||||
|
return auth.GetImpersonationToken200JSONResponse{}, fmt.Errorf("failed to get gin.Context from context.Context")
|
||||||
|
}
|
||||||
|
|
||||||
// token, err := jwt.Parse(req.Body.Token, func(t *jwt.Token) (interface{}, error) {
|
token, err := ExtractBearerToken(ginCtx.Request.Header.Get("Authorization"))
|
||||||
// if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
if err != nil {
|
||||||
// return nil, fmt.Errorf("unexpected signing method")
|
// TODO: return 500
|
||||||
// }
|
log.Errorf("failed to extract bearer token: %v", err)
|
||||||
// return accessSecret, nil
|
return auth.GetImpersonationToken401Response{}, err
|
||||||
// })
|
}
|
||||||
|
log.Printf("got auth token: %s", token)
|
||||||
|
|
||||||
// if err != nil {
|
ext_service, err := s.db.GetExternalServiceByToken(context.Background(), &token)
|
||||||
// e := err.Error()
|
if err != nil {
|
||||||
// errStr = &e
|
log.Errorf("failed to get external service by token: %v", err)
|
||||||
// return auth.PostAuthVerifyToken200JSONResponse{
|
return auth.GetImpersonationToken401Response{}, err
|
||||||
// Valid: &valid,
|
// TODO: check err and retyrn 400/500
|
||||||
// UserId: userID,
|
}
|
||||||
// Error: errStr,
|
|
||||||
// }, nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
var user_id string = ""
|
||||||
// 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
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return auth.PostAuthVerifyToken200JSONResponse{
|
if req.Body.ExternalId != nil {
|
||||||
// Valid: &valid,
|
user, err := s.db.GetUserByExternalServiceId(context.Background(), sqlc.GetUserByExternalServiceIdParams{
|
||||||
// UserId: userID,
|
ExternalID: fmt.Sprintf("%d", *req.Body.ExternalId),
|
||||||
// Error: errStr,
|
ServiceID: ext_service.ID,
|
||||||
// }, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Body.UserId != nil {
|
||||||
|
// TODO: check user existence
|
||||||
|
if user_id != "" && user_id != fmt.Sprintf("%d", *req.Body.UserId) {
|
||||||
|
log.Error("user_id and external_d are incorrect")
|
||||||
|
// TODO: 405
|
||||||
|
return auth.GetImpersonationToken401Response{}, nil
|
||||||
|
} else {
|
||||||
|
user_id = fmt.Sprintf("%d", *req.Body.UserId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
accessToken, err := s.generateImpersonationToken(user_id, fmt.Sprintf("%d", ext_service.ID))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to generate impersonation token: %v", err)
|
||||||
|
return auth.GetImpersonationToken401Response{}, err
|
||||||
|
// TODO: check err and retyrn 400/500
|
||||||
|
}
|
||||||
|
|
||||||
|
return auth.GetImpersonationToken200JSONResponse{AccessToken: accessToken}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// func (s Server) PostAuthRefreshToken(ctx context.Context, req auth.PostAuthRefreshTokenRequestObject) (auth.PostAuthRefreshTokenResponseObject, error) {
|
// func (s Server) PostAuthRefreshToken(ctx context.Context, req auth.PostAuthRefreshTokenRequestObject) (auth.PostAuthRefreshTokenResponseObject, error) {
|
||||||
// valid := false
|
// valid := false
|
||||||
|
|
@ -236,3 +269,11 @@ func (s Server) PostSignIn(ctx context.Context, req auth.PostSignInRequestObject
|
||||||
// Error: errStr,
|
// Error: errStr,
|
||||||
// }, nil
|
// }, 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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,13 @@ 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');
|
||||||
|
|
@ -9,24 +9,24 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
// type Handler struct {
|
||||||
publisher *rmq.Publisher
|
// publisher *rmq.Publisher
|
||||||
}
|
// }
|
||||||
|
|
||||||
func New(publisher *rmq.Publisher) *Handler {
|
// func New(publisher *rmq.Publisher) *Handler {
|
||||||
return &Handler{publisher: publisher}
|
// return &Handler{publisher: publisher}
|
||||||
}
|
// }
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
db *sqlc.Queries
|
db *sqlc.Queries
|
||||||
publisher *rmq.Publisher
|
// publisher *rmq.Publisher
|
||||||
RPCclient *rmq.RPCClient
|
RPCclient *rmq.RPCClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(db *sqlc.Queries, publisher *rmq.Publisher, rpcclient *rmq.RPCClient) *Server {
|
func NewServer(db *sqlc.Queries, rpcclient *rmq.RPCClient) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
db: db,
|
db: db,
|
||||||
publisher: publisher,
|
// publisher: publisher,
|
||||||
RPCclient: rpcclient,
|
RPCclient: rpcclient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -73,6 +73,14 @@ 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)
|
||||||
|
|
|
||||||
141
modules/backend/handlers/images.go
Normal file
141
modules/backend/handlers/images.go
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -197,7 +197,6 @@ func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObje
|
||||||
// Делаем RPC-вызов — и ЖДЁМ ответа
|
// Делаем RPC-вызов — и ЖДЁМ ответа
|
||||||
err := s.RPCclient.Call(
|
err := s.RPCclient.Call(
|
||||||
ctx,
|
ctx,
|
||||||
"svc.media.process.requests", // ← очередь микросервиса
|
|
||||||
mqreq,
|
mqreq,
|
||||||
&reply,
|
&reply,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,16 @@ 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{Valid: false}
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
|
@ -365,6 +375,7 @@ 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)
|
||||||
|
|
@ -428,6 +439,7 @@ 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)
|
||||||
|
|
@ -485,3 +497,39 @@ func (s Server) GetUserTitle(ctx context.Context, request oapi.GetUserTitleReque
|
||||||
|
|
||||||
return oapi.GetUserTitle200JSONResponse(oapi_usertitle), nil
|
return oapi.GetUserTitle200JSONResponse(oapi_usertitle), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUsers implements oapi.StrictServerInterface.
|
||||||
|
func (s *Server) GetUsers(ctx context.Context, request oapi.GetUsersRequestObject) (oapi.GetUsersResponseObject, error) {
|
||||||
|
params := sqlc.SearchUserParams{
|
||||||
|
Word: request.Params.Word,
|
||||||
|
Cursor: request.Params.CursorId,
|
||||||
|
Limit: request.Params.Limit,
|
||||||
|
}
|
||||||
|
_users, err := s.db.SearchUser(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("%v", err)
|
||||||
|
return oapi.GetUsers500Response{}, nil
|
||||||
|
}
|
||||||
|
if len(_users) == 0 {
|
||||||
|
return oapi.GetUsers204Response{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var users []oapi.User
|
||||||
|
var cursor int64
|
||||||
|
for _, user := range _users {
|
||||||
|
oapi_user := oapi.User{ // maybe its possible to make one sqlc type and use one map func iinstead of this shit
|
||||||
|
// add image
|
||||||
|
CreationDate: &user.CreationDate,
|
||||||
|
DispName: user.DispName,
|
||||||
|
Id: &user.ID,
|
||||||
|
Mail: StringToEmail(user.Mail),
|
||||||
|
Nickname: user.Nickname,
|
||||||
|
UserDesc: user.UserDesc,
|
||||||
|
}
|
||||||
|
users = append(users, oapi_user)
|
||||||
|
|
||||||
|
cursor = user.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
return oapi.GetUsers200JSONResponse{Data: users, Cursor: cursor}, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,10 +59,9 @@ func main() {
|
||||||
}
|
}
|
||||||
defer rmqConn.Close()
|
defer rmqConn.Close()
|
||||||
|
|
||||||
publisher := rmq.NewPublisher(rmqConn)
|
|
||||||
rpcClient := rmq.NewRPCClient(rmqConn, 30*time.Second)
|
rpcClient := rmq.NewRPCClient(rmqConn, 30*time.Second)
|
||||||
|
|
||||||
server := handlers.NewServer(queries, publisher, rpcClient)
|
server := handlers.NewServer(queries, rpcClient)
|
||||||
|
|
||||||
r.Use(cors.New(cors.Config{
|
r.Use(cors.New(cors.Config{
|
||||||
AllowOrigins: []string{AppConfig.ServiceAddress},
|
AllowOrigins: []string{AppConfig.ServiceAddress},
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,37 @@ FROM users as t
|
||||||
LEFT JOIN images as i ON (t.avatar_id = i.id)
|
LEFT JOIN images as i ON (t.avatar_id = i.id)
|
||||||
WHERE t.id = sqlc.arg('id')::bigint;
|
WHERE t.id = sqlc.arg('id')::bigint;
|
||||||
|
|
||||||
|
-- name: SearchUser :many
|
||||||
|
SELECT
|
||||||
|
u.id AS id,
|
||||||
|
u.avatar_id AS avatar_id,
|
||||||
|
u.mail AS mail,
|
||||||
|
u.nickname AS nickname,
|
||||||
|
u.disp_name AS disp_name,
|
||||||
|
u.user_desc AS user_desc,
|
||||||
|
u.creation_date AS creation_date,
|
||||||
|
i.storage_type AS storage_type,
|
||||||
|
i.image_path AS image_path
|
||||||
|
FROM users AS u
|
||||||
|
LEFT JOIN images AS i ON u.avatar_id = i.id
|
||||||
|
WHERE
|
||||||
|
(
|
||||||
|
sqlc.narg('word')::text IS NULL
|
||||||
|
OR (
|
||||||
|
SELECT bool_and(
|
||||||
|
u.nickname ILIKE ('%' || term || '%')
|
||||||
|
OR u.disp_name ILIKE ('%' || term || '%')
|
||||||
|
)
|
||||||
|
FROM unnest(string_to_array(trim(sqlc.narg('word')::text), ' ')) AS term
|
||||||
|
WHERE term <> ''
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
sqlc.narg('cursor')::int IS NULL
|
||||||
|
OR u.id > sqlc.narg('cursor')::int
|
||||||
|
)
|
||||||
|
ORDER BY u.id ASC
|
||||||
|
LIMIT COALESCE(sqlc.narg('limit')::int, 20);
|
||||||
|
|
||||||
-- name: GetStudioByID :one
|
-- name: GetStudioByID :one
|
||||||
SELECT *
|
SELECT *
|
||||||
|
|
@ -369,13 +400,14 @@ 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)
|
INSERT INTO usertitles (user_id, title_id, status, rate, review_id, ctime)
|
||||||
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, ctime;
|
RETURNING user_id, title_id, status, rate, review_id, ctime;
|
||||||
|
|
||||||
|
|
@ -384,7 +416,8 @@ RETURNING user_id, title_id, status, rate, review_id, ctime;
|
||||||
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),
|
||||||
|
ctime = COALESCE(sqlc.narg('ftime')::timestamptz, ctime)
|
||||||
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')
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,16 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
oapi "nyanimedb/api"
|
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
oapi "nyanimedb/api"
|
||||||
|
|
||||||
amqp "github.com/rabbitmq/amqp091-go"
|
amqp "github.com/rabbitmq/amqp091-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const RPCQueueName = "anime_import_rpc"
|
||||||
|
|
||||||
|
// RabbitRequest не меняем
|
||||||
type RabbitRequest struct {
|
type RabbitRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Statuses []oapi.TitleStatus `json:"statuses,omitempty"`
|
Statuses []oapi.TitleStatus `json:"statuses,omitempty"`
|
||||||
|
|
@ -20,151 +23,6 @@ type RabbitRequest struct {
|
||||||
Timestamp time.Time `json:"timestamp"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publisher — потокобезопасный публикатор с пулом каналов.
|
|
||||||
type Publisher struct {
|
|
||||||
conn *amqp.Connection
|
|
||||||
pool *sync.Pool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPublisher создаёт новый Publisher.
|
|
||||||
// conn должен быть уже установленным и healthy.
|
|
||||||
// Рекомендуется передавать durable connection с reconnect-логикой.
|
|
||||||
func NewPublisher(conn *amqp.Connection) *Publisher {
|
|
||||||
return &Publisher{
|
|
||||||
conn: conn,
|
|
||||||
pool: &sync.Pool{
|
|
||||||
New: func() any {
|
|
||||||
ch, err := conn.Channel()
|
|
||||||
if err != nil {
|
|
||||||
// Паника уместна: невозможность открыть канал — критическая ошибка инициализации
|
|
||||||
panic(fmt.Errorf("rmqpool: failed to create channel: %w", err))
|
|
||||||
}
|
|
||||||
return ch
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Publish публикует сообщение в указанную очередь.
|
|
||||||
// Очередь объявляется как durable (если не существует).
|
|
||||||
// Поддерживает context для отмены/таймаута.
|
|
||||||
func (p *Publisher) Publish(
|
|
||||||
ctx context.Context,
|
|
||||||
queueName string,
|
|
||||||
payload RabbitRequest,
|
|
||||||
opts ...PublishOption,
|
|
||||||
) error {
|
|
||||||
// Применяем опции
|
|
||||||
options := &publishOptions{
|
|
||||||
contentType: "application/json",
|
|
||||||
deliveryMode: amqp.Persistent,
|
|
||||||
timestamp: time.Now(),
|
|
||||||
}
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сериализуем payload
|
|
||||||
body, err := json.Marshal(payload)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("rmqpool: failed to marshal payload: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Берём канал из пула
|
|
||||||
ch := p.getChannel()
|
|
||||||
if ch == nil {
|
|
||||||
return fmt.Errorf("rmqpool: channel is nil (connection may be closed)")
|
|
||||||
}
|
|
||||||
defer p.returnChannel(ch)
|
|
||||||
|
|
||||||
// Объявляем очередь (idempotent)
|
|
||||||
q, err := ch.QueueDeclare(
|
|
||||||
queueName,
|
|
||||||
true, // durable
|
|
||||||
false, // auto-delete
|
|
||||||
false, // exclusive
|
|
||||||
false, // no-wait
|
|
||||||
nil, // args
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("rmqpool: failed to declare queue %q: %w", queueName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Подготавливаем сообщение
|
|
||||||
msg := amqp.Publishing{
|
|
||||||
DeliveryMode: options.deliveryMode,
|
|
||||||
ContentType: options.contentType,
|
|
||||||
Timestamp: options.timestamp,
|
|
||||||
Body: body,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Публикуем с учётом контекста
|
|
||||||
done := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
err := ch.Publish(
|
|
||||||
"", // exchange (default)
|
|
||||||
q.Name, // routing key
|
|
||||||
false, // mandatory
|
|
||||||
false, // immediate
|
|
||||||
msg,
|
|
||||||
)
|
|
||||||
done <- err
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case err := <-done:
|
|
||||||
return err
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Publisher) getChannel() *amqp.Channel {
|
|
||||||
raw := p.pool.Get()
|
|
||||||
if raw == nil {
|
|
||||||
ch, _ := p.conn.Channel()
|
|
||||||
return ch
|
|
||||||
}
|
|
||||||
ch := raw.(*amqp.Channel)
|
|
||||||
if ch.IsClosed() { // ← теперь есть!
|
|
||||||
ch.Close() // освободить ресурсы
|
|
||||||
ch, _ = p.conn.Channel()
|
|
||||||
}
|
|
||||||
return ch
|
|
||||||
}
|
|
||||||
|
|
||||||
// returnChannel возвращает канал в пул, если он жив.
|
|
||||||
func (p *Publisher) returnChannel(ch *amqp.Channel) {
|
|
||||||
if ch != nil && !ch.IsClosed() {
|
|
||||||
p.pool.Put(ch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PublishOption позволяет кастомизировать публикацию.
|
|
||||||
type PublishOption func(*publishOptions)
|
|
||||||
|
|
||||||
type publishOptions struct {
|
|
||||||
contentType string
|
|
||||||
deliveryMode uint8
|
|
||||||
timestamp time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithContentType устанавливает Content-Type (по умолчанию "application/json").
|
|
||||||
func WithContentType(ct string) PublishOption {
|
|
||||||
return func(o *publishOptions) { o.contentType = ct }
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithTransient делает сообщение transient (не сохраняется на диск).
|
|
||||||
// По умолчанию — Persistent.
|
|
||||||
func WithTransient() PublishOption {
|
|
||||||
return func(o *publishOptions) { o.deliveryMode = amqp.Transient }
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithTimestamp устанавливает кастомную метку времени.
|
|
||||||
func WithTimestamp(ts time.Time) PublishOption {
|
|
||||||
return func(o *publishOptions) { o.timestamp = ts }
|
|
||||||
}
|
|
||||||
|
|
||||||
type RPCClient struct {
|
type RPCClient struct {
|
||||||
conn *amqp.Connection
|
conn *amqp.Connection
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
|
|
@ -174,37 +32,48 @@ func NewRPCClient(conn *amqp.Connection, timeout time.Duration) *RPCClient {
|
||||||
return &RPCClient{conn: conn, timeout: timeout}
|
return &RPCClient{conn: conn, timeout: timeout}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call отправляет запрос в очередь и ждёт ответа.
|
|
||||||
// replyPayload — указатель на структуру, в которую раскодировать ответ (например, &MediaResponse{}).
|
|
||||||
func (c *RPCClient) Call(
|
func (c *RPCClient) Call(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
requestQueue string,
|
|
||||||
request RabbitRequest,
|
request RabbitRequest,
|
||||||
replyPayload any,
|
replyPayload any,
|
||||||
) error {
|
) error {
|
||||||
// 1. Создаём временный канал (не из пула!)
|
|
||||||
|
// 1. Канал для запроса и ответа
|
||||||
ch, err := c.conn.Channel()
|
ch, err := c.conn.Channel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("channel: %w", err)
|
return fmt.Errorf("channel: %w", err)
|
||||||
}
|
}
|
||||||
defer ch.Close()
|
defer ch.Close()
|
||||||
|
|
||||||
// 2. Создаём временную очередь для ответов
|
// 2. Декларируем фиксированную очередь RPC (идемпотентно)
|
||||||
q, err := ch.QueueDeclare(
|
_, err = ch.QueueDeclare(
|
||||||
"", // auto name
|
RPCQueueName,
|
||||||
false, // not durable
|
true, // durable
|
||||||
true, // exclusive
|
false, // auto-delete
|
||||||
true, // auto-delete
|
false, // exclusive
|
||||||
|
false, // no-wait
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("declare rpc queue: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Создаём временную очередь ДЛЯ ОТВЕТА
|
||||||
|
replyQueue, err := ch.QueueDeclare(
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
false,
|
false,
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("reply queue: %w", err)
|
return fmt.Errorf("declare reply queue: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Подписываемся на ответы
|
// 4. Подписываемся на очередь ответов
|
||||||
msgs, err := ch.Consume(
|
msgs, err := ch.Consume(
|
||||||
q.Name,
|
replyQueue.Name,
|
||||||
"",
|
"",
|
||||||
true, // auto-ack
|
true, // auto-ack
|
||||||
true, // exclusive
|
true, // exclusive
|
||||||
|
|
@ -213,28 +82,28 @@ func (c *RPCClient) Call(
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("consume: %w", err)
|
return fmt.Errorf("consume reply: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Готовим correlation ID
|
// correlation ID
|
||||||
corrID := time.Now().UnixNano()
|
corrID := fmt.Sprintf("%d", time.Now().UnixNano())
|
||||||
|
|
||||||
// 5. Сериализуем запрос
|
// 5. сериализация запроса
|
||||||
body, err := json.Marshal(request)
|
body, err := json.Marshal(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("marshal request: %w", err)
|
return fmt.Errorf("marshal request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Публикуем запрос
|
// 6. Публикация RPC-запроса
|
||||||
err = ch.Publish(
|
err = ch.Publish(
|
||||||
"",
|
"",
|
||||||
requestQueue,
|
RPCQueueName, // ← фиксированная очередь!
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
amqp.Publishing{
|
amqp.Publishing{
|
||||||
ContentType: "application/json",
|
ContentType: "application/json",
|
||||||
CorrelationId: fmt.Sprintf("%d", corrID),
|
CorrelationId: corrID,
|
||||||
ReplyTo: q.Name,
|
ReplyTo: replyQueue.Name,
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
Body: body,
|
Body: body,
|
||||||
},
|
},
|
||||||
|
|
@ -244,18 +113,17 @@ func (c *RPCClient) Call(
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Ждём ответ с таймаутом
|
// 7. Ждём ответ с таймаутом
|
||||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
timeoutCtx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case msg := <-msgs:
|
case msg := <-msgs:
|
||||||
if msg.CorrelationId == fmt.Sprintf("%d", corrID) {
|
if msg.CorrelationId == corrID {
|
||||||
return json.Unmarshal(msg.Body, replyPayload)
|
return json.Unmarshal(msg.Body, replyPayload)
|
||||||
}
|
}
|
||||||
// игнорируем другие сообщения (маловероятно, но возможно)
|
case <-timeoutCtx.Done():
|
||||||
case <-ctx.Done():
|
return fmt.Errorf("rpc timeout: %w", timeoutCtx.Err())
|
||||||
return ctx.Err() // timeout or cancelled
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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: 'http://10.1.0.65:8081/api/v1' }));
|
export const client = createClient(createConfig<ClientOptions2>({ baseUrl: '/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, GetUsersIdData, GetUsersIdErrors, GetUsersIdResponses, GetUserTitleData, GetUserTitleErrors, GetUserTitleResponses, GetUserTitlesData, GetUserTitlesErrors, GetUserTitlesResponses, UpdateUserData, UpdateUserErrors, UpdateUserResponses, UpdateUserTitleData, UpdateUserTitleErrors, UpdateUserTitleResponses } from './types.gen';
|
import type { AddUserTitleData, AddUserTitleErrors, AddUserTitleResponses, DeleteUserTitleData, DeleteUserTitleErrors, DeleteUserTitleResponses, GetTitleData, GetTitleErrors, GetTitleResponses, GetTitlesData, GetTitlesErrors, GetTitlesResponses, GetUsersData, GetUsersErrors, GetUsersIdData, GetUsersIdErrors, GetUsersIdResponses, GetUsersResponses, GetUserTitleData, GetUserTitleErrors, GetUserTitleResponses, GetUserTitlesData, GetUserTitlesErrors, GetUserTitlesResponses, UpdateUserData, UpdateUserErrors, UpdateUserResponses, UpdateUserTitleData, UpdateUserTitleErrors, UpdateUserTitleResponses } from './types.gen';
|
||||||
|
|
||||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
|
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
|
||||||
/**
|
/**
|
||||||
|
|
@ -32,6 +32,11 @@ 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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,12 @@ 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;
|
||||||
|
|
@ -231,6 +237,50 @@ 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: {
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,16 @@ const handleLoadMore = async () => {
|
||||||
</div>
|
</div>
|
||||||
<TitlesFilterPanel filters={filters} setFilters={setFilters} />
|
<TitlesFilterPanel filters={filters} setFilters={setFilters} />
|
||||||
|
|
||||||
{loading && <div className="mt-20 font-medium text-black">Loading...</div>}
|
{loading && (
|
||||||
|
<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
modules/frontend/src/pages/UsersPage/UsersPage.tsx
Normal file
0
modules/frontend/src/pages/UsersPage/UsersPage.tsx
Normal file
|
|
@ -33,8 +33,6 @@ 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,
|
||||||
|
|
@ -47,6 +45,8 @@ CREATE TABLE titles (
|
||||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
-- example {"ru": ["Атака титанов", "Атака Титанов"],"en": ["Attack on Titan", "AoT"],"ja": ["進撃の巨人", "しんげきのきょじん"]}
|
-- example {"ru": ["Атака титанов", "Атака Титанов"],"en": ["Attack on Titan", "AoT"],"ja": ["進撃の巨人", "しんげきのきょじん"]}
|
||||||
title_names jsonb NOT NULL,
|
title_names jsonb NOT NULL,
|
||||||
|
-- example {"ru": "Кулинарное аниме как правильно приготовить людей.","en": "A culinary anime about how to cook people properly."}
|
||||||
|
title_desc jsonb,
|
||||||
studio_id bigint NOT NULL REFERENCES studios (id),
|
studio_id bigint NOT NULL REFERENCES studios (id),
|
||||||
poster_id bigint REFERENCES images (id) ON DELETE SET NULL,
|
poster_id bigint REFERENCES images (id) ON DELETE SET NULL,
|
||||||
title_status title_status_t NOT NULL,
|
title_status title_status_t NOT NULL,
|
||||||
|
|
@ -106,12 +106,13 @@ 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 REFERENCES external_services (id),
|
service_id bigint NOT NULL REFERENCES external_services (id),
|
||||||
external_id text NOT NULL
|
external_id text NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -168,17 +169,4 @@ EXECUTE FUNCTION update_title_rating();
|
||||||
CREATE TRIGGER trg_notify_new_signal
|
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,13 +188,14 @@ 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 {
|
||||||
|
|
@ -246,6 +247,7 @@ type Tag struct {
|
||||||
type Title struct {
|
type Title struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
TitleNames json.RawMessage `json:"title_names"`
|
TitleNames json.RawMessage `json:"title_names"`
|
||||||
|
TitleDesc []byte `json:"title_desc"`
|
||||||
StudioID int64 `json:"studio_id"`
|
StudioID int64 `json:"studio_id"`
|
||||||
PosterID *int64 `json:"poster_id"`
|
PosterID *int64 `json:"poster_id"`
|
||||||
TitleStatus TitleStatusT `json:"title_status"`
|
TitleStatus TitleStatusT `json:"title_status"`
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ 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
|
||||||
|
|
@ -74,6 +76,19 @@ func (q *Queries) DeleteUserTitle(ctx context.Context, arg DeleteUserTitleParams
|
||||||
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
|
||||||
|
|
@ -129,7 +144,7 @@ func (q *Queries) GetStudioByID(ctx context.Context, studioID int64) (Studio, er
|
||||||
|
|
||||||
const getTitleByID = `-- name: GetTitleByID :one
|
const getTitleByID = `-- name: GetTitleByID :one
|
||||||
SELECT
|
SELECT
|
||||||
t.id, t.title_names, t.studio_id, t.poster_id, t.title_status, t.rating, t.rating_count, t.release_year, t.release_season, t.season, t.episodes_aired, t.episodes_all, t.episodes_len,
|
t.id, t.title_names, t.title_desc, t.studio_id, t.poster_id, t.title_status, t.rating, t.rating_count, t.release_year, t.release_season, t.season, t.episodes_aired, t.episodes_all, t.episodes_len,
|
||||||
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(
|
||||||
|
|
@ -157,6 +172,7 @@ GROUP BY
|
||||||
type GetTitleByIDRow struct {
|
type GetTitleByIDRow struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
TitleNames json.RawMessage `json:"title_names"`
|
TitleNames json.RawMessage `json:"title_names"`
|
||||||
|
TitleDesc []byte `json:"title_desc"`
|
||||||
StudioID int64 `json:"studio_id"`
|
StudioID int64 `json:"studio_id"`
|
||||||
PosterID *int64 `json:"poster_id"`
|
PosterID *int64 `json:"poster_id"`
|
||||||
TitleStatus TitleStatusT `json:"title_status"`
|
TitleStatus TitleStatusT `json:"title_status"`
|
||||||
|
|
@ -185,6 +201,7 @@ func (q *Queries) GetTitleByID(ctx context.Context, titleID int64) (GetTitleByID
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.TitleNames,
|
&i.TitleNames,
|
||||||
|
&i.TitleDesc,
|
||||||
&i.StudioID,
|
&i.StudioID,
|
||||||
&i.PosterID,
|
&i.PosterID,
|
||||||
&i.TitleStatus,
|
&i.TitleStatus,
|
||||||
|
|
@ -236,6 +253,35 @@ 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,
|
||||||
|
|
@ -392,23 +438,25 @@ 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)
|
INSERT INTO usertitles (user_id, title_id, status, rate, review_id, ctime)
|
||||||
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, ctime
|
RETURNING user_id, title_id, status, rate, review_id, ctime
|
||||||
`
|
`
|
||||||
|
|
||||||
type InsertUserTitleParams struct {
|
type InsertUserTitleParams struct {
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
TitleID int64 `json:"title_id"`
|
TitleID int64 `json:"title_id"`
|
||||||
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) {
|
||||||
|
|
@ -418,6 +466,7 @@ 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(
|
||||||
|
|
@ -638,6 +687,87 @@ func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]S
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const searchUser = `-- name: SearchUser :many
|
||||||
|
SELECT
|
||||||
|
u.id AS id,
|
||||||
|
u.avatar_id AS avatar_id,
|
||||||
|
u.mail AS mail,
|
||||||
|
u.nickname AS nickname,
|
||||||
|
u.disp_name AS disp_name,
|
||||||
|
u.user_desc AS user_desc,
|
||||||
|
u.creation_date AS creation_date,
|
||||||
|
i.storage_type AS storage_type,
|
||||||
|
i.image_path AS image_path
|
||||||
|
FROM users AS u
|
||||||
|
LEFT JOIN images AS i ON u.avatar_id = i.id
|
||||||
|
WHERE
|
||||||
|
(
|
||||||
|
$1::text IS NULL
|
||||||
|
OR (
|
||||||
|
SELECT bool_and(
|
||||||
|
u.nickname ILIKE ('%' || term || '%')
|
||||||
|
OR u.disp_name ILIKE ('%' || term || '%')
|
||||||
|
)
|
||||||
|
FROM unnest(string_to_array(trim($1::text), ' ')) AS term
|
||||||
|
WHERE term <> ''
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
$2::int IS NULL
|
||||||
|
OR u.id > $2::int
|
||||||
|
)
|
||||||
|
ORDER BY u.id ASC
|
||||||
|
LIMIT COALESCE($3::int, 20)
|
||||||
|
`
|
||||||
|
|
||||||
|
type SearchUserParams struct {
|
||||||
|
Word *string `json:"word"`
|
||||||
|
Cursor *int32 `json:"cursor"`
|
||||||
|
Limit *int32 `json:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchUserRow struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
AvatarID *int64 `json:"avatar_id"`
|
||||||
|
Mail *string `json:"mail"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
DispName *string `json:"disp_name"`
|
||||||
|
UserDesc *string `json:"user_desc"`
|
||||||
|
CreationDate time.Time `json:"creation_date"`
|
||||||
|
StorageType *StorageTypeT `json:"storage_type"`
|
||||||
|
ImagePath *string `json:"image_path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) SearchUser(ctx context.Context, arg SearchUserParams) ([]SearchUserRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, searchUser, arg.Word, arg.Cursor, arg.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []SearchUserRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i SearchUserRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.AvatarID,
|
||||||
|
&i.Mail,
|
||||||
|
&i.Nickname,
|
||||||
|
&i.DispName,
|
||||||
|
&i.UserDesc,
|
||||||
|
&i.CreationDate,
|
||||||
|
&i.StorageType,
|
||||||
|
&i.ImagePath,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
const searchUserTitles = `-- name: SearchUserTitles :many
|
const searchUserTitles = `-- name: SearchUserTitles :many
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -934,18 +1064,20 @@ 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),
|
||||||
|
ctime = COALESCE($3::timestamptz, ctime)
|
||||||
WHERE
|
WHERE
|
||||||
user_id = $3
|
user_id = $4
|
||||||
AND title_id = $4
|
AND title_id = $5
|
||||||
RETURNING user_id, title_id, status, rate, review_id, ctime
|
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"`
|
||||||
UserID int64 `json:"user_id"`
|
Ftime pgtype.Timestamptz `json:"ftime"`
|
||||||
TitleID int64 `json:"title_id"`
|
UserID int64 `json:"user_id"`
|
||||||
|
TitleID int64 `json:"title_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fails with sql.ErrNoRows if (user_id, title_id) not found
|
// Fails with sql.ErrNoRows if (user_id, title_id) not found
|
||||||
|
|
@ -953,6 +1085,7 @@ 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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue