diff --git a/api/_build/openapi.yaml b/api/_build/openapi.yaml index 58dd890..225e7cd 100644 --- a/api/_build/openapi.yaml +++ b/api/_build/openapi.yaml @@ -150,6 +150,8 @@ paths: description: User not found '500': description: Unknown server error + security: + - JwtAuthCookies: [] patch: operationId: updateUser summary: Partially update a user account @@ -158,8 +160,7 @@ 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' + - $ref: '#/components/parameters/csrfTokenHeader' - name: user_id in: path description: User ID (primary key) @@ -404,11 +405,14 @@ paths: description: User or title not found '500': description: Unknown server error + security: + - JwtAuthCookies: [] patch: operationId: updateUserTitle summary: Update a usertitle description: User updating title list of watched parameters: + - $ref: '#/components/parameters/csrfTokenHeader' - name: user_id in: path required: true @@ -450,11 +454,14 @@ paths: description: User or Title not found '500': description: Internal server error + security: + - JwtAuthCookies: [] delete: operationId: deleteUserTitle summary: Delete a usertitle description: User deleting title from list of watched parameters: + - $ref: '#/components/parameters/csrfTokenHeader' - name: user_id in: path required: true @@ -478,6 +485,8 @@ paths: description: User or Title not found '500': description: Internal server error + security: + - JwtAuthCookies: [] components: parameters: accessToken: @@ -491,7 +500,7 @@ components: description: | JWT access token. csrfToken: - name: XSRF-TOKEN + name: xsrf_token in: cookie required: true schema: diff --git a/api/paths/users-id-titles-id.yaml b/api/paths/users-id-titles-id.yaml index b4ad884..b56d07a 100644 --- a/api/paths/users-id-titles-id.yaml +++ b/api/paths/users-id-titles-id.yaml @@ -1,6 +1,8 @@ get: summary: Get user title operationId: getUserTitle + security: + - JwtAuthCookies: [] parameters: - in: path name: user_id @@ -34,7 +36,10 @@ patch: summary: Update a usertitle description: User updating title list of watched operationId: updateUserTitle + security: + - JwtAuthCookies: [] parameters: + - $ref: '../parameters/xsrf_token_header.yaml' - in: path name: user_id required: true @@ -81,7 +86,10 @@ delete: summary: Delete a usertitle description: User deleting title from list of watched operationId: deleteUserTitle + security: + - JwtAuthCookies: [] parameters: + - $ref: '../parameters/xsrf_token_header.yaml' - in: path name: user_id required: true diff --git a/api/paths/users-id.yaml b/api/paths/users-id.yaml index 0f2f367..abb170e 100644 --- a/api/paths/users-id.yaml +++ b/api/paths/users-id.yaml @@ -1,6 +1,8 @@ get: summary: Get user info operationId: getUsersId + security: + - JwtAuthCookies: [] parameters: - in: path name: user_id @@ -36,8 +38,7 @@ patch: 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 + - $ref: '../parameters/xsrf_token_header.yaml' - name: user_id in: path required: true diff --git a/auth/auth.gen.go b/auth/auth.gen.go index 7276545..b7cd839 100644 --- a/auth/auth.gen.go +++ b/auth/auth.gen.go @@ -13,32 +13,32 @@ import ( strictgin "github.com/oapi-codegen/runtime/strictmiddleware/gin" ) -// PostAuthSignInJSONBody defines parameters for PostAuthSignIn. -type PostAuthSignInJSONBody struct { +// PostSignInJSONBody defines parameters for PostSignIn. +type PostSignInJSONBody struct { Nickname string `json:"nickname"` Pass string `json:"pass"` } -// PostAuthSignUpJSONBody defines parameters for PostAuthSignUp. -type PostAuthSignUpJSONBody struct { +// PostSignUpJSONBody defines parameters for PostSignUp. +type PostSignUpJSONBody struct { Nickname string `json:"nickname"` Pass string `json:"pass"` } -// PostAuthSignInJSONRequestBody defines body for PostAuthSignIn for application/json ContentType. -type PostAuthSignInJSONRequestBody PostAuthSignInJSONBody +// PostSignInJSONRequestBody defines body for PostSignIn for application/json ContentType. +type PostSignInJSONRequestBody PostSignInJSONBody -// PostAuthSignUpJSONRequestBody defines body for PostAuthSignUp for application/json ContentType. -type PostAuthSignUpJSONRequestBody PostAuthSignUpJSONBody +// PostSignUpJSONRequestBody defines body for PostSignUp for application/json ContentType. +type PostSignUpJSONRequestBody PostSignUpJSONBody // ServerInterface represents all server handlers. type ServerInterface interface { // Sign in a user and return JWT - // (POST /auth/sign-in) - PostAuthSignIn(c *gin.Context) + // (POST /sign-in) + PostSignIn(c *gin.Context) // Sign up a new user - // (POST /auth/sign-up) - PostAuthSignUp(c *gin.Context) + // (POST /sign-up) + PostSignUp(c *gin.Context) } // ServerInterfaceWrapper converts contexts to parameters. @@ -50,8 +50,8 @@ type ServerInterfaceWrapper struct { type MiddlewareFunc func(c *gin.Context) -// PostAuthSignIn operation middleware -func (siw *ServerInterfaceWrapper) PostAuthSignIn(c *gin.Context) { +// PostSignIn operation middleware +func (siw *ServerInterfaceWrapper) PostSignIn(c *gin.Context) { for _, middleware := range siw.HandlerMiddlewares { middleware(c) @@ -60,11 +60,11 @@ func (siw *ServerInterfaceWrapper) PostAuthSignIn(c *gin.Context) { } } - siw.Handler.PostAuthSignIn(c) + siw.Handler.PostSignIn(c) } -// PostAuthSignUp operation middleware -func (siw *ServerInterfaceWrapper) PostAuthSignUp(c *gin.Context) { +// PostSignUp operation middleware +func (siw *ServerInterfaceWrapper) PostSignUp(c *gin.Context) { for _, middleware := range siw.HandlerMiddlewares { middleware(c) @@ -73,7 +73,7 @@ func (siw *ServerInterfaceWrapper) PostAuthSignUp(c *gin.Context) { } } - siw.Handler.PostAuthSignUp(c) + siw.Handler.PostSignUp(c) } // GinServerOptions provides options for the Gin server. @@ -103,54 +103,54 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options ErrorHandler: errorHandler, } - router.POST(options.BaseURL+"/auth/sign-in", wrapper.PostAuthSignIn) - router.POST(options.BaseURL+"/auth/sign-up", wrapper.PostAuthSignUp) + router.POST(options.BaseURL+"/sign-in", wrapper.PostSignIn) + router.POST(options.BaseURL+"/sign-up", wrapper.PostSignUp) } -type PostAuthSignInRequestObject struct { - Body *PostAuthSignInJSONRequestBody +type PostSignInRequestObject struct { + Body *PostSignInJSONRequestBody } -type PostAuthSignInResponseObject interface { - VisitPostAuthSignInResponse(w http.ResponseWriter) error +type PostSignInResponseObject interface { + VisitPostSignInResponse(w http.ResponseWriter) error } -type PostAuthSignIn200JSONResponse struct { +type PostSignIn200JSONResponse struct { UserId int64 `json:"user_id"` UserName string `json:"user_name"` } -func (response PostAuthSignIn200JSONResponse) VisitPostAuthSignInResponse(w http.ResponseWriter) error { +func (response PostSignIn200JSONResponse) VisitPostSignInResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) return json.NewEncoder(w).Encode(response) } -type PostAuthSignIn401JSONResponse struct { +type PostSignIn401JSONResponse struct { Error *string `json:"error,omitempty"` } -func (response PostAuthSignIn401JSONResponse) VisitPostAuthSignInResponse(w http.ResponseWriter) error { +func (response PostSignIn401JSONResponse) VisitPostSignInResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(401) return json.NewEncoder(w).Encode(response) } -type PostAuthSignUpRequestObject struct { - Body *PostAuthSignUpJSONRequestBody +type PostSignUpRequestObject struct { + Body *PostSignUpJSONRequestBody } -type PostAuthSignUpResponseObject interface { - VisitPostAuthSignUpResponse(w http.ResponseWriter) error +type PostSignUpResponseObject interface { + VisitPostSignUpResponse(w http.ResponseWriter) error } -type PostAuthSignUp200JSONResponse struct { +type PostSignUp200JSONResponse struct { UserId int64 `json:"user_id"` } -func (response PostAuthSignUp200JSONResponse) VisitPostAuthSignUpResponse(w http.ResponseWriter) error { +func (response PostSignUp200JSONResponse) VisitPostSignUpResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) @@ -160,11 +160,11 @@ func (response PostAuthSignUp200JSONResponse) VisitPostAuthSignUpResponse(w http // StrictServerInterface represents all server handlers. type StrictServerInterface interface { // Sign in a user and return JWT - // (POST /auth/sign-in) - PostAuthSignIn(ctx context.Context, request PostAuthSignInRequestObject) (PostAuthSignInResponseObject, error) + // (POST /sign-in) + PostSignIn(ctx context.Context, request PostSignInRequestObject) (PostSignInResponseObject, error) // Sign up a new user - // (POST /auth/sign-up) - PostAuthSignUp(ctx context.Context, request PostAuthSignUpRequestObject) (PostAuthSignUpResponseObject, error) + // (POST /sign-up) + PostSignUp(ctx context.Context, request PostSignUpRequestObject) (PostSignUpResponseObject, error) } type StrictHandlerFunc = strictgin.StrictGinHandlerFunc @@ -179,11 +179,11 @@ type strictHandler struct { middlewares []StrictMiddlewareFunc } -// PostAuthSignIn operation middleware -func (sh *strictHandler) PostAuthSignIn(ctx *gin.Context) { - var request PostAuthSignInRequestObject +// PostSignIn operation middleware +func (sh *strictHandler) PostSignIn(ctx *gin.Context) { + var request PostSignInRequestObject - var body PostAuthSignInJSONRequestBody + var body PostSignInJSONRequestBody if err := ctx.ShouldBindJSON(&body); err != nil { ctx.Status(http.StatusBadRequest) ctx.Error(err) @@ -192,10 +192,10 @@ func (sh *strictHandler) PostAuthSignIn(ctx *gin.Context) { request.Body = &body handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.PostAuthSignIn(ctx, request.(PostAuthSignInRequestObject)) + return sh.ssi.PostSignIn(ctx, request.(PostSignInRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "PostAuthSignIn") + handler = middleware(handler, "PostSignIn") } response, err := handler(ctx, request) @@ -203,8 +203,8 @@ func (sh *strictHandler) PostAuthSignIn(ctx *gin.Context) { if err != nil { ctx.Error(err) ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(PostAuthSignInResponseObject); ok { - if err := validResponse.VisitPostAuthSignInResponse(ctx.Writer); err != nil { + } else if validResponse, ok := response.(PostSignInResponseObject); ok { + if err := validResponse.VisitPostSignInResponse(ctx.Writer); err != nil { ctx.Error(err) } } else if response != nil { @@ -212,11 +212,11 @@ func (sh *strictHandler) PostAuthSignIn(ctx *gin.Context) { } } -// PostAuthSignUp operation middleware -func (sh *strictHandler) PostAuthSignUp(ctx *gin.Context) { - var request PostAuthSignUpRequestObject +// PostSignUp operation middleware +func (sh *strictHandler) PostSignUp(ctx *gin.Context) { + var request PostSignUpRequestObject - var body PostAuthSignUpJSONRequestBody + var body PostSignUpJSONRequestBody if err := ctx.ShouldBindJSON(&body); err != nil { ctx.Status(http.StatusBadRequest) ctx.Error(err) @@ -225,10 +225,10 @@ func (sh *strictHandler) PostAuthSignUp(ctx *gin.Context) { request.Body = &body handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.PostAuthSignUp(ctx, request.(PostAuthSignUpRequestObject)) + return sh.ssi.PostSignUp(ctx, request.(PostSignUpRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "PostAuthSignUp") + handler = middleware(handler, "PostSignUp") } response, err := handler(ctx, request) @@ -236,8 +236,8 @@ func (sh *strictHandler) PostAuthSignUp(ctx *gin.Context) { if err != nil { ctx.Error(err) ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(PostAuthSignUpResponseObject); ok { - if err := validResponse.VisitPostAuthSignUpResponse(ctx.Writer); err != nil { + } else if validResponse, ok := response.(PostSignUpResponseObject); ok { + if err := validResponse.VisitPostSignUpResponse(ctx.Writer); err != nil { ctx.Error(err) } } else if response != nil { diff --git a/auth/openapi-auth.yaml b/auth/openapi-auth.yaml index 239b03b..5f3ebd6 100644 --- a/auth/openapi-auth.yaml +++ b/auth/openapi-auth.yaml @@ -7,7 +7,7 @@ servers: - url: /auth paths: - /auth/sign-up: + /sign-up: post: summary: Sign up a new user tags: [Auth] @@ -38,7 +38,7 @@ paths: type: integer format: int64 - /auth/sign-in: + /sign-in: post: summary: Sign in a user and return JWT tags: [Auth] diff --git a/modules/auth/handlers/handlers.go b/modules/auth/handlers/handlers.go index 6fee512..09907bc 100644 --- a/modules/auth/handlers/handlers.go +++ b/modules/auth/handlers/handlers.go @@ -78,7 +78,7 @@ func (s Server) generateTokens(userID string) (accessToken string, refreshToken return accessToken, refreshToken, csrfToken, nil } -func (s Server) PostAuthSignUp(ctx context.Context, req auth.PostAuthSignUpRequestObject) (auth.PostAuthSignUpResponseObject, error) { +func (s Server) PostSignUp(ctx context.Context, req auth.PostSignUpRequestObject) (auth.PostSignUpResponseObject, error) { passhash, err := HashPassword(req.Body.Pass) if err != nil { log.Errorf("failed to hash password: %v", err) @@ -94,17 +94,17 @@ func (s Server) PostAuthSignUp(ctx context.Context, req auth.PostAuthSignUpReque // TODO: check err and retyrn 400/500 } - return auth.PostAuthSignUp200JSONResponse{ + return auth.PostSignUp200JSONResponse{ UserId: user_id, }, nil } -func (s Server) PostAuthSignIn(ctx context.Context, req auth.PostAuthSignInRequestObject) (auth.PostAuthSignInResponseObject, error) { +func (s Server) PostSignIn(ctx context.Context, req auth.PostSignInRequestObject) (auth.PostSignInResponseObject, error) { ginCtx, ok := ctx.Value(gin.ContextKey).(*gin.Context) if !ok { log.Print("failed to get gin context") // TODO: change to 500 - return auth.PostAuthSignIn200JSONResponse{}, fmt.Errorf("failed to get gin.Context from context.Context") + return auth.PostSignIn200JSONResponse{}, fmt.Errorf("failed to get gin.Context from context.Context") } user, err := s.db.GetUserByNickname(context.Background(), req.Body.Nickname) @@ -120,7 +120,7 @@ func (s Server) PostAuthSignIn(ctx context.Context, req auth.PostAuthSignInReque } if !ok { err_msg := "invalid credentials" - return auth.PostAuthSignIn401JSONResponse{ + return auth.PostSignIn401JSONResponse{ Error: &err_msg, }, nil } @@ -137,7 +137,7 @@ func (s Server) PostAuthSignIn(ctx context.Context, req auth.PostAuthSignInReque ginCtx.SetCookie("refresh_token", refreshToken, 1209600, "/auth", "", false, true) ginCtx.SetCookie("xsrf_token", csrfToken, 1209600, "/api", "", false, false) - result := auth.PostAuthSignIn200JSONResponse{ + result := auth.PostSignIn200JSONResponse{ UserId: user.ID, UserName: user.Nickname, } diff --git a/modules/frontend/package-lock.json b/modules/frontend/package-lock.json index 40bb520..d2b5573 100644 --- a/modules/frontend/package-lock.json +++ b/modules/frontend/package-lock.json @@ -13,6 +13,7 @@ "@tailwindcss/vite": "^4.1.17", "axios": "^1.12.2", "react": "^19.1.1", + "react-cookie": "^8.0.1", "react-dom": "^19.1.1", "react-router-dom": "^7.9.4", "tailwindcss": "^4.1.17" @@ -1868,6 +1869,18 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", + "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", + "license": "MIT", + "dependencies": { + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1890,7 +1903,6 @@ "version": "19.2.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", - "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -2524,7 +2536,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -3260,6 +3271,15 @@ "node": ">= 0.4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4068,6 +4088,20 @@ "node": ">=0.10.0" } }, + "node_modules/react-cookie": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-8.0.1.tgz", + "integrity": "sha512-QNdAd0MLuAiDiLcDU/2s/eyKmmfMHtjPUKJ2dZ/5CcQ9QKUium4B3o61/haq6PQl/YWFqC5PO8GvxeHKhy3GFA==", + "license": "MIT", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.6", + "hoist-non-react-statics": "^3.3.2", + "universal-cookie": "^8.0.0" + }, + "peerDependencies": { + "react": ">= 16.3.0" + } + }, "node_modules/react-dom": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", @@ -4081,6 +4115,12 @@ "react": "^19.2.0" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -4481,6 +4521,15 @@ "devOptional": true, "license": "MIT" }, + "node_modules/universal-cookie": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-8.0.1.tgz", + "integrity": "sha512-B6ks9FLLnP1UbPPcveOidfvB9pHjP+wekP2uRYB9YDfKVpvcjKgy1W5Zj+cEXJ9KTPnqOKGfVDQBmn8/YCQfRg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.2" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", diff --git a/modules/frontend/package.json b/modules/frontend/package.json index e0b65ba..af07b41 100644 --- a/modules/frontend/package.json +++ b/modules/frontend/package.json @@ -15,6 +15,7 @@ "@tailwindcss/vite": "^4.1.17", "axios": "^1.12.2", "react": "^19.1.1", + "react-cookie": "^8.0.1", "react-dom": "^19.1.1", "react-router-dom": "^7.9.4", "tailwindcss": "^4.1.17" diff --git a/modules/frontend/src/api/index.ts b/modules/frontend/src/api/index.ts index 9013fc7..c1e9cdc 100644 --- a/modules/frontend/src/api/index.ts +++ b/modules/frontend/src/api/index.ts @@ -7,6 +7,9 @@ export { CancelablePromise, CancelError } from './core/CancelablePromise'; export { OpenAPI } from './core/OpenAPI'; export type { OpenAPIConfig } from './core/OpenAPI'; +export type { accessToken } from './models/accessToken'; +export type { csrfToken } from './models/csrfToken'; +export type { csrfTokenHeader } from './models/csrfTokenHeader'; export type { cursor } from './models/cursor'; export type { CursorObj } from './models/CursorObj'; export type { Image } from './models/Image'; diff --git a/modules/frontend/src/api/models/accessToken.ts b/modules/frontend/src/api/models/accessToken.ts new file mode 100644 index 0000000..adc8fb7 --- /dev/null +++ b/modules/frontend/src/api/models/accessToken.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * JWT access token. + * + */ +export type accessToken = string; diff --git a/modules/frontend/src/api/models/csrfToken.ts b/modules/frontend/src/api/models/csrfToken.ts new file mode 100644 index 0000000..4af805b --- /dev/null +++ b/modules/frontend/src/api/models/csrfToken.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * 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). + * + */ +export type csrfToken = string; diff --git a/modules/frontend/src/api/models/csrfTokenHeader.ts b/modules/frontend/src/api/models/csrfTokenHeader.ts new file mode 100644 index 0000000..354c8a3 --- /dev/null +++ b/modules/frontend/src/api/models/csrfTokenHeader.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * Anti-CSRF token. Must match the `XSRF-TOKEN` cookie. + * Required for all state-changing requests (POST/PUT/PATCH/DELETE). + * + */ +export type csrfTokenHeader = string; diff --git a/modules/frontend/src/api/services/DefaultService.ts b/modules/frontend/src/api/services/DefaultService.ts index 6898c46..f3d803d 100644 --- a/modules/frontend/src/api/services/DefaultService.ts +++ b/modules/frontend/src/api/services/DefaultService.ts @@ -135,12 +135,16 @@ export class DefaultService { * Password updates must be done via the dedicated auth-service (`/auth/`). * Fields not provided in the request body remain unchanged. * + * @param xXsrfToken Anti-CSRF token. Must match the `XSRF-TOKEN` cookie. + * Required for all state-changing requests (POST/PUT/PATCH/DELETE). + * * @param userId User ID (primary key) * @param requestBody * @returns User User updated successfully. Returns updated user representation (excluding sensitive fields). * @throws ApiError */ public static updateUser( + xXsrfToken: string, userId: number, requestBody: { /** @@ -171,6 +175,9 @@ export class DefaultService { path: { 'user_id': userId, }, + headers: { + 'X-XSRF-TOKEN': xXsrfToken, + }, body: requestBody, mediaType: 'application/json', errors: { @@ -309,6 +316,9 @@ export class DefaultService { /** * Update a usertitle * User updating title list of watched + * @param xXsrfToken Anti-CSRF token. Must match the `XSRF-TOKEN` cookie. + * Required for all state-changing requests (POST/PUT/PATCH/DELETE). + * * @param userId * @param titleId * @param requestBody @@ -316,6 +326,7 @@ export class DefaultService { * @throws ApiError */ public static updateUserTitle( + xXsrfToken: string, userId: number, titleId: number, requestBody: { @@ -330,6 +341,9 @@ export class DefaultService { 'user_id': userId, 'title_id': titleId, }, + headers: { + 'X-XSRF-TOKEN': xXsrfToken, + }, body: requestBody, mediaType: 'application/json', errors: { @@ -344,12 +358,16 @@ export class DefaultService { /** * Delete a usertitle * User deleting title from list of watched + * @param xXsrfToken Anti-CSRF token. Must match the `XSRF-TOKEN` cookie. + * Required for all state-changing requests (POST/PUT/PATCH/DELETE). + * * @param userId * @param titleId * @returns any Title successfully deleted * @throws ApiError */ public static deleteUserTitle( + xXsrfToken: string, userId: number, titleId: number, ): CancelablePromise { @@ -360,6 +378,9 @@ export class DefaultService { 'user_id': userId, 'title_id': titleId, }, + headers: { + 'X-XSRF-TOKEN': xXsrfToken, + }, errors: { 401: `Unauthorized — missing or invalid auth token`, 403: `Forbidden — user not allowed to delete title`, diff --git a/modules/frontend/src/auth/services/AuthService.ts b/modules/frontend/src/auth/services/AuthService.ts index 94578d8..74a8fa7 100644 --- a/modules/frontend/src/auth/services/AuthService.ts +++ b/modules/frontend/src/auth/services/AuthService.ts @@ -12,19 +12,17 @@ export class AuthService { * @returns any Sign-up result * @throws ApiError */ - public static postAuthSignUp( + public static postSignUp( requestBody: { nickname: string; pass: string; }, ): CancelablePromise<{ - success?: boolean; - error?: string | null; - user_id?: string | null; + user_id: number; }> { return __request(OpenAPI, { method: 'POST', - url: '/auth/sign-up', + url: '/sign-up', body: requestBody, mediaType: 'application/json', }); @@ -35,19 +33,18 @@ export class AuthService { * @returns any Sign-in result with JWT * @throws ApiError */ - public static postAuthSignIn( + public static postSignIn( requestBody: { nickname: string; pass: string; }, ): CancelablePromise<{ - error?: string | null; - user_id?: string | null; - user_name?: string | null; + user_id: number; + user_name: string; }> { return __request(OpenAPI, { method: 'POST', - url: '/auth/sign-in', + url: '/sign-in', body: requestBody, mediaType: 'application/json', errors: { diff --git a/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx b/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx index 0c9c741..4fb535a 100644 --- a/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx +++ b/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx @@ -1,6 +1,8 @@ import { useEffect, useState } from "react"; import { DefaultService } from "../../api"; import type { UserTitleStatus } from "../../api"; +import { useCookies } from 'react-cookie'; + import { ClockIcon, CheckCircleIcon, @@ -17,6 +19,9 @@ const STATUS_BUTTONS: { status: UserTitleStatus; icon: React.ReactNode; label: s ]; export function TitleStatusControls({ titleId }: { titleId: number }) { + const [cookies] = useCookies(['xsrf_token']); + const xsrfToken = cookies['xsrf_token'] || null; + const [currentStatus, setCurrentStatus] = useState(null); const [loading, setLoading] = useState(false); @@ -41,7 +46,7 @@ export function TitleStatusControls({ titleId }: { titleId: number }) { try { // 1) Если кликнули на текущий статус — DELETE if (currentStatus === status) { - await DefaultService.deleteUserTitle(userId, titleId); + await DefaultService.deleteUserTitle(xsrfToken, userId, titleId); setCurrentStatus(null); return; } @@ -56,7 +61,7 @@ export function TitleStatusControls({ titleId }: { titleId: number }) { setCurrentStatus(added.status); } else { // уже есть запись — PATCH - const updated = await DefaultService.updateUserTitle(userId, titleId, { status }); + const updated = await DefaultService.updateUserTitle(xsrfToken, userId, titleId, { status }); setCurrentStatus(updated.status); } } finally { diff --git a/modules/frontend/src/pages/LoginPage/LoginPage.tsx b/modules/frontend/src/pages/LoginPage/LoginPage.tsx index 89ee88c..928766e 100644 --- a/modules/frontend/src/pages/LoginPage/LoginPage.tsx +++ b/modules/frontend/src/pages/LoginPage/LoginPage.tsx @@ -17,23 +17,23 @@ export const LoginPage: React.FC = () => { try { if (isLogin) { - const res = await AuthService.postAuthSignIn({ nickname, pass: password }); + const res = await AuthService.postSignIn({ nickname, pass: password }); if (res.user_id && res.user_name) { // Сохраняем user_id и username в localStorage - localStorage.setItem("userId", res.user_id); + localStorage.setItem("userId", res.user_id.toString()); localStorage.setItem("username", res.user_name); navigate("/profile"); // редирект на профиль } else { - setError(res.error || "Login failed"); + setError("Login failed"); } } else { // SignUp оставляем без сохранения данных - const res = await AuthService.postAuthSignUp({ nickname, pass: password }); + const res = await AuthService.postSignUp({ nickname, pass: password }); if (res.user_id) { setIsLogin(true); // переключаемся на login после регистрации } else { - setError(res.error || "Sign up failed"); + setError("Sign up failed"); } } } catch (err: any) {