diff --git a/.forgejo/workflows/build-and-deploy.yml b/.forgejo/workflows/build-and-deploy.yml index 3c473d2..0338440 100644 --- a/.forgejo/workflows/build-and-deploy.yml +++ b/.forgejo/workflows/build-and-deploy.yml @@ -18,10 +18,14 @@ jobs: - uses: actions/setup-go@v6 with: go-version: '^1.25' + check-latest: false + cache-dependency-path: | + go.sum - name: Build backend run: | cd modules/backend + go mod tidy go build -o nyanimedb . tar -czvf nyanimedb-backend.tar.gz nyanimedb @@ -34,6 +38,7 @@ jobs: - name: Build auth run: | cd modules/auth + go mod tidy go build -o auth . tar -czvf nyanimedb-auth.tar.gz auth @@ -101,7 +106,7 @@ jobs: tags: meowgit.nekoea.red/nihonium/nyanimedb-frontend:latest deploy: - runs-on: debian-test + runs-on: self-hosted needs: build env: POSTGRES_USER: ${{ secrets.POSTGRES_USER }} diff --git a/api/_build/openapi.yaml b/api/_build/openapi.yaml index 58dd890..f875ba2 100644 --- a/api/_build/openapi.yaml +++ b/api/_build/openapi.yaml @@ -120,8 +120,6 @@ paths: description: Title not found '500': description: Unknown server error - security: - - JwtAuthCookies: [] '/users/{user_id}': get: operationId: getUsersId @@ -158,8 +156,6 @@ paths: Password updates must be done via the dedicated auth-service (`/auth/`). Fields not provided in the request body remain unchanged. parameters: - - $ref: '#/components/parameters/accessToken' - - $ref: '#/components/parameters/csrfToken' - name: user_id in: path description: User ID (primary key) @@ -227,11 +223,8 @@ paths: description: 'Unprocessable Entity — semantic errors not caught by schema (e.g., invalid `avatar_id`)' '500': description: Unknown server error - security: - - JwtAuthCookies: [] '/users/{user_id}/titles': get: - operationId: getUserTitles summary: Get user titles parameters: - $ref: '#/components/parameters/cursor' @@ -372,38 +365,6 @@ paths: description: Conflict — title already assigned to user (if applicable) '500': description: Internal server error - '/users/{user_id}/titles/{title_id}': - get: - operationId: getUserTitle - summary: Get user title - parameters: - - name: user_id - in: path - required: true - schema: - type: integer - format: int64 - - name: title_id - in: path - required: true - schema: - type: integer - format: int64 - responses: - '200': - description: User titles - content: - application/json: - schema: - $ref: '#/components/schemas/UserTitleMini' - '204': - description: No user title found - '400': - description: Request params are not correct - '404': - description: User or title not found - '500': - description: Unknown server error patch: operationId: updateUserTitle summary: Update a usertitle @@ -411,16 +372,12 @@ paths: parameters: - name: user_id in: path + description: ID of the user to assign the title to required: true schema: type: integer format: int64 - - name: title_id - in: path - required: true - schema: - type: integer - format: int64 + example: 123 requestBody: required: true content: @@ -428,11 +385,16 @@ paths: schema: type: object properties: + title_id: + type: integer + format: int64 status: $ref: '#/components/schemas/UserTitleStatus' rate: type: integer format: int32 + required: + - title_id responses: '200': description: Title successfully updated @@ -457,12 +419,13 @@ paths: parameters: - name: user_id in: path + description: ID of the user to assign the title to required: true schema: type: integer format: int64 - name: title_id - in: path + in: query required: true schema: type: integer @@ -480,39 +443,6 @@ paths: description: Internal server error components: parameters: - accessToken: - name: access_token - in: cookie - required: true - schema: - type: string - format: jwt - example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.x.y - description: | - JWT access token. - csrfToken: - name: XSRF-TOKEN - in: cookie - required: true - schema: - type: string - pattern: '^[a-zA-Z0-9_-]{32,64}$' - example: abc123def456ghi789jkl012mno345pqr - description: | - Anti-CSRF token (Double Submit Cookie pattern). - Stored in non-HttpOnly cookie, readable by JavaScript. - Must be echoed in `X-XSRF-TOKEN` header for state-changing requests (POST/PUT/PATCH/DELETE). - csrfTokenHeader: - name: X-XSRF-TOKEN - in: header - required: true - schema: - type: string - pattern: '^[a-zA-Z0-9_-]{32,64}$' - description: | - Anti-CSRF token. Must match the `XSRF-TOKEN` cookie. - Required for all state-changing requests (POST/PUT/PATCH/DELETE). - example: abc123def456ghi789jkl012mno345pqr cursor: in: query name: cursor diff --git a/api/api.gen.go b/api/api.gen.go index 62450e0..2294d74 100644 --- a/api/api.gen.go +++ b/api/api.gen.go @@ -16,10 +16,6 @@ import ( openapi_types "github.com/oapi-codegen/runtime/types" ) -const ( - JwtAuthCookiesScopes = "JwtAuthCookies.Scopes" -) - // Defines values for ReleaseSeason. const ( Fall ReleaseSeason = "fall" @@ -174,12 +170,6 @@ type UserTitleMini struct { // UserTitleStatus User's title status type UserTitleStatus string -// AccessToken defines model for accessToken. -type AccessToken = string - -// CsrfToken defines model for csrfToken. -type CsrfToken = string - // Cursor defines model for cursor. type Cursor = string @@ -229,19 +219,13 @@ type UpdateUserJSONBody struct { UserDesc *string `json:"user_desc,omitempty"` } -// UpdateUserParams defines parameters for UpdateUser. -type UpdateUserParams struct { - // AccessToken JWT access token. - AccessToken AccessToken `form:"access_token" json:"access_token"` - - // XSRFTOKEN Anti-CSRF token (Double Submit Cookie pattern). - // Stored in non-HttpOnly cookie, readable by JavaScript. - // Must be echoed in `X-XSRF-TOKEN` header for state-changing requests (POST/PUT/PATCH/DELETE). - XSRFTOKEN CsrfToken `form:"XSRF-TOKEN" json:"XSRF-TOKEN"` +// DeleteUserTitleParams defines parameters for DeleteUserTitle. +type DeleteUserTitleParams struct { + TitleId int64 `form:"title_id" json:"title_id"` } -// GetUserTitlesParams defines parameters for GetUserTitles. -type GetUserTitlesParams struct { +// GetUsersUserIdTitlesParams defines parameters for GetUsersUserIdTitles. +type GetUsersUserIdTitlesParams struct { Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"` Sort *TitleSort `form:"sort,omitempty" json:"sort,omitempty"` SortForward *bool `form:"sort_forward,omitempty" json:"sort_forward,omitempty"` @@ -258,6 +242,15 @@ type GetUserTitlesParams struct { Fields *string `form:"fields,omitempty" json:"fields,omitempty"` } +// UpdateUserTitleJSONBody defines parameters for UpdateUserTitle. +type UpdateUserTitleJSONBody struct { + Rate *int32 `json:"rate,omitempty"` + + // Status User's title status + Status *UserTitleStatus `json:"status,omitempty"` + TitleId int64 `json:"title_id"` +} + // AddUserTitleJSONBody defines parameters for AddUserTitle. type AddUserTitleJSONBody struct { Rate *int32 `json:"rate,omitempty"` @@ -267,23 +260,15 @@ type AddUserTitleJSONBody struct { TitleId int64 `json:"title_id"` } -// UpdateUserTitleJSONBody defines parameters for UpdateUserTitle. -type UpdateUserTitleJSONBody struct { - Rate *int32 `json:"rate,omitempty"` - - // Status User's title status - Status *UserTitleStatus `json:"status,omitempty"` -} - // UpdateUserJSONRequestBody defines body for UpdateUser for application/json ContentType. type UpdateUserJSONRequestBody UpdateUserJSONBody -// AddUserTitleJSONRequestBody defines body for AddUserTitle for application/json ContentType. -type AddUserTitleJSONRequestBody AddUserTitleJSONBody - // UpdateUserTitleJSONRequestBody defines body for UpdateUserTitle for application/json ContentType. type UpdateUserTitleJSONRequestBody UpdateUserTitleJSONBody +// AddUserTitleJSONRequestBody defines body for AddUserTitle for application/json ContentType. +type AddUserTitleJSONRequestBody AddUserTitleJSONBody + // ServerInterface represents all server handlers. type ServerInterface interface { // Get titles @@ -297,22 +282,19 @@ type ServerInterface interface { GetUsersId(c *gin.Context, userId string, params GetUsersIdParams) // Partially update a user account // (PATCH /users/{user_id}) - UpdateUser(c *gin.Context, userId int64, params UpdateUserParams) + UpdateUser(c *gin.Context, userId int64) + // Delete a usertitle + // (DELETE /users/{user_id}/titles) + DeleteUserTitle(c *gin.Context, userId int64, params DeleteUserTitleParams) // Get user titles // (GET /users/{user_id}/titles) - GetUserTitles(c *gin.Context, userId string, params GetUserTitlesParams) + GetUsersUserIdTitles(c *gin.Context, userId string, params GetUsersUserIdTitlesParams) + // Update a usertitle + // (PATCH /users/{user_id}/titles) + UpdateUserTitle(c *gin.Context, userId int64) // Add a title to a user // (POST /users/{user_id}/titles) AddUserTitle(c *gin.Context, userId int64) - // Delete a usertitle - // (DELETE /users/{user_id}/titles/{title_id}) - DeleteUserTitle(c *gin.Context, userId int64, titleId int64) - // Get user title - // (GET /users/{user_id}/titles/{title_id}) - GetUserTitle(c *gin.Context, userId int64, titleId int64) - // Update a usertitle - // (PATCH /users/{user_id}/titles/{title_id}) - UpdateUserTitle(c *gin.Context, userId int64, titleId int64) } // ServerInterfaceWrapper converts contexts to parameters. @@ -452,8 +434,6 @@ func (siw *ServerInterfaceWrapper) GetTitle(c *gin.Context) { return } - c.Set(JwtAuthCookiesScopes, []string{}) - // Parameter object where we will unmarshal all parameters from the context var params GetTitleParams @@ -524,45 +504,46 @@ func (siw *ServerInterfaceWrapper) UpdateUser(c *gin.Context) { return } - c.Set(JwtAuthCookiesScopes, []string{}) - - // Parameter object where we will unmarshal all parameters from the context - var params UpdateUserParams - - { - var cookie string - - if cookie, err = c.Cookie("access_token"); err == nil { - var value AccessToken - err = runtime.BindStyledParameterWithOptions("simple", "access_token", cookie, &value, runtime.BindStyledParameterOptions{Explode: true, Required: true}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter access_token: %w", err), http.StatusBadRequest) - return - } - params.AccessToken = value - - } else { - siw.ErrorHandler(c, fmt.Errorf("Query argument access_token is required, but not found"), http.StatusBadRequest) + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { return } } - { - var cookie string + siw.Handler.UpdateUser(c, userId) +} - if cookie, err = c.Cookie("XSRF-TOKEN"); err == nil { - var value CsrfToken - err = runtime.BindStyledParameterWithOptions("simple", "XSRF-TOKEN", cookie, &value, runtime.BindStyledParameterOptions{Explode: true, Required: true}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter XSRF-TOKEN: %w", err), http.StatusBadRequest) - return - } - params.XSRFTOKEN = value +// DeleteUserTitle operation middleware +func (siw *ServerInterfaceWrapper) DeleteUserTitle(c *gin.Context) { - } else { - siw.ErrorHandler(c, fmt.Errorf("Query argument XSRF-TOKEN is required, but not found"), http.StatusBadRequest) - return - } + 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 + } + + // Parameter object where we will unmarshal all parameters from the context + var params DeleteUserTitleParams + + // ------------- Required query parameter "title_id" ------------- + + if paramValue := c.Query("title_id"); paramValue != "" { + + } else { + siw.ErrorHandler(c, fmt.Errorf("Query argument title_id is required, but not found"), http.StatusBadRequest) + return + } + + err = runtime.BindQueryParameter("form", true, true, "title_id", c.Request.URL.Query(), ¶ms.TitleId) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter title_id: %w", err), http.StatusBadRequest) + return } for _, middleware := range siw.HandlerMiddlewares { @@ -572,11 +553,11 @@ func (siw *ServerInterfaceWrapper) UpdateUser(c *gin.Context) { } } - siw.Handler.UpdateUser(c, userId, params) + siw.Handler.DeleteUserTitle(c, userId, params) } -// GetUserTitles operation middleware -func (siw *ServerInterfaceWrapper) GetUserTitles(c *gin.Context) { +// GetUsersUserIdTitles operation middleware +func (siw *ServerInterfaceWrapper) GetUsersUserIdTitles(c *gin.Context) { var err error @@ -590,7 +571,7 @@ func (siw *ServerInterfaceWrapper) GetUserTitles(c *gin.Context) { } // Parameter object where we will unmarshal all parameters from the context - var params GetUserTitlesParams + var params GetUsersUserIdTitlesParams // ------------- Optional query parameter "cursor" ------------- @@ -695,7 +676,31 @@ func (siw *ServerInterfaceWrapper) GetUserTitles(c *gin.Context) { } } - siw.Handler.GetUserTitles(c, userId, params) + siw.Handler.GetUsersUserIdTitles(c, userId, params) +} + +// UpdateUserTitle operation middleware +func (siw *ServerInterfaceWrapper) UpdateUserTitle(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.UpdateUserTitle(c, userId) } // AddUserTitle operation middleware @@ -722,105 +727,6 @@ func (siw *ServerInterfaceWrapper) AddUserTitle(c *gin.Context) { siw.Handler.AddUserTitle(c, userId) } -// DeleteUserTitle operation middleware -func (siw *ServerInterfaceWrapper) DeleteUserTitle(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 - } - - // ------------- Path parameter "title_id" ------------- - var titleId int64 - - err = runtime.BindStyledParameterWithOptions("simple", "title_id", c.Param("title_id"), &titleId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter title_id: %w", err), http.StatusBadRequest) - return - } - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.DeleteUserTitle(c, userId, titleId) -} - -// GetUserTitle operation middleware -func (siw *ServerInterfaceWrapper) GetUserTitle(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 - } - - // ------------- Path parameter "title_id" ------------- - var titleId int64 - - err = runtime.BindStyledParameterWithOptions("simple", "title_id", c.Param("title_id"), &titleId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter title_id: %w", err), http.StatusBadRequest) - return - } - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.GetUserTitle(c, userId, titleId) -} - -// UpdateUserTitle operation middleware -func (siw *ServerInterfaceWrapper) UpdateUserTitle(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 - } - - // ------------- Path parameter "title_id" ------------- - var titleId int64 - - err = runtime.BindStyledParameterWithOptions("simple", "title_id", c.Param("title_id"), &titleId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter title_id: %w", err), http.StatusBadRequest) - return - } - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.UpdateUserTitle(c, userId, titleId) -} - // GinServerOptions provides options for the Gin server. type GinServerOptions struct { BaseURL string @@ -852,11 +758,10 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options router.GET(options.BaseURL+"/titles/:title_id", wrapper.GetTitle) router.GET(options.BaseURL+"/users/:user_id", wrapper.GetUsersId) router.PATCH(options.BaseURL+"/users/:user_id", wrapper.UpdateUser) - router.GET(options.BaseURL+"/users/:user_id/titles", wrapper.GetUserTitles) + router.DELETE(options.BaseURL+"/users/:user_id/titles", wrapper.DeleteUserTitle) + router.GET(options.BaseURL+"/users/:user_id/titles", wrapper.GetUsersUserIdTitles) + router.PATCH(options.BaseURL+"/users/:user_id/titles", wrapper.UpdateUserTitle) router.POST(options.BaseURL+"/users/:user_id/titles", wrapper.AddUserTitle) - router.DELETE(options.BaseURL+"/users/:user_id/titles/:title_id", wrapper.DeleteUserTitle) - router.GET(options.BaseURL+"/users/:user_id/titles/:title_id", wrapper.GetUserTitle) - router.PATCH(options.BaseURL+"/users/:user_id/titles/:title_id", wrapper.UpdateUserTitle) } type GetTitlesRequestObject struct { @@ -999,7 +904,6 @@ func (response GetUsersId500Response) VisitGetUsersIdResponse(w http.ResponseWri type UpdateUserRequestObject struct { UserId int64 `json:"user_id"` - Params UpdateUserParams Body *UpdateUserJSONRequestBody } @@ -1072,55 +976,162 @@ func (response UpdateUser500Response) VisitUpdateUserResponse(w http.ResponseWri return nil } -type GetUserTitlesRequestObject struct { +type DeleteUserTitleRequestObject struct { + UserId int64 `json:"user_id"` + Params DeleteUserTitleParams +} + +type DeleteUserTitleResponseObject interface { + VisitDeleteUserTitleResponse(w http.ResponseWriter) error +} + +type DeleteUserTitle200Response struct { +} + +func (response DeleteUserTitle200Response) VisitDeleteUserTitleResponse(w http.ResponseWriter) error { + w.WriteHeader(200) + return nil +} + +type DeleteUserTitle401Response struct { +} + +func (response DeleteUserTitle401Response) VisitDeleteUserTitleResponse(w http.ResponseWriter) error { + w.WriteHeader(401) + return nil +} + +type DeleteUserTitle403Response struct { +} + +func (response DeleteUserTitle403Response) VisitDeleteUserTitleResponse(w http.ResponseWriter) error { + w.WriteHeader(403) + return nil +} + +type DeleteUserTitle404Response struct { +} + +func (response DeleteUserTitle404Response) VisitDeleteUserTitleResponse(w http.ResponseWriter) error { + w.WriteHeader(404) + return nil +} + +type DeleteUserTitle500Response struct { +} + +func (response DeleteUserTitle500Response) VisitDeleteUserTitleResponse(w http.ResponseWriter) error { + w.WriteHeader(500) + return nil +} + +type GetUsersUserIdTitlesRequestObject struct { UserId string `json:"user_id"` - Params GetUserTitlesParams + Params GetUsersUserIdTitlesParams } -type GetUserTitlesResponseObject interface { - VisitGetUserTitlesResponse(w http.ResponseWriter) error +type GetUsersUserIdTitlesResponseObject interface { + VisitGetUsersUserIdTitlesResponse(w http.ResponseWriter) error } -type GetUserTitles200JSONResponse struct { +type GetUsersUserIdTitles200JSONResponse struct { Cursor CursorObj `json:"cursor"` Data []UserTitle `json:"data"` } -func (response GetUserTitles200JSONResponse) VisitGetUserTitlesResponse(w http.ResponseWriter) error { +func (response GetUsersUserIdTitles200JSONResponse) VisitGetUsersUserIdTitlesResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) return json.NewEncoder(w).Encode(response) } -type GetUserTitles204Response struct { +type GetUsersUserIdTitles204Response struct { } -func (response GetUserTitles204Response) VisitGetUserTitlesResponse(w http.ResponseWriter) error { +func (response GetUsersUserIdTitles204Response) VisitGetUsersUserIdTitlesResponse(w http.ResponseWriter) error { w.WriteHeader(204) return nil } -type GetUserTitles400Response struct { +type GetUsersUserIdTitles400Response struct { } -func (response GetUserTitles400Response) VisitGetUserTitlesResponse(w http.ResponseWriter) error { +func (response GetUsersUserIdTitles400Response) VisitGetUsersUserIdTitlesResponse(w http.ResponseWriter) error { w.WriteHeader(400) return nil } -type GetUserTitles404Response struct { +type GetUsersUserIdTitles404Response struct { } -func (response GetUserTitles404Response) VisitGetUserTitlesResponse(w http.ResponseWriter) error { +func (response GetUsersUserIdTitles404Response) VisitGetUsersUserIdTitlesResponse(w http.ResponseWriter) error { w.WriteHeader(404) return nil } -type GetUserTitles500Response struct { +type GetUsersUserIdTitles500Response struct { } -func (response GetUserTitles500Response) VisitGetUserTitlesResponse(w http.ResponseWriter) error { +func (response GetUsersUserIdTitles500Response) VisitGetUsersUserIdTitlesResponse(w http.ResponseWriter) error { + w.WriteHeader(500) + return nil +} + +type UpdateUserTitleRequestObject struct { + UserId int64 `json:"user_id"` + Body *UpdateUserTitleJSONRequestBody +} + +type UpdateUserTitleResponseObject interface { + VisitUpdateUserTitleResponse(w http.ResponseWriter) error +} + +type UpdateUserTitle200JSONResponse UserTitleMini + +func (response UpdateUserTitle200JSONResponse) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateUserTitle400Response struct { +} + +func (response UpdateUserTitle400Response) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { + w.WriteHeader(400) + return nil +} + +type UpdateUserTitle401Response struct { +} + +func (response UpdateUserTitle401Response) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { + w.WriteHeader(401) + return nil +} + +type UpdateUserTitle403Response struct { +} + +func (response UpdateUserTitle403Response) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { + w.WriteHeader(403) + return nil +} + +type UpdateUserTitle404Response struct { +} + +func (response UpdateUserTitle404Response) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { + w.WriteHeader(404) + return nil +} + +type UpdateUserTitle500Response struct { +} + +func (response UpdateUserTitle500Response) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { w.WriteHeader(500) return nil } @@ -1191,164 +1202,6 @@ func (response AddUserTitle500Response) VisitAddUserTitleResponse(w http.Respons return nil } -type DeleteUserTitleRequestObject struct { - UserId int64 `json:"user_id"` - TitleId int64 `json:"title_id"` -} - -type DeleteUserTitleResponseObject interface { - VisitDeleteUserTitleResponse(w http.ResponseWriter) error -} - -type DeleteUserTitle200Response struct { -} - -func (response DeleteUserTitle200Response) VisitDeleteUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(200) - return nil -} - -type DeleteUserTitle401Response struct { -} - -func (response DeleteUserTitle401Response) VisitDeleteUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(401) - return nil -} - -type DeleteUserTitle403Response struct { -} - -func (response DeleteUserTitle403Response) VisitDeleteUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(403) - return nil -} - -type DeleteUserTitle404Response struct { -} - -func (response DeleteUserTitle404Response) VisitDeleteUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(404) - return nil -} - -type DeleteUserTitle500Response struct { -} - -func (response DeleteUserTitle500Response) VisitDeleteUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(500) - return nil -} - -type GetUserTitleRequestObject struct { - UserId int64 `json:"user_id"` - TitleId int64 `json:"title_id"` -} - -type GetUserTitleResponseObject interface { - VisitGetUserTitleResponse(w http.ResponseWriter) error -} - -type GetUserTitle200JSONResponse UserTitleMini - -func (response GetUserTitle200JSONResponse) VisitGetUserTitleResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type GetUserTitle204Response struct { -} - -func (response GetUserTitle204Response) VisitGetUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(204) - return nil -} - -type GetUserTitle400Response struct { -} - -func (response GetUserTitle400Response) VisitGetUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(400) - return nil -} - -type GetUserTitle404Response struct { -} - -func (response GetUserTitle404Response) VisitGetUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(404) - return nil -} - -type GetUserTitle500Response struct { -} - -func (response GetUserTitle500Response) VisitGetUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(500) - return nil -} - -type UpdateUserTitleRequestObject struct { - UserId int64 `json:"user_id"` - TitleId int64 `json:"title_id"` - Body *UpdateUserTitleJSONRequestBody -} - -type UpdateUserTitleResponseObject interface { - VisitUpdateUserTitleResponse(w http.ResponseWriter) error -} - -type UpdateUserTitle200JSONResponse UserTitleMini - -func (response UpdateUserTitle200JSONResponse) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type UpdateUserTitle400Response struct { -} - -func (response UpdateUserTitle400Response) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(400) - return nil -} - -type UpdateUserTitle401Response struct { -} - -func (response UpdateUserTitle401Response) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(401) - return nil -} - -type UpdateUserTitle403Response struct { -} - -func (response UpdateUserTitle403Response) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(403) - return nil -} - -type UpdateUserTitle404Response struct { -} - -func (response UpdateUserTitle404Response) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(404) - return nil -} - -type UpdateUserTitle500Response struct { -} - -func (response UpdateUserTitle500Response) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(500) - return nil -} - // StrictServerInterface represents all server handlers. type StrictServerInterface interface { // Get titles @@ -1363,21 +1216,18 @@ type StrictServerInterface interface { // Partially update a user account // (PATCH /users/{user_id}) UpdateUser(ctx context.Context, request UpdateUserRequestObject) (UpdateUserResponseObject, error) + // Delete a usertitle + // (DELETE /users/{user_id}/titles) + DeleteUserTitle(ctx context.Context, request DeleteUserTitleRequestObject) (DeleteUserTitleResponseObject, error) // Get user titles // (GET /users/{user_id}/titles) - GetUserTitles(ctx context.Context, request GetUserTitlesRequestObject) (GetUserTitlesResponseObject, error) + GetUsersUserIdTitles(ctx context.Context, request GetUsersUserIdTitlesRequestObject) (GetUsersUserIdTitlesResponseObject, error) + // Update a usertitle + // (PATCH /users/{user_id}/titles) + UpdateUserTitle(ctx context.Context, request UpdateUserTitleRequestObject) (UpdateUserTitleResponseObject, error) // Add a title to a user // (POST /users/{user_id}/titles) AddUserTitle(ctx context.Context, request AddUserTitleRequestObject) (AddUserTitleResponseObject, error) - // Delete a usertitle - // (DELETE /users/{user_id}/titles/{title_id}) - DeleteUserTitle(ctx context.Context, request DeleteUserTitleRequestObject) (DeleteUserTitleResponseObject, error) - // Get user title - // (GET /users/{user_id}/titles/{title_id}) - GetUserTitle(ctx context.Context, request GetUserTitleRequestObject) (GetUserTitleResponseObject, error) - // Update a usertitle - // (PATCH /users/{user_id}/titles/{title_id}) - UpdateUserTitle(ctx context.Context, request UpdateUserTitleRequestObject) (UpdateUserTitleResponseObject, error) } type StrictHandlerFunc = strictgin.StrictGinHandlerFunc @@ -1476,11 +1326,10 @@ func (sh *strictHandler) GetUsersId(ctx *gin.Context, userId string, params GetU } // UpdateUser operation middleware -func (sh *strictHandler) UpdateUser(ctx *gin.Context, userId int64, params UpdateUserParams) { +func (sh *strictHandler) UpdateUser(ctx *gin.Context, userId int64) { var request UpdateUserRequestObject request.UserId = userId - request.Params = params var body UpdateUserJSONRequestBody if err := ctx.ShouldBindJSON(&body); err != nil { @@ -1511,18 +1360,18 @@ func (sh *strictHandler) UpdateUser(ctx *gin.Context, userId int64, params Updat } } -// GetUserTitles operation middleware -func (sh *strictHandler) GetUserTitles(ctx *gin.Context, userId string, params GetUserTitlesParams) { - var request GetUserTitlesRequestObject +// DeleteUserTitle operation middleware +func (sh *strictHandler) DeleteUserTitle(ctx *gin.Context, userId int64, params DeleteUserTitleParams) { + var request DeleteUserTitleRequestObject request.UserId = userId request.Params = params handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.GetUserTitles(ctx, request.(GetUserTitlesRequestObject)) + return sh.ssi.DeleteUserTitle(ctx, request.(DeleteUserTitleRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetUserTitles") + handler = middleware(handler, "DeleteUserTitle") } response, err := handler(ctx, request) @@ -1530,8 +1379,71 @@ func (sh *strictHandler) GetUserTitles(ctx *gin.Context, userId string, params G if err != nil { ctx.Error(err) ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(GetUserTitlesResponseObject); ok { - if err := validResponse.VisitGetUserTitlesResponse(ctx.Writer); err != nil { + } else if validResponse, ok := response.(DeleteUserTitleResponseObject); ok { + if err := validResponse.VisitDeleteUserTitleResponse(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 + + request.UserId = userId + request.Params = params + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.GetUsersUserIdTitles(ctx, request.(GetUsersUserIdTitlesRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetUsersUserIdTitles") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(GetUsersUserIdTitlesResponseObject); ok { + if err := validResponse.VisitGetUsersUserIdTitlesResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + +// UpdateUserTitle operation middleware +func (sh *strictHandler) UpdateUserTitle(ctx *gin.Context, userId int64) { + var request UpdateUserTitleRequestObject + + request.UserId = userId + + var body UpdateUserTitleJSONRequestBody + 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.UpdateUserTitle(ctx, request.(UpdateUserTitleRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "UpdateUserTitle") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(UpdateUserTitleResponseObject); ok { + if err := validResponse.VisitUpdateUserTitleResponse(ctx.Writer); err != nil { ctx.Error(err) } } else if response != nil { @@ -1573,95 +1485,3 @@ func (sh *strictHandler) AddUserTitle(ctx *gin.Context, userId int64) { ctx.Error(fmt.Errorf("unexpected response type: %T", response)) } } - -// DeleteUserTitle operation middleware -func (sh *strictHandler) DeleteUserTitle(ctx *gin.Context, userId int64, titleId int64) { - var request DeleteUserTitleRequestObject - - request.UserId = userId - request.TitleId = titleId - - handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.DeleteUserTitle(ctx, request.(DeleteUserTitleRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "DeleteUserTitle") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(DeleteUserTitleResponseObject); ok { - if err := validResponse.VisitDeleteUserTitleResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - -// GetUserTitle operation middleware -func (sh *strictHandler) GetUserTitle(ctx *gin.Context, userId int64, titleId int64) { - var request GetUserTitleRequestObject - - request.UserId = userId - request.TitleId = titleId - - handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.GetUserTitle(ctx, request.(GetUserTitleRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetUserTitle") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(GetUserTitleResponseObject); ok { - if err := validResponse.VisitGetUserTitleResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - -// UpdateUserTitle operation middleware -func (sh *strictHandler) UpdateUserTitle(ctx *gin.Context, userId int64, titleId int64) { - var request UpdateUserTitleRequestObject - - request.UserId = userId - request.TitleId = titleId - - var body UpdateUserTitleJSONRequestBody - 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.UpdateUserTitle(ctx, request.(UpdateUserTitleRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "UpdateUserTitle") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(UpdateUserTitleResponseObject); ok { - if err := validResponse.VisitUpdateUserTitleResponse(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 08a4d54..23f2058 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -15,8 +15,6 @@ paths: $ref: "./paths/users-id.yaml" /users/{user_id}/titles: $ref: "./paths/users-id-titles.yaml" - /users/{user_id}/titles/{title_id}: - $ref: "./paths/users-id-titles-id.yaml" components: parameters: diff --git a/api/parameters/_index.yaml b/api/parameters/_index.yaml index d2e12a8..6249e7d 100644 --- a/api/parameters/_index.yaml +++ b/api/parameters/_index.yaml @@ -1,10 +1,4 @@ cursor: $ref: "./cursor.yaml" title_sort: - $ref: "./title_sort.yaml" -accessToken: - $ref: "./access_token.yaml" -csrfToken: - $ref: "./xsrf_token_cookie.yaml" -csrfTokenHeader: - $ref: "./xsrf_token_header.yaml" \ No newline at end of file + $ref: "./title_sort.yaml" \ No newline at end of file diff --git a/api/parameters/access_token.yaml b/api/parameters/access_token.yaml deleted file mode 100644 index a7e727e..0000000 --- a/api/parameters/access_token.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: access_token -in: cookie -required: true -schema: - type: string - format: jwt -example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.x.y" -description: | - JWT access token. diff --git a/api/parameters/xsrf_token_cookie.yaml b/api/parameters/xsrf_token_cookie.yaml deleted file mode 100644 index cf85999..0000000 --- a/api/parameters/xsrf_token_cookie.yaml +++ /dev/null @@ -1,11 +0,0 @@ -name: XSRF-TOKEN -in: cookie -required: true -schema: - type: string - pattern: "^[a-zA-Z0-9_-]{32,64}$" -example: "abc123def456ghi789jkl012mno345pqr" -description: | - Anti-CSRF token (Double Submit Cookie pattern). - Stored in non-HttpOnly cookie, readable by JavaScript. - Must be echoed in `X-XSRF-TOKEN` header for state-changing requests (POST/PUT/PATCH/DELETE). \ No newline at end of file diff --git a/api/parameters/xsrf_token_header.yaml b/api/parameters/xsrf_token_header.yaml deleted file mode 100644 index ac14dc1..0000000 --- a/api/parameters/xsrf_token_header.yaml +++ /dev/null @@ -1,10 +0,0 @@ -name: X-XSRF-TOKEN -in: header -required: true -schema: - type: string - pattern: "^[a-zA-Z0-9_-]{32,64}$" -description: | - Anti-CSRF token. Must match the `XSRF-TOKEN` cookie. - Required for all state-changing requests (POST/PUT/PATCH/DELETE). -example: "abc123def456ghi789jkl012mno345pqr" \ No newline at end of file diff --git a/api/paths/titles-id.yaml b/api/paths/titles-id.yaml index f1b9c55..235743f 100644 --- a/api/paths/titles-id.yaml +++ b/api/paths/titles-id.yaml @@ -1,7 +1,5 @@ get: summary: Get title description - security: - - JwtAuthCookies: [] operationId: getTitle parameters: - in: path diff --git a/api/paths/users-id-titles-id.yaml b/api/paths/users-id-titles-id.yaml deleted file mode 100644 index b4ad884..0000000 --- a/api/paths/users-id-titles-id.yaml +++ /dev/null @@ -1,107 +0,0 @@ -get: - summary: Get user title - operationId: getUserTitle - parameters: - - in: path - name: user_id - required: true - schema: - type: integer - format: int64 - - in: path - name: title_id - required: true - schema: - type: integer - format: int64 - responses: - '200': - description: User titles - content: - application/json: - schema: - $ref: '../schemas/UserTitleMini.yaml' - '204': - description: No user title found - '400': - description: Request params are not correct - '404': - description: User or title not found - '500': - description: Unknown server error - -patch: - summary: Update a usertitle - description: User updating title list of watched - operationId: updateUserTitle - parameters: - - in: path - name: user_id - required: true - schema: - type: integer - format: int64 - - in: path - name: title_id - required: true - schema: - type: integer - format: int64 - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - status: - $ref: '../schemas/enums/UserTitleStatus.yaml' - rate: - type: integer - format: int32 - responses: - '200': - description: Title successfully updated - content: - application/json: - schema: - $ref: '../schemas/UserTitleMini.yaml' - '400': - description: Invalid request body (missing fields, invalid types, etc.) - '401': - description: Unauthorized — missing or invalid auth token - '403': - description: Forbidden — user not allowed to update title - '404': - description: User or Title not found - '500': - description: Internal server error - -delete: - summary: Delete a usertitle - description: User deleting title from list of watched - operationId: deleteUserTitle - parameters: - - in: path - name: user_id - required: true - schema: - type: integer - format: int64 - - in: path - name: title_id - required: true - schema: - type: integer - format: int64 - responses: - '200': - description: Title successfully deleted - '401': - description: Unauthorized — missing or invalid auth token - '403': - description: Forbidden — user not allowed to delete title - '404': - description: User or Title not found - '500': - description: Internal server error \ No newline at end of file diff --git a/api/paths/users-id-titles.yaml b/api/paths/users-id-titles.yaml index 75f5461..0cb7092 100644 --- a/api/paths/users-id-titles.yaml +++ b/api/paths/users-id-titles.yaml @@ -1,6 +1,5 @@ get: summary: Get user titles - operationId: getUserTitles parameters: - $ref: '../parameters/cursor.yaml' - $ref: "../parameters/title_sort.yaml" @@ -139,5 +138,88 @@ post: description: User or Title not found '409': description: Conflict — title already assigned to user (if applicable) + '500': + description: Internal server error + +patch: + summary: Update a usertitle + description: User updating title list of watched + operationId: updateUserTitle + parameters: + - name: user_id + in: path + required: true + schema: + type: integer + format: int64 + description: ID of the user to assign the title to + example: 123 + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - title_id + properties: + title_id: + type: integer + format: int64 + status: + $ref: '../schemas/enums/UserTitleStatus.yaml' + rate: + type: integer + format: int32 + + responses: + '200': + description: Title successfully updated + content: + application/json: + schema: + $ref: '../schemas/UserTitleMini.yaml' + '400': + description: Invalid request body (missing fields, invalid types, etc.) + '401': + description: Unauthorized — missing or invalid auth token + '403': + description: Forbidden — user not allowed to update title + '404': + description: User or Title not found + '500': + description: Internal server error + +delete: + summary: Delete a usertitle + description: User deleting title from list of watched + operationId: deleteUserTitle + parameters: + - name: user_id + in: path + required: true + schema: + type: integer + format: int64 + description: ID of the user to assign the title to + - in: query + name: title_id + required: true + schema: + type: integer + format: int64 + + + responses: + '200': + description: Title successfully deleted + # '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 delete title + '404': + description: User or Title not found '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 0f2f367..fe62e46 100644 --- a/api/paths/users-id.yaml +++ b/api/paths/users-id.yaml @@ -28,16 +28,12 @@ get: patch: summary: Partially update a user account - security: - - JwtAuthCookies: [] 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: - - $ref: '../parameters/access_token.yaml' # ← для поля в UI и GoDoc - - $ref: '../parameters/xsrf_token_cookie.yaml' # ← для CSRF - name: user_id in: path required: true diff --git a/api/schemas/JWTAuth.yaml b/api/schemas/JWTAuth.yaml deleted file mode 100644 index 63c3baa..0000000 --- a/api/schemas/JWTAuth.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# type: apiKey -# in: cookie -# name: access_token -# scheme: bearer -# bearerFormat: JWT -# description: | -# JWT access token sent in `Cookie: access_token=...`. \ No newline at end of file diff --git a/api/schemas/_index.yaml b/api/schemas/_index.yaml index 0cc0f9d..d893ced 100644 --- a/api/schemas/_index.yaml +++ b/api/schemas/_index.yaml @@ -24,5 +24,3 @@ User: $ref: "./User.yaml" UserTitle: $ref: "./UserTitle.yaml" -# JwtAuth: -# $ref: "./JWTAuth.yaml" diff --git a/auth/auth.gen.go b/auth/auth.gen.go index 7276545..b24deb5 100644 --- a/auth/auth.gen.go +++ b/auth/auth.gen.go @@ -116,8 +116,9 @@ type PostAuthSignInResponseObject interface { } type PostAuthSignIn200JSONResponse struct { - UserId int64 `json:"user_id"` - UserName string `json:"user_name"` + Error *string `json:"error"` + UserId *string `json:"user_id"` + UserName *string `json:"user_name"` } func (response PostAuthSignIn200JSONResponse) VisitPostAuthSignInResponse(w http.ResponseWriter) error { @@ -147,7 +148,9 @@ type PostAuthSignUpResponseObject interface { } type PostAuthSignUp200JSONResponse struct { - UserId int64 `json:"user_id"` + Error *string `json:"error"` + Success *bool `json:"success,omitempty"` + UserId *string `json:"user_id"` } func (response PostAuthSignUp200JSONResponse) VisitPostAuthSignUpResponse(w http.ResponseWriter) error { diff --git a/auth/openapi-auth.yaml b/auth/openapi-auth.yaml index 239b03b..0fe308c 100644 --- a/auth/openapi-auth.yaml +++ b/auth/openapi-auth.yaml @@ -30,13 +30,16 @@ paths: content: application/json: schema: - required: - - user_id type: object properties: + success: + type: boolean + error: + type: string + nullable: true user_id: - type: integer - format: int64 + type: string + nullable: true /auth/sign-in: post: @@ -62,16 +65,17 @@ paths: content: application/json: schema: - required: - - user_id - - user_name type: object properties: + error: + type: string + nullable: true user_id: - type: integer - format: int64 + type: string + nullable: true user_name: type: string + nullable: true "401": description: Access denied due to invalid credentials content: diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 79ad2f5..7f53da5 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -11,34 +11,20 @@ services: - "${POSTGRES_PORT}:5432" volumes: - postgres_data:/var/lib/postgresql - networks: - - nyanimedb-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 10s - rabbitmq: - image: rabbitmq:3-management - container_name: rabbitmq - ports: - - "5672:5672" - - "15672:15672" - environment: - RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER} - RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD} - volumes: - - rabbitmq_data:/var/lib/rabbitmq - networks: - - nyanimedb-network - healthcheck: - test: ["CMD", "rabbitmqctl", "status"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 10s + # pgadmin: + # image: dpage/pgadmin4:${PGADMIN_VERSION} + # container_name: pgadmin + # restart: always + # environment: + # PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL} + # PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD} + # ports: + # - "${PGADMIN_PORT}:80" + # depends_on: + # - postgres + # volumes: + # - pgadmin_data:/var/lib/pgadmin nyanimedb-backend: image: meowgit.nekoea.red/nihonium/nyanimedb-backend:latest @@ -51,9 +37,6 @@ services: - "8080:8080" depends_on: - postgres - - rabbitmq - networks: - - nyanimedb-network nyanimedb-auth: image: meowgit.nekoea.red/nihonium/nyanimedb-auth:latest @@ -66,8 +49,6 @@ services: - "8082:8082" depends_on: - postgres - networks: - - nyanimedb-network nyanimedb-frontend: image: meowgit.nekoea.red/nihonium/nyanimedb-frontend:latest @@ -77,12 +58,7 @@ services: - "8081:80" depends_on: - nyanimedb-backend - networks: - - nyanimedb-network volumes: postgres_data: - rabbitmq_data: - -networks: - nyanimedb-network: + pgadmin_data: diff --git a/go.mod b/go.mod index 6662bc1..7fc0e5f 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module nyanimedb go 1.25.0 require ( - github.com/alexedwards/argon2id v1.0.0 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 @@ -37,7 +36,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/quic-go v0.54.0 // indirect - github.com/rabbitmq/amqp091-go v1.10.0 // indirect + github.com/streadway/amqp v1.1.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect go.uber.org/mock v0.5.0 // indirect diff --git a/go.sum b/go.sum index 520a22b..e52e5c9 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,4 @@ github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w= -github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= @@ -72,8 +70,6 @@ github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= -github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= -github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= @@ -93,64 +89,26 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/modules/auth/handlers/handlers.go b/modules/auth/handlers/handlers.go index 261826c..7f675aa 100644 --- a/modules/auth/handlers/handlers.go +++ b/modules/auth/handlers/handlers.go @@ -3,21 +3,22 @@ package handlers import ( "context" "fmt" + "log" "net/http" auth "nyanimedb/auth" sqlc "nyanimedb/sql" "strconv" "time" - "github.com/alexedwards/argon2id" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" - log "github.com/sirupsen/logrus" ) 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 } @@ -31,22 +32,6 @@ func parseInt64(s string) (int32, error) { return int32(i), err } -func HashPassword(password string) (string, error) { - params := &argon2id.Params{ - Memory: 64 * 1024, - Iterations: 3, - Parallelism: 2, - SaltLength: 16, - KeyLength: 32, - } - - return argon2id.CreateHash(password, params) -} - -func CheckPassword(password, hash string) (bool, error) { - return argon2id.ComparePasswordAndHash(password, hash) -} - func generateTokens(userID string) (accessToken string, refreshToken string, err error) { accessClaims := jwt.MapClaims{ "user_id": userID, @@ -72,27 +57,19 @@ func generateTokens(userID string) (accessToken string, refreshToken string, err } func (s Server) PostAuthSignUp(ctx context.Context, req auth.PostAuthSignUpRequestObject) (auth.PostAuthSignUpResponseObject, error) { - passhash, err := HashPassword(req.Body.Pass) - if err != nil { - log.Errorf("failed to hash password: %v", err) - // TODO: return 500 - } - - user_id, err := s.db.CreateNewUser(context.Background(), sqlc.CreateNewUserParams{ - Passhash: passhash, - Nickname: req.Body.Nickname, - }) - if err != nil { - log.Errorf("failed to create user %s: %v", req.Body.Nickname, err) - // TODO: check err and retyrn 400/500 - } + err := "" + success := true + UserDb[req.Body.Nickname] = req.Body.Pass return auth.PostAuthSignUp200JSONResponse{ - UserId: user_id, + 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") @@ -100,38 +77,27 @@ func (s Server) PostAuthSignIn(ctx context.Context, req auth.PostAuthSignInReque return auth.PostAuthSignIn200JSONResponse{}, fmt.Errorf("failed to get gin.Context from context.Context") } - user, err := s.db.GetUserByNickname(context.Background(), req.Body.Nickname) - if err != nil { - log.Errorf("failed to get user by nickname %s: %v", req.Body.Nickname, err) - // TODO: return 400/500 - } + err := "" - ok, err = CheckPassword(req.Body.Pass, user.Passhash) - if err != nil { - log.Errorf("failed to check password for user %s: %v", req.Body.Nickname, err) - // TODO: return 500 - } - if !ok { - err_msg := "invalid credentials" + pass, ok := UserDb[req.Body.Nickname] + if !ok || pass != req.Body.Pass { + e := "invalid credentials" return auth.PostAuthSignIn401JSONResponse{ - Error: &err_msg, + Error: &e, }, nil } - accessToken, refreshToken, err := generateTokens(req.Body.Nickname) - if err != nil { - log.Errorf("failed to generate tokens for user %s: %v", req.Body.Nickname, err) - // TODO: return 500 - } + accessToken, refreshToken, _ := generateTokens(req.Body.Nickname) - // TODO: check cookie settings carefully ginCtx.SetSameSite(http.SameSiteStrictMode) - ginCtx.SetCookie("access_token", accessToken, 604800, "/auth", "", false, true) - ginCtx.SetCookie("refresh_token", refreshToken, 604800, "/api", "", false, true) + 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{ - UserId: user.ID, - UserName: user.Nickname, + Error: &err, + UserId: &req.Body.Nickname, + UserName: &req.Body.Nickname, } return result, nil } diff --git a/modules/auth/main.go b/modules/auth/main.go index 7554f42..c001e8b 100644 --- a/modules/auth/main.go +++ b/modules/auth/main.go @@ -1,9 +1,6 @@ package main import ( - "context" - "fmt" - "os" "time" auth "nyanimedb/auth" @@ -12,22 +9,14 @@ import ( "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" - "github.com/jackc/pgx/v5/pgxpool" ) var AppConfig Config func main() { - // TODO: env args r := gin.Default() - pool, err := pgxpool.New(context.Background(), os.Getenv("DATABASE_URL")) - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err) - os.Exit(1) - } - - var queries *sqlc.Queries = sqlc.New(pool) + var queries *sqlc.Queries = nil server := handlers.NewServer(queries) diff --git a/modules/auth/queries.sql b/modules/auth/queries.sql deleted file mode 100644 index 828d2af..0000000 --- a/modules/auth/queries.sql +++ /dev/null @@ -1,11 +0,0 @@ --- name: GetUserByNickname :one -SELECT * -FROM users -WHERE nickname = sqlc.arg('nickname'); - --- name: CreateNewUser :one -INSERT -INTO users (passhash, nickname) -VALUES (sqlc.arg(passhash), sqlc.arg(nickname)) -RETURNING id; - diff --git a/modules/backend/handlers/common.go b/modules/backend/handlers/common.go index cad4f0f..f820db6 100644 --- a/modules/backend/handlers/common.go +++ b/modules/backend/handlers/common.go @@ -4,31 +4,16 @@ import ( "encoding/json" "fmt" oapi "nyanimedb/api" - "nyanimedb/modules/backend/rmq" sqlc "nyanimedb/sql" "strconv" ) -type Handler struct { - publisher *rmq.Publisher -} - -func New(publisher *rmq.Publisher) *Handler { - return &Handler{publisher: publisher} -} - type Server struct { - db *sqlc.Queries - publisher *rmq.Publisher - RPCclient *rmq.RPCClient + db *sqlc.Queries } -func NewServer(db *sqlc.Queries, publisher *rmq.Publisher, rpcclient *rmq.RPCClient) *Server { - return &Server{ - db: db, - publisher: publisher, - RPCclient: rpcclient, - } +func NewServer(db *sqlc.Queries) Server { + return Server{db: db} } func sql2StorageType(s *sqlc.StorageTypeT) (*oapi.StorageType, error) { diff --git a/modules/backend/handlers/titles.go b/modules/backend/handlers/titles.go index 300cc87..77af7e4 100644 --- a/modules/backend/handlers/titles.go +++ b/modules/backend/handlers/titles.go @@ -5,10 +5,8 @@ import ( "encoding/json" "fmt" oapi "nyanimedb/api" - "nyanimedb/modules/backend/rmq" sqlc "nyanimedb/sql" "strconv" - "time" "github.com/jackc/pgx/v5" log "github.com/sirupsen/logrus" @@ -156,62 +154,21 @@ func (s Server) GetTitle(ctx context.Context, request oapi.GetTitleRequestObject } func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObject) (oapi.GetTitlesResponseObject, error) { - opai_titles := make([]oapi.Title, 0) - mqreq := rmq.RabbitRequest{ - Timestamp: time.Now(), - } word := Word2Sqlc(request.Params.Word) - if word != nil { - mqreq.Name = *word - } season, err := ReleaseSeason2sqlc(request.Params.ReleaseSeason) if err != nil { log.Errorf("%v", err) return oapi.GetTitles400Response{}, err } - if season != nil { - mqreq.Season = *request.Params.ReleaseSeason - } title_statuses, err := TitleStatus2Sqlc(request.Params.Status) if err != nil { log.Errorf("%v", err) return oapi.GetTitles400Response{}, err } - if title_statuses != nil { - mqreq.Statuses = *request.Params.Status - } - - if request.Params.ExtSearch != nil && *request.Params.ExtSearch { - - // Структура для ответа (должна совпадать с тем, что шлёт микросервис) - var reply struct { - Status string `json:"status"` - Result string `json:"result"` - Preview string `json:"preview_url"` - } - - // Делаем RPC-вызов — и ЖДЁМ ответа - err := s.RPCclient.Call( - ctx, - "svc.media.process.requests", // ← очередь микросервиса - mqreq, - &reply, - ) - if err != nil { - log.Errorf("RabitMQ: %v", err) - // return oapi.GetTitles500Response{}, err - } - // // Возвращаем результат - // return oapi.ProcessMedia200JSONResponse{ - // Status: reply.Status, - // Result: reply.Result, - // Preview: reply.Preview, - // }, nil - } params := sqlc.SearchTitlesParams{ Word: word, diff --git a/modules/backend/handlers/users.go b/modules/backend/handlers/users.go index d6faade..563a244 100644 --- a/modules/backend/handlers/users.go +++ b/modules/backend/handlers/users.go @@ -204,7 +204,7 @@ func (s Server) mapUsertitle(ctx context.Context, t sqlc.SearchUserTitlesRow) (o return oapi_usertitle, nil } -func (s Server) GetUserTitles(ctx context.Context, request oapi.GetUserTitlesRequestObject) (oapi.GetUserTitlesResponseObject, error) { +func (s Server) GetUsersUserIdTitles(ctx context.Context, request oapi.GetUsersUserIdTitlesRequestObject) (oapi.GetUsersUserIdTitlesResponseObject, error) { oapi_usertitles := make([]oapi.UserTitle, 0) @@ -213,7 +213,7 @@ func (s Server) GetUserTitles(ctx context.Context, request oapi.GetUserTitlesReq season, err := ReleaseSeason2sqlc(request.Params.ReleaseSeason) if err != nil { log.Errorf("%v", err) - return oapi.GetUserTitles400Response{}, err + return oapi.GetUsersUserIdTitles400Response{}, err } // var statuses_sort []string @@ -227,19 +227,19 @@ func (s Server) GetUserTitles(ctx context.Context, request oapi.GetUserTitlesReq watch_status, err := UserTitleStatus2Sqlc(request.Params.WatchStatus) if err != nil { log.Errorf("%v", err) - return oapi.GetUserTitles400Response{}, err + return oapi.GetUsersUserIdTitles400Response{}, err } title_statuses, err := TitleStatus2Sqlc(request.Params.Status) if err != nil { log.Errorf("%v", err) - return oapi.GetUserTitles400Response{}, err + return oapi.GetUsersUserIdTitles400Response{}, err } userID, err := parseInt64(request.UserId) if err != nil { log.Errorf("get user titles: %v", err) - return oapi.GetUserTitles404Response{}, err + return oapi.GetUsersUserIdTitles404Response{}, err } params := sqlc.SearchUserTitlesParams{ UserID: userID, @@ -265,7 +265,7 @@ func (s Server) GetUserTitles(ctx context.Context, request oapi.GetUserTitlesReq err := ParseCursorInto(string(*request.Params.Sort), string(*request.Params.Cursor), ¶ms) if err != nil { log.Errorf("%v", err) - return oapi.GetUserTitles400Response{}, nil + return oapi.GetUsersUserIdTitles400Response{}, nil } } } @@ -273,10 +273,10 @@ func (s Server) GetUserTitles(ctx context.Context, request oapi.GetUserTitlesReq titles, err := s.db.SearchUserTitles(ctx, params) if err != nil { log.Errorf("%v", err) - return oapi.GetUserTitles500Response{}, nil + return oapi.GetUsersUserIdTitles500Response{}, nil } if len(titles) == 0 { - return oapi.GetUserTitles204Response{}, nil + return oapi.GetUsersUserIdTitles204Response{}, nil } var new_cursor oapi.CursorObj @@ -286,7 +286,7 @@ func (s Server) GetUserTitles(ctx context.Context, request oapi.GetUserTitlesReq t, err := s.mapUsertitle(ctx, title) if err != nil { log.Errorf("%v", err) - return oapi.GetUserTitles500Response{}, nil + return oapi.GetUsersUserIdTitles500Response{}, nil } oapi_usertitles = append(oapi_usertitles, t) @@ -303,7 +303,7 @@ func (s Server) GetUserTitles(ctx context.Context, request oapi.GetUserTitlesReq } } - return oapi.GetUserTitles200JSONResponse{Cursor: new_cursor, Data: oapi_usertitles}, nil + return oapi.GetUsersUserIdTitles200JSONResponse{Cursor: new_cursor, Data: oapi_usertitles}, nil } func EmailToStringPtr(e *types.Email) *string { @@ -402,7 +402,7 @@ func (s Server) AddUserTitle(ctx context.Context, request oapi.AddUserTitleReque func (s Server) DeleteUserTitle(ctx context.Context, request oapi.DeleteUserTitleRequestObject) (oapi.DeleteUserTitleResponseObject, error) { params := sqlc.DeleteUserTitleParams{ UserID: request.UserId, - TitleID: request.TitleId, + TitleID: request.Params.TitleId, } _, err := s.db.DeleteUserTitle(ctx, params) if err != nil { @@ -427,7 +427,7 @@ func (s Server) UpdateUserTitle(ctx context.Context, request oapi.UpdateUserTitl Status: status, Rate: request.Body.Rate, UserID: request.UserId, - TitleID: request.TitleId, + TitleID: request.Body.TitleId, } user_title, err := s.db.UpdateUserTitle(ctx, params) @@ -455,33 +455,3 @@ func (s Server) UpdateUserTitle(ctx context.Context, request oapi.UpdateUserTitl return oapi.UpdateUserTitle200JSONResponse(oapi_usertitle), nil } - -func (s Server) GetUserTitle(ctx context.Context, request oapi.GetUserTitleRequestObject) (oapi.GetUserTitleResponseObject, error) { - user_title, err := s.db.GetUserTitleByID(ctx, sqlc.GetUserTitleByIDParams{ - TitleID: request.TitleId, - UserID: request.UserId, - }) - if err != nil { - if err == pgx.ErrNoRows { - return oapi.GetUserTitle404Response{}, nil - } else { - log.Errorf("%v", err) - return oapi.GetUserTitle500Response{}, nil - } - } - oapi_status, err := sql2usertitlestatus(user_title.Status) - if err != nil { - log.Errorf("%v", err) - return oapi.GetUserTitle500Response{}, nil - } - oapi_usertitle := oapi.UserTitleMini{ - Ctime: &user_title.Ctime, - Rate: user_title.Rate, - ReviewId: user_title.ReviewID, - Status: oapi_status, - TitleId: user_title.TitleID, - UserId: user_title.UserID, - } - - return oapi.GetUserTitle200JSONResponse(oapi_usertitle), nil -} diff --git a/modules/backend/main.go b/modules/backend/main.go index aab1287..3ac6603 100644 --- a/modules/backend/main.go +++ b/modules/backend/main.go @@ -3,7 +3,6 @@ package main import ( "context" "fmt" - "net/http" sqlc "nyanimedb/sql" "os" "reflect" @@ -11,15 +10,11 @@ import ( oapi "nyanimedb/api" handlers "nyanimedb/modules/backend/handlers" - middleware "nyanimedb/modules/backend/middlewares" - "nyanimedb/modules/backend/rmq" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/jackc/pgx/v5/pgxpool" "github.com/pelletier/go-toml/v2" - "github.com/rabbitmq/amqp091-go" - log "github.com/sirupsen/logrus" ) var AppConfig Config @@ -46,30 +41,14 @@ func main() { r := gin.Default() - r.Use(middleware.CSRFMiddleware()) - // jwt middle will be here queries := sqlc.New(pool) - // === RabbitMQ setup === - rmqURL := os.Getenv("RABBITMQ_URL") - if rmqURL == "" { - rmqURL = "amqp://guest:guest@rabbitmq:5672/" - } - - rmqConn, err := amqp091.Dial(rmqURL) - if err != nil { - log.Fatalf("Failed to connect to RabbitMQ: %v", err) - } - defer rmqConn.Close() - - publisher := rmq.NewPublisher(rmqConn) - rpcClient := rmq.NewRPCClient(rmqConn, 30*time.Second) - - server := handlers.NewServer(queries, publisher, rpcClient) + server := handlers.NewServer(queries) + // r.LoadHTMLGlob("templates/*") r.Use(cors.New(cors.Config{ AllowOrigins: []string{"*"}, // allow all origins, change to specific domains in production - AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "PATCH"}, + AllowMethods: []string{"GET", "POST", "PUT", "DELETE"}, AllowHeaders: []string{"Origin", "Content-Type", "Accept"}, ExposeHeaders: []string{"Content-Length"}, AllowCredentials: true, @@ -81,12 +60,24 @@ func main() { // сюда можно добавить middlewares, если нужно []oapi.StrictMiddlewareFunc{}, )) + // r.GET("/", func(c *gin.Context) { + // c.HTML(http.StatusOK, "index.html", gin.H{ + // "title": "Welcome Page", + // "message": "Hello, Gin with HTML templates!", + // }) + // }) - // Запуск - log.Infof("Server starting on :8080") - if err := r.Run(":8080"); err != nil && err != http.ErrServerClosed { - log.Fatalf("server failed: %v", err) - } + // r.GET("/api", func(c *gin.Context) { + // items := []Item{ + // {ID: 1, Title: "First Item", Description: "This is the description of the first item."}, + // {ID: 2, Title: "Second Item", Description: "This is the description of the second item."}, + // {ID: 3, Title: "Third Item", Description: "This is the description of the third item."}, + // } + + // c.JSON(http.StatusOK, items) + // }) + + r.Run(":8080") } func InitConfig() error { diff --git a/modules/backend/middlewares/csrf.go b/modules/backend/middlewares/csrf.go deleted file mode 100644 index 41fad7b..0000000 --- a/modules/backend/middlewares/csrf.go +++ /dev/null @@ -1,70 +0,0 @@ -package middleware - -import ( - "crypto/subtle" - "net/http" - - "github.com/gin-gonic/gin" -) - -// CSRFMiddleware для Gin -func CSRFMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - // Пропускаем безопасные методы - if !isStateChangingMethod(c.Request.Method) { - c.Next() - return - } - - // 1. Получаем токен из заголовка - headerToken := c.GetHeader("X-XSRF-TOKEN") - if headerToken == "" { - c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ - "error": "missing X-XSRF-TOKEN header", - }) - return - } - - // 2. Получаем токен из cookie - cookie, err := c.Cookie("xsrf_token") - if err != nil { - c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ - "error": "missing xsrf_token cookie", - }) - return - } - - // 3. Безопасное сравнение - if subtle.ConstantTimeCompare([]byte(headerToken), []byte(cookie)) != 1 { - c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ - "error": "CSRF token mismatch", - }) - return - } - - // 4. Опционально: сохраняем токен в контексте - c.Set("csrf_token", headerToken) - c.Next() - } -} - -func isStateChangingMethod(method string) bool { - switch method { - case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete: - return true - default: - return false - } -} - -// CSRFTokenFromGin извлекает токен из Gin context -func CSRFTokenFromGin(c *gin.Context) (string, bool) { - token, exists := c.Get("xsrf_token") - if !exists { - return "", false - } - if s, ok := token.(string); ok { - return s, true - } - return "", false -} diff --git a/modules/backend/queries.sql b/modules/backend/queries.sql index ff41cb1..5ac2c5c 100644 --- a/modules/backend/queries.sql +++ b/modules/backend/queries.sql @@ -394,10 +394,4 @@ RETURNING *; DELETE FROM usertitles WHERE user_id = sqlc.arg('user_id') AND title_id = sqlc.arg('title_id') -RETURNING *; - --- name: GetUserTitleByID :one -SELECT - ut.* -FROM usertitles as ut -WHERE ut.title_id = sqlc.arg('title_id')::bigint AND ut.user_id = sqlc.arg('user_id')::bigint; \ No newline at end of file +RETURNING *; \ No newline at end of file diff --git a/modules/backend/rabbit.go b/modules/backend/rabbit.go new file mode 100644 index 0000000..f08bf39 --- /dev/null +++ b/modules/backend/rabbit.go @@ -0,0 +1,103 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + oapi "nyanimedb/api" + "time" + + "github.com/google/uuid" + "github.com/sirupsen/logrus" + "github.com/streadway/amqp" +) + +type RabbitRequest struct { + Name string `json:"name"` + Status oapi.TitleStatus `json:"titlestatus,omitempty"` + Rating float64 `json:"titleraring,omitempty"` + Year int32 `json:"year,omitempty"` + Season oapi.ReleaseSeason `json:"season,omitempty"` + Timestamp time.Time `json:"timestamp"` +} + +// PublishAndAwaitReply отправляет запрос и ждёт ответа от worker’а. +// Возвращает раскодированный ответ или ошибку. +func PublishAndAwaitReply( + ctx context.Context, + ch *amqp.Channel, + requestQueue string, // например: "svc.media.process.requests" + request RabbitRequest, // ваша структура запроса + replyCh chan<- any, // куда положить ответ (вы читаете извне) +) error { + // 1. Создаём временную очередь для ответов + replyQueue, err := ch.QueueDeclare( + "", // auto-generated name + false, // not durable + true, // exclusive + true, // auto-delete + false, // no-wait + nil, + ) + if err != nil { + return fmt.Errorf("failed to declare reply queue: %w", err) + } + + // 2. Готовим корреляционный ID + corrID := uuid.New().String() // ← используйте github.com/google/uuid + logrus.Infof("New CorrID: %s", corrID) + + // 3. Сериализуем запрос + body, err := json.Marshal(request) + if err != nil { + return fmt.Errorf("failed to marshal request: %w", err) + } + + // 4. Публикуем запрос + err = ch.Publish( + "", // default exchange (или свой, если используете) + requestQueue, + false, + false, + amqp.Publishing{ + ContentType: "application/json", + CorrelationId: corrID, + ReplyTo: replyQueue.Name, + DeliveryMode: amqp.Persistent, + Timestamp: time.Now(), + Body: body, + }, + ) + if err != nil { + return fmt.Errorf("failed to publish request, corrID: %s : %w", corrID, err) + } + + // 5. Подписываемся на ответы + msgs, err := ch.Consume( + replyQueue.Name, + "", // consumer tag + true, // auto-ack + true, // exclusive + false, // no-local + false, // no-wait + nil, // args + ) + if err != nil { + return fmt.Errorf("failed to consume from reply queue: %w", err) + } + + // 6. Ожидаем ответ с таймаутом + select { + case msg := <-msgs: + if msg.CorrelationId != corrID { + return fmt.Errorf("correlation ID mismatch: got %s, expected %s", msg.CorrelationId, corrID) + } + // Десериализуем — тут можно передать target-структуру или использовать interface{} + // В данном случае просто возвращаем байты или пусть вызывающая сторона парсит + replyCh <- msg.Body // или json.Unmarshal → и отправить структуру в канал + return nil + + case <-ctx.Done(): + return ctx.Err() + } +} diff --git a/modules/backend/rmq/rabbit.go b/modules/backend/rmq/rabbit.go deleted file mode 100644 index 52c1979..0000000 --- a/modules/backend/rmq/rabbit.go +++ /dev/null @@ -1,261 +0,0 @@ -package rmq - -import ( - "context" - "encoding/json" - "fmt" - oapi "nyanimedb/api" - "sync" - "time" - - amqp "github.com/rabbitmq/amqp091-go" -) - -type RabbitRequest struct { - Name string `json:"name"` - Statuses []oapi.TitleStatus `json:"statuses,omitempty"` - Rating float64 `json:"rating,omitempty"` - Year int32 `json:"year,omitempty"` - Season oapi.ReleaseSeason `json:"season,omitempty"` - Timestamp time.Time `json:"timestamp"` -} - -// Publisher — потокобезопасный публикатор с пулом каналов. -type Publisher struct { - conn *amqp.Connection - pool *sync.Pool -} - -// NewPublisher создаёт новый Publisher. -// conn должен быть уже установленным и healthy. -// Рекомендуется передавать durable connection с reconnect-логикой. -func NewPublisher(conn *amqp.Connection) *Publisher { - return &Publisher{ - conn: conn, - pool: &sync.Pool{ - New: func() any { - ch, err := conn.Channel() - if err != nil { - // Паника уместна: невозможность открыть канал — критическая ошибка инициализации - panic(fmt.Errorf("rmqpool: failed to create channel: %w", err)) - } - return ch - }, - }, - } -} - -// Publish публикует сообщение в указанную очередь. -// Очередь объявляется как durable (если не существует). -// Поддерживает context для отмены/таймаута. -func (p *Publisher) Publish( - ctx context.Context, - queueName string, - payload RabbitRequest, - opts ...PublishOption, -) error { - // Применяем опции - options := &publishOptions{ - contentType: "application/json", - deliveryMode: amqp.Persistent, - timestamp: time.Now(), - } - for _, opt := range opts { - opt(options) - } - - // Сериализуем payload - body, err := json.Marshal(payload) - if err != nil { - return fmt.Errorf("rmqpool: failed to marshal payload: %w", err) - } - - // Берём канал из пула - ch := p.getChannel() - if ch == nil { - return fmt.Errorf("rmqpool: channel is nil (connection may be closed)") - } - defer p.returnChannel(ch) - - // Объявляем очередь (idempotent) - q, err := ch.QueueDeclare( - queueName, - true, // durable - false, // auto-delete - false, // exclusive - false, // no-wait - nil, // args - ) - if err != nil { - return fmt.Errorf("rmqpool: failed to declare queue %q: %w", queueName, err) - } - - // Подготавливаем сообщение - msg := amqp.Publishing{ - DeliveryMode: options.deliveryMode, - ContentType: options.contentType, - Timestamp: options.timestamp, - Body: body, - } - - // Публикуем с учётом контекста - done := make(chan error, 1) - go func() { - err := ch.Publish( - "", // exchange (default) - q.Name, // routing key - false, // mandatory - false, // immediate - msg, - ) - done <- err - }() - - select { - case err := <-done: - return err - case <-ctx.Done(): - return ctx.Err() - } -} - -func (p *Publisher) getChannel() *amqp.Channel { - raw := p.pool.Get() - if raw == nil { - ch, _ := p.conn.Channel() - return ch - } - ch := raw.(*amqp.Channel) - if ch.IsClosed() { // ← теперь есть! - ch.Close() // освободить ресурсы - ch, _ = p.conn.Channel() - } - return ch -} - -// returnChannel возвращает канал в пул, если он жив. -func (p *Publisher) returnChannel(ch *amqp.Channel) { - if ch != nil && !ch.IsClosed() { - p.pool.Put(ch) - } -} - -// PublishOption позволяет кастомизировать публикацию. -type PublishOption func(*publishOptions) - -type publishOptions struct { - contentType string - deliveryMode uint8 - timestamp time.Time -} - -// WithContentType устанавливает Content-Type (по умолчанию "application/json"). -func WithContentType(ct string) PublishOption { - return func(o *publishOptions) { o.contentType = ct } -} - -// WithTransient делает сообщение transient (не сохраняется на диск). -// По умолчанию — Persistent. -func WithTransient() PublishOption { - return func(o *publishOptions) { o.deliveryMode = amqp.Transient } -} - -// WithTimestamp устанавливает кастомную метку времени. -func WithTimestamp(ts time.Time) PublishOption { - return func(o *publishOptions) { o.timestamp = ts } -} - -type RPCClient struct { - conn *amqp.Connection - timeout time.Duration -} - -func NewRPCClient(conn *amqp.Connection, timeout time.Duration) *RPCClient { - return &RPCClient{conn: conn, timeout: timeout} -} - -// Call отправляет запрос в очередь и ждёт ответа. -// replyPayload — указатель на структуру, в которую раскодировать ответ (например, &MediaResponse{}). -func (c *RPCClient) Call( - ctx context.Context, - requestQueue string, - request RabbitRequest, - replyPayload any, -) error { - // 1. Создаём временный канал (не из пула!) - ch, err := c.conn.Channel() - if err != nil { - return fmt.Errorf("channel: %w", err) - } - defer ch.Close() - - // 2. Создаём временную очередь для ответов - q, err := ch.QueueDeclare( - "", // auto name - false, // not durable - true, // exclusive - true, // auto-delete - false, - nil, - ) - if err != nil { - return fmt.Errorf("reply queue: %w", err) - } - - // 3. Подписываемся на ответы - msgs, err := ch.Consume( - q.Name, - "", - true, // auto-ack - true, // exclusive - false, - false, - nil, - ) - if err != nil { - return fmt.Errorf("consume: %w", err) - } - - // 4. Готовим correlation ID - corrID := time.Now().UnixNano() - - // 5. Сериализуем запрос - body, err := json.Marshal(request) - if err != nil { - return fmt.Errorf("marshal request: %w", err) - } - - // 6. Публикуем запрос - err = ch.Publish( - "", - requestQueue, - false, - false, - amqp.Publishing{ - ContentType: "application/json", - CorrelationId: fmt.Sprintf("%d", corrID), - ReplyTo: q.Name, - Timestamp: time.Now(), - Body: body, - }, - ) - if err != nil { - return fmt.Errorf("publish: %w", err) - } - - // 7. Ждём ответ с таймаутом - ctx, cancel := context.WithTimeout(ctx, c.timeout) - defer cancel() - - for { - select { - case msg := <-msgs: - if msg.CorrelationId == fmt.Sprintf("%d", corrID) { - return json.Unmarshal(msg.Body, replyPayload) - } - // игнорируем другие сообщения (маловероятно, но возможно) - case <-ctx.Done(): - return ctx.Err() // timeout or cancelled - } - } -} diff --git a/modules/frontend/src/App.tsx b/modules/frontend/src/App.tsx index 95b59e3..e2c909f 100644 --- a/modules/frontend/src/App.tsx +++ b/modules/frontend/src/App.tsx @@ -1,12 +1,13 @@ import React from "react"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; -import UserPage from "./pages/UserPage/UserPage"; +import UsersIdPage from "./pages/UsersIdPage/UsersIdPage"; import TitlesPage from "./pages/TitlesPage/TitlesPage"; import TitlePage from "./pages/TitlePage/TitlePage"; import { LoginPage } from "./pages/LoginPage/LoginPage"; import { Header } from "./components/Header/Header"; const App: React.FC = () => { + // Получаем username из localStorage const username = localStorage.getItem("username") || undefined; const userId = localStorage.getItem("userId"); @@ -14,20 +15,17 @@ const App: React.FC = () => {
- {/* auth */} } /> } /> - {/*} />*/} - - {/* users */} - {/*} />*/} - } /> + + {/* /profile рендерит UsersIdPage с id из localStorage */} : } + element={userId ? : } /> - {/* titles */} + } /> + } /> } /> diff --git a/modules/frontend/src/api/core/OpenAPI.ts b/modules/frontend/src/api/core/OpenAPI.ts index 185e5c3..6ce873e 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: '/api/v1', + BASE: 'http://10.1.0.65:8081/api/v1', VERSION: '1.0.0', WITH_CREDENTIALS: false, CREDENTIALS: 'include', diff --git a/modules/frontend/src/api/services/DefaultService.ts b/modules/frontend/src/api/services/DefaultService.ts index 218b461..5070fae 100644 --- a/modules/frontend/src/api/services/DefaultService.ts +++ b/modules/frontend/src/api/services/DefaultService.ts @@ -199,7 +199,7 @@ export class DefaultService { * @returns any List of user titles * @throws ApiError */ - public static getUserTitles( + public static getUsersTitles( userId: string, cursor?: string, sort?: TitleSort, @@ -278,54 +278,27 @@ export class DefaultService { }, }); } - /** - * Get user title - * @param userId - * @param titleId - * @returns UserTitleMini User titles - * @throws ApiError - */ - public static getUserTitle( - userId: number, - titleId: number, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/users/{user_id}/titles/{title_id}', - path: { - 'user_id': userId, - 'title_id': titleId, - }, - errors: { - 400: `Request params are not correct`, - 404: `User or title not found`, - 500: `Unknown server error`, - }, - }); - } /** * Update a usertitle * User updating title list of watched - * @param userId - * @param titleId + * @param userId ID of the user to assign the title to * @param requestBody * @returns UserTitleMini Title successfully updated * @throws ApiError */ public static updateUserTitle( userId: number, - titleId: number, requestBody: { + title_id: number; status?: UserTitleStatus; rate?: number; }, ): CancelablePromise { return __request(OpenAPI, { method: 'PATCH', - url: '/users/{user_id}/titles/{title_id}', + url: '/users/{user_id}/titles', path: { 'user_id': userId, - 'title_id': titleId, }, body: requestBody, mediaType: 'application/json', @@ -338,31 +311,4 @@ export class DefaultService { }, }); } - /** - * Delete a usertitle - * User deleting title from list of watched - * @param userId - * @param titleId - * @returns any Title successfully deleted - * @throws ApiError - */ - public static deleteUserTitle( - userId: number, - titleId: number, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'DELETE', - url: '/users/{user_id}/titles/{title_id}', - path: { - 'user_id': userId, - 'title_id': titleId, - }, - errors: { - 401: `Unauthorized — missing or invalid auth token`, - 403: `Forbidden — user not allowed to delete title`, - 404: `User or Title not found`, - 500: `Internal server error`, - }, - }); - } } diff --git a/modules/frontend/src/auth/core/OpenAPI.ts b/modules/frontend/src/auth/core/OpenAPI.ts index 2d0edf8..79aa305 100644 --- a/modules/frontend/src/auth/core/OpenAPI.ts +++ b/modules/frontend/src/auth/core/OpenAPI.ts @@ -20,7 +20,7 @@ export type OpenAPIConfig = { }; export const OpenAPI: OpenAPIConfig = { - BASE: '/auth', + BASE: 'http://10.1.0.65:8081/auth', VERSION: '1.0.0', WITH_CREDENTIALS: false, CREDENTIALS: 'include', diff --git a/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx b/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx deleted file mode 100644 index 0c9c741..0000000 --- a/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useEffect, useState } from "react"; -import { DefaultService } from "../../api"; -import type { UserTitleStatus } from "../../api"; -import { - ClockIcon, - CheckCircleIcon, - PlayCircleIcon, - XCircleIcon, -} from "@heroicons/react/24/solid"; - -// Статусы с иконками и подписью -const STATUS_BUTTONS: { status: UserTitleStatus; icon: React.ReactNode; label: string }[] = [ - { status: "planned", icon: , label: "Planned" }, - { status: "finished", icon: , label: "Finished" }, - { status: "in-progress", icon: , label: "In Progress" }, - { status: "dropped", icon: , label: "Dropped" }, -]; - -export function TitleStatusControls({ titleId }: { titleId: number }) { - const [currentStatus, setCurrentStatus] = useState(null); - const [loading, setLoading] = useState(false); - - const userIdStr = localStorage.getItem("userId"); - const userId = userIdStr ? Number(userIdStr) : null; - - // --- Load initial status --- - useEffect(() => { - if (!userId) return; - - DefaultService.getUserTitle(userId, titleId) - .then((res) => setCurrentStatus(res.status)) - .catch(() => setCurrentStatus(null)); // 404 = user title does not exist - }, [titleId, userId]); - - // --- Handle click --- - const handleStatusClick = async (status: UserTitleStatus) => { - if (!userId || loading) return; - - setLoading(true); - - try { - // 1) Если кликнули на текущий статус — DELETE - if (currentStatus === status) { - await DefaultService.deleteUserTitle(userId, titleId); - setCurrentStatus(null); - return; - } - - // 2) Если другой статус — POST или PATCH - if (!currentStatus) { - // ещё нет записи — POST - const added = await DefaultService.addUserTitle(userId, { - title_id: titleId, - status, - }); - setCurrentStatus(added.status); - } else { - // уже есть запись — PATCH - const updated = await DefaultService.updateUserTitle(userId, titleId, { status }); - setCurrentStatus(updated.status); - } - } finally { - setLoading(false); - } - }; - - return ( -
- {STATUS_BUTTONS.map(btn => ( - - ))} -
- ); -} diff --git a/modules/frontend/src/pages/TitlePage/TitlePage.tsx b/modules/frontend/src/pages/TitlePage/TitlePage.tsx index 01f9c49..5ea0e3d 100644 --- a/modules/frontend/src/pages/TitlePage/TitlePage.tsx +++ b/modules/frontend/src/pages/TitlePage/TitlePage.tsx @@ -1,8 +1,20 @@ import { useEffect, useState } from "react"; -import { useParams, Link } from "react-router-dom"; +import { useParams } from "react-router-dom"; import { DefaultService } from "../../api/services/DefaultService"; -import type { Title } from "../../api"; -import { TitleStatusControls } from "../../components/TitleStatusControls/TitleStatusControls"; +import type { Title, UserTitleStatus } from "../../api"; +import { + ClockIcon, + CheckCircleIcon, + PlayCircleIcon, + XCircleIcon, +} from "@heroicons/react/24/solid"; + +const STATUS_BUTTONS: { status: UserTitleStatus; icon: React.ReactNode; label: string }[] = [ + { status: "planned", icon: , label: "Planned" }, + { status: "finished", icon: , label: "Finished" }, + { status: "in-progress", icon: , label: "In Progress" }, + { status: "dropped", icon: , label: "Dropped" }, +]; export default function TitlePage() { const params = useParams(); @@ -12,9 +24,9 @@ export default function TitlePage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - // --------------------------- - // LOAD TITLE INFO - // --------------------------- + const [userStatus, setUserStatus] = useState(null); + const [updatingStatus, setUpdatingStatus] = useState(false); + useEffect(() => { const fetchTitle = async () => { setLoading(true); @@ -32,6 +44,30 @@ export default function TitlePage() { fetchTitle(); }, [titleId]); + const handleStatusClick = async (status: UserTitleStatus) => { + if (updatingStatus || userStatus === status) return; + + const userId = Number(localStorage.getItem("userId")); + if (!userId) { + alert("You must be logged in to set status."); + return; + } + + setUpdatingStatus(true); + try { + await DefaultService.addUserTitle(userId, { + title_id: titleId, + status, + }); + setUserStatus(status); + } catch (err: any) { + console.error(err); + alert(err?.message || "Failed to set status"); + } finally { + setUpdatingStatus(false); + } + }; + const getTagsString = () => title?.tags?.map(tag => tag.en).filter(Boolean).join(", "); @@ -42,7 +78,7 @@ export default function TitlePage() { return (
- {/* Poster + status buttons */} + {/* Постер */}
- {/* Status buttons */} - + {/* Статус кнопки с иконками */} +
+ {STATUS_BUTTONS.map(btn => ( + + ))} +
- {/* Title info */} + {/* Информация о тайтле */}

{title.title_names?.en?.[0] || "Untitled"}

- - {title.studio && ( -

- Studio:{" "} - {title.studio.id ? ( - - {title.studio.name} - - ) : ( - title.studio.name - )} -

- )} - + {title.studio &&

Studio: {title.studio.name}

} {title.title_status &&

Status: {title.title_status}

} - {title.rating !== undefined && (

Rating: {title.rating} ({title.rating_count} votes)

)} - {title.release_year && (

Released: {title.release_year} {title.release_season || ""}

)} - {title.episodes_aired !== undefined && (

Episodes: {title.episodes_aired}/{title.episodes_all}

)} - {title.tags && title.tags.length > 0 && (

Tags: {getTagsString()} diff --git a/modules/frontend/src/pages/TitlesPage/TitlesPage.module.css b/modules/frontend/src/pages/TitlesPage/TitlesPage.module.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/modules/frontend/src/pages/TitlesPage/TitlesPage.module.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx b/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx index c9911b9..0fec3c8 100644 --- a/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx +++ b/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx @@ -7,7 +7,6 @@ import { TitleCardSquare } from "../../components/cards/TitleCardSquare"; import { TitleCardHorizontal } from "../../components/cards/TitleCardHorizontal"; import type { CursorObj, Title, TitleSort } from "../../api"; import { LayoutSwitch } from "../../components/LayoutSwitch/LayoutSwitch"; -import { Link } from "react-router-dom"; const PAGE_SIZE = 10; @@ -136,11 +135,11 @@ const handleLoadMore = async () => { hasMore={!!cursor || nextPage.length > 1} loadingMore={loadingMore} onLoadMore={handleLoadMore} - renderItem={(title, layout) => ( - - {layout === "square" ? : } - - )} + renderItem={(title, layout) => + layout === "square" + ? + : + } /> {!cursor && nextPage.length == 0 && ( diff --git a/modules/frontend/src/pages/UserPage/UserPage.module.css b/modules/frontend/src/pages/UserPage/UserPage.module.css new file mode 100644 index 0000000..7f350c8 --- /dev/null +++ b/modules/frontend/src/pages/UserPage/UserPage.module.css @@ -0,0 +1,103 @@ +body, +html { + width: 100%; + margin: 0; + background-color: #777; + color: #fff; +} + +html, +body, +#root { + height: 100%; +} + +.header { + width: 100vw; + padding: 30px 40px; + background: #f7f7f7; + display: flex; + align-items: center; + gap: 25px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + border-bottom: 1px solid #e5e5e5; + color: #000000; +} + +.avatarWrapper { + width: 120px; + height: 120px; + min-width: 120px; + border-radius: 50%; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + background: #ddd; +} + +.avatarImg { + width: 100%; + height: 100%; + object-fit: cover; +} + +.avatarPlaceholder { + width: 100%; + height: 100%; + border-radius: 50%; + background: #ccc; + font-size: 42px; + font-weight: bold; + color: #555; + display: flex; + align-items: center; + justify-content: center; +} + +.userInfo { + display: flex; + flex-direction: column; +} + +.name { + font-size: 32px; + font-weight: 700; + margin: 0; +} + +.nickname { + font-size: 18px; + color: #666; + margin-top: 6px; +} + +.container { + max-width: 100vw; + width: 100%; + position: absolute; + top: 0%; + /* margin: 25px auto; */ + /* padding: 0 20px; */ +} + +.content { + margin-top: 20px; +} + +.desc { + font-size: 18px; + margin-bottom: 10px; +} + +.created { + font-size: 16px; + color: #888; +} + +.loader, +.error { + text-align: center; + margin-top: 40px; + font-size: 18px; +} diff --git a/modules/frontend/src/pages/UserPage/UserPage.tsx b/modules/frontend/src/pages/UserPage/UserPage.tsx index 7cc0db5..eafdf6b 100644 --- a/modules/frontend/src/pages/UserPage/UserPage.tsx +++ b/modules/frontend/src/pages/UserPage/UserPage.tsx @@ -1,184 +1,67 @@ -// pages/UserPage/UserPage.tsx -import { useEffect, useState } from "react"; -import { useParams } from "react-router-dom"; +import React, { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; // <-- import import { DefaultService } from "../../api/services/DefaultService"; -import { SearchBar } from "../../components/SearchBar/SearchBar"; -import { TitlesSortBox } from "../../components/TitlesSortBox/TitlesSortBox"; -import { LayoutSwitch } from "../../components/LayoutSwitch/LayoutSwitch"; -import { ListView } from "../../components/ListView/ListView"; -import { UserTitleCardSquare } from "../../components/cards/UserTitleCardSquare"; -import { UserTitleCardHorizontal } from "../../components/cards/UserTitleCardHorizontal"; -import type { User, UserTitle, CursorObj, TitleSort } from "../../api"; -import { Link } from "react-router-dom"; - -const PAGE_SIZE = 10; - -type UserPageProps = { - userId?: string; -}; - -export default function UserPage({ userId }: UserPageProps) { - const params = useParams(); - const id = userId || params?.id; +import type { User } from "../../api/models/User"; +import styles from "./UserPage.module.css"; +const UserPage: React.FC = () => { + const { id } = useParams<{ id: string }>(); // <-- get user id from URL const [user, setUser] = useState(null); - const [loadingUser, setLoadingUser] = useState(true); - const [errorUser, setErrorUser] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - // Для списка тайтлов - const [titles, setTitles] = useState([]); - const [nextPage, setNextPage] = useState([]); - const [cursor, setCursor] = useState(null); - const [loadingTitles, setLoadingTitles] = useState(true); - const [loadingMore, setLoadingMore] = useState(false); - const [search, setSearch] = useState(""); - const [sort, setSort] = useState("id"); - const [sortForward, setSortForward] = useState(true); - const [layout, setLayout] = useState<"square" | "horizontal">("square"); - - // --- Получение данных пользователя --- useEffect(() => { - const fetchUser = async () => { - if (!id) return; - setLoadingUser(true); + if (!id) return; + + const getUserInfo = async () => { try { - const result = await DefaultService.getUsersId(id, "all"); - setUser(result); - setErrorUser(null); - } catch (err: any) { - console.error(err); - setErrorUser(err?.message || "Failed to fetch user data"); - } finally { - setLoadingUser(false); - } - }; - fetchUser(); - }, [id]); - - // --- Получение списка тайтлов пользователя --- - const fetchPage = async (cursorObj: CursorObj | null) => { - if (!id) return { items: [], nextCursor: null }; - const cursorStr = cursorObj - ? btoa(JSON.stringify(cursorObj)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "") - : ""; - - try { - const result = await DefaultService.getUserTitles( - id, - cursorStr, - sort, - sortForward, - search.trim() || undefined, - undefined, // status фильтр, можно добавить - undefined, // watchStatus - undefined, // rating - undefined, // myRate - undefined, // releaseYear - undefined, // releaseSeason - PAGE_SIZE, - "all" - ); - - if (!result?.data?.length) return { items: [], nextCursor: null }; - - return { items: result.data, nextCursor: result.cursor ?? null }; - } catch (err: any) { - if (err.status === 204) return { items: [], nextCursor: null }; - throw err; - } - }; - - // Инициализация: загружаем сразу две страницы - useEffect(() => { - const initLoad = async () => { - setLoadingTitles(true); - setTitles([]); - setNextPage([]); - setCursor(null); - - const firstPage = await fetchPage(null); - const secondPage = firstPage.nextCursor ? await fetchPage(firstPage.nextCursor) : { items: [], nextCursor: null }; - - setTitles(firstPage.items); - setNextPage(secondPage.items); - setCursor(secondPage.nextCursor); - setLoadingTitles(false); - }; - initLoad(); - }, [id, search, sort, sortForward]); - - const handleLoadMore = async () => { - if (nextPage.length === 0) { - setLoadingMore(false); - return; - } - setLoadingMore(true); - - setTitles(prev => [...prev, ...nextPage]); - setNextPage([]); - - if (cursor) { - try { - const next = await fetchPage(cursor); - if (next.items.length > 0) setNextPage(next.items); - setCursor(next.nextCursor); + const userInfo = await DefaultService.getUsersId(id, "all"); // <-- use dynamic id + setUser(userInfo); } catch (err) { console.error(err); + setError("Failed to fetch user info."); + } finally { + setLoading(false); } - } + }; + getUserInfo(); + }, [id]); - setLoadingMore(false); - }; + if (loading) return

Loading...
; + if (error) return
{error}
; + if (!user) return
User not found.
; return ( -
- - {/* --- Карточка пользователя --- */} - {loadingUser &&
Loading user...
} - {errorUser &&
{errorUser}
} - {user && ( -
- {user.nickname} -

{user.disp_name || user.nickname}

- {user.mail &&

{user.mail}

} - {user.user_desc &&

{user.user_desc}

} - {user.creation_date &&

Registered: {new Date(user.creation_date).toLocaleDateString()}

} -
- )} - - {/* --- Панель поиска, сортировки и лейаута --- */} -
- - - -
- - {/* --- Список тайтлов --- */} - {loadingTitles &&
Loading titles...
} - {!loadingTitles && titles.length === 0 &&
No titles found.
} - - {titles.length > 0 && ( - <> - - items={titles} - layout={layout} - hasMore={!!cursor || nextPage.length > 1} - loadingMore={loadingMore} - onLoadMore={handleLoadMore} - renderItem={(title, layout) => ( - - {layout === "square" ? : } - - )} - /> - - {!cursor && nextPage.length === 0 && ( -
- Результатов больше нет, было найдено {titles.length} тайтлов. +
+
+
+ {user.image?.image_path ? ( + User Avatar + ) : ( +
+ {user.disp_name?.[0] || "U"}
)} - - )} +
+ +
+

{user.disp_name || user.nickname}

+

@{user.nickname}

+ {/*

+ Joined: {new Date(user.creation_date).toLocaleDateString()} +

*/} +
+ +
+ {user.user_desc &&

{user.user_desc}

} +
+
); -} +}; + +export default UserPage; diff --git a/modules/frontend/src/pages/UsersIdPage/UsersIdPage.tsx b/modules/frontend/src/pages/UsersIdPage/UsersIdPage.tsx new file mode 100644 index 0000000..729da20 --- /dev/null +++ b/modules/frontend/src/pages/UsersIdPage/UsersIdPage.tsx @@ -0,0 +1,183 @@ +// pages/UserPage/UserPage.tsx +import { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { DefaultService } from "../../api/services/DefaultService"; +import { SearchBar } from "../../components/SearchBar/SearchBar"; +import { TitlesSortBox } from "../../components/TitlesSortBox/TitlesSortBox"; +import { LayoutSwitch } from "../../components/LayoutSwitch/LayoutSwitch"; +import { ListView } from "../../components/ListView/ListView"; +import { UserTitleCardSquare } from "../../components/cards/UserTitleCardSquare"; +import { UserTitleCardHorizontal } from "../../components/cards/UserTitleCardHorizontal"; +import type { User, UserTitle, CursorObj, TitleSort } from "../../api"; + +const PAGE_SIZE = 10; + +type UsersIdPageProps = { + userId?: string; +}; + +export default function UsersIdPage({ userId }: UsersIdPageProps) { + const params = useParams(); + const id = userId || params?.id; + + const [user, setUser] = useState(null); + const [loadingUser, setLoadingUser] = useState(true); + const [errorUser, setErrorUser] = useState(null); + + // Для списка тайтлов + const [titles, setTitles] = useState([]); + const [nextPage, setNextPage] = useState([]); + const [cursor, setCursor] = useState(null); + const [loadingTitles, setLoadingTitles] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + const [search, setSearch] = useState(""); + const [sort, setSort] = useState("id"); + const [sortForward, setSortForward] = useState(true); + const [layout, setLayout] = useState<"square" | "horizontal">("square"); + + // --- Получение данных пользователя --- + useEffect(() => { + const fetchUser = async () => { + if (!id) return; + setLoadingUser(true); + try { + const result = await DefaultService.getUsersId(id, "all"); + setUser(result); + setErrorUser(null); + } catch (err: any) { + console.error(err); + setErrorUser(err?.message || "Failed to fetch user data"); + } finally { + setLoadingUser(false); + } + }; + fetchUser(); + }, [id]); + + // --- Получение списка тайтлов пользователя --- + const fetchPage = async (cursorObj: CursorObj | null) => { + if (!id) return { items: [], nextCursor: null }; + const cursorStr = cursorObj + ? btoa(JSON.stringify(cursorObj)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "") + : ""; + + try { + const result = await DefaultService.getUsersTitles( + id, + cursorStr, + sort, + sortForward, + search.trim() || undefined, + undefined, // status фильтр, можно добавить + undefined, // watchStatus + undefined, // rating + undefined, // myRate + undefined, // releaseYear + undefined, // releaseSeason + PAGE_SIZE, + "all" + ); + + if (!result?.data?.length) return { items: [], nextCursor: null }; + + return { items: result.data, nextCursor: result.cursor ?? null }; + } catch (err: any) { + if (err.status === 204) return { items: [], nextCursor: null }; + throw err; + } + }; + + // Инициализация: загружаем сразу две страницы + useEffect(() => { + const initLoad = async () => { + setLoadingTitles(true); + setTitles([]); + setNextPage([]); + setCursor(null); + + const firstPage = await fetchPage(null); + const secondPage = firstPage.nextCursor ? await fetchPage(firstPage.nextCursor) : { items: [], nextCursor: null }; + + setTitles(firstPage.items); + setNextPage(secondPage.items); + setCursor(secondPage.nextCursor); + setLoadingTitles(false); + }; + initLoad(); + }, [id, search, sort, sortForward]); + + const handleLoadMore = async () => { + if (nextPage.length === 0) { + setLoadingMore(false); + return; + } + setLoadingMore(true); + + setTitles(prev => [...prev, ...nextPage]); + setNextPage([]); + + if (cursor) { + try { + const next = await fetchPage(cursor); + if (next.items.length > 0) setNextPage(next.items); + setCursor(next.nextCursor); + } catch (err) { + console.error(err); + } + } + + setLoadingMore(false); + }; + + // const getAvatarUrl = (avatarId?: number) => (avatarId ? `/api/images/${avatarId}` : "/default-avatar.png"); + + return ( +
+ + {/* --- Карточка пользователя --- */} + {loadingUser &&
Loading user...
} + {errorUser &&
{errorUser}
} + {user && ( +
+ {user.nickname} +

{user.disp_name || user.nickname}

+ {user.mail &&

{user.mail}

} + {user.user_desc &&

{user.user_desc}

} + {user.creation_date &&

Registered: {new Date(user.creation_date).toLocaleDateString()}

} +
+ )} + + {/* --- Панель поиска, сортировки и лейаута --- */} +
+ + + +
+ + {/* --- Список тайтлов --- */} + {loadingTitles &&
Loading titles...
} + {!loadingTitles && titles.length === 0 &&
No titles found.
} + + {titles.length > 0 && ( + <> + + items={titles} + layout={layout} + hasMore={!!cursor || nextPage.length > 1} + loadingMore={loadingMore} + onLoadMore={handleLoadMore} + renderItem={(title, layout) => + layout === "square" ? : + } + /> + + {!cursor && nextPage.length === 0 && ( +
+ Результатов больше нет, было найдено {titles.length} тайтлов. +
+ )} + + )} +
+ ); +} diff --git a/sql/queries.sql.go b/sql/queries.sql.go index 1cca986..9338717 100644 --- a/sql/queries.sql.go +++ b/sql/queries.sql.go @@ -29,25 +29,6 @@ func (q *Queries) CreateImage(ctx context.Context, arg CreateImageParams) (Image return i, err } -const createNewUser = `-- name: CreateNewUser :one -INSERT -INTO users (passhash, nickname) -VALUES ($1, $2) -RETURNING id -` - -type CreateNewUserParams struct { - Passhash string `json:"passhash"` - Nickname string `json:"nickname"` -} - -func (q *Queries) CreateNewUser(ctx context.Context, arg CreateNewUserParams) (int64, error) { - row := q.db.QueryRow(ctx, createNewUser, arg.Passhash, arg.Nickname) - var id int64 - err := row.Scan(&id) - return id, err -} - const deleteUserTitle = `-- name: DeleteUserTitle :one DELETE FROM usertitles WHERE user_id = $1 @@ -281,55 +262,6 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (GetUserByIDRow, er return i, err } -const getUserByNickname = `-- name: GetUserByNickname :one -SELECT id, avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date, last_login -FROM users -WHERE nickname = $1 -` - -func (q *Queries) GetUserByNickname(ctx context.Context, nickname string) (User, error) { - row := q.db.QueryRow(ctx, getUserByNickname, nickname) - var i User - err := row.Scan( - &i.ID, - &i.AvatarID, - &i.Passhash, - &i.Mail, - &i.Nickname, - &i.DispName, - &i.UserDesc, - &i.CreationDate, - &i.LastLogin, - ) - return i, err -} - -const getUserTitleByID = `-- name: GetUserTitleByID :one -SELECT - ut.user_id, ut.title_id, ut.status, ut.rate, ut.review_id, ut.ctime -FROM usertitles as ut -WHERE ut.title_id = $1::bigint AND ut.user_id = $2::bigint -` - -type GetUserTitleByIDParams struct { - TitleID int64 `json:"title_id"` - UserID int64 `json:"user_id"` -} - -func (q *Queries) GetUserTitleByID(ctx context.Context, arg GetUserTitleByIDParams) (Usertitle, error) { - row := q.db.QueryRow(ctx, getUserTitleByID, arg.TitleID, arg.UserID) - var i Usertitle - err := row.Scan( - &i.UserID, - &i.TitleID, - &i.Status, - &i.Rate, - &i.ReviewID, - &i.Ctime, - ) - return i, err -} - const insertStudio = `-- name: InsertStudio :one INSERT INTO studios (studio_name, illust_id, studio_desc) VALUES ( diff --git a/sql/sqlc.yaml b/sql/sqlc.yaml index 904abaf..8f8626a 100644 --- a/sql/sqlc.yaml +++ b/sql/sqlc.yaml @@ -3,7 +3,6 @@ sql: - engine: "postgresql" queries: - "../modules/backend/queries.sql" - - "../modules/auth/queries.sql" schema: "migrations" gen: go: