diff --git a/api/_build/openapi.yaml b/api/_build/openapi.yaml index c0e6bff..d7a6fac 100644 --- a/api/_build/openapi.yaml +++ b/api/_build/openapi.yaml @@ -122,53 +122,6 @@ paths: description: Unknown server error security: - JwtAuthCookies: [] - /users/: - get: - summary: 'Search user by nickname or dispname (both in one param), response is always sorted by id' - parameters: - - name: word - in: query - schema: - type: string - - name: limit - in: query - schema: - type: integer - format: int32 - default: 10 - - name: cursor_id - in: query - description: pass cursor naked - schema: - type: integer - format: int32 - default: 1 - responses: - '200': - description: List of users with cursor - content: - application/json: - schema: - type: object - properties: - data: - description: List of users - type: array - items: - $ref: '#/components/schemas/User' - cursor: - type: integer - format: int64 - default: 1 - required: - - data - - cursor - '204': - description: No users found - '400': - description: Request params are not correct - '500': - description: Unknown server error '/users/{user_id}': get: operationId: getUsersId @@ -395,9 +348,6 @@ paths: rate: type: integer format: int32 - ftime: - type: string - format: date-time required: - title_id - status @@ -481,9 +431,6 @@ paths: rate: type: integer format: int32 - ftime: - type: string - format: date-time responses: '200': description: Title successfully updated @@ -533,42 +480,6 @@ paths: description: Internal server error security: - XsrfAuthHeader: [] - /media/upload: - post: - summary: 'Upload an image (PNG, JPEG, or WebP)' - description: | - Uploads a single image file. Supported formats: **PNG**, **JPEG/JPG**, **WebP**. - requestBody: - required: true - content: - encoding: - image: - contentType: 'image/png, image/jpeg, image/webp' - multipart/form-data: - schema: - image: - type: string - format: binary - description: 'Image file (PNG, JPEG, or WebP)' - responses: - '200': - description: Image uploaded successfully - content: - application/json: - schema: - $ref: '#/components/schemas/Image' - '400': - description: 'Bad request — e.g., invalid/malformed image, empty file' - content: - application/json: - schema: - type: string - '415': - description: | - Unsupported Media Type — e.g., request `Content-Type` is not `multipart/form-data`, - or the `image` part has an unsupported `Content-Type` (not image/png, image/jpeg, or image/webp) - '500': - description: Internal server error components: parameters: cursor: @@ -682,11 +593,6 @@ components: example: - Attack on Titan - AoT - title_desc: - description: 'Localized description. Key = language (ISO 639-1), value = description.' - type: object - additionalProperties: - type: string studio: $ref: '#/components/schemas/Studio' tags: diff --git a/api/api.gen.go b/api/api.gen.go index 04d10c0..459a3e4 100644 --- a/api/api.gen.go +++ b/api/api.gen.go @@ -7,10 +7,7 @@ import ( "context" "encoding/json" "fmt" - "io" - "mime/multipart" "net/http" - "strings" "time" "github.com/gin-gonic/gin" @@ -116,9 +113,6 @@ type Title struct { // Tags Array of localized 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 map[string][]string `json:"title_names"` @@ -184,9 +178,6 @@ type UserTitleStatus string // Cursor defines model for cursor. type Cursor = string -// PostMediaUploadMultipartBody defines parameters for PostMediaUpload. -type PostMediaUploadMultipartBody = interface{} - // GetTitlesParams defines parameters for GetTitles. type GetTitlesParams struct { Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"` @@ -210,15 +201,6 @@ type GetTitleParams struct { 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. type GetUsersIdParams struct { Fields *string `form:"fields,omitempty" json:"fields,omitempty"` @@ -262,8 +244,7 @@ type GetUserTitlesParams struct { // AddUserTitleJSONBody defines parameters for AddUserTitle. type AddUserTitleJSONBody struct { - Ftime *time.Time `json:"ftime,omitempty"` - Rate *int32 `json:"rate,omitempty"` + Rate *int32 `json:"rate,omitempty"` // Status User's title status Status UserTitleStatus `json:"status"` @@ -272,16 +253,12 @@ type AddUserTitleJSONBody struct { // UpdateUserTitleJSONBody defines parameters for UpdateUserTitle. type UpdateUserTitleJSONBody struct { - Ftime *time.Time `json:"ftime,omitempty"` - Rate *int32 `json:"rate,omitempty"` + Rate *int32 `json:"rate,omitempty"` // Status User's title status Status *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. type UpdateUserJSONRequestBody UpdateUserJSONBody @@ -293,18 +270,12 @@ type UpdateUserTitleJSONRequestBody UpdateUserTitleJSONBody // ServerInterface represents all server handlers. type ServerInterface interface { - // Upload an image (PNG, JPEG, or WebP) - // (POST /media/upload) - PostMediaUpload(c *gin.Context) // Get titles // (GET /titles) GetTitles(c *gin.Context, params GetTitlesParams) // Get title description // (GET /titles/{title_id}) 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 /users/{user_id}) GetUsersId(c *gin.Context, userId string, params GetUsersIdParams) @@ -337,19 +308,6 @@ type ServerInterfaceWrapper struct { 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 func (siw *ServerInterfaceWrapper) GetTitles(c *gin.Context) { @@ -501,48 +459,6 @@ func (siw *ServerInterfaceWrapper) GetTitle(c *gin.Context) { 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 func (siw *ServerInterfaceWrapper) GetUsersId(c *gin.Context) { @@ -881,10 +797,8 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options ErrorHandler: errorHandler, } - router.POST(options.BaseURL+"/media/upload", wrapper.PostMediaUpload) router.GET(options.BaseURL+"/titles", wrapper.GetTitles) 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.PATCH(options.BaseURL+"/users/:user_id", wrapper.UpdateUser) router.GET(options.BaseURL+"/users/:user_id/titles", wrapper.GetUserTitles) @@ -894,49 +808,6 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options 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 { Params GetTitlesParams } @@ -1033,52 +904,6 @@ func (response GetTitle500Response) VisitGetTitleResponse(w http.ResponseWriter) 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 { UserId string `json:"user_id"` Params GetUsersIdParams @@ -1474,18 +1299,12 @@ func (response UpdateUserTitle500Response) VisitUpdateUserTitleResponse(w http.R // StrictServerInterface represents all server handlers. 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) GetTitles(ctx context.Context, request GetTitlesRequestObject) (GetTitlesResponseObject, error) // Get title description // (GET /titles/{title_id}) 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 /users/{user_id}) GetUsersId(ctx context.Context, request GetUsersIdRequestObject) (GetUsersIdResponseObject, error) @@ -1521,43 +1340,6 @@ type strictHandler struct { 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 func (sh *strictHandler) GetTitles(ctx *gin.Context, params GetTitlesParams) { var request GetTitlesRequestObject @@ -1613,33 +1395,6 @@ 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 func (sh *strictHandler) GetUsersId(ctx *gin.Context, userId string, params GetUsersIdParams) { var request GetUsersIdRequestObject diff --git a/api/openapi.yaml b/api/openapi.yaml index 26813fc..d84797f 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -11,17 +11,13 @@ paths: $ref: "./paths/titles.yaml" /titles/{title_id}: $ref: "./paths/titles-id.yaml" - /users/: - $ref: "./paths/users.yaml" /users/{user_id}: $ref: "./paths/users-id.yaml" /users/{user_id}/titles: $ref: "./paths/users-id-titles.yaml" /users/{user_id}/titles/{title_id}: $ref: "./paths/users-id-titles-id.yaml" - /media/upload: - $ref: "./paths/media_upload.yaml" - + components: parameters: $ref: "./parameters/_index.yaml" diff --git a/api/paths/media_upload.yaml b/api/paths/media_upload.yaml deleted file mode 100644 index 0453952..0000000 --- a/api/paths/media_upload.yaml +++ /dev/null @@ -1,37 +0,0 @@ -post: - summary: Upload an image (PNG, JPEG, or WebP) - description: | - Uploads a single image file. Supported formats: **PNG**, **JPEG/JPG**, **WebP**. - requestBody: - required: true - content: - multipart/form-data: - schema: - image: - type: string - format: binary - description: Image file (PNG, JPEG, or WebP) - encoding: - image: - contentType: image/png, image/jpeg, image/webp - - responses: - '200': - description: Image uploaded successfully - content: - application/json: - schema: - $ref: "../schemas/Image.yaml" - '400': - description: Bad request — e.g., invalid/malformed image, empty file - content: - application/json: - schema: - type: string - '415': - description: | - Unsupported Media Type — e.g., request `Content-Type` is not `multipart/form-data`, - or the `image` part has an unsupported `Content-Type` (not image/png, image/jpeg, or image/webp) - - '500': - description: Internal server error \ No newline at end of file diff --git a/api/paths/users-id-titles-id.yaml b/api/paths/users-id-titles-id.yaml index 20a174f..1da2b81 100644 --- a/api/paths/users-id-titles-id.yaml +++ b/api/paths/users-id-titles-id.yaml @@ -61,9 +61,6 @@ patch: rate: type: integer format: int32 - ftime: - type: string - format: date-time responses: '200': description: Title successfully updated diff --git a/api/paths/users-id-titles.yaml b/api/paths/users-id-titles.yaml index f1e5e95..75f5461 100644 --- a/api/paths/users-id-titles.yaml +++ b/api/paths/users-id-titles.yaml @@ -122,9 +122,6 @@ post: rate: type: integer format: int32 - ftime: - type: string - format: date-time responses: '200': description: Title successfully added to user diff --git a/api/paths/users.yaml b/api/paths/users.yaml deleted file mode 100644 index 14fb0c0..0000000 --- a/api/paths/users.yaml +++ /dev/null @@ -1,46 +0,0 @@ -get: - summary: Search user by nickname or dispname (both in one param), response is always sorted by id - parameters: - - in: query - name: word - schema: - type: string - - in: query - name: limit - schema: - type: integer - format: int32 - default: 10 - - in: query - name: cursor_id - description: pass cursor naked - schema: - type: integer - format: int32 - default: 1 - responses: - '200': - description: List of users with cursor - content: - application/json: - schema: - type: object - properties: - data: - type: array - items: - $ref: '../schemas/User.yaml' - description: List of users - cursor: - type: integer - format: int64 - default: 1 - required: - - data - - cursor - '204': - description: No users found - '400': - description: Request params are not correct - '500': - description: Unknown server error diff --git a/api/schemas/Title.yaml b/api/schemas/Title.yaml index fac4a3f..877ee24 100644 --- a/api/schemas/Title.yaml +++ b/api/schemas/Title.yaml @@ -30,11 +30,6 @@ properties: - Титаны ja: - 進撃の巨人 - title_desc: - type: object - description: Localized description. Key = language (ISO 639-1), value = description. - additionalProperties: - type: string studio: $ref: ./Studio.yaml tags: diff --git a/auth/auth.gen.go b/auth/auth.gen.go index 1e8803e..b7cd839 100644 --- a/auth/auth.gen.go +++ b/auth/auth.gen.go @@ -13,23 +13,6 @@ import ( 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. type PostSignInJSONBody struct { Nickname string `json:"nickname"` @@ -42,9 +25,6 @@ type PostSignUpJSONBody struct { 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. type PostSignInJSONRequestBody PostSignInJSONBody @@ -53,9 +33,6 @@ type PostSignUpJSONRequestBody PostSignUpJSONBody // ServerInterface represents all server handlers. type ServerInterface interface { - // Get service impersontaion token - // (POST /get-impersonation-token) - GetImpersonationToken(c *gin.Context) // Sign in a user and return JWT // (POST /sign-in) PostSignIn(c *gin.Context) @@ -73,21 +50,6 @@ type ServerInterfaceWrapper struct { 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 func (siw *ServerInterfaceWrapper) PostSignIn(c *gin.Context) { @@ -141,41 +103,10 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options ErrorHandler: errorHandler, } - router.POST(options.BaseURL+"/get-impersonation-token", wrapper.GetImpersonationToken) router.POST(options.BaseURL+"/sign-in", wrapper.PostSignIn) 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 { Body *PostSignInJSONRequestBody } @@ -196,11 +127,15 @@ func (response PostSignIn200JSONResponse) VisitPostSignInResponse(w http.Respons return json.NewEncoder(w).Encode(response) } -type PostSignIn401Response = UnauthorizedErrorResponse +type PostSignIn401JSONResponse struct { + Error *string `json:"error,omitempty"` +} -func (response PostSignIn401Response) VisitPostSignInResponse(w http.ResponseWriter) error { +func (response PostSignIn401JSONResponse) VisitPostSignInResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") w.WriteHeader(401) - return nil + + return json.NewEncoder(w).Encode(response) } type PostSignUpRequestObject struct { @@ -224,9 +159,6 @@ func (response PostSignUp200JSONResponse) VisitPostSignUpResponse(w http.Respons // StrictServerInterface represents all server handlers. 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 // (POST /sign-in) PostSignIn(ctx context.Context, request PostSignInRequestObject) (PostSignInResponseObject, error) @@ -247,39 +179,6 @@ type strictHandler struct { 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 func (sh *strictHandler) PostSignIn(ctx *gin.Context) { var request PostSignInRequestObject diff --git a/auth/openapi-auth.yaml b/auth/openapi-auth.yaml index 803a4ae..5f3ebd6 100644 --- a/auth/openapi-auth.yaml +++ b/auth/openapi-auth.yaml @@ -10,7 +10,6 @@ paths: /sign-up: post: summary: Sign up a new user - operationId: postSignUp tags: [Auth] requestBody: required: true @@ -42,7 +41,6 @@ paths: /sign-in: post: summary: Sign in a user and return JWT - operationId: postSignIn tags: [Auth] requestBody: required: true @@ -75,52 +73,88 @@ paths: user_name: type: string "401": - $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 + description: Access denied due to invalid credentials content: application/json: schema: type: object - required: - - access_token properties: - access_token: + error: type: string - description: JWT access token - "401": - $ref: '#/components/responses/UnauthorizedError' - -components: - securitySchemes: - bearerAuth: - type: http - scheme: bearer - responses: - UnauthorizedError: - description: Access token is missing or invalid \ No newline at end of file + example: "Access denied" + # /auth/verify-token: + # post: + # summary: Verify JWT validity + # tags: [Auth] + # requestBody: + # required: true + # content: + # application/json: + # schema: + # type: object + # required: [token] + # 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 diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 3eff3d3..1119335 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -40,22 +40,6 @@ services: retries: 5 start_period: 10s - redis: - image: redis:8.4.0-alpine - container_name: redis - ports: - - "6379:6379" - restart: always - command: ["redis-server", "--appendonly", "yes"] - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 5s - volumes: - - redis_data:/data - nyanimedb-backend: image: meowgit.nekoea.red/nihonium/nyanimedb-backend:latest container_name: nyanimedb-backend @@ -67,8 +51,8 @@ services: RABBITMQ_URL: ${RABBITMQ_URL} JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY} AUTH_ENABLED: ${AUTH_ENABLED} - # ports: - # - "8080:8080" + ports: + - "8080:8080" depends_on: - postgres - rabbitmq @@ -84,8 +68,8 @@ services: DATABASE_URL: ${DATABASE_URL} SERVICE_ADDRESS: ${SERVICE_ADDRESS} JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY} - # ports: - # - "8082:8082" + ports: + - "8082:8082" depends_on: - postgres networks: @@ -105,7 +89,6 @@ services: volumes: postgres_data: rabbitmq_data: - redis_data: networks: nyanimedb-network: diff --git a/go.mod b/go.mod index 08a3dc1..6662bc1 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,6 @@ require ( github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // 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/gin-contrib/sse v1.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -43,13 +42,12 @@ require ( github.com/ugorji/go/codec v1.3.0 // indirect go.uber.org/mock v0.5.0 // indirect golang.org/x/arch v0.20.0 // indirect - golang.org/x/crypto v0.43.0 // indirect - golang.org/x/image v0.33.0 // indirect - golang.org/x/mod v0.29.0 // indirect - golang.org/x/net v0.46.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect - golang.org/x/tools v0.38.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/tools v0.34.0 // indirect google.golang.org/protobuf v1.36.9 // indirect ) diff --git a/go.sum b/go.sum index dc41797..520a22b 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,6 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= @@ -105,18 +103,10 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.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.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= -golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= -golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ= -golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -124,15 +114,11 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -145,8 +131,6 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -160,16 +144,12 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= diff --git a/modules/auth/handlers/handlers.go b/modules/auth/handlers/handlers.go index 3af44f3..ac55abe 100644 --- a/modules/auth/handlers/handlers.go +++ b/modules/auth/handlers/handlers.go @@ -47,28 +47,10 @@ func CheckPassword(password, hash string) (bool, error) { 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) { accessClaims := jwt.MapClaims{ "user_id": userID, "exp": time.Now().Add(15 * time.Minute).Unix(), - //TODO: add created_at } at := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims) accessToken, err = at.SignedString([]byte(s.JwtPrivateKey)) @@ -137,7 +119,10 @@ func (s Server) PostSignIn(ctx context.Context, req auth.PostSignInRequestObject // TODO: return 500 } if !ok { - return auth.PostSignIn401Response{}, nil + err_msg := "invalid credentials" + return auth.PostSignIn401JSONResponse{ + Error: &err_msg, + }, nil } accessToken, refreshToken, csrfToken, err := s.generateTokens(req.Body.Nickname) @@ -159,65 +144,47 @@ func (s Server) PostSignIn(ctx context.Context, req auth.PostSignInRequestObject return result, nil } -func (s Server) GetImpersonationToken(ctx context.Context, req auth.GetImpersonationTokenRequestObject) (auth.GetImpersonationTokenResponseObject, error) { - ginCtx, ok := ctx.Value(gin.ContextKey).(*gin.Context) - if !ok { - log.Print("failed to get gin context") - // TODO: change to 500 - return auth.GetImpersonationToken200JSONResponse{}, fmt.Errorf("failed to get gin.Context from context.Context") - } +// func (s Server) PostAuthVerifyToken(ctx context.Context, req auth.PostAuthVerifyTokenRequestObject) (auth.PostAuthVerifyTokenResponseObject, error) { +// valid := false +// var userID *string +// var errStr *string - token, err := ExtractBearerToken(ginCtx.Request.Header.Get("Authorization")) - if err != nil { - // TODO: return 500 - log.Errorf("failed to extract bearer token: %v", err) - return auth.GetImpersonationToken401Response{}, err - } - log.Printf("got auth token: %s", token) +// token, err := jwt.Parse(req.Body.Token, func(t *jwt.Token) (interface{}, error) { +// if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { +// return nil, fmt.Errorf("unexpected signing method") +// } +// return accessSecret, nil +// }) - ext_service, err := s.db.GetExternalServiceByToken(context.Background(), &token) - if err != nil { - log.Errorf("failed to get external service by token: %v", err) - return auth.GetImpersonationToken401Response{}, err - // TODO: check err and retyrn 400/500 - } +// if err != nil { +// e := err.Error() +// errStr = &e +// return auth.PostAuthVerifyToken200JSONResponse{ +// Valid: &valid, +// UserId: userID, +// Error: errStr, +// }, nil +// } - var user_id string = "" +// if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { +// if uid, ok := claims["user_id"].(string); ok { +// valid = true +// userID = &uid +// } else { +// e := "user_id not found in token" +// errStr = &e +// } +// } else { +// e := "invalid token claims" +// errStr = &e +// } - if req.Body.ExternalId != nil { - user, err := s.db.GetUserByExternalServiceId(context.Background(), sqlc.GetUserByExternalServiceIdParams{ - ExternalID: fmt.Sprintf("%d", *req.Body.ExternalId), - ServiceID: ext_service.ID, - }) - 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 -} +// return auth.PostAuthVerifyToken200JSONResponse{ +// Valid: &valid, +// UserId: userID, +// Error: errStr, +// }, nil +// } // func (s Server) PostAuthRefreshToken(ctx context.Context, req auth.PostAuthRefreshTokenRequestObject) (auth.PostAuthRefreshTokenResponseObject, error) { // valid := false @@ -269,11 +236,3 @@ func (s Server) GetImpersonationToken(ctx context.Context, req auth.GetImpersona // Error: errStr, // }, nil // } - -func ExtractBearerToken(header string) (string, error) { - const prefix = "Bearer " - if len(header) <= len(prefix) || header[:len(prefix)] != prefix { - return "", fmt.Errorf("invalid bearer token format") - } - return header[len(prefix):], nil -} diff --git a/modules/auth/queries.sql b/modules/auth/queries.sql index 0b9b941..828d2af 100644 --- a/modules/auth/queries.sql +++ b/modules/auth/queries.sql @@ -9,13 +9,3 @@ INTO users (passhash, nickname) VALUES (sqlc.arg(passhash), sqlc.arg(nickname)) RETURNING id; --- name: GetExternalServiceByToken :one -SELECT * -FROM external_services -WHERE auth_token = sqlc.arg('auth_token'); - --- name: GetUserByExternalServiceId :one -SELECT u.* -FROM users u -LEFT JOIN external_ids ei ON eu.user_id = u.id -WHERE ei.external_id = sqlc.arg('external_id') AND ei.service_id = sqlc.arg('service_id'); \ No newline at end of file diff --git a/modules/backend/handlers/common.go b/modules/backend/handlers/common.go index 7f2807f..cad4f0f 100644 --- a/modules/backend/handlers/common.go +++ b/modules/backend/handlers/common.go @@ -9,24 +9,24 @@ import ( "strconv" ) -// type Handler struct { -// publisher *rmq.Publisher -// } +type Handler struct { + publisher *rmq.Publisher +} -// func New(publisher *rmq.Publisher) *Handler { -// return &Handler{publisher: publisher} -// } +func New(publisher *rmq.Publisher) *Handler { + return &Handler{publisher: publisher} +} type Server struct { - db *sqlc.Queries - // publisher *rmq.Publisher + db *sqlc.Queries + publisher *rmq.Publisher RPCclient *rmq.RPCClient } -func NewServer(db *sqlc.Queries, rpcclient *rmq.RPCClient) *Server { +func NewServer(db *sqlc.Queries, publisher *rmq.Publisher, rpcclient *rmq.RPCClient) *Server { return &Server{ - db: db, - // publisher: publisher, + db: db, + publisher: publisher, RPCclient: rpcclient, } } @@ -73,14 +73,6 @@ func (s Server) mapTitle(title sqlc.GetTitleByIDRow) (oapi.Title, error) { } oapi_title.TitleNames = title_names - if len(title.TitleDesc) > 0 { - title_descs := make(map[string]string, 0) - err = json.Unmarshal(title.TitleDesc, &title_descs) - if err != nil { - return oapi.Title{}, fmt.Errorf("unmarshal TitleDesc: %v", err) - } - oapi_title.TitleDesc = &title_descs - } if len(title.EpisodesLen) > 0 { episodes_lens := make(map[string]float64, 0) err = json.Unmarshal(title.EpisodesLen, &episodes_lens) diff --git a/modules/backend/handlers/images.go b/modules/backend/handlers/images.go deleted file mode 100644 index c1e3d4b..0000000 --- a/modules/backend/handlers/images.go +++ /dev/null @@ -1,141 +0,0 @@ -package handlers - -import ( - "bytes" - "context" - "fmt" - "image" - "image/jpeg" - "image/png" - "io" - "net/http" - oapi "nyanimedb/api" - "os" - "path/filepath" - "strings" - - "github.com/disintegration/imaging" - log "github.com/sirupsen/logrus" - "golang.org/x/image/webp" -) - -// PostMediaUpload implements oapi.StrictServerInterface. -func (s *Server) PostMediaUpload(ctx context.Context, request oapi.PostMediaUploadRequestObject) (oapi.PostMediaUploadResponseObject, error) { - // Получаем multipart body - mp := request.MultipartBody - if mp == nil { - log.Errorf("PostMedia without body") - return oapi.PostMediaUpload400JSONResponse("Multipart body is required"), nil - } - - // Парсим первую часть (предполагаем, что файл в поле "file") - part, err := mp.NextPart() - if err != nil { - log.Errorf("PostMedia without file") - return oapi.PostMediaUpload400JSONResponse("File required"), nil - } - defer part.Close() - - // Читаем ВЕСЬ файл в память (для небольших изображений — нормально) - // Если файлы могут быть большими — используйте лимитированный буфер (см. ниже) - data, err := io.ReadAll(part) - if err != nil { - log.Errorf("PostMedia cannot read file") - return oapi.PostMediaUpload400JSONResponse("File required"), nil - } - - if len(data) == 0 { - log.Errorf("PostMedia empty file") - return oapi.PostMediaUpload400JSONResponse("Empty file"), nil - } - - // Проверка MIME по первым 512 байтам - mimeType := http.DetectContentType(data) - if mimeType != "image/jpeg" && mimeType != "image/png" && mimeType != "image/webp" { - log.Errorf("PostMedia bad type") - return oapi.PostMediaUpload400JSONResponse("Bad data type"), nil - } - - // Декодируем изображение из буфера - var img image.Image - switch mimeType { - case "image/jpeg": - { - img, err = jpeg.Decode(bytes.NewReader(data)) - if err != nil { - log.Errorf("PostMedia cannot decode file: %v", err) - return oapi.PostMediaUpload500Response{}, nil - } - } - case "image/png": - { - img, err = png.Decode(bytes.NewReader(data)) - if err != nil { - log.Errorf("PostMedia cannot decode file: %v", err) - return oapi.PostMediaUpload500Response{}, nil - } - } - case "image/webp": - { - img, err = webp.Decode(bytes.NewReader(data)) - if err != nil { - log.Errorf("PostMedia cannot decode file: %v", err) - return oapi.PostMediaUpload500Response{}, nil - } - } - } - - var buf bytes.Buffer - err = imaging.Encode(&buf, img, imaging.PNG) - if err != nil { - log.Errorf("PostMedia failed to re-encode JPEG: %v", err) - return oapi.PostMediaUpload500Response{}, nil - } - - // TODO: to delete - filename := part.FileName() - if filename == "" { - filename = "upload_" + generateRandomHex(8) + ".jpg" - } else { - filename = sanitizeFilename(filename) - if !strings.HasSuffix(strings.ToLower(filename), ".png") { - filename += ".png" - } - } - - // TODO: пойти на хуй ( вызвать файловую помойку) - os.Mkdir("uploads", 0644) - err = os.WriteFile(filepath.Join("./uploads", filename), buf.Bytes(), 0644) - if err != nil { - log.Errorf("PostMedia failed to write: %v", err) - return oapi.PostMediaUpload500Response{}, nil - } - - return oapi.PostMediaUpload200JSONResponse{}, nil -} - -// Вспомогательные функции — как раньше -func generateRandomHex(n int) string { - b := make([]byte, n) - for i := range b { - b[i] = byte('a' + (i % 16)) - } - return fmt.Sprintf("%x", b) -} - -func sanitizeFilename(name string) string { - var clean strings.Builder - for _, r := range name { - if (r >= 'a' && r <= 'z') || - (r >= 'A' && r <= 'Z') || - (r >= '0' && r <= '9') || - r == '.' || r == '_' || r == '-' { - clean.WriteRune(r) - } - } - s := clean.String() - if s == "" { - return "file" - } - return s -} diff --git a/modules/backend/handlers/titles.go b/modules/backend/handlers/titles.go index 7aeeb11..300cc87 100644 --- a/modules/backend/handlers/titles.go +++ b/modules/backend/handlers/titles.go @@ -197,6 +197,7 @@ func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObje // Делаем RPC-вызов — и ЖДЁМ ответа err := s.RPCclient.Call( ctx, + "svc.media.process.requests", // ← очередь микросервиса mqreq, &reply, ) diff --git a/modules/backend/handlers/users.go b/modules/backend/handlers/users.go index eecd82f..d6faade 100644 --- a/modules/backend/handlers/users.go +++ b/modules/backend/handlers/users.go @@ -69,16 +69,6 @@ func sqlDate2oapi(p_date pgtype.Timestamptz) *time.Time { return nil } -func oapiDate2sql(t *time.Time) pgtype.Timestamptz { - if t == nil { - return pgtype.Timestamptz{Valid: false} - } - return pgtype.Timestamptz{ - Time: *t, - Valid: true, - } -} - // func UserTitleStatus2Sqlc(s *[]oapi.UserTitleStatus) (*SqlcUserStatus, error) { // var sqlc_status SqlcUserStatus // if s == nil { @@ -375,7 +365,6 @@ func (s Server) AddUserTitle(ctx context.Context, request oapi.AddUserTitleReque TitleID: request.Body.TitleId, Status: *status, Rate: request.Body.Rate, - Ftime: oapiDate2sql(request.Body.Ftime), } user_title, err := s.db.InsertUserTitle(ctx, params) @@ -439,7 +428,6 @@ func (s Server) UpdateUserTitle(ctx context.Context, request oapi.UpdateUserTitl Rate: request.Body.Rate, UserID: request.UserId, TitleID: request.TitleId, - Ftime: oapiDate2sql(request.Body.Ftime), } user_title, err := s.db.UpdateUserTitle(ctx, params) @@ -497,39 +485,3 @@ func (s Server) GetUserTitle(ctx context.Context, request oapi.GetUserTitleReque 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 -} diff --git a/modules/backend/main.go b/modules/backend/main.go index e7e6ec8..755e3ef 100644 --- a/modules/backend/main.go +++ b/modules/backend/main.go @@ -59,9 +59,10 @@ func main() { } defer rmqConn.Close() + publisher := rmq.NewPublisher(rmqConn) rpcClient := rmq.NewRPCClient(rmqConn, 30*time.Second) - server := handlers.NewServer(queries, rpcClient) + server := handlers.NewServer(queries, publisher, rpcClient) r.Use(cors.New(cors.Config{ AllowOrigins: []string{AppConfig.ServiceAddress}, diff --git a/modules/backend/queries.sql b/modules/backend/queries.sql index 19971e5..ff41cb1 100644 --- a/modules/backend/queries.sql +++ b/modules/backend/queries.sql @@ -23,37 +23,6 @@ FROM users as t LEFT JOIN images as i ON (t.avatar_id = i.id) WHERE t.id = sqlc.arg('id')::bigint; --- name: SearchUser :many -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 SELECT * @@ -400,14 +369,13 @@ FROM reviews WHERE review_id = sqlc.arg('review_id')::bigint; -- name: InsertUserTitle :one -INSERT INTO usertitles (user_id, title_id, status, rate, review_id, ctime) +INSERT INTO usertitles (user_id, title_id, status, rate, review_id) VALUES ( sqlc.arg('user_id')::bigint, sqlc.arg('title_id')::bigint, sqlc.arg('status')::usertitle_status_t, sqlc.narg('rate')::int, - sqlc.narg('review_id')::bigint, - sqlc.narg('ftime')::timestamptz + sqlc.narg('review_id')::bigint ) RETURNING user_id, title_id, status, rate, review_id, ctime; @@ -416,8 +384,7 @@ RETURNING user_id, title_id, status, rate, review_id, ctime; UPDATE usertitles SET status = COALESCE(sqlc.narg('status')::usertitle_status_t, status), - rate = COALESCE(sqlc.narg('rate')::int, rate), - ctime = COALESCE(sqlc.narg('ftime')::timestamptz, ctime) + rate = COALESCE(sqlc.narg('rate')::int, rate) WHERE user_id = sqlc.arg('user_id') AND title_id = sqlc.arg('title_id') diff --git a/modules/backend/rmq/rabbit.go b/modules/backend/rmq/rabbit.go index 25abbdb..52c1979 100644 --- a/modules/backend/rmq/rabbit.go +++ b/modules/backend/rmq/rabbit.go @@ -4,16 +4,13 @@ import ( "context" "encoding/json" "fmt" - "time" - oapi "nyanimedb/api" + "sync" + "time" amqp "github.com/rabbitmq/amqp091-go" ) -const RPCQueueName = "anime_import_rpc" - -// RabbitRequest не меняем type RabbitRequest struct { Name string `json:"name"` Statuses []oapi.TitleStatus `json:"statuses,omitempty"` @@ -23,6 +20,151 @@ type RabbitRequest struct { 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 { conn *amqp.Connection timeout time.Duration @@ -32,48 +174,37 @@ func NewRPCClient(conn *amqp.Connection, timeout time.Duration) *RPCClient { return &RPCClient{conn: conn, timeout: timeout} } +// Call отправляет запрос в очередь и ждёт ответа. +// replyPayload — указатель на структуру, в которую раскодировать ответ (например, &MediaResponse{}). func (c *RPCClient) Call( ctx context.Context, + requestQueue string, request RabbitRequest, replyPayload any, ) error { - - // 1. Канал для запроса и ответа + // 1. Создаём временный канал (не из пула!) ch, err := c.conn.Channel() if err != nil { return fmt.Errorf("channel: %w", err) } defer ch.Close() - // 2. Декларируем фиксированную очередь RPC (идемпотентно) - _, err = ch.QueueDeclare( - RPCQueueName, - true, // durable - false, // auto-delete - false, // exclusive - false, // no-wait - nil, - ) - if err != nil { - return fmt.Errorf("declare rpc queue: %w", err) - } - - // 3. Создаём временную очередь ДЛЯ ОТВЕТА - replyQueue, err := ch.QueueDeclare( - "", - false, - true, - true, + // 2. Создаём временную очередь для ответов + q, err := ch.QueueDeclare( + "", // auto name + false, // not durable + true, // exclusive + true, // auto-delete false, nil, ) if err != nil { - return fmt.Errorf("declare reply queue: %w", err) + return fmt.Errorf("reply queue: %w", err) } - // 4. Подписываемся на очередь ответов + // 3. Подписываемся на ответы msgs, err := ch.Consume( - replyQueue.Name, + q.Name, "", true, // auto-ack true, // exclusive @@ -82,28 +213,28 @@ func (c *RPCClient) Call( nil, ) if err != nil { - return fmt.Errorf("consume reply: %w", err) + return fmt.Errorf("consume: %w", err) } - // correlation ID - corrID := fmt.Sprintf("%d", time.Now().UnixNano()) + // 4. Готовим correlation ID + corrID := time.Now().UnixNano() - // 5. сериализация запроса + // 5. Сериализуем запрос body, err := json.Marshal(request) if err != nil { return fmt.Errorf("marshal request: %w", err) } - // 6. Публикация RPC-запроса + // 6. Публикуем запрос err = ch.Publish( "", - RPCQueueName, // ← фиксированная очередь! + requestQueue, false, false, amqp.Publishing{ ContentType: "application/json", - CorrelationId: corrID, - ReplyTo: replyQueue.Name, + CorrelationId: fmt.Sprintf("%d", corrID), + ReplyTo: q.Name, Timestamp: time.Now(), Body: body, }, @@ -113,17 +244,18 @@ func (c *RPCClient) Call( } // 7. Ждём ответ с таймаутом - timeoutCtx, cancel := context.WithTimeout(ctx, c.timeout) + ctx, cancel := context.WithTimeout(ctx, c.timeout) defer cancel() for { select { case msg := <-msgs: - if msg.CorrelationId == corrID { + if msg.CorrelationId == fmt.Sprintf("%d", corrID) { return json.Unmarshal(msg.Body, replyPayload) } - case <-timeoutCtx.Done(): - return fmt.Errorf("rpc timeout: %w", timeoutCtx.Err()) + // игнорируем другие сообщения (маловероятно, но возможно) + case <-ctx.Done(): + return ctx.Err() // timeout or cancelled } } } diff --git a/modules/frontend/src/api/client.gen.ts b/modules/frontend/src/api/client.gen.ts index 952c663..2de06ac 100644 --- a/modules/frontend/src/api/client.gen.ts +++ b/modules/frontend/src/api/client.gen.ts @@ -13,4 +13,4 @@ import type { ClientOptions as ClientOptions2 } from './types.gen'; */ export type CreateClientConfig = (override?: Config) => Config & T>; -export const client = createClient(createConfig({ baseUrl: '/api/v1' })); +export const client = createClient(createConfig({ baseUrl: 'http://10.1.0.65:8081/api/v1' })); diff --git a/modules/frontend/src/api/sdk.gen.ts b/modules/frontend/src/api/sdk.gen.ts index 7d46120..5359156 100644 --- a/modules/frontend/src/api/sdk.gen.ts +++ b/modules/frontend/src/api/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { AddUserTitleData, AddUserTitleErrors, AddUserTitleResponses, DeleteUserTitleData, DeleteUserTitleErrors, DeleteUserTitleResponses, GetTitleData, GetTitleErrors, GetTitleResponses, GetTitlesData, GetTitlesErrors, GetTitlesResponses, GetUsersData, GetUsersErrors, GetUsersIdData, GetUsersIdErrors, GetUsersIdResponses, GetUsersResponses, GetUserTitleData, GetUserTitleErrors, GetUserTitleResponses, GetUserTitlesData, GetUserTitlesErrors, GetUserTitlesResponses, UpdateUserData, UpdateUserErrors, UpdateUserResponses, UpdateUserTitleData, UpdateUserTitleErrors, UpdateUserTitleResponses } from './types.gen'; +import type { AddUserTitleData, AddUserTitleErrors, AddUserTitleResponses, DeleteUserTitleData, DeleteUserTitleErrors, DeleteUserTitleResponses, GetTitleData, GetTitleErrors, GetTitleResponses, GetTitlesData, GetTitlesErrors, GetTitlesResponses, GetUsersIdData, GetUsersIdErrors, GetUsersIdResponses, GetUserTitleData, GetUserTitleErrors, GetUserTitleResponses, GetUserTitlesData, GetUserTitlesErrors, GetUserTitlesResponses, UpdateUserData, UpdateUserErrors, UpdateUserResponses, UpdateUserTitleData, UpdateUserTitleErrors, UpdateUserTitleResponses } from './types.gen'; export type Options = Options2 & { /** @@ -32,11 +32,6 @@ export const getTitles = (options?: Option */ export const getTitle = (options: Options) => (options.client ?? client).get({ url: '/titles/{title_id}', ...options }); -/** - * Search user by nickname or dispname (both in one param), response is always sorted by id - */ -export const getUsers = (options?: Options) => (options?.client ?? client).get({ url: '/users/', ...options }); - /** * Get user info */ diff --git a/modules/frontend/src/api/types.gen.ts b/modules/frontend/src/api/types.gen.ts index d4526a7..ce4db4b 100644 --- a/modules/frontend/src/api/types.gen.ts +++ b/modules/frontend/src/api/types.gen.ts @@ -60,12 +60,6 @@ export type Title = { title_names: { [key: string]: Array; }; - /** - * Localized description. Key = language (ISO 639-1), value = description. - */ - title_desc?: { - [key: string]: string; - }; studio?: Studio; tags: Tags; poster?: Image; @@ -237,50 +231,6 @@ export type 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; - cursor: number; - }; - /** - * No users found - */ - 204: void; -}; - -export type GetUsersResponse = GetUsersResponses[keyof GetUsersResponses]; - export type GetUsersIdData = { body?: never; path: { diff --git a/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx b/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx index 727e072..481d116 100644 --- a/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx +++ b/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx @@ -127,16 +127,7 @@ const handleLoadMore = async () => { - {loading && ( -
- Loading... - Loading animation -
- )} + {loading &&
Loading...
} {!loading && titles.length === 0 && (
No titles found.
diff --git a/modules/frontend/src/pages/UsersPage/UsersPage.tsx b/modules/frontend/src/pages/UsersPage/UsersPage.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/sql/migrations/000001_init.up.sql b/sql/migrations/000001_init.up.sql index 369e455..3499fe2 100644 --- a/sql/migrations/000001_init.up.sql +++ b/sql/migrations/000001_init.up.sql @@ -33,6 +33,8 @@ CREATE TABLE users ( last_login timestamptz ); + + CREATE TABLE studios ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, studio_name text NOT NULL UNIQUE, @@ -45,8 +47,6 @@ CREATE TABLE titles ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- example {"ru": ["Атака титанов", "Атака Титанов"],"en": ["Attack on Titan", "AoT"],"ja": ["進撃の巨人", "しんげきのきょじん"]} 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), poster_id bigint REFERENCES images (id) ON DELETE SET NULL, title_status title_status_t NOT NULL, @@ -106,13 +106,12 @@ CREATE TABLE signals ( CREATE TABLE external_services ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - name text UNIQUE NOT NULL, - auth_token text + name text UNIQUE NOT NULL ); CREATE TABLE external_ids ( user_id bigint NOT NULL REFERENCES users (id), - service_id bigint NOT NULL REFERENCES external_services (id), + service_id bigint REFERENCES external_services (id), external_id text NOT NULL ); @@ -169,4 +168,17 @@ EXECUTE FUNCTION update_title_rating(); CREATE TRIGGER trg_notify_new_signal AFTER INSERT ON signals FOR EACH ROW -EXECUTE FUNCTION notify_new_signal(); \ No newline at end of file +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(); \ No newline at end of file diff --git a/sql/models.go b/sql/models.go index c299609..842d58c 100644 --- a/sql/models.go +++ b/sql/models.go @@ -188,14 +188,13 @@ func (ns NullUsertitleStatusT) Value() (driver.Value, error) { type ExternalID struct { UserID int64 `json:"user_id"` - ServiceID int64 `json:"service_id"` + ServiceID *int64 `json:"service_id"` ExternalID string `json:"external_id"` } type ExternalService struct { - ID int64 `json:"id"` - Name string `json:"name"` - AuthToken *string `json:"auth_token"` + ID int64 `json:"id"` + Name string `json:"name"` } type Image struct { @@ -247,7 +246,6 @@ type Tag struct { type Title struct { ID int64 `json:"id"` TitleNames json.RawMessage `json:"title_names"` - TitleDesc []byte `json:"title_desc"` StudioID int64 `json:"studio_id"` PosterID *int64 `json:"poster_id"` TitleStatus TitleStatusT `json:"title_status"` diff --git a/sql/queries.sql.go b/sql/queries.sql.go index 0384ccd..1cca986 100644 --- a/sql/queries.sql.go +++ b/sql/queries.sql.go @@ -9,8 +9,6 @@ import ( "context" "encoding/json" "time" - - "github.com/jackc/pgx/v5/pgtype" ) const createImage = `-- name: CreateImage :one @@ -76,19 +74,6 @@ func (q *Queries) DeleteUserTitle(ctx context.Context, arg DeleteUserTitleParams 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 SELECT id, storage_type, image_path FROM images @@ -144,7 +129,7 @@ func (q *Queries) GetStudioByID(ctx context.Context, studioID int64) (Studio, er const getTitleByID = `-- name: GetTitleByID :one SELECT - 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, + 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, i.storage_type as title_storage_type, i.image_path as title_image_path, COALESCE( @@ -172,7 +157,6 @@ GROUP BY type GetTitleByIDRow struct { ID int64 `json:"id"` TitleNames json.RawMessage `json:"title_names"` - TitleDesc []byte `json:"title_desc"` StudioID int64 `json:"studio_id"` PosterID *int64 `json:"poster_id"` TitleStatus TitleStatusT `json:"title_status"` @@ -201,7 +185,6 @@ func (q *Queries) GetTitleByID(ctx context.Context, titleID int64) (GetTitleByID err := row.Scan( &i.ID, &i.TitleNames, - &i.TitleDesc, &i.StudioID, &i.PosterID, &i.TitleStatus, @@ -253,35 +236,6 @@ func (q *Queries) GetTitleTags(ctx context.Context, titleID int64) ([]json.RawMe 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 SELECT t.id as id, @@ -438,25 +392,23 @@ func (q *Queries) InsertTitleTags(ctx context.Context, arg InsertTitleTagsParams } const insertUserTitle = `-- name: InsertUserTitle :one -INSERT INTO usertitles (user_id, title_id, status, rate, review_id, ctime) +INSERT INTO usertitles (user_id, title_id, status, rate, review_id) VALUES ( $1::bigint, $2::bigint, $3::usertitle_status_t, $4::int, - $5::bigint, - $6::timestamptz + $5::bigint ) RETURNING user_id, title_id, status, rate, review_id, ctime ` type InsertUserTitleParams struct { - UserID int64 `json:"user_id"` - TitleID int64 `json:"title_id"` - Status UsertitleStatusT `json:"status"` - Rate *int32 `json:"rate"` - ReviewID *int64 `json:"review_id"` - Ftime pgtype.Timestamptz `json:"ftime"` + UserID int64 `json:"user_id"` + TitleID int64 `json:"title_id"` + Status UsertitleStatusT `json:"status"` + Rate *int32 `json:"rate"` + ReviewID *int64 `json:"review_id"` } func (q *Queries) InsertUserTitle(ctx context.Context, arg InsertUserTitleParams) (Usertitle, error) { @@ -466,7 +418,6 @@ func (q *Queries) InsertUserTitle(ctx context.Context, arg InsertUserTitleParams arg.Status, arg.Rate, arg.ReviewID, - arg.Ftime, ) var i Usertitle err := row.Scan( @@ -687,87 +638,6 @@ func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]S 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 SELECT @@ -1064,20 +934,18 @@ const updateUserTitle = `-- name: UpdateUserTitle :one UPDATE usertitles SET status = COALESCE($1::usertitle_status_t, status), - rate = COALESCE($2::int, rate), - ctime = COALESCE($3::timestamptz, ctime) + rate = COALESCE($2::int, rate) WHERE - user_id = $4 - AND title_id = $5 + user_id = $3 + AND title_id = $4 RETURNING user_id, title_id, status, rate, review_id, ctime ` type UpdateUserTitleParams struct { - Status *UsertitleStatusT `json:"status"` - Rate *int32 `json:"rate"` - Ftime pgtype.Timestamptz `json:"ftime"` - UserID int64 `json:"user_id"` - TitleID int64 `json:"title_id"` + Status *UsertitleStatusT `json:"status"` + Rate *int32 `json:"rate"` + UserID int64 `json:"user_id"` + TitleID int64 `json:"title_id"` } // Fails with sql.ErrNoRows if (user_id, title_id) not found @@ -1085,7 +953,6 @@ func (q *Queries) UpdateUserTitle(ctx context.Context, arg UpdateUserTitleParams row := q.db.QueryRow(ctx, updateUserTitle, arg.Status, arg.Rate, - arg.Ftime, arg.UserID, arg.TitleID, )