diff --git a/Dockerfiles/Dockerfile_auth b/Dockerfiles/Dockerfile_auth deleted file mode 100644 index 5280e86..0000000 --- a/Dockerfiles/Dockerfile_auth +++ /dev/null @@ -1,6 +0,0 @@ -FROM ubuntu:22.04 - -WORKDIR /app -COPY --chmod=755 modules/auth/auth /app -EXPOSE 8082 -ENTRYPOINT ["/app/auth"] \ No newline at end of file diff --git a/api/_build/openapi.yaml b/api/_build/openapi.yaml index 6b39558..215eabc 100644 --- a/api/_build/openapi.yaml +++ b/api/_build/openapi.yaml @@ -141,82 +141,7 @@ paths: description: User not found '500': description: Unknown server error - patch: - summary: Partially update a user account - description: | - Update selected user profile fields (excluding password). - Password updates must be done via the dedicated auth-service (`/auth/`). - Fields not provided in the request body remain unchanged. - operationId: updateUser - parameters: - - name: user_id - in: path - required: true - schema: - type: integer - format: int64 - description: User ID (primary key) - example: 123 - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - avatar_id: - type: integer - format: int64 - nullable: true - description: ID of the user avatar (references `images.id`); set to `null` to remove avatar - example: 42 - mail: - type: string - format: email - pattern: '^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\\.[a-zA-Z0-9_-]+$' - description: User email (must be unique and valid) - example: john.doe.updated@example.com - nickname: - type: string - pattern: '^[a-zA-Z0-9_-]{3,16}$' - description: 'Username (alphanumeric + `_` or `-`, 3–16 chars)' - maxLength: 16 - minLength: 3 - example: john_doe_43 - disp_name: - type: string - description: Display name - maxLength: 32 - example: John Smith - user_desc: - type: string - description: User description / bio - maxLength: 512 - example: Just a curious developer. - additionalProperties: false - description: Only provided fields are updated. Omitted fields remain unchanged. - responses: - '200': - description: User updated successfully. Returns updated user representation (excluding sensitive fields). - content: - application/json: - schema: - $ref: '#/components/schemas/User' - '400': - description: 'Invalid input (e.g., validation failed, nickname/email conflict, malformed JSON)' - '401': - description: Unauthorized — missing or invalid authentication token - '403': - description: 'Forbidden — user is not allowed to modify this resource (e.g., not own profile & no admin rights)' - '404': - description: User not found - '409': - description: 'Conflict — e.g., requested `nickname` or `mail` already taken by another user' - '422': - description: 'Unprocessable Entity — semantic errors not caught by schema (e.g., invalid `avatar_id`)' - '500': - description: Unknown server error - '/users/{user_id}/titles': + '/users/{user_id}/titles/': get: summary: Get user titles parameters: @@ -304,74 +229,8 @@ paths: description: No titles found '400': description: Request params are not correct - '404': - description: User not found '500': description: Unknown server error - post: - summary: Add a title to a user - description: 'User adding title to list af watched, status required' - operationId: addUserTitle - parameters: - - name: user_id - in: path - required: true - schema: - type: integer - format: int64 - description: ID of the user to assign the title to - example: 123 - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UserTitle' - responses: - '200': - description: Title successfully added to user - content: - application/json: - schema: - type: object - properties: - data: - type: object - required: - - user_id - - title_id - - status - properties: - user_id: - type: integer - format: int64 - title_id: - type: integer - format: int64 - status: - $ref: '#/components/schemas/UserTitleStatus' - rate: - type: integer - format: int32 - review_id: - type: integer - format: int64 - ctime: - type: string - format: date-time - additionalProperties: false - '400': - description: 'Invalid request body (missing fields, invalid types, etc.)' - '401': - description: Unauthorized — missing or invalid auth token - '403': - description: Forbidden — user not allowed to assign titles to this user - '404': - description: User or Title not found - '409': - description: Conflict — title already assigned to user (if applicable) - '500': - description: Internal server error components: parameters: cursor: @@ -414,10 +273,6 @@ components: format: int64 storage_type: type: string - description: Image storage type - enum: - - s3 - - local image_path: type: string TitleStatus: @@ -554,8 +409,11 @@ components: format: int64 description: Unique user ID (primary key) example: 1 - image: - $ref: '#/components/schemas/Image' + avatar_id: + type: integer + format: int64 + description: ID of the user avatar (references images table) + example: null mail: type: string format: email diff --git a/api/api.gen.go b/api/api.gen.go index f3e935c..dcc2f89 100644 --- a/api/api.gen.go +++ b/api/api.gen.go @@ -16,12 +16,6 @@ import ( openapi_types "github.com/oapi-codegen/runtime/types" ) -// Defines values for ImageStorageType. -const ( - Local ImageStorageType = "local" - S3 ImageStorageType = "s3" -) - // Defines values for ReleaseSeason. const ( Fall ReleaseSeason = "fall" @@ -61,16 +55,11 @@ type CursorObj struct { // Image defines model for Image. type Image struct { - Id *int64 `json:"id,omitempty"` - ImagePath *string `json:"image_path,omitempty"` - - // StorageType Image storage type - StorageType *ImageStorageType `json:"storage_type,omitempty"` + Id *int64 `json:"id,omitempty"` + ImagePath *string `json:"image_path,omitempty"` + StorageType *string `json:"storage_type,omitempty"` } -// ImageStorageType Image storage type -type ImageStorageType string - // ReleaseSeason Title release season type ReleaseSeason string @@ -124,6 +113,9 @@ type TitleStatus string // User defines model for User. type User struct { + // AvatarId ID of the user avatar (references images table) + AvatarId *int64 `json:"avatar_id,omitempty"` + // CreationDate Timestamp when the user was created CreationDate *time.Time `json:"creation_date,omitempty"` @@ -131,8 +123,7 @@ type User struct { DispName *string `json:"disp_name,omitempty"` // Id Unique user ID (primary key) - Id *int64 `json:"id,omitempty"` - Image *Image `json:"image,omitempty"` + Id *int64 `json:"id,omitempty"` // Mail User email Mail *openapi_types.Email `json:"mail,omitempty"` @@ -190,24 +181,6 @@ type GetUsersUserIdParams struct { Fields *string `form:"fields,omitempty" json:"fields,omitempty"` } -// UpdateUserJSONBody defines parameters for UpdateUser. -type UpdateUserJSONBody struct { - // AvatarId ID of the user avatar (references `images.id`); set to `null` to remove avatar - AvatarId *int64 `json:"avatar_id"` - - // DispName Display name - DispName *string `json:"disp_name,omitempty"` - - // Mail User email (must be unique and valid) - Mail *openapi_types.Email `json:"mail,omitempty"` - - // Nickname Username (alphanumeric + `_` or `-`, 3–16 chars) - Nickname *string `json:"nickname,omitempty"` - - // UserDesc User description / bio - UserDesc *string `json:"user_desc,omitempty"` -} - // GetUsersUserIdTitlesParams defines parameters for GetUsersUserIdTitles. type GetUsersUserIdTitlesParams struct { Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"` @@ -226,12 +199,6 @@ type GetUsersUserIdTitlesParams struct { Fields *string `form:"fields,omitempty" json:"fields,omitempty"` } -// UpdateUserJSONRequestBody defines body for UpdateUser for application/json ContentType. -type UpdateUserJSONRequestBody UpdateUserJSONBody - -// AddUserTitleJSONRequestBody defines body for AddUserTitle for application/json ContentType. -type AddUserTitleJSONRequestBody = UserTitle - // Getter for additional properties for Title. Returns the specified // element and whether it was found func (a Title) Get(fieldName string) (value interface{}, found bool) { @@ -624,15 +591,9 @@ type ServerInterface interface { // Get user info // (GET /users/{user_id}) GetUsersUserId(c *gin.Context, userId string, params GetUsersUserIdParams) - // Partially update a user account - // (PATCH /users/{user_id}) - UpdateUser(c *gin.Context, userId int64) // Get user titles - // (GET /users/{user_id}/titles) + // (GET /users/{user_id}/titles/) GetUsersUserIdTitles(c *gin.Context, userId string, params GetUsersUserIdTitlesParams) - // Add a title to a user - // (POST /users/{user_id}/titles) - AddUserTitle(c *gin.Context, userId int64) } // ServerInterfaceWrapper converts contexts to parameters. @@ -820,30 +781,6 @@ func (siw *ServerInterfaceWrapper) GetUsersUserId(c *gin.Context) { siw.Handler.GetUsersUserId(c, userId, params) } -// UpdateUser operation middleware -func (siw *ServerInterfaceWrapper) UpdateUser(c *gin.Context) { - - var err error - - // ------------- Path parameter "user_id" ------------- - var userId int64 - - err = runtime.BindStyledParameterWithOptions("simple", "user_id", c.Param("user_id"), &userId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter user_id: %w", err), http.StatusBadRequest) - return - } - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.UpdateUser(c, userId) -} - // GetUsersUserIdTitles operation middleware func (siw *ServerInterfaceWrapper) GetUsersUserIdTitles(c *gin.Context) { @@ -967,30 +904,6 @@ func (siw *ServerInterfaceWrapper) GetUsersUserIdTitles(c *gin.Context) { siw.Handler.GetUsersUserIdTitles(c, userId, params) } -// AddUserTitle operation middleware -func (siw *ServerInterfaceWrapper) AddUserTitle(c *gin.Context) { - - var err error - - // ------------- Path parameter "user_id" ------------- - var userId int64 - - err = runtime.BindStyledParameterWithOptions("simple", "user_id", c.Param("user_id"), &userId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter user_id: %w", err), http.StatusBadRequest) - return - } - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.AddUserTitle(c, userId) -} - // GinServerOptions provides options for the Gin server. type GinServerOptions struct { BaseURL string @@ -1021,9 +934,7 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options router.GET(options.BaseURL+"/titles", wrapper.GetTitles) router.GET(options.BaseURL+"/titles/:title_id", wrapper.GetTitlesTitleId) router.GET(options.BaseURL+"/users/:user_id", wrapper.GetUsersUserId) - router.PATCH(options.BaseURL+"/users/:user_id", wrapper.UpdateUser) - router.GET(options.BaseURL+"/users/:user_id/titles", wrapper.GetUsersUserIdTitles) - router.POST(options.BaseURL+"/users/:user_id/titles", wrapper.AddUserTitle) + router.GET(options.BaseURL+"/users/:user_id/titles/", wrapper.GetUsersUserIdTitles) } type GetTitlesRequestObject struct { @@ -1164,80 +1075,6 @@ func (response GetUsersUserId500Response) VisitGetUsersUserIdResponse(w http.Res return nil } -type UpdateUserRequestObject struct { - UserId int64 `json:"user_id"` - Body *UpdateUserJSONRequestBody -} - -type UpdateUserResponseObject interface { - VisitUpdateUserResponse(w http.ResponseWriter) error -} - -type UpdateUser200JSONResponse User - -func (response UpdateUser200JSONResponse) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type UpdateUser400Response struct { -} - -func (response UpdateUser400Response) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.WriteHeader(400) - return nil -} - -type UpdateUser401Response struct { -} - -func (response UpdateUser401Response) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.WriteHeader(401) - return nil -} - -type UpdateUser403Response struct { -} - -func (response UpdateUser403Response) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.WriteHeader(403) - return nil -} - -type UpdateUser404Response struct { -} - -func (response UpdateUser404Response) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.WriteHeader(404) - return nil -} - -type UpdateUser409Response struct { -} - -func (response UpdateUser409Response) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.WriteHeader(409) - return nil -} - -type UpdateUser422Response struct { -} - -func (response UpdateUser422Response) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.WriteHeader(422) - return nil -} - -type UpdateUser500Response struct { -} - -func (response UpdateUser500Response) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.WriteHeader(500) - return nil -} - type GetUsersUserIdTitlesRequestObject struct { UserId string `json:"user_id"` Params GetUsersUserIdTitlesParams @@ -1275,14 +1112,6 @@ func (response GetUsersUserIdTitles400Response) VisitGetUsersUserIdTitlesRespons return nil } -type GetUsersUserIdTitles404Response struct { -} - -func (response GetUsersUserIdTitles404Response) VisitGetUsersUserIdTitlesResponse(w http.ResponseWriter) error { - w.WriteHeader(404) - return nil -} - type GetUsersUserIdTitles500Response struct { } @@ -1291,83 +1120,6 @@ func (response GetUsersUserIdTitles500Response) VisitGetUsersUserIdTitlesRespons return nil } -type AddUserTitleRequestObject struct { - UserId int64 `json:"user_id"` - Body *AddUserTitleJSONRequestBody -} - -type AddUserTitleResponseObject interface { - VisitAddUserTitleResponse(w http.ResponseWriter) error -} - -type AddUserTitle200JSONResponse struct { - Data *struct { - Ctime *time.Time `json:"ctime,omitempty"` - Rate *int32 `json:"rate,omitempty"` - ReviewId *int64 `json:"review_id,omitempty"` - - // Status User's title status - Status UserTitleStatus `json:"status"` - TitleId int64 `json:"title_id"` - UserId int64 `json:"user_id"` - } `json:"data,omitempty"` -} - -func (response AddUserTitle200JSONResponse) VisitAddUserTitleResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type AddUserTitle400Response struct { -} - -func (response AddUserTitle400Response) VisitAddUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(400) - return nil -} - -type AddUserTitle401Response struct { -} - -func (response AddUserTitle401Response) VisitAddUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(401) - return nil -} - -type AddUserTitle403Response struct { -} - -func (response AddUserTitle403Response) VisitAddUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(403) - return nil -} - -type AddUserTitle404Response struct { -} - -func (response AddUserTitle404Response) VisitAddUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(404) - return nil -} - -type AddUserTitle409Response struct { -} - -func (response AddUserTitle409Response) VisitAddUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(409) - return nil -} - -type AddUserTitle500Response struct { -} - -func (response AddUserTitle500Response) VisitAddUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(500) - return nil -} - // StrictServerInterface represents all server handlers. type StrictServerInterface interface { // Get titles @@ -1379,15 +1131,9 @@ type StrictServerInterface interface { // Get user info // (GET /users/{user_id}) GetUsersUserId(ctx context.Context, request GetUsersUserIdRequestObject) (GetUsersUserIdResponseObject, error) - // Partially update a user account - // (PATCH /users/{user_id}) - UpdateUser(ctx context.Context, request UpdateUserRequestObject) (UpdateUserResponseObject, error) // Get user titles - // (GET /users/{user_id}/titles) + // (GET /users/{user_id}/titles/) GetUsersUserIdTitles(ctx context.Context, request GetUsersUserIdTitlesRequestObject) (GetUsersUserIdTitlesResponseObject, error) - // Add a title to a user - // (POST /users/{user_id}/titles) - AddUserTitle(ctx context.Context, request AddUserTitleRequestObject) (AddUserTitleResponseObject, error) } type StrictHandlerFunc = strictgin.StrictGinHandlerFunc @@ -1485,41 +1231,6 @@ func (sh *strictHandler) GetUsersUserId(ctx *gin.Context, userId string, params } } -// UpdateUser operation middleware -func (sh *strictHandler) UpdateUser(ctx *gin.Context, userId int64) { - var request UpdateUserRequestObject - - request.UserId = userId - - var body UpdateUserJSONRequestBody - 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.UpdateUser(ctx, request.(UpdateUserRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "UpdateUser") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(UpdateUserResponseObject); ok { - if err := validResponse.VisitUpdateUserResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - // GetUsersUserIdTitles operation middleware func (sh *strictHandler) GetUsersUserIdTitles(ctx *gin.Context, userId string, params GetUsersUserIdTitlesParams) { var request GetUsersUserIdTitlesRequestObject @@ -1547,38 +1258,3 @@ func (sh *strictHandler) GetUsersUserIdTitles(ctx *gin.Context, userId string, p ctx.Error(fmt.Errorf("unexpected response type: %T", response)) } } - -// AddUserTitle operation middleware -func (sh *strictHandler) AddUserTitle(ctx *gin.Context, userId int64) { - var request AddUserTitleRequestObject - - request.UserId = userId - - var body AddUserTitleJSONRequestBody - 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.AddUserTitle(ctx, request.(AddUserTitleRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "AddUserTitle") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(AddUserTitleResponseObject); ok { - if err := validResponse.VisitAddUserTitleResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} diff --git a/api/openapi.yaml b/api/openapi.yaml index 7da26f8..c8bdbc4 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -13,9 +13,8 @@ paths: $ref: "./paths/titles-id.yaml" /users/{user_id}: $ref: "./paths/users-id.yaml" - /users/{user_id}/titles: + /users/{user_id}/titles/: $ref: "./paths/users-id-titles.yaml" - components: parameters: $ref: "./parameters/_index.yaml" diff --git a/api/paths/users-id-titles.yaml b/api/paths/users-id-titles.yaml index 23ea761..a76cc40 100644 --- a/api/paths/users-id-titles.yaml +++ b/api/paths/users-id-titles.yaml @@ -85,59 +85,5 @@ get: description: No titles found '400': description: Request params are not correct - '404': - description: User not found '500': description: Unknown server error - -post: - summary: Add a title to a user - description: User adding title to list af watched, status required - operationId: addUserTitle - parameters: - - name: user_id - in: path - required: true - schema: - type: integer - format: int64 - description: ID of the user to assign the title to - example: 123 - requestBody: - required: true - content: - application/json: - schema: - $ref: '../schemas/UserTitle.yaml' - responses: - '200': - description: Title successfully added to user - content: - application/json: - schema: - type: object - properties: - # success: - # type: boolean - # example: true - # error: - # type: string - # nullable: true - # example: null - data: - $ref: '../schemas/UserTitleMini.yaml' - # required: - # - success - # - error - '400': - description: Invalid request body (missing fields, invalid types, etc.) - '401': - description: Unauthorized — missing or invalid auth token - '403': - description: Forbidden — user not allowed to assign titles to this user - '404': - description: User or Title not found - '409': - description: Conflict — title already assigned to user (if applicable) - '500': - description: Internal server error \ No newline at end of file diff --git a/api/paths/users-id.yaml b/api/paths/users-id.yaml index 06f4a19..0acdb81 100644 --- a/api/paths/users-id.yaml +++ b/api/paths/users-id.yaml @@ -24,79 +24,3 @@ get: description: Request params are not correct '500': description: Unknown server error - -patch: - summary: Partially update a user account - description: | - Update selected user profile fields (excluding password). - Password updates must be done via the dedicated auth-service (`/auth/`). - Fields not provided in the request body remain unchanged. - operationId: updateUser - parameters: - - name: user_id - in: path - required: true - schema: - type: integer - format: int64 - description: User ID (primary key) - example: 123 - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - avatar_id: - type: integer - format: int64 - nullable: true - description: ID of the user avatar (references `images.id`); set to `null` to remove avatar - example: 42 - mail: - type: string - format: email - pattern: '^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\\.[a-zA-Z0-9_-]+$' - description: User email (must be unique and valid) - example: john.doe.updated@example.com - nickname: - type: string - pattern: '^[a-zA-Z0-9_-]{3,16}$' - description: Username (alphanumeric + `_` or `-`, 3–16 chars) - maxLength: 16 - minLength: 3 - example: john_doe_43 - disp_name: - type: string - description: Display name - maxLength: 32 - example: John Smith - user_desc: - type: string - description: User description / bio - maxLength: 512 - example: Just a curious developer. - additionalProperties: false - description: Only provided fields are updated. Omitted fields remain unchanged. - responses: - '200': - description: User updated successfully. Returns updated user representation (excluding sensitive fields). - content: - application/json: - schema: - $ref: '../schemas/User.yaml' - '400': - description: Invalid input (e.g., validation failed, nickname/email conflict, malformed JSON) - '401': - description: Unauthorized — missing or invalid authentication token - '403': - description: Forbidden — user is not allowed to modify this resource (e.g., not own profile & no admin rights) - '404': - description: User not found - '409': - description: Conflict — e.g., requested `nickname` or `mail` already taken by another user - '422': - description: Unprocessable Entity — semantic errors not caught by schema (e.g., invalid `avatar_id`) - '500': - description: Unknown server error diff --git a/api/schemas/Image.yaml b/api/schemas/Image.yaml index 3fb520b..4ae3cb7 100644 --- a/api/schemas/Image.yaml +++ b/api/schemas/Image.yaml @@ -5,6 +5,6 @@ properties: type: integer format: int64 storage_type: - $ref: './enums/StorageType.yaml' + type: string image_path: type: string diff --git a/api/schemas/User.yaml b/api/schemas/User.yaml index 4e53534..8b4d88d 100644 --- a/api/schemas/User.yaml +++ b/api/schemas/User.yaml @@ -5,8 +5,11 @@ properties: format: int64 description: Unique user ID (primary key) example: 1 - image: - $ref: '../schemas/Image.yaml' + avatar_id: + type: integer + format: int64 + description: ID of the user avatar (references images table) + example: null mail: type: string format: email diff --git a/api/schemas/UserTitleMini.yaml b/api/schemas/UserTitleMini.yaml deleted file mode 100644 index 9e45e95..0000000 --- a/api/schemas/UserTitleMini.yaml +++ /dev/null @@ -1,24 +0,0 @@ -type: object -required: - - user_id - - title_id - - status -properties: - user_id: - type: integer - format: int64 - title_id: - type: integer - format: int64 - status: - $ref: ./enums/UserTitleStatus.yaml - rate: - type: integer - format: int32 - review_id: - type: integer - format: int64 - ctime: - type: string - format: date-time -additionalProperties: false diff --git a/api/schemas/enums/StorageType.yaml b/api/schemas/enums/StorageType.yaml deleted file mode 100644 index 1984a87..0000000 --- a/api/schemas/enums/StorageType.yaml +++ /dev/null @@ -1,5 +0,0 @@ -type: string -description: Image storage type -enum: - - s3 - - local \ No newline at end of file diff --git a/api/schemas/updateUser.yaml b/api/schemas/updateUser.yaml deleted file mode 100644 index e1d77af..0000000 --- a/api/schemas/updateUser.yaml +++ /dev/null @@ -1,26 +0,0 @@ -type: object -properties: - avatar_id: - type: integer - format: int64 - nullable: true - description: ID of the user avatar (references `images.id`); set to `null` to remove avatar - example: 42 - mail: - type: string - format: email - pattern: '^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\\.[a-zA-Z0-9_-]+$' - description: User email (must be unique and valid) - example: john.doe.updated@example.com - disp_name: - type: string - description: Display name - maxLength: 32 - example: John Smith - user_desc: - type: string - description: User description / bio - maxLength: 512 - example: Just a curious developer. -additionalProperties: false -description: Only provided fields are updated. Omitted fields remain unchanged. \ No newline at end of file diff --git a/auth/auth.gen.go b/auth/auth.gen.go deleted file mode 100644 index adb2b06..0000000 --- a/auth/auth.gen.go +++ /dev/null @@ -1,249 +0,0 @@ -// Package auth provides primitives to interact with the openapi HTTP API. -// -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.0 DO NOT EDIT. -package auth - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - - "github.com/gin-gonic/gin" - strictgin "github.com/oapi-codegen/runtime/strictmiddleware/gin" -) - -// PostAuthSignInJSONBody defines parameters for PostAuthSignIn. -type PostAuthSignInJSONBody struct { - Nickname string `json:"nickname"` - Pass string `json:"pass"` -} - -// PostAuthSignUpJSONBody defines parameters for PostAuthSignUp. -type PostAuthSignUpJSONBody struct { - Nickname string `json:"nickname"` - Pass string `json:"pass"` -} - -// PostAuthSignInJSONRequestBody defines body for PostAuthSignIn for application/json ContentType. -type PostAuthSignInJSONRequestBody PostAuthSignInJSONBody - -// PostAuthSignUpJSONRequestBody defines body for PostAuthSignUp for application/json ContentType. -type PostAuthSignUpJSONRequestBody PostAuthSignUpJSONBody - -// ServerInterface represents all server handlers. -type ServerInterface interface { - // Sign in a user and return JWT - // (POST /auth/sign-in) - PostAuthSignIn(c *gin.Context) - // Sign up a new user - // (POST /auth/sign-up) - PostAuthSignUp(c *gin.Context) -} - -// ServerInterfaceWrapper converts contexts to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface - HandlerMiddlewares []MiddlewareFunc - ErrorHandler func(*gin.Context, error, int) -} - -type MiddlewareFunc func(c *gin.Context) - -// PostAuthSignIn operation middleware -func (siw *ServerInterfaceWrapper) PostAuthSignIn(c *gin.Context) { - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.PostAuthSignIn(c) -} - -// PostAuthSignUp operation middleware -func (siw *ServerInterfaceWrapper) PostAuthSignUp(c *gin.Context) { - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.PostAuthSignUp(c) -} - -// GinServerOptions provides options for the Gin server. -type GinServerOptions struct { - BaseURL string - Middlewares []MiddlewareFunc - ErrorHandler func(*gin.Context, error, int) -} - -// RegisterHandlers creates http.Handler with routing matching OpenAPI spec. -func RegisterHandlers(router gin.IRouter, si ServerInterface) { - RegisterHandlersWithOptions(router, si, GinServerOptions{}) -} - -// RegisterHandlersWithOptions creates http.Handler with additional options -func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options GinServerOptions) { - errorHandler := options.ErrorHandler - if errorHandler == nil { - errorHandler = func(c *gin.Context, err error, statusCode int) { - c.JSON(statusCode, gin.H{"msg": err.Error()}) - } - } - - wrapper := ServerInterfaceWrapper{ - Handler: si, - HandlerMiddlewares: options.Middlewares, - ErrorHandler: errorHandler, - } - - router.POST(options.BaseURL+"/auth/sign-in", wrapper.PostAuthSignIn) - router.POST(options.BaseURL+"/auth/sign-up", wrapper.PostAuthSignUp) -} - -type PostAuthSignInRequestObject struct { - Body *PostAuthSignInJSONRequestBody -} - -type PostAuthSignInResponseObject interface { - VisitPostAuthSignInResponse(w http.ResponseWriter) error -} - -type PostAuthSignIn200JSONResponse struct { - Error *string `json:"error"` - Success *bool `json:"success,omitempty"` - UserId *string `json:"user_id"` -} - -func (response PostAuthSignIn200JSONResponse) VisitPostAuthSignInResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type PostAuthSignIn401JSONResponse struct { - Error *string `json:"error,omitempty"` -} - -func (response PostAuthSignIn401JSONResponse) VisitPostAuthSignInResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(401) - - return json.NewEncoder(w).Encode(response) -} - -type PostAuthSignUpRequestObject struct { - Body *PostAuthSignUpJSONRequestBody -} - -type PostAuthSignUpResponseObject interface { - VisitPostAuthSignUpResponse(w http.ResponseWriter) error -} - -type PostAuthSignUp200JSONResponse struct { - Error *string `json:"error"` - Success *bool `json:"success,omitempty"` - UserId *string `json:"user_id"` -} - -func (response PostAuthSignUp200JSONResponse) VisitPostAuthSignUpResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -// StrictServerInterface represents all server handlers. -type StrictServerInterface interface { - // Sign in a user and return JWT - // (POST /auth/sign-in) - PostAuthSignIn(ctx context.Context, request PostAuthSignInRequestObject) (PostAuthSignInResponseObject, error) - // Sign up a new user - // (POST /auth/sign-up) - PostAuthSignUp(ctx context.Context, request PostAuthSignUpRequestObject) (PostAuthSignUpResponseObject, error) -} - -type StrictHandlerFunc = strictgin.StrictGinHandlerFunc -type StrictMiddlewareFunc = strictgin.StrictGinMiddlewareFunc - -func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { - return &strictHandler{ssi: ssi, middlewares: middlewares} -} - -type strictHandler struct { - ssi StrictServerInterface - middlewares []StrictMiddlewareFunc -} - -// PostAuthSignIn operation middleware -func (sh *strictHandler) PostAuthSignIn(ctx *gin.Context) { - var request PostAuthSignInRequestObject - - var body PostAuthSignInJSONRequestBody - 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.PostAuthSignIn(ctx, request.(PostAuthSignInRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "PostAuthSignIn") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(PostAuthSignInResponseObject); ok { - if err := validResponse.VisitPostAuthSignInResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - -// PostAuthSignUp operation middleware -func (sh *strictHandler) PostAuthSignUp(ctx *gin.Context) { - var request PostAuthSignUpRequestObject - - var body PostAuthSignUpJSONRequestBody - 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.PostAuthSignUp(ctx, request.(PostAuthSignUpRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "PostAuthSignUp") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(PostAuthSignUpResponseObject); ok { - if err := validResponse.VisitPostAuthSignUpResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} diff --git a/auth/auth/auth.gen.go b/auth/auth/auth.gen.go deleted file mode 100644 index 12b6622..0000000 --- a/auth/auth/auth.gen.go +++ /dev/null @@ -1,329 +0,0 @@ -// Package oapi_auth provides primitives to interact with the openapi HTTP API. -// -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.0 DO NOT EDIT. -package oapi_auth - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - - "github.com/gin-gonic/gin" - strictgin "github.com/oapi-codegen/runtime/strictmiddleware/gin" -) - -// PostAuthSignInJSONBody defines parameters for PostAuthSignIn. -type PostAuthSignInJSONBody struct { - Nickname string `json:"nickname"` - Pass string `json:"pass"` -} - -// PostAuthSignUpJSONBody defines parameters for PostAuthSignUp. -type PostAuthSignUpJSONBody struct { - Nickname string `json:"nickname"` - Pass string `json:"pass"` -} - -// PostAuthVerifyTokenJSONBody defines parameters for PostAuthVerifyToken. -type PostAuthVerifyTokenJSONBody struct { - // Token JWT token to validate - Token string `json:"token"` -} - -// PostAuthSignInJSONRequestBody defines body for PostAuthSignIn for application/json ContentType. -type PostAuthSignInJSONRequestBody PostAuthSignInJSONBody - -// PostAuthSignUpJSONRequestBody defines body for PostAuthSignUp for application/json ContentType. -type PostAuthSignUpJSONRequestBody PostAuthSignUpJSONBody - -// PostAuthVerifyTokenJSONRequestBody defines body for PostAuthVerifyToken for application/json ContentType. -type PostAuthVerifyTokenJSONRequestBody PostAuthVerifyTokenJSONBody - -// ServerInterface represents all server handlers. -type ServerInterface interface { - // Sign in a user and return JWT - // (POST /auth/sign-in) - PostAuthSignIn(c *gin.Context) - // Sign up a new user - // (POST /auth/sign-up) - PostAuthSignUp(c *gin.Context) - // Verify JWT validity - // (POST /auth/verify-token) - PostAuthVerifyToken(c *gin.Context) -} - -// ServerInterfaceWrapper converts contexts to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface - HandlerMiddlewares []MiddlewareFunc - ErrorHandler func(*gin.Context, error, int) -} - -type MiddlewareFunc func(c *gin.Context) - -// PostAuthSignIn operation middleware -func (siw *ServerInterfaceWrapper) PostAuthSignIn(c *gin.Context) { - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.PostAuthSignIn(c) -} - -// PostAuthSignUp operation middleware -func (siw *ServerInterfaceWrapper) PostAuthSignUp(c *gin.Context) { - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.PostAuthSignUp(c) -} - -// PostAuthVerifyToken operation middleware -func (siw *ServerInterfaceWrapper) PostAuthVerifyToken(c *gin.Context) { - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.PostAuthVerifyToken(c) -} - -// GinServerOptions provides options for the Gin server. -type GinServerOptions struct { - BaseURL string - Middlewares []MiddlewareFunc - ErrorHandler func(*gin.Context, error, int) -} - -// RegisterHandlers creates http.Handler with routing matching OpenAPI spec. -func RegisterHandlers(router gin.IRouter, si ServerInterface) { - RegisterHandlersWithOptions(router, si, GinServerOptions{}) -} - -// RegisterHandlersWithOptions creates http.Handler with additional options -func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options GinServerOptions) { - errorHandler := options.ErrorHandler - if errorHandler == nil { - errorHandler = func(c *gin.Context, err error, statusCode int) { - c.JSON(statusCode, gin.H{"msg": err.Error()}) - } - } - - wrapper := ServerInterfaceWrapper{ - Handler: si, - HandlerMiddlewares: options.Middlewares, - ErrorHandler: errorHandler, - } - - router.POST(options.BaseURL+"/auth/sign-in", wrapper.PostAuthSignIn) - router.POST(options.BaseURL+"/auth/sign-up", wrapper.PostAuthSignUp) - router.POST(options.BaseURL+"/auth/verify-token", wrapper.PostAuthVerifyToken) -} - -type PostAuthSignInRequestObject struct { - Body *PostAuthSignInJSONRequestBody -} - -type PostAuthSignInResponseObject interface { - VisitPostAuthSignInResponse(w http.ResponseWriter) error -} - -type PostAuthSignIn200JSONResponse struct { - Error *string `json:"error"` - Success *bool `json:"success,omitempty"` - - // Token JWT token to access protected endpoints - Token *string `json:"token"` - UserId *string `json:"user_id"` -} - -func (response PostAuthSignIn200JSONResponse) VisitPostAuthSignInResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type PostAuthSignUpRequestObject struct { - Body *PostAuthSignUpJSONRequestBody -} - -type PostAuthSignUpResponseObject interface { - VisitPostAuthSignUpResponse(w http.ResponseWriter) error -} - -type PostAuthSignUp200JSONResponse struct { - Error *string `json:"error"` - Success *bool `json:"success,omitempty"` - UserId *string `json:"user_id"` -} - -func (response PostAuthSignUp200JSONResponse) VisitPostAuthSignUpResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type PostAuthVerifyTokenRequestObject struct { - Body *PostAuthVerifyTokenJSONRequestBody -} - -type PostAuthVerifyTokenResponseObject interface { - VisitPostAuthVerifyTokenResponse(w http.ResponseWriter) error -} - -type PostAuthVerifyToken200JSONResponse struct { - // Error Error message if token is invalid - Error *string `json:"error"` - - // UserId User ID extracted from token if valid - UserId *string `json:"user_id"` - - // Valid True if token is valid - Valid *bool `json:"valid,omitempty"` -} - -func (response PostAuthVerifyToken200JSONResponse) VisitPostAuthVerifyTokenResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -// StrictServerInterface represents all server handlers. -type StrictServerInterface interface { - // Sign in a user and return JWT - // (POST /auth/sign-in) - PostAuthSignIn(ctx context.Context, request PostAuthSignInRequestObject) (PostAuthSignInResponseObject, error) - // Sign up a new user - // (POST /auth/sign-up) - PostAuthSignUp(ctx context.Context, request PostAuthSignUpRequestObject) (PostAuthSignUpResponseObject, error) - // Verify JWT validity - // (POST /auth/verify-token) - PostAuthVerifyToken(ctx context.Context, request PostAuthVerifyTokenRequestObject) (PostAuthVerifyTokenResponseObject, error) -} - -type StrictHandlerFunc = strictgin.StrictGinHandlerFunc -type StrictMiddlewareFunc = strictgin.StrictGinMiddlewareFunc - -func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { - return &strictHandler{ssi: ssi, middlewares: middlewares} -} - -type strictHandler struct { - ssi StrictServerInterface - middlewares []StrictMiddlewareFunc -} - -// PostAuthSignIn operation middleware -func (sh *strictHandler) PostAuthSignIn(ctx *gin.Context) { - var request PostAuthSignInRequestObject - - var body PostAuthSignInJSONRequestBody - 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.PostAuthSignIn(ctx, request.(PostAuthSignInRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "PostAuthSignIn") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(PostAuthSignInResponseObject); ok { - if err := validResponse.VisitPostAuthSignInResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - -// PostAuthSignUp operation middleware -func (sh *strictHandler) PostAuthSignUp(ctx *gin.Context) { - var request PostAuthSignUpRequestObject - - var body PostAuthSignUpJSONRequestBody - 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.PostAuthSignUp(ctx, request.(PostAuthSignUpRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "PostAuthSignUp") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(PostAuthSignUpResponseObject); ok { - if err := validResponse.VisitPostAuthSignUpResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - -// PostAuthVerifyToken operation middleware -func (sh *strictHandler) PostAuthVerifyToken(ctx *gin.Context) { - var request PostAuthVerifyTokenRequestObject - - var body PostAuthVerifyTokenJSONRequestBody - 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.PostAuthVerifyToken(ctx, request.(PostAuthVerifyTokenRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "PostAuthVerifyToken") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(PostAuthVerifyTokenResponseObject); ok { - if err := validResponse.VisitPostAuthVerifyTokenResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} diff --git a/auth/oapi-auth-codegen.yaml b/auth/oapi-auth-codegen.yaml deleted file mode 100644 index 6792391..0000000 --- a/auth/oapi-auth-codegen.yaml +++ /dev/null @@ -1,6 +0,0 @@ -package: auth -generate: - strict-server: true - gin-server: true - models: true -output: auth/auth.gen.go \ No newline at end of file diff --git a/auth/openapi-auth.yaml b/auth/openapi-auth.yaml deleted file mode 100644 index 913c000..0000000 --- a/auth/openapi-auth.yaml +++ /dev/null @@ -1,170 +0,0 @@ -openapi: 3.1.1 -info: - title: Auth Service - version: 1.0.0 - -servers: - - url: /auth - -paths: - /auth/sign-up: - post: - summary: Sign up a new user - tags: [Auth] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: [nickname, pass] - properties: - nickname: - type: string - pass: - type: string - format: password - responses: - "200": - description: Sign-up result - content: - application/json: - schema: - type: object - properties: - success: - type: boolean - error: - type: string - nullable: true - user_id: - type: string - nullable: true - - /auth/sign-in: - post: - summary: Sign in a user and return JWT - tags: [Auth] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: [nickname, pass] - properties: - nickname: - type: string - pass: - type: string - format: password - responses: - "200": - description: Sign-in result with JWT - # headers: - # Set-Cookie: - # schema: - # type: array - # items: - # type: string - # explode: true - # style: simple - content: - application/json: - schema: - type: object - properties: - success: - type: boolean - error: - type: string - nullable: true - user_id: - type: string - nullable: true - "401": - description: Access denied due to invalid credentials - content: - application/json: - schema: - type: object - properties: - error: - type: string - 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 7f53da5..1a96253 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -37,18 +37,6 @@ services: - "8080:8080" depends_on: - postgres - - nyanimedb-auth: - image: meowgit.nekoea.red/nihonium/nyanimedb-auth:latest - container_name: nyanimedb-auth - restart: always - environment: - LOG_LEVEL: ${LOG_LEVEL} - DATABASE_URL: ${DATABASE_URL} - ports: - - "8082:8082" - depends_on: - - postgres nyanimedb-frontend: image: meowgit.nekoea.red/nihonium/nyanimedb-frontend:latest diff --git a/deploy/generate.sh b/deploy/generate.sh index 29587cf..d7d94a2 100644 --- a/deploy/generate.sh +++ b/deploy/generate.sh @@ -1,3 +1,3 @@ -npx openapi-typescript-codegen --input ..\..\api\openapi.yaml --output ./src/api --client axios --useUnionTypes +npx openapi-typescript-codegen --input ..\..\api\openapi.yaml --output ./src/api --client axios oapi-codegen --config=api/oapi-codegen.yaml .\api\openapi.yaml sqlc generate -f .\sql\sqlc.yaml \ No newline at end of file diff --git a/go.mod b/go.mod index bf73121..80a9ab1 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.25.0 require ( github.com/gin-contrib/cors v1.7.6 github.com/gin-gonic/gin v1.11.0 - github.com/golang-jwt/jwt/v5 v5.3.0 github.com/jackc/pgx/v5 v5.7.6 github.com/oapi-codegen/runtime v1.1.2 github.com/pelletier/go-toml/v2 v2.2.4 diff --git a/go.sum b/go.sum index 8f46514..121ca40 100644 --- a/go.sum +++ b/go.sum @@ -31,8 +31,6 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= diff --git a/modules/auth/handlers/handlers.go b/modules/auth/handlers/handlers.go deleted file mode 100644 index 9b9b0d3..0000000 --- a/modules/auth/handlers/handlers.go +++ /dev/null @@ -1,197 +0,0 @@ -package handlers - -import ( - "context" - "fmt" - "log" - "net/http" - auth "nyanimedb/auth" - sqlc "nyanimedb/sql" - "strconv" - "time" - - "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v5" -) - -var accessSecret = []byte("my_access_secret_key") -var refreshSecret = []byte("my_refresh_secret_key") - -var UserDb = make(map[string]string) // TEMP: stores passwords - -type Server struct { - db *sqlc.Queries -} - -func NewServer(db *sqlc.Queries) Server { - return Server{db: db} -} - -func parseInt64(s string) (int32, error) { - i, err := strconv.ParseInt(s, 10, 64) - return int32(i), err -} - -func generateTokens(userID string) (accessToken string, refreshToken string, err error) { - accessClaims := jwt.MapClaims{ - "user_id": userID, - "exp": time.Now().Add(15 * time.Minute).Unix(), - } - at := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims) - accessToken, err = at.SignedString(accessSecret) - if err != nil { - return "", "", err - } - - refreshClaims := jwt.MapClaims{ - "user_id": userID, - "exp": time.Now().Add(7 * 24 * time.Hour).Unix(), - } - rt := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims) - refreshToken, err = rt.SignedString(refreshSecret) - if err != nil { - return "", "", err - } - - return accessToken, refreshToken, nil -} - -func (s Server) PostAuthSignUp(ctx context.Context, req auth.PostAuthSignUpRequestObject) (auth.PostAuthSignUpResponseObject, error) { - err := "" - success := true - UserDb[req.Body.Nickname] = req.Body.Pass - - return auth.PostAuthSignUp200JSONResponse{ - Error: &err, - Success: &success, - UserId: &req.Body.Nickname, - }, nil -} - -func (s Server) PostAuthSignIn(ctx context.Context, req auth.PostAuthSignInRequestObject) (auth.PostAuthSignInResponseObject, error) { - // ctx.SetCookie("122") - ginCtx, ok := ctx.Value(gin.ContextKey).(*gin.Context) - if !ok { - log.Print("failed to get gin context") - // TODO: change to 500 - return auth.PostAuthSignIn200JSONResponse{}, fmt.Errorf("failed to get gin.Context from context.Context") - } - - err := "" - success := true - - pass, ok := UserDb[req.Body.Nickname] - if !ok || pass != req.Body.Pass { - e := "invalid credentials" - return auth.PostAuthSignIn401JSONResponse{ - Error: &e, - }, nil - } - - accessToken, refreshToken, _ := generateTokens(req.Body.Nickname) - - ginCtx.SetSameSite(http.SameSiteStrictMode) - ginCtx.SetCookie("access_token", accessToken, 604800, "/auth", "", true, true) - ginCtx.SetCookie("refresh_token", refreshToken, 604800, "/api", "", true, true) - - // Return access token; refresh token can be returned in response or HttpOnly cookie - result := auth.PostAuthSignIn200JSONResponse{ - Error: &err, - Success: &success, - UserId: &req.Body.Nickname, - } - return result, nil -} - -// func (s Server) PostAuthVerifyToken(ctx context.Context, req auth.PostAuthVerifyTokenRequestObject) (auth.PostAuthVerifyTokenResponseObject, error) { -// valid := false -// var userID *string -// var errStr *string - -// token, err := jwt.Parse(req.Body.Token, func(t *jwt.Token) (interface{}, error) { -// if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { -// return nil, fmt.Errorf("unexpected signing method") -// } -// return accessSecret, nil -// }) - -// if err != nil { -// e := err.Error() -// errStr = &e -// return auth.PostAuthVerifyToken200JSONResponse{ -// Valid: &valid, -// UserId: userID, -// Error: errStr, -// }, nil -// } - -// if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { -// if uid, ok := claims["user_id"].(string); ok { -// valid = true -// userID = &uid -// } else { -// e := "user_id not found in token" -// errStr = &e -// } -// } else { -// e := "invalid token claims" -// errStr = &e -// } - -// 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 -// var userID *string -// var errStr *string - -// token, err := jwt.Parse(req.Body.Token, func(t *jwt.Token) (interface{}, error) { -// if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { -// return nil, fmt.Errorf("unexpected signing method") -// } -// return refreshSecret, nil -// }) - -// if err != nil { -// e := err.Error() -// errStr = &e -// return auth.PostAuthVerifyToken200JSONResponse{ -// Valid: &valid, -// UserId: userID, -// Error: errStr, -// }, nil -// } - -// if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { -// if uid, ok := claims["user_id"].(string); ok { -// // Refresh token is valid, generate new tokens -// newAccessToken, newRefreshToken, _ := generateTokens(uid) -// valid = true -// userID = &uid -// return auth.PostAuthVerifyToken200JSONResponse{ -// Valid: &valid, -// UserId: userID, -// Error: nil, -// Token: &newAccessToken, // return new access token -// // optionally return newRefreshToken as well -// }, nil -// } else { -// e := "user_id not found in refresh token" -// errStr = &e -// } -// } else { -// e := "invalid refresh token claims" -// errStr = &e -// } - -// return auth.PostAuthVerifyToken200JSONResponse{ -// Valid: &valid, -// UserId: userID, -// Error: errStr, -// }, nil -// } diff --git a/modules/auth/main.go b/modules/auth/main.go deleted file mode 100644 index c001e8b..0000000 --- a/modules/auth/main.go +++ /dev/null @@ -1,38 +0,0 @@ -package main - -import ( - "time" - - auth "nyanimedb/auth" - handlers "nyanimedb/modules/auth/handlers" - sqlc "nyanimedb/sql" - - "github.com/gin-contrib/cors" - "github.com/gin-gonic/gin" -) - -var AppConfig Config - -func main() { - r := gin.Default() - - var queries *sqlc.Queries = nil - - server := handlers.NewServer(queries) - - r.Use(cors.New(cors.Config{ - AllowOrigins: []string{"*"}, // allow all origins, change to specific domains in production - AllowMethods: []string{"GET", "POST", "PUT", "DELETE"}, - AllowHeaders: []string{"Origin", "Content-Type", "Accept"}, - ExposeHeaders: []string{"Content-Length"}, - AllowCredentials: true, - MaxAge: 12 * time.Hour, - })) - - auth.RegisterHandlers(r, auth.NewStrictHandler( - server, - []auth.StrictMiddlewareFunc{}, - )) - - r.Run(":8082") -} diff --git a/modules/auth/types.go b/modules/auth/types.go deleted file mode 100644 index 038b179..0000000 --- a/modules/auth/types.go +++ /dev/null @@ -1,6 +0,0 @@ -package main - -type Config struct { - JwtPrivateKey string - LogLevel string `toml:"LogLevel" env:"LOG_LEVEL"` -} diff --git a/modules/backend/handlers/common.go b/modules/backend/handlers/common.go index 2cf2283..e22ce3f 100644 --- a/modules/backend/handlers/common.go +++ b/modules/backend/handlers/common.go @@ -17,27 +17,11 @@ func NewServer(db *sqlc.Queries) Server { return Server{db: db} } -func sql2StorageType(s *sqlc.StorageTypeT) (*oapi.ImageStorageType, error) { - if s == nil { - return nil, nil - } - var t oapi.ImageStorageType - switch *s { - case sqlc.StorageTypeTLocal: - t = oapi.Local - case sqlc.StorageTypeTS3: - t = oapi.S3 - default: - return nil, fmt.Errorf("unexpected storage type: %s", *s) - } - return &t, nil -} - func (s Server) mapTitle(ctx context.Context, title sqlc.GetTitleByIDRow) (oapi.Title, error) { oapi_title := oapi.Title{ EpisodesAired: title.EpisodesAired, - EpisodesAll: title.EpisodesAll, + EpisodesAll: title.EpisodesAired, // EpisodesLen: &episodes_lens, Id: title.ID, // Poster: &oapi_image, @@ -57,7 +41,6 @@ func (s Server) mapTitle(ctx context.Context, title sqlc.GetTitleByIDRow) (oapi. if err != nil { return oapi.Title{}, fmt.Errorf("unmarshal TitleNames: %v", err) } - oapi_title.TitleNames = title_names if len(title.EpisodesLen) > 0 { episodes_lens := make(map[string]float64, 0) @@ -73,7 +56,6 @@ func (s Server) mapTitle(ctx context.Context, title sqlc.GetTitleByIDRow) (oapi. if err != nil { return oapi.Title{}, fmt.Errorf("unmarshalling title_tag: %v", err) } - oapi_title.Tags = oapi_tag_names var oapi_studio oapi.Studio if title.StudioName != nil { @@ -86,13 +68,7 @@ func (s Server) mapTitle(ctx context.Context, title sqlc.GetTitleByIDRow) (oapi. oapi_studio.Poster = &oapi.Image{} oapi_studio.Poster.Id = title.StudioIllustID oapi_studio.Poster.ImagePath = title.StudioImagePath - - s, err := sql2StorageType(title.StudioStorageType) - if err != nil { - return oapi.Title{}, fmt.Errorf("mapTitle, studio storage type: %v", err) - } - oapi_studio.Poster.StorageType = s - + oapi_studio.Poster.StorageType = &title.StudioStorageType } } oapi_title.Studio = &oapi_studio @@ -102,13 +78,8 @@ func (s Server) mapTitle(ctx context.Context, title sqlc.GetTitleByIDRow) (oapi. if title.PosterID != nil { oapi_image.Id = title.PosterID oapi_image.ImagePath = title.TitleImagePath - s, err := sql2StorageType(title.TitleStorageType) - if err != nil { - return oapi.Title{}, fmt.Errorf("mapTitle, title starage type: %v", err) - } - oapi_image.StorageType = s + oapi_image.StorageType = &title.TitleStorageType } - oapi_title.Poster = &oapi_image var release_season oapi.ReleaseSeason if title.ReleaseSeason != nil { @@ -125,9 +96,9 @@ func (s Server) mapTitle(ctx context.Context, title sqlc.GetTitleByIDRow) (oapi. return oapi_title, nil } -func parseInt64(s string) (int64, error) { +func parseInt64(s string) (int32, error) { i, err := strconv.ParseInt(s, 10, 64) - return i, err + return int32(i), err } func TitleStatus2Sqlc(s *[]oapi.TitleStatus) ([]sqlc.TitleStatusT, error) { diff --git a/modules/backend/handlers/titles.go b/modules/backend/handlers/titles.go index c67177f..054b745 100644 --- a/modules/backend/handlers/titles.go +++ b/modules/backend/handlers/titles.go @@ -81,56 +81,56 @@ func (s Server) GetTagsByTitleId(ctx context.Context, id int64) (oapi.Tags, erro return oapi_tag_names, nil } -// func (s Server) GetImage(ctx context.Context, id int64) (*oapi.Image, error) { +func (s Server) GetImage(ctx context.Context, id int64) (*oapi.Image, error) { -// var oapi_image oapi.Image + var oapi_image oapi.Image -// sqlc_image, err := s.db.GetImageByID(ctx, id) -// if err != nil { -// if err == pgx.ErrNoRows { -// return nil, nil //todo: error reference in db -// } -// return &oapi_image, fmt.Errorf("query GetImageByID: %v", err) -// } + sqlc_image, err := s.db.GetImageByID(ctx, id) + if err != nil { + if err == pgx.ErrNoRows { + return nil, nil //todo: error reference in db + } + return &oapi_image, fmt.Errorf("query GetImageByID: %v", err) + } -// //can cast and dont use brain cause all this fields required in image table -// oapi_image.Id = &sqlc_image.ID -// oapi_image.ImagePath = &sqlc_image.ImagePath -// storageTypeStr := string(sqlc_image.StorageType) -// oapi_image.StorageType = string(storageTypeStr) + //can cast and dont use brain cause all this fields required in image table + oapi_image.Id = &sqlc_image.ID + oapi_image.ImagePath = &sqlc_image.ImagePath + storageTypeStr := string(sqlc_image.StorageType) + oapi_image.StorageType = &storageTypeStr -// return &oapi_image, nil -// } + return &oapi_image, nil +} -// func (s Server) GetStudio(ctx context.Context, id int64) (*oapi.Studio, error) { +func (s Server) GetStudio(ctx context.Context, id int64) (*oapi.Studio, error) { -// var oapi_studio oapi.Studio + var oapi_studio oapi.Studio -// sqlc_studio, err := s.db.GetStudioByID(ctx, id) -// if err != nil { -// if err == pgx.ErrNoRows { -// return nil, nil -// } -// return &oapi_studio, fmt.Errorf("query GetStudioByID: %v", err) -// } + sqlc_studio, err := s.db.GetStudioByID(ctx, id) + if err != nil { + if err == pgx.ErrNoRows { + return nil, nil + } + return &oapi_studio, fmt.Errorf("query GetStudioByID: %v", err) + } -// oapi_studio.Id = sqlc_studio.ID -// oapi_studio.Name = sqlc_studio.StudioName -// oapi_studio.Description = sqlc_studio.StudioDesc + oapi_studio.Id = sqlc_studio.ID + oapi_studio.Name = sqlc_studio.StudioName + oapi_studio.Description = sqlc_studio.StudioDesc -// if sqlc_studio.IllustID == nil { -// return &oapi_studio, nil -// } -// oapi_illust, err := s.GetImage(ctx, *sqlc_studio.IllustID) -// if err != nil { -// return &oapi_studio, fmt.Errorf("GetImage: %v", err) -// } -// if oapi_illust != nil { -// oapi_studio.Poster = oapi_illust -// } + if sqlc_studio.IllustID == nil { + return &oapi_studio, nil + } + oapi_illust, err := s.GetImage(ctx, *sqlc_studio.IllustID) + if err != nil { + return &oapi_studio, fmt.Errorf("GetImage: %v", err) + } + if oapi_illust != nil { + oapi_studio.Poster = oapi_illust + } -// return &oapi_studio, nil -// } + return &oapi_studio, nil +} func (s Server) GetTitlesTitleId(ctx context.Context, request oapi.GetTitlesTitleIdRequestObject) (oapi.GetTitlesTitleIdResponseObject, error) { var oapi_title oapi.Title @@ -224,8 +224,7 @@ func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObje // EpisodesLen: title.EpisodesLen, TitleStorageType: title.TitleStorageType, TitleImagePath: title.TitleImagePath, - TitleNames: title.TitleNames, - TagNames: title.TagNames, + TagNames: title.TitleNames, StudioName: title.StudioName, // StudioIllustID: title.StudioIllustID, // StudioDesc: title.StudioDesc, @@ -233,11 +232,6 @@ func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObje // StudioImagePath: title.StudioImagePath, } - // if title.TitleStorageType != nil { - // s := *title.TitleStorageType - // _title.TitleStorageType = string(s) - // } - t, err := s.mapTitle(ctx, _title) if err != nil { log.Errorf("%v", err) diff --git a/modules/backend/handlers/users.go b/modules/backend/handlers/users.go index 927c1c1..3a271d7 100644 --- a/modules/backend/handlers/users.go +++ b/modules/backend/handlers/users.go @@ -27,25 +27,16 @@ import ( // return int32(i), err // } -func mapUser(u sqlc.GetUserByIDRow) (oapi.User, error) { - i := oapi.Image{ - Id: u.AvatarID, - ImagePath: u.ImagePath, - } - s, err := sql2StorageType(u.StorageType) - if err != nil { - return oapi.User{}, fmt.Errorf("mapUser, storage type: %v", err) - } - i.StorageType = s +func mapUser(u sqlc.GetUserByIDRow) oapi.User { return oapi.User{ - Image: &i, + AvatarId: u.AvatarID, CreationDate: &u.CreationDate, DispName: u.DispName, Id: &u.ID, - Mail: StringToEmail(u.Mail), + Mail: (*types.Email)(u.Mail), Nickname: u.Nickname, UserDesc: u.UserDesc, - }, nil + } } func (s Server) GetUsersUserId(ctx context.Context, req oapi.GetUsersUserIdRequestObject) (oapi.GetUsersUserIdResponseObject, error) { @@ -53,19 +44,14 @@ func (s Server) GetUsersUserId(ctx context.Context, req oapi.GetUsersUserIdReque if err != nil { return oapi.GetUsersUserId404Response{}, nil } - _user, err := s.db.GetUserByID(context.TODO(), userID) + user, err := s.db.GetUserByID(context.TODO(), int64(userID)) if err != nil { if err == pgx.ErrNoRows { return oapi.GetUsersUserId404Response{}, nil } return nil, err } - user, err := mapUser(_user) - if err != nil { - log.Errorf("%v", err) - return oapi.GetUsersUserId500Response{}, err - } - return oapi.GetUsersUserId200JSONResponse(user), nil + return oapi.GetUsersUserId200JSONResponse(mapUser(user)), nil } func sqlDate2oapi(p_date pgtype.Timestamptz) *time.Time { @@ -139,32 +125,10 @@ func UserTitleStatus2Sqlc(s *[]oapi.UserTitleStatus) ([]sqlc.UsertitleStatusT, e return sqlc_status, nil } -func UserTitleStatus2Sqlc1(s *oapi.UserTitleStatus) (*sqlc.UsertitleStatusT, error) { - var sqlc_status sqlc.UsertitleStatusT - if s == nil { - return nil, nil - } - - switch *s { - case oapi.UserTitleStatusFinished: - sqlc_status = sqlc.UsertitleStatusTFinished - case oapi.UserTitleStatusInProgress: - sqlc_status = sqlc.UsertitleStatusTInProgress - case oapi.UserTitleStatusDropped: - sqlc_status = sqlc.UsertitleStatusTDropped - case oapi.UserTitleStatusPlanned: - sqlc_status = sqlc.UsertitleStatusTPlanned - default: - return nil, fmt.Errorf("unexpected tittle status: %s", *s) - } - - return &sqlc_status, nil -} - func (s Server) mapUsertitle(ctx context.Context, t sqlc.SearchUserTitlesRow) (oapi.UserTitle, error) { oapi_usertitle := oapi.UserTitle{ - Ctime: &t.UserCtime, + Ctime: sqlDate2oapi(t.UserCtime), Rate: t.UserRate, ReviewId: t.ReviewID, // Status: , @@ -193,9 +157,8 @@ func (s Server) mapUsertitle(ctx context.Context, t sqlc.SearchUserTitlesRow) (o // EpisodesLen: title.EpisodesLen, TitleStorageType: t.TitleStorageType, TitleImagePath: t.TitleImagePath, + TagNames: t.TitleNames, StudioName: t.StudioName, - TitleNames: t.TitleNames, - TagNames: t.TagNames, // StudioIllustID: title.StudioIllustID, // StudioDesc: title.StudioDesc, // StudioStorageType: title.StudioStorageType, @@ -243,13 +206,7 @@ func (s Server) GetUsersUserIdTitles(ctx context.Context, request oapi.GetUsersU return oapi.GetUsersUserIdTitles400Response{}, err } - userID, err := parseInt64(request.UserId) - if err != nil { - log.Errorf("get user titles: %v", err) - return oapi.GetUsersUserIdTitles404Response{}, err - } params := sqlc.SearchUserTitlesParams{ - UserID: userID, Word: word, TitleStatuses: title_statuses, UsertitleStatuses: watch_status, @@ -312,97 +269,3 @@ func (s Server) GetUsersUserIdTitles(ctx context.Context, request oapi.GetUsersU return oapi.GetUsersUserIdTitles200JSONResponse{Cursor: new_cursor, Data: oapi_usertitles}, nil } - -func EmailToStringPtr(e *types.Email) *string { - if e == nil { - return nil - } - s := string(*e) - return &s -} - -func StringToEmail(e *string) *types.Email { - if e == nil { - return nil - } - s := types.Email(*e) - return &s -} - -// UpdateUser implements oapi.StrictServerInterface. -func (s Server) UpdateUser(ctx context.Context, request oapi.UpdateUserRequestObject) (oapi.UpdateUserResponseObject, error) { - - params := sqlc.UpdateUserParams{ - AvatarID: request.Body.AvatarId, - DispName: request.Body.DispName, - UserDesc: request.Body.UserDesc, - Mail: EmailToStringPtr(request.Body.Mail), - UserID: request.UserId, - } - - user, err := s.db.UpdateUser(ctx, params) - if err != nil { - log.Errorf("%v", err) - return oapi.UpdateUser500Response{}, nil - } - - oapi_user := oapi.User{ // maybe its possible to make one sqlc type and use one map func iinstead of this shit - // AvatarId: user.AvatarID, - CreationDate: &user.CreationDate, - DispName: user.DispName, - Id: &user.ID, - Mail: StringToEmail(user.Mail), - Nickname: user.Nickname, - UserDesc: user.UserDesc, - } - - return oapi.UpdateUser200JSONResponse(oapi_user), nil -} - -func (s Server) AddUserTitle(ctx context.Context, request oapi.AddUserTitleRequestObject) (oapi.AddUserTitleResponseObject, error) { - - status, err := UserTitleStatus2Sqlc1(&request.Body.Status) - if err != nil { - log.Errorf("%v", err) - return oapi.AddUserTitle400Response{}, nil - } - - params := sqlc.InsertUserTitleParams{ - UserID: request.UserId, - TitleID: request.Body.Title.Id, - Status: *status, - Rate: request.Body.Rate, - ReviewID: request.Body.ReviewId, - } - - user_title, err := s.db.InsertUserTitle(ctx, params) - if err != nil { - log.Errorf("%v", err) - return oapi.AddUserTitle500Response{}, nil - } - - oapi_status, err := sql2usertitlestatus(user_title.Status) - if err != nil { - log.Errorf("%v", err) - return oapi.AddUserTitle500Response{}, nil - } - oapi_usertitle := struct { - Ctime *time.Time `json:"ctime,omitempty"` - Rate *int32 `json:"rate,omitempty"` - ReviewId *int64 `json:"review_id,omitempty"` - - // Status User's title status - Status oapi.UserTitleStatus `json:"status"` - TitleId int64 `json:"title_id"` - UserId int64 `json:"user_id"` - }{ - Ctime: &user_title.Ctime, - Rate: user_title.Rate, - ReviewId: user_title.ReviewID, - Status: oapi_status, - TitleId: user_title.TitleID, - UserId: user_title.UserID, - } - - return oapi.AddUserTitle200JSONResponse{Data: &oapi_usertitle}, nil -} diff --git a/modules/backend/queries.sql b/modules/backend/queries.sql index 0146b25..d064660 100644 --- a/modules/backend/queries.sql +++ b/modules/backend/queries.sql @@ -9,19 +9,9 @@ VALUES ($1, $2) RETURNING id, storage_type, image_path; -- name: GetUserByID :one -SELECT - t.id as id, - t.avatar_id as avatar_id, - t.mail as mail, - t.nickname as nickname, - t.disp_name as disp_name, - t.user_desc as user_desc, - t.creation_date as creation_date, - i.storage_type as storage_type, - i.image_path as image_path -FROM users as t -LEFT JOIN images as i ON (t.avatar_id = i.id) -WHERE t.id = sqlc.arg('id')::bigint; +SELECT id, avatar_id, mail, nickname, disp_name, user_desc, creation_date +FROM users +WHERE id = $1; -- name: GetStudioByID :one @@ -68,15 +58,15 @@ RETURNING id, tag_names; -- VALUES ($1, $2, $3, $4, $5, $6, $7) -- RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date; --- name: UpdateUser :one -UPDATE users -SET - avatar_id = COALESCE(sqlc.narg('avatar_id'), avatar_id), - disp_name = COALESCE(sqlc.narg('disp_name'), disp_name), - user_desc = COALESCE(sqlc.narg('user_desc'), user_desc), - mail = COALESCE(sqlc.narg('mail'), mail) -WHERE id = sqlc.arg('user_id') -RETURNING id, avatar_id, nickname, disp_name, user_desc, creation_date, mail; +-- -- name: UpdateUser :one +-- UPDATE users +-- SET +-- avatar_id = COALESCE(sqlc.narg('avatar_id'), avatar_id), +-- disp_name = COALESCE(sqlc.narg('disp_name'), disp_name), +-- user_desc = COALESCE(sqlc.narg('user_desc'), user_desc), +-- passhash = COALESCE(sqlc.narg('passhash'), passhash) +-- WHERE user_id = sqlc.arg('user_id') +-- RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date; -- -- name: DeleteUser :exec -- DELETE FROM users @@ -86,7 +76,7 @@ RETURNING id, avatar_id, nickname, disp_name, user_desc, creation_date, mail; -- sqlc.struct: TitlesFull SELECT t.*, - i.storage_type as title_storage_type, + i.storage_type::text as title_storage_type, i.image_path as title_image_path, COALESCE( jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), @@ -95,7 +85,7 @@ SELECT s.studio_name as studio_name, s.illust_id as studio_illust_id, s.studio_desc as studio_desc, - si.storage_type as studio_storage_type, + si.storage_type::text as studio_storage_type, si.image_path as studio_image_path FROM titles as t @@ -122,7 +112,7 @@ SELECT t.season as season, t.episodes_aired as episodes_aired, t.episodes_all as episodes_all, - i.storage_type as title_storage_type, + i.storage_type::text as title_storage_type, i.image_path as title_image_path, COALESCE( jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), @@ -203,10 +193,10 @@ WHERE ) AND ( - sqlc.narg('title_statuses')::title_status_t[] IS NULL - OR array_length(sqlc.narg('title_statuses')::title_status_t[], 1) IS NULL - OR array_length(sqlc.narg('title_statuses')::title_status_t[], 1) = 0 - OR t.title_status = ANY(sqlc.narg('title_statuses')::title_status_t[]) + 'title_statuses'::title_status_t[] IS NULL + OR array_length('title_statuses'::title_status_t[], 1) IS NULL + OR array_length('title_statuses'::title_status_t[], 1) = 0 + OR t.title_status = ANY('title_statuses'::title_status_t[]) ) AND (sqlc.narg('rating')::float IS NULL OR t.rating >= sqlc.narg('rating')::float) AND (sqlc.narg('release_year')::int IS NULL OR t.release_year = sqlc.narg('release_year')::int) @@ -253,7 +243,7 @@ SELECT u.rate as user_rate, u.review_id as review_id, u.ctime as user_ctime, - i.storage_type as title_storage_type, + i.storage_type::text as title_storage_type, i.image_path as title_image_path, COALESCE( jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), @@ -269,8 +259,6 @@ LEFT JOIN tags as g ON (tt.tag_id = g.id) LEFT JOIN studios as s ON (t.studio_id = s.id) WHERE - u.user_id = sqlc.arg('user_id')::bigint - AND CASE WHEN sqlc.arg('forward')::boolean THEN -- forward: greater than cursor (next page) @@ -337,16 +325,16 @@ WHERE ) AND ( - sqlc.narg('title_statuses')::title_status_t[] IS NULL - OR array_length(sqlc.narg('title_statuses')::title_status_t[], 1) IS NULL - OR array_length(sqlc.narg('title_statuses')::title_status_t[], 1) = 0 - OR t.title_status = ANY(sqlc.narg('title_statuses')::title_status_t[]) + 'title_statuses'::title_status_t[] IS NULL + OR array_length('title_statuses'::title_status_t[], 1) IS NULL + OR array_length('title_statuses'::title_status_t[], 1) = 0 + OR t.title_status = ANY('title_statuses'::title_status_t[]) ) AND ( - sqlc.narg('usertitle_statuses')::usertitle_status_t[] IS NULL - OR array_length(sqlc.narg('usertitle_statuses')::usertitle_status_t[], 1) IS NULL - OR array_length(sqlc.narg('usertitle_statuses')::usertitle_status_t[], 1) = 0 - OR u.status = ANY(sqlc.narg('usertitle_statuses')::usertitle_status_t[]) + 'usertitle_statuses'::title_status_t[] IS NULL + OR array_length('usertitle_statuses'::title_status_t[], 1) IS NULL + OR array_length('usertitle_statuses'::title_status_t[], 1) = 0 + OR t.title_status = ANY('usertitle_statuses'::title_status_t[]) ) AND (sqlc.narg('rate')::int IS NULL OR u.rate >= sqlc.narg('rate')::int) AND (sqlc.narg('rating')::float IS NULL OR t.rating >= sqlc.narg('rating')::float) @@ -354,7 +342,7 @@ WHERE AND (sqlc.narg('release_season')::release_season_t IS NULL OR t.release_season = sqlc.narg('release_season')::release_season_t) GROUP BY - t.id, u.user_id, u.status, u.rate, u.review_id, u.ctime, i.id, s.id + t.id, i.id, s.id ORDER BY CASE WHEN sqlc.arg('forward')::boolean THEN @@ -424,7 +412,7 @@ WHERE review_id = sqlc.arg('review_id')::bigint; -- DELETE FROM reviews -- WHERE review_id = $1; --- -- name: ListReviewsByTitle :many +-- name: ListReviewsByTitle :many -- SELECT review_id, user_id, title_id, image_ids, review_text, creation_date -- FROM reviews -- WHERE title_id = $1 @@ -450,16 +438,10 @@ WHERE review_id = sqlc.arg('review_id')::bigint; -- ORDER BY usertitle_id -- LIMIT $2 OFFSET $3; --- name: InsertUserTitle :one -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 -) -RETURNING user_id, title_id, status, rate, review_id, ctime; +-- -- name: CreateUserTitle :one +-- INSERT INTO usertitles (user_id, title_id, status, rate, review_id) +-- VALUES ($1, $2, $3, $4, $5) +-- RETURNING usertitle_id, user_id, title_id, status, rate, review_id; -- -- name: UpdateUserTitle :one -- UPDATE usertitles diff --git a/modules/frontend/nginx-default.conf b/modules/frontend/nginx-default.conf index c3a851f..a538968 100644 --- a/modules/frontend/nginx-default.conf +++ b/modules/frontend/nginx-default.conf @@ -19,15 +19,6 @@ server { proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } - location /auth/ { - rewrite ^/auth/(.*)$ /$1 break; - proxy_pass http://nyanimedb-auth:8082/; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; - } #error_page 404 /404.html; error_page 500 502 503 504 /50x.html; diff --git a/modules/frontend/src/App.tsx b/modules/frontend/src/App.tsx index 5a25313..909ad6c 100644 --- a/modules/frontend/src/App.tsx +++ b/modules/frontend/src/App.tsx @@ -2,7 +2,6 @@ import React from "react"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import UserPage from "./pages/UserPage/UserPage"; import TitlesPage from "./pages/TitlesPage/TitlesPage"; -import { LoginPage } from "./pages/LoginPage/LoginPage"; import { Header } from "./components/Header/Header"; const App: React.FC = () => { @@ -11,8 +10,6 @@ const App: React.FC = () => {
- } /> {/* <-- маршрут для логина */} - } /> {/* <-- можно использовать тот же компонент для регистрации */} } /> } /> diff --git a/modules/frontend/src/api/core/OpenAPI.ts b/modules/frontend/src/api/core/OpenAPI.ts index 6ce873e..185e5c3 100644 --- a/modules/frontend/src/api/core/OpenAPI.ts +++ b/modules/frontend/src/api/core/OpenAPI.ts @@ -20,7 +20,7 @@ export type OpenAPIConfig = { }; export const OpenAPI: OpenAPIConfig = { - BASE: 'http://10.1.0.65:8081/api/v1', + BASE: '/api/v1', VERSION: '1.0.0', WITH_CREDENTIALS: false, CREDENTIALS: 'include', diff --git a/modules/frontend/src/auth/core/ApiError.ts b/modules/frontend/src/auth/core/ApiError.ts deleted file mode 100644 index ec7b16a..0000000 --- a/modules/frontend/src/auth/core/ApiError.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { ApiRequestOptions } from './ApiRequestOptions'; -import type { ApiResult } from './ApiResult'; - -export class ApiError extends Error { - public readonly url: string; - public readonly status: number; - public readonly statusText: string; - public readonly body: any; - public readonly request: ApiRequestOptions; - - constructor(request: ApiRequestOptions, response: ApiResult, message: string) { - super(message); - - this.name = 'ApiError'; - this.url = response.url; - this.status = response.status; - this.statusText = response.statusText; - this.body = response.body; - this.request = request; - } -} diff --git a/modules/frontend/src/auth/core/ApiRequestOptions.ts b/modules/frontend/src/auth/core/ApiRequestOptions.ts deleted file mode 100644 index 93143c3..0000000 --- a/modules/frontend/src/auth/core/ApiRequestOptions.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ApiRequestOptions = { - readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH'; - readonly url: string; - readonly path?: Record; - readonly cookies?: Record; - readonly headers?: Record; - readonly query?: Record; - readonly formData?: Record; - readonly body?: any; - readonly mediaType?: string; - readonly responseHeader?: string; - readonly errors?: Record; -}; diff --git a/modules/frontend/src/auth/core/ApiResult.ts b/modules/frontend/src/auth/core/ApiResult.ts deleted file mode 100644 index ee1126e..0000000 --- a/modules/frontend/src/auth/core/ApiResult.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ApiResult = { - readonly url: string; - readonly ok: boolean; - readonly status: number; - readonly statusText: string; - readonly body: any; -}; diff --git a/modules/frontend/src/auth/core/CancelablePromise.ts b/modules/frontend/src/auth/core/CancelablePromise.ts deleted file mode 100644 index d70de92..0000000 --- a/modules/frontend/src/auth/core/CancelablePromise.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export class CancelError extends Error { - - constructor(message: string) { - super(message); - this.name = 'CancelError'; - } - - public get isCancelled(): boolean { - return true; - } -} - -export interface OnCancel { - readonly isResolved: boolean; - readonly isRejected: boolean; - readonly isCancelled: boolean; - - (cancelHandler: () => void): void; -} - -export class CancelablePromise implements Promise { - #isResolved: boolean; - #isRejected: boolean; - #isCancelled: boolean; - readonly #cancelHandlers: (() => void)[]; - readonly #promise: Promise; - #resolve?: (value: T | PromiseLike) => void; - #reject?: (reason?: any) => void; - - constructor( - executor: ( - resolve: (value: T | PromiseLike) => void, - reject: (reason?: any) => void, - onCancel: OnCancel - ) => void - ) { - this.#isResolved = false; - this.#isRejected = false; - this.#isCancelled = false; - this.#cancelHandlers = []; - this.#promise = new Promise((resolve, reject) => { - this.#resolve = resolve; - this.#reject = reject; - - const onResolve = (value: T | PromiseLike): void => { - if (this.#isResolved || this.#isRejected || this.#isCancelled) { - return; - } - this.#isResolved = true; - if (this.#resolve) this.#resolve(value); - }; - - const onReject = (reason?: any): void => { - if (this.#isResolved || this.#isRejected || this.#isCancelled) { - return; - } - this.#isRejected = true; - if (this.#reject) this.#reject(reason); - }; - - const onCancel = (cancelHandler: () => void): void => { - if (this.#isResolved || this.#isRejected || this.#isCancelled) { - return; - } - this.#cancelHandlers.push(cancelHandler); - }; - - Object.defineProperty(onCancel, 'isResolved', { - get: (): boolean => this.#isResolved, - }); - - Object.defineProperty(onCancel, 'isRejected', { - get: (): boolean => this.#isRejected, - }); - - Object.defineProperty(onCancel, 'isCancelled', { - get: (): boolean => this.#isCancelled, - }); - - return executor(onResolve, onReject, onCancel as OnCancel); - }); - } - - get [Symbol.toStringTag]() { - return "Cancellable Promise"; - } - - public then( - onFulfilled?: ((value: T) => TResult1 | PromiseLike) | null, - onRejected?: ((reason: any) => TResult2 | PromiseLike) | null - ): Promise { - return this.#promise.then(onFulfilled, onRejected); - } - - public catch( - onRejected?: ((reason: any) => TResult | PromiseLike) | null - ): Promise { - return this.#promise.catch(onRejected); - } - - public finally(onFinally?: (() => void) | null): Promise { - return this.#promise.finally(onFinally); - } - - public cancel(): void { - if (this.#isResolved || this.#isRejected || this.#isCancelled) { - return; - } - this.#isCancelled = true; - if (this.#cancelHandlers.length) { - try { - for (const cancelHandler of this.#cancelHandlers) { - cancelHandler(); - } - } catch (error) { - console.warn('Cancellation threw an error', error); - return; - } - } - this.#cancelHandlers.length = 0; - if (this.#reject) this.#reject(new CancelError('Request aborted')); - } - - public get isCancelled(): boolean { - return this.#isCancelled; - } -} diff --git a/modules/frontend/src/auth/core/OpenAPI.ts b/modules/frontend/src/auth/core/OpenAPI.ts deleted file mode 100644 index 2d0edf8..0000000 --- a/modules/frontend/src/auth/core/OpenAPI.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { ApiRequestOptions } from './ApiRequestOptions'; - -type Resolver = (options: ApiRequestOptions) => Promise; -type Headers = Record; - -export type OpenAPIConfig = { - BASE: string; - VERSION: string; - WITH_CREDENTIALS: boolean; - CREDENTIALS: 'include' | 'omit' | 'same-origin'; - TOKEN?: string | Resolver | undefined; - USERNAME?: string | Resolver | undefined; - PASSWORD?: string | Resolver | undefined; - HEADERS?: Headers | Resolver | undefined; - ENCODE_PATH?: ((path: string) => string) | undefined; -}; - -export const OpenAPI: OpenAPIConfig = { - BASE: '/auth', - VERSION: '1.0.0', - WITH_CREDENTIALS: false, - CREDENTIALS: 'include', - TOKEN: undefined, - USERNAME: undefined, - PASSWORD: undefined, - HEADERS: undefined, - ENCODE_PATH: undefined, -}; diff --git a/modules/frontend/src/auth/core/request.ts b/modules/frontend/src/auth/core/request.ts deleted file mode 100644 index 1dc6fef..0000000 --- a/modules/frontend/src/auth/core/request.ts +++ /dev/null @@ -1,323 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import axios from 'axios'; -import type { AxiosError, AxiosRequestConfig, AxiosResponse, AxiosInstance } from 'axios'; -import FormData from 'form-data'; - -import { ApiError } from './ApiError'; -import type { ApiRequestOptions } from './ApiRequestOptions'; -import type { ApiResult } from './ApiResult'; -import { CancelablePromise } from './CancelablePromise'; -import type { OnCancel } from './CancelablePromise'; -import type { OpenAPIConfig } from './OpenAPI'; - -export const isDefined = (value: T | null | undefined): value is Exclude => { - return value !== undefined && value !== null; -}; - -export const isString = (value: any): value is string => { - return typeof value === 'string'; -}; - -export const isStringWithValue = (value: any): value is string => { - return isString(value) && value !== ''; -}; - -export const isBlob = (value: any): value is Blob => { - return ( - typeof value === 'object' && - typeof value.type === 'string' && - typeof value.stream === 'function' && - typeof value.arrayBuffer === 'function' && - typeof value.constructor === 'function' && - typeof value.constructor.name === 'string' && - /^(Blob|File)$/.test(value.constructor.name) && - /^(Blob|File)$/.test(value[Symbol.toStringTag]) - ); -}; - -export const isFormData = (value: any): value is FormData => { - return value instanceof FormData; -}; - -export const isSuccess = (status: number): boolean => { - return status >= 200 && status < 300; -}; - -export const base64 = (str: string): string => { - try { - return btoa(str); - } catch (err) { - // @ts-ignore - return Buffer.from(str).toString('base64'); - } -}; - -export const getQueryString = (params: Record): string => { - const qs: string[] = []; - - const append = (key: string, value: any) => { - qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); - }; - - const process = (key: string, value: any) => { - if (isDefined(value)) { - if (Array.isArray(value)) { - value.forEach(v => { - process(key, v); - }); - } else if (typeof value === 'object') { - Object.entries(value).forEach(([k, v]) => { - process(`${key}[${k}]`, v); - }); - } else { - append(key, value); - } - } - }; - - Object.entries(params).forEach(([key, value]) => { - process(key, value); - }); - - if (qs.length > 0) { - return `?${qs.join('&')}`; - } - - return ''; -}; - -const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => { - const encoder = config.ENCODE_PATH || encodeURI; - - const path = options.url - .replace('{api-version}', config.VERSION) - .replace(/{(.*?)}/g, (substring: string, group: string) => { - if (options.path?.hasOwnProperty(group)) { - return encoder(String(options.path[group])); - } - return substring; - }); - - const url = `${config.BASE}${path}`; - if (options.query) { - return `${url}${getQueryString(options.query)}`; - } - return url; -}; - -export const getFormData = (options: ApiRequestOptions): FormData | undefined => { - if (options.formData) { - const formData = new FormData(); - - const process = (key: string, value: any) => { - if (isString(value) || isBlob(value)) { - formData.append(key, value); - } else { - formData.append(key, JSON.stringify(value)); - } - }; - - Object.entries(options.formData) - .filter(([_, value]) => isDefined(value)) - .forEach(([key, value]) => { - if (Array.isArray(value)) { - value.forEach(v => process(key, v)); - } else { - process(key, value); - } - }); - - return formData; - } - return undefined; -}; - -type Resolver = (options: ApiRequestOptions) => Promise; - -export const resolve = async (options: ApiRequestOptions, resolver?: T | Resolver): Promise => { - if (typeof resolver === 'function') { - return (resolver as Resolver)(options); - } - return resolver; -}; - -export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions, formData?: FormData): Promise> => { - const [token, username, password, additionalHeaders] = await Promise.all([ - resolve(options, config.TOKEN), - resolve(options, config.USERNAME), - resolve(options, config.PASSWORD), - resolve(options, config.HEADERS), - ]); - - const formHeaders = typeof formData?.getHeaders === 'function' && formData?.getHeaders() || {} - - const headers = Object.entries({ - Accept: 'application/json', - ...additionalHeaders, - ...options.headers, - ...formHeaders, - }) - .filter(([_, value]) => isDefined(value)) - .reduce((headers, [key, value]) => ({ - ...headers, - [key]: String(value), - }), {} as Record); - - if (isStringWithValue(token)) { - headers['Authorization'] = `Bearer ${token}`; - } - - if (isStringWithValue(username) && isStringWithValue(password)) { - const credentials = base64(`${username}:${password}`); - headers['Authorization'] = `Basic ${credentials}`; - } - - if (options.body !== undefined) { - if (options.mediaType) { - headers['Content-Type'] = options.mediaType; - } else if (isBlob(options.body)) { - headers['Content-Type'] = options.body.type || 'application/octet-stream'; - } else if (isString(options.body)) { - headers['Content-Type'] = 'text/plain'; - } else if (!isFormData(options.body)) { - headers['Content-Type'] = 'application/json'; - } - } - - return headers; -}; - -export const getRequestBody = (options: ApiRequestOptions): any => { - if (options.body) { - return options.body; - } - return undefined; -}; - -export const sendRequest = async ( - config: OpenAPIConfig, - options: ApiRequestOptions, - url: string, - body: any, - formData: FormData | undefined, - headers: Record, - onCancel: OnCancel, - axiosClient: AxiosInstance -): Promise> => { - const source = axios.CancelToken.source(); - - const requestConfig: AxiosRequestConfig = { - url, - headers, - data: body ?? formData, - method: options.method, - withCredentials: config.WITH_CREDENTIALS, - withXSRFToken: config.CREDENTIALS === 'include' ? config.WITH_CREDENTIALS : false, - cancelToken: source.token, - }; - - onCancel(() => source.cancel('The user aborted a request.')); - - try { - return await axiosClient.request(requestConfig); - } catch (error) { - const axiosError = error as AxiosError; - if (axiosError.response) { - return axiosError.response; - } - throw error; - } -}; - -export const getResponseHeader = (response: AxiosResponse, responseHeader?: string): string | undefined => { - if (responseHeader) { - const content = response.headers[responseHeader]; - if (isString(content)) { - return content; - } - } - return undefined; -}; - -export const getResponseBody = (response: AxiosResponse): any => { - if (response.status !== 204) { - return response.data; - } - return undefined; -}; - -export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => { - const errors: Record = { - 400: 'Bad Request', - 401: 'Unauthorized', - 403: 'Forbidden', - 404: 'Not Found', - 500: 'Internal Server Error', - 502: 'Bad Gateway', - 503: 'Service Unavailable', - ...options.errors, - } - - const error = errors[result.status]; - if (error) { - throw new ApiError(options, result, error); - } - - if (!result.ok) { - const errorStatus = result.status ?? 'unknown'; - const errorStatusText = result.statusText ?? 'unknown'; - const errorBody = (() => { - try { - return JSON.stringify(result.body, null, 2); - } catch (e) { - return undefined; - } - })(); - - throw new ApiError(options, result, - `Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}` - ); - } -}; - -/** - * Request method - * @param config The OpenAPI configuration object - * @param options The request options from the service - * @param axiosClient The axios client instance to use - * @returns CancelablePromise - * @throws ApiError - */ -export const request = (config: OpenAPIConfig, options: ApiRequestOptions, axiosClient: AxiosInstance = axios): CancelablePromise => { - return new CancelablePromise(async (resolve, reject, onCancel) => { - try { - const url = getUrl(config, options); - const formData = getFormData(options); - const body = getRequestBody(options); - const headers = await getHeaders(config, options, formData); - - if (!onCancel.isCancelled) { - const response = await sendRequest(config, options, url, body, formData, headers, onCancel, axiosClient); - const responseBody = getResponseBody(response); - const responseHeader = getResponseHeader(response, options.responseHeader); - - const result: ApiResult = { - url, - ok: isSuccess(response.status), - status: response.status, - statusText: response.statusText, - body: responseHeader ?? responseBody, - }; - - catchErrorCodes(options, result); - - resolve(result.body); - } - } catch (error) { - reject(error); - } - }); -}; diff --git a/modules/frontend/src/auth/index.ts b/modules/frontend/src/auth/index.ts deleted file mode 100644 index b0989c4..0000000 --- a/modules/frontend/src/auth/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export { ApiError } from './core/ApiError'; -export { CancelablePromise, CancelError } from './core/CancelablePromise'; -export { OpenAPI } from './core/OpenAPI'; -export type { OpenAPIConfig } from './core/OpenAPI'; - -export { AuthService } from './services/AuthService'; diff --git a/modules/frontend/src/auth/services/AuthService.ts b/modules/frontend/src/auth/services/AuthService.ts deleted file mode 100644 index bab9c77..0000000 --- a/modules/frontend/src/auth/services/AuthService.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { CancelablePromise } from '../core/CancelablePromise'; -import { OpenAPI } from '../core/OpenAPI'; -import { request as __request } from '../core/request'; -export class AuthService { - /** - * Sign up a new user - * @param requestBody - * @returns any Sign-up result - * @throws ApiError - */ - public static postAuthSignUp( - requestBody: { - nickname: string; - pass: string; - }, - ): CancelablePromise<{ - success?: boolean; - error?: string | null; - user_id?: string | null; - }> { - return __request(OpenAPI, { - method: 'POST', - url: '/auth/sign-up', - body: requestBody, - mediaType: 'application/json', - }); - } - /** - * Sign in a user and return JWT - * @param requestBody - * @returns any Sign-in result with JWT - * @throws ApiError - */ - public static postAuthSignIn( - requestBody: { - nickname: string; - pass: string; - }, - ): CancelablePromise<{ - success?: boolean; - error?: string | null; - user_id?: string | null; - }> { - return __request(OpenAPI, { - method: 'POST', - url: '/auth/sign-in', - body: requestBody, - mediaType: 'application/json', - errors: { - 401: `Access denied due to invalid credentials`, - }, - }); - } -} diff --git a/modules/frontend/src/pages/LoginPage/LoginPage.tsx b/modules/frontend/src/pages/LoginPage/LoginPage.tsx deleted file mode 100644 index dcd6965..0000000 --- a/modules/frontend/src/pages/LoginPage/LoginPage.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import React, { useState } from "react"; -import { AuthService } from "../../auth/services/AuthService"; -import { useNavigate } from "react-router-dom"; - -export const LoginPage: React.FC = () => { - const navigate = useNavigate(); - const [isLogin, setIsLogin] = useState(true); // true = login, false = signup - const [nickname, setNickname] = useState(""); - const [password, setPassword] = useState(""); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setLoading(true); - setError(null); - - try { - if (isLogin) { - const res = await AuthService.postAuthSignIn({ nickname, pass: password }); - if (res.success) { - // TODO: сохранить JWT в localStorage/cookie - console.log("Logged in user id:", res.user_id); - navigate("/"); // редирект после успешного входа - } else { - setError(res.error || "Login failed"); - } - } else { - const res = await AuthService.postAuthSignUp({ nickname, pass: password }); - if (res.success) { - console.log("User signed up with id:", res.user_id); - setIsLogin(true); // переключаемся на login после регистрации - } else { - setError(res.error || "Sign up failed"); - } - } - } catch (err: any) { - console.error(err); - setError(err?.message || "Something went wrong"); - } finally { - setLoading(false); - } - }; - - return ( -
-
-

- {isLogin ? "Login" : "Sign Up"} -

- - {error &&
{error}
} - -
-
- - setNickname(e.target.value)} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" - required - /> -
- -
- - setPassword(e.target.value)} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" - required - /> -
- - -
- -
- {isLogin ? ( - <> - Don't have an account?{" "} - - - ) : ( - <> - Already have an account?{" "} - - - )} -
-
-
- ); -}; diff --git a/sql/migrations/000001_init.up.sql b/sql/migrations/000001_init.up.sql index f8781de..e6ed628 100644 --- a/sql/migrations/000001_init.up.sql +++ b/sql/migrations/000001_init.up.sql @@ -1,3 +1,6 @@ +-- TODO: +-- maybe jsonb constraints +-- clean unused images CREATE TYPE usertitle_status_t AS ENUM ('finished', 'planned', 'dropped', 'in-progress'); CREATE TYPE storage_type_t AS ENUM ('local', 's3'); CREATE TYPE title_status_t AS ENUM ('finished', 'ongoing', 'planned'); @@ -21,24 +24,37 @@ CREATE TABLE images ( image_path text UNIQUE NOT NULL ); +CREATE TABLE reviews ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + data text NOT NULL, + rating int CHECK (rating >= 0 AND rating <= 10), + user_id bigint REFERENCES users (id), + title_id bigint REFERENCES titles (id), + created_at timestamptz DEFAULT NOW() +); + +CREATE TABLE review_images ( + PRIMARY KEY (review_id, image_id), + review_id bigint NOT NULL REFERENCES reviews(id) ON DELETE CASCADE, + image_id bigint NOT NULL REFERENCES images(id) ON DELETE CASCADE +); + CREATE TABLE users ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - avatar_id bigint REFERENCES images (id) ON DELETE SET NULL, + avatar_id bigint REFERENCES images (id), passhash text NOT NULL, mail text CHECK (mail ~ '^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+$'), nickname text UNIQUE NOT NULL CHECK (nickname ~ '^[a-zA-Z0-9_-]{3,}$'), disp_name text, user_desc text, - creation_date timestamptz NOT NULL DEFAULT NOW(), + creation_date timestamptz NOT NULL, last_login timestamptz ); - - CREATE TABLE studios ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, studio_name text NOT NULL UNIQUE, - illust_id bigint REFERENCES images (id) ON DELETE SET NULL, + illust_id bigint REFERENCES images (id), studio_desc text ); @@ -48,7 +64,7 @@ CREATE TABLE titles ( -- example {"ru": ["Атака титанов", "Атака Титанов"],"en": ["Attack on Titan", "AoT"],"ja": ["進撃の巨人", "しんげきのきょじん"]} title_names jsonb NOT NULL, studio_id bigint NOT NULL REFERENCES studios (id), - poster_id bigint REFERENCES images (id) ON DELETE SET NULL, + poster_id bigint REFERENCES images (id), title_status title_status_t NOT NULL, rating float CHECK (rating >= 0 AND rating <= 10), rating_count int CHECK (rating_count >= 0), @@ -64,36 +80,21 @@ CREATE TABLE titles ( AND episodes_aired <= episodes_all)) ); -CREATE TABLE reviews ( - id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - data text NOT NULL, - rating int CHECK (rating >= 0 AND rating <= 10), - user_id bigint REFERENCES users (id) ON DELETE SET NULL, - title_id bigint REFERENCES titles (id) ON DELETE CASCADE, - created_at timestamptz DEFAULT NOW() -); - -CREATE TABLE review_images ( - PRIMARY KEY (review_id, image_id), - review_id bigint NOT NULL REFERENCES reviews(id) ON DELETE CASCADE, - image_id bigint NOT NULL REFERENCES images(id) ON DELETE CASCADE -); - CREATE TABLE usertitles ( PRIMARY KEY (user_id, title_id), - user_id bigint NOT NULL REFERENCES users (id) ON DELETE CASCADE, - title_id bigint NOT NULL REFERENCES titles (id) ON DELETE CASCADE, + user_id bigint NOT NULL REFERENCES users (id), + title_id bigint NOT NULL REFERENCES titles (id), status usertitle_status_t NOT NULL, rate int CHECK (rate > 0 AND rate <= 10), - review_id bigint REFERENCES reviews (id) ON DELETE SET NULL, - ctime timestamptz NOT NULL DEFAULT now() + review_id bigint REFERENCES reviews (id), + ctime timestamptz -- // TODO: series status ); CREATE TABLE title_tags ( PRIMARY KEY (title_id, tag_id), - title_id bigint NOT NULL REFERENCES titles (id) ON DELETE CASCADE, - tag_id bigint NOT NULL REFERENCES tags (id) ON DELETE CASCADE + title_id bigint NOT NULL REFERENCES titles (id), + tag_id bigint NOT NULL REFERENCES tags (id) ); CREATE TABLE signals ( @@ -104,17 +105,17 @@ CREATE TABLE signals ( pending boolean NOT NULL ); -CREATE TABLE external_services ( - id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - name text UNIQUE NOT NULL -); - CREATE TABLE external_ids ( user_id bigint NOT NULL REFERENCES users (id), service_id bigint REFERENCES external_services (id), external_id text NOT NULL ); +CREATE TABLE external_services ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name text UNIQUE NOT NULL +); + -- Functions CREATE OR REPLACE FUNCTION update_title_rating() RETURNS TRIGGER AS $$ @@ -168,17 +169,4 @@ EXECUTE FUNCTION update_title_rating(); CREATE TRIGGER trg_notify_new_signal AFTER INSERT ON signals FOR EACH ROW -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 -AFTER UPDATE ON usertitles -FOR EACH ROW -EXECUTE FUNCTION set_ctime(); \ No newline at end of file +EXECUTE FUNCTION notify_new_signal(); \ No newline at end of file diff --git a/sql/models.go b/sql/models.go index 842d58c..36d4c07 100644 --- a/sql/models.go +++ b/sql/models.go @@ -277,10 +277,10 @@ type User struct { } type Usertitle struct { - UserID int64 `json:"user_id"` - TitleID int64 `json:"title_id"` - Status UsertitleStatusT `json:"status"` - Rate *int32 `json:"rate"` - ReviewID *int64 `json:"review_id"` - Ctime time.Time `json:"ctime"` + UserID int64 `json:"user_id"` + TitleID int64 `json:"title_id"` + Status UsertitleStatusT `json:"status"` + Rate *int32 `json:"rate"` + ReviewID *int64 `json:"review_id"` + Ctime pgtype.Timestamptz `json:"ctime"` } diff --git a/sql/queries.sql.go b/sql/queries.sql.go index a46da86..daa2b56 100644 --- a/sql/queries.sql.go +++ b/sql/queries.sql.go @@ -9,6 +9,8 @@ import ( "context" "encoding/json" "time" + + "github.com/jackc/pgx/v5/pgtype" ) const createImage = `-- name: CreateImage :one @@ -112,9 +114,12 @@ func (q *Queries) GetStudioByID(ctx context.Context, studioID int64) (Studio, er const getTitleByID = `-- name: GetTitleByID :one + + + 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, - i.storage_type as title_storage_type, + i.storage_type::text as title_storage_type, i.image_path as title_image_path, COALESCE( jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), @@ -123,7 +128,7 @@ SELECT s.studio_name as studio_name, s.illust_id as studio_illust_id, s.studio_desc as studio_desc, - si.storage_type as studio_storage_type, + si.storage_type::text as studio_storage_type, si.image_path as studio_image_path FROM titles as t @@ -152,16 +157,36 @@ type GetTitleByIDRow struct { EpisodesAired *int32 `json:"episodes_aired"` EpisodesAll *int32 `json:"episodes_all"` EpisodesLen []byte `json:"episodes_len"` - TitleStorageType *StorageTypeT `json:"title_storage_type"` + TitleStorageType string `json:"title_storage_type"` TitleImagePath *string `json:"title_image_path"` TagNames json.RawMessage `json:"tag_names"` StudioName *string `json:"studio_name"` StudioIllustID *int64 `json:"studio_illust_id"` StudioDesc *string `json:"studio_desc"` - StudioStorageType *StorageTypeT `json:"studio_storage_type"` + StudioStorageType string `json:"studio_storage_type"` StudioImagePath *string `json:"studio_image_path"` } +// -- name: ListUsers :many +// SELECT user_id, avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date +// FROM users +// ORDER BY user_id +// LIMIT $1 OFFSET $2; +// -- name: CreateUser :one +// INSERT INTO users (avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date) +// VALUES ($1, $2, $3, $4, $5, $6, $7) +// RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date; +// -- name: UpdateUser :one +// UPDATE users +// SET +// +// avatar_id = COALESCE(sqlc.narg('avatar_id'), avatar_id), +// disp_name = COALESCE(sqlc.narg('disp_name'), disp_name), +// user_desc = COALESCE(sqlc.narg('user_desc'), user_desc), +// passhash = COALESCE(sqlc.narg('passhash'), passhash) +// +// WHERE user_id = sqlc.arg('user_id') +// RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date; // -- name: DeleteUser :exec // DELETE FROM users // WHERE user_id = $1; @@ -224,31 +249,19 @@ func (q *Queries) GetTitleTags(ctx context.Context, titleID int64) ([]json.RawMe } const getUserByID = `-- name: GetUserByID :one -SELECT - t.id as id, - t.avatar_id as avatar_id, - t.mail as mail, - t.nickname as nickname, - t.disp_name as disp_name, - t.user_desc as user_desc, - t.creation_date as creation_date, - i.storage_type as storage_type, - i.image_path as image_path -FROM users as t -LEFT JOIN images as i ON (t.avatar_id = i.id) -WHERE t.id = $1::bigint +SELECT id, avatar_id, mail, nickname, disp_name, user_desc, creation_date +FROM users +WHERE id = $1 ` type GetUserByIDRow 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"` + 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"` } func (q *Queries) GetUserByID(ctx context.Context, id int64) (GetUserByIDRow, error) { @@ -262,8 +275,6 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (GetUserByIDRow, er &i.DispName, &i.UserDesc, &i.CreationDate, - &i.StorageType, - &i.ImagePath, ) return i, err } @@ -329,93 +340,6 @@ func (q *Queries) InsertTitleTags(ctx context.Context, arg InsertTitleTagsParams return i, err } -const insertUserTitle = `-- name: InsertUserTitle :one - - - - - - - -INSERT INTO usertitles (user_id, title_id, status, rate, review_id) -VALUES ( - $1::bigint, - $2::bigint, - $3::usertitle_status_t, - $4::int, - $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"` -} - -// -- name: CreateReview :one -// INSERT INTO reviews (user_id, title_id, image_ids, review_text, creation_date) -// VALUES ($1, $2, $3, $4, $5) -// RETURNING review_id, user_id, title_id, image_ids, review_text, creation_date; -// -- name: UpdateReview :one -// UPDATE reviews -// SET -// -// image_ids = COALESCE(sqlc.narg('image_ids'), image_ids), -// review_text = COALESCE(sqlc.narg('review_text'), review_text) -// -// WHERE review_id = sqlc.arg('review_id') -// RETURNING *; -// -- name: DeleteReview :exec -// DELETE FROM reviews -// WHERE review_id = $1; -// -// -- name: ListReviewsByTitle :many -// -// SELECT review_id, user_id, title_id, image_ids, review_text, creation_date -// FROM reviews -// WHERE title_id = $1 -// ORDER BY creation_date DESC -// LIMIT $2 OFFSET $3; -// -- name: ListReviewsByUser :many -// SELECT review_id, user_id, title_id, image_ids, review_text, creation_date -// FROM reviews -// WHERE user_id = $1 -// ORDER BY creation_date DESC -// LIMIT $2 OFFSET $3; -// -- name: GetUserTitle :one -// SELECT usertitle_id, user_id, title_id, status, rate, review_id -// FROM usertitles -// WHERE user_id = $1 AND title_id = $2; -// -- name: ListUserTitles :many -// SELECT usertitle_id, user_id, title_id, status, rate, review_id -// FROM usertitles -// WHERE user_id = $1 -// ORDER BY usertitle_id -// LIMIT $2 OFFSET $3; -func (q *Queries) InsertUserTitle(ctx context.Context, arg InsertUserTitleParams) (Usertitle, error) { - row := q.db.QueryRow(ctx, insertUserTitle, - arg.UserID, - arg.TitleID, - arg.Status, - arg.Rate, - arg.ReviewID, - ) - var i Usertitle - err := row.Scan( - &i.UserID, - &i.TitleID, - &i.Status, - &i.Rate, - &i.ReviewID, - &i.Ctime, - ) - return i, err -} - const searchTitles = `-- name: SearchTitles :many SELECT t.id as id, @@ -429,7 +353,7 @@ SELECT t.season as season, t.episodes_aired as episodes_aired, t.episodes_all as episodes_all, - i.storage_type as title_storage_type, + i.storage_type::text as title_storage_type, i.image_path as title_image_path, COALESCE( jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), @@ -510,14 +434,14 @@ WHERE ) AND ( - $7::title_status_t[] IS NULL - OR array_length($7::title_status_t[], 1) IS NULL - OR array_length($7::title_status_t[], 1) = 0 - OR t.title_status = ANY($7::title_status_t[]) + 'title_statuses'::title_status_t[] IS NULL + OR array_length('title_statuses'::title_status_t[], 1) IS NULL + OR array_length('title_statuses'::title_status_t[], 1) = 0 + OR t.title_status = ANY('title_statuses'::title_status_t[]) ) - AND ($8::float IS NULL OR t.rating >= $8::float) - AND ($9::int IS NULL OR t.release_year = $9::int) - AND ($10::release_season_t IS NULL OR t.release_season = $10::release_season_t) + AND ($7::float IS NULL OR t.rating >= $7::float) + AND ($8::int IS NULL OR t.release_year = $8::int) + AND ($9::release_season_t IS NULL OR t.release_season = $9::release_season_t) GROUP BY t.id, i.id, s.id @@ -540,7 +464,7 @@ ORDER BY CASE WHEN $2::text <> 'id' THEN t.id END ASC -LIMIT COALESCE($11::int, 100) +LIMIT COALESCE($10::int, 100) ` type SearchTitlesParams struct { @@ -550,7 +474,6 @@ type SearchTitlesParams struct { CursorID *int64 `json:"cursor_id"` CursorRating *float64 `json:"cursor_rating"` Word *string `json:"word"` - TitleStatuses []TitleStatusT `json:"title_statuses"` Rating *float64 `json:"rating"` ReleaseYear *int32 `json:"release_year"` ReleaseSeason *ReleaseSeasonT `json:"release_season"` @@ -569,7 +492,7 @@ type SearchTitlesRow struct { Season *int32 `json:"season"` EpisodesAired *int32 `json:"episodes_aired"` EpisodesAll *int32 `json:"episodes_all"` - TitleStorageType *StorageTypeT `json:"title_storage_type"` + TitleStorageType string `json:"title_storage_type"` TitleImagePath *string `json:"title_image_path"` TagNames json.RawMessage `json:"tag_names"` StudioName *string `json:"studio_name"` @@ -583,7 +506,6 @@ func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]S arg.CursorID, arg.CursorRating, arg.Word, - arg.TitleStatuses, arg.Rating, arg.ReleaseYear, arg.ReleaseSeason, @@ -642,7 +564,7 @@ SELECT u.rate as user_rate, u.review_id as review_id, u.ctime as user_ctime, - i.storage_type as title_storage_type, + i.storage_type::text as title_storage_type, i.image_path as title_image_path, COALESCE( jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), @@ -658,45 +580,43 @@ LEFT JOIN tags as g ON (tt.tag_id = g.id) LEFT JOIN studios as s ON (t.studio_id = s.id) WHERE - u.user_id = $1::bigint - AND CASE - WHEN $2::boolean THEN + WHEN $1::boolean THEN -- forward: greater than cursor (next page) - CASE $3::text + CASE $2::text WHEN 'year' THEN - ($4::int IS NULL) OR - (t.release_year > $4::int) OR - (t.release_year = $4::int AND t.id > $5::bigint) + ($3::int IS NULL) OR + (t.release_year > $3::int) OR + (t.release_year = $3::int AND t.id > $4::bigint) WHEN 'rating' THEN - ($6::float IS NULL) OR - (t.rating > $6::float) OR - (t.rating = $6::float AND t.id > $5::bigint) + ($5::float IS NULL) OR + (t.rating > $5::float) OR + (t.rating = $5::float AND t.id > $4::bigint) WHEN 'id' THEN - ($5::bigint IS NULL) OR - (t.id > $5::bigint) + ($4::bigint IS NULL) OR + (t.id > $4::bigint) ELSE true -- fallback END ELSE -- backward: less than cursor (prev page) - CASE $3::text + CASE $2::text WHEN 'year' THEN - ($4::int IS NULL) OR - (t.release_year < $4::int) OR - (t.release_year = $4::int AND t.id < $5::bigint) + ($3::int IS NULL) OR + (t.release_year < $3::int) OR + (t.release_year = $3::int AND t.id < $4::bigint) WHEN 'rating' THEN - ($6::float IS NULL) OR - (t.rating < $6::float) OR - (t.rating = $6::float AND t.id < $5::bigint) + ($5::float IS NULL) OR + (t.rating < $5::float) OR + (t.rating = $5::float AND t.id < $4::bigint) WHEN 'id' THEN - ($5::bigint IS NULL) OR - (t.id < $5::bigint) + ($4::bigint IS NULL) OR + (t.id < $4::bigint) ELSE true END @@ -704,7 +624,7 @@ WHERE AND ( CASE - WHEN $7::text IS NOT NULL THEN + WHEN $6::text IS NOT NULL THEN ( SELECT bool_and( EXISTS ( @@ -716,7 +636,7 @@ WHERE FROM unnest( ARRAY( SELECT '%' || trim(w) || '%' - FROM unnest(string_to_array($7::text, ' ')) AS w + FROM unnest(string_to_array($6::text, ' ')) AS w WHERE trim(w) <> '' ) ) AS pattern @@ -726,100 +646,94 @@ WHERE ) AND ( - $8::title_status_t[] IS NULL - OR array_length($8::title_status_t[], 1) IS NULL - OR array_length($8::title_status_t[], 1) = 0 - OR t.title_status = ANY($8::title_status_t[]) + 'title_statuses'::title_status_t[] IS NULL + OR array_length('title_statuses'::title_status_t[], 1) IS NULL + OR array_length('title_statuses'::title_status_t[], 1) = 0 + OR t.title_status = ANY('title_statuses'::title_status_t[]) ) AND ( - $9::usertitle_status_t[] IS NULL - OR array_length($9::usertitle_status_t[], 1) IS NULL - OR array_length($9::usertitle_status_t[], 1) = 0 - OR u.status = ANY($9::usertitle_status_t[]) + 'usertitle_statuses'::title_status_t[] IS NULL + OR array_length('usertitle_statuses'::title_status_t[], 1) IS NULL + OR array_length('usertitle_statuses'::title_status_t[], 1) = 0 + OR t.title_status = ANY('usertitle_statuses'::title_status_t[]) ) - AND ($10::int IS NULL OR u.rate >= $10::int) - AND ($11::float IS NULL OR t.rating >= $11::float) - AND ($12::int IS NULL OR t.release_year = $12::int) - AND ($13::release_season_t IS NULL OR t.release_season = $13::release_season_t) + AND ($7::int IS NULL OR u.rate >= $7::int) + AND ($8::float IS NULL OR t.rating >= $8::float) + AND ($9::int IS NULL OR t.release_year = $9::int) + AND ($10::release_season_t IS NULL OR t.release_season = $10::release_season_t) GROUP BY - t.id, u.user_id, u.status, u.rate, u.review_id, u.ctime, i.id, s.id + t.id, i.id, s.id ORDER BY - CASE WHEN $2::boolean THEN + CASE WHEN $1::boolean THEN CASE - WHEN $3::text = 'id' THEN t.id - WHEN $3::text = 'year' THEN t.release_year - WHEN $3::text = 'rating' THEN t.rating - WHEN $3::text = 'rate' THEN u.rate + WHEN $2::text = 'id' THEN t.id + WHEN $2::text = 'year' THEN t.release_year + WHEN $2::text = 'rating' THEN t.rating + WHEN $2::text = 'rate' THEN u.rate END END ASC, - CASE WHEN NOT $2::boolean THEN + CASE WHEN NOT $1::boolean THEN CASE - WHEN $3::text = 'id' THEN t.id - WHEN $3::text = 'year' THEN t.release_year - WHEN $3::text = 'rating' THEN t.rating - WHEN $3::text = 'rate' THEN u.rate + WHEN $2::text = 'id' THEN t.id + WHEN $2::text = 'year' THEN t.release_year + WHEN $2::text = 'rating' THEN t.rating + WHEN $2::text = 'rate' THEN u.rate END END DESC, - CASE WHEN $3::text <> 'id' THEN t.id END ASC + CASE WHEN $2::text <> 'id' THEN t.id END ASC -LIMIT COALESCE($14::int, 100) +LIMIT COALESCE($11::int, 100) ` type SearchUserTitlesParams struct { - UserID int64 `json:"user_id"` - Forward bool `json:"forward"` - SortBy string `json:"sort_by"` - CursorYear *int32 `json:"cursor_year"` - CursorID *int64 `json:"cursor_id"` - CursorRating *float64 `json:"cursor_rating"` - Word *string `json:"word"` - TitleStatuses []TitleStatusT `json:"title_statuses"` - UsertitleStatuses []UsertitleStatusT `json:"usertitle_statuses"` - Rate *int32 `json:"rate"` - Rating *float64 `json:"rating"` - ReleaseYear *int32 `json:"release_year"` - ReleaseSeason *ReleaseSeasonT `json:"release_season"` - Limit *int32 `json:"limit"` + Forward bool `json:"forward"` + SortBy string `json:"sort_by"` + CursorYear *int32 `json:"cursor_year"` + CursorID *int64 `json:"cursor_id"` + CursorRating *float64 `json:"cursor_rating"` + Word *string `json:"word"` + Rate *int32 `json:"rate"` + Rating *float64 `json:"rating"` + ReleaseYear *int32 `json:"release_year"` + ReleaseSeason *ReleaseSeasonT `json:"release_season"` + Limit *int32 `json:"limit"` } type SearchUserTitlesRow struct { - ID int64 `json:"id"` - TitleNames json.RawMessage `json:"title_names"` - PosterID *int64 `json:"poster_id"` - TitleStatus TitleStatusT `json:"title_status"` - Rating *float64 `json:"rating"` - RatingCount *int32 `json:"rating_count"` - ReleaseYear *int32 `json:"release_year"` - ReleaseSeason *ReleaseSeasonT `json:"release_season"` - Season *int32 `json:"season"` - EpisodesAired *int32 `json:"episodes_aired"` - EpisodesAll *int32 `json:"episodes_all"` - UserID int64 `json:"user_id"` - UsertitleStatus UsertitleStatusT `json:"usertitle_status"` - UserRate *int32 `json:"user_rate"` - ReviewID *int64 `json:"review_id"` - UserCtime time.Time `json:"user_ctime"` - TitleStorageType *StorageTypeT `json:"title_storage_type"` - TitleImagePath *string `json:"title_image_path"` - TagNames json.RawMessage `json:"tag_names"` - StudioName *string `json:"studio_name"` + ID int64 `json:"id"` + TitleNames json.RawMessage `json:"title_names"` + PosterID *int64 `json:"poster_id"` + TitleStatus TitleStatusT `json:"title_status"` + Rating *float64 `json:"rating"` + RatingCount *int32 `json:"rating_count"` + ReleaseYear *int32 `json:"release_year"` + ReleaseSeason *ReleaseSeasonT `json:"release_season"` + Season *int32 `json:"season"` + EpisodesAired *int32 `json:"episodes_aired"` + EpisodesAll *int32 `json:"episodes_all"` + UserID int64 `json:"user_id"` + UsertitleStatus UsertitleStatusT `json:"usertitle_status"` + UserRate *int32 `json:"user_rate"` + ReviewID *int64 `json:"review_id"` + UserCtime pgtype.Timestamptz `json:"user_ctime"` + TitleStorageType string `json:"title_storage_type"` + TitleImagePath *string `json:"title_image_path"` + TagNames json.RawMessage `json:"tag_names"` + StudioName *string `json:"studio_name"` } // 100 is default limit func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesParams) ([]SearchUserTitlesRow, error) { rows, err := q.db.Query(ctx, searchUserTitles, - arg.UserID, arg.Forward, arg.SortBy, arg.CursorYear, arg.CursorID, arg.CursorRating, arg.Word, - arg.TitleStatuses, - arg.UsertitleStatuses, arg.Rate, arg.Rating, arg.ReleaseYear, @@ -864,64 +778,3 @@ func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesPara } return items, nil } - -const updateUser = `-- name: UpdateUser :one - - -UPDATE users -SET - avatar_id = COALESCE($1, avatar_id), - disp_name = COALESCE($2, disp_name), - user_desc = COALESCE($3, user_desc), - mail = COALESCE($4, mail) -WHERE id = $5 -RETURNING id, avatar_id, nickname, disp_name, user_desc, creation_date, mail -` - -type UpdateUserParams struct { - AvatarID *int64 `json:"avatar_id"` - DispName *string `json:"disp_name"` - UserDesc *string `json:"user_desc"` - Mail *string `json:"mail"` - UserID int64 `json:"user_id"` -} - -type UpdateUserRow struct { - ID int64 `json:"id"` - AvatarID *int64 `json:"avatar_id"` - Nickname string `json:"nickname"` - DispName *string `json:"disp_name"` - UserDesc *string `json:"user_desc"` - CreationDate time.Time `json:"creation_date"` - Mail *string `json:"mail"` -} - -// -- name: ListUsers :many -// SELECT user_id, avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date -// FROM users -// ORDER BY user_id -// LIMIT $1 OFFSET $2; -// -- name: CreateUser :one -// INSERT INTO users (avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date) -// VALUES ($1, $2, $3, $4, $5, $6, $7) -// RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date; -func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (UpdateUserRow, error) { - row := q.db.QueryRow(ctx, updateUser, - arg.AvatarID, - arg.DispName, - arg.UserDesc, - arg.Mail, - arg.UserID, - ) - var i UpdateUserRow - err := row.Scan( - &i.ID, - &i.AvatarID, - &i.Nickname, - &i.DispName, - &i.UserDesc, - &i.CreationDate, - &i.Mail, - ) - return i, err -} diff --git a/sql/sqlc.yaml b/sql/sqlc.yaml index de67bcf..3338c35 100644 --- a/sql/sqlc.yaml +++ b/sql/sqlc.yaml @@ -14,11 +14,6 @@ sql: emit_pointers_for_null_types: true emit_empty_slices: true #slices returned by :many queries will be empty instead of nil overrides: - - db_type: "storage_type_t" - nullable: true - go_type: - type: "StorageTypeT" - pointer: true - db_type: "jsonb" go_type: "encoding/json.RawMessage" - db_type: "uuid"