From 1bbfa338d92b4122a658bb3487c98666aae4652a Mon Sep 17 00:00:00 2001 From: nihonium Date: Thu, 4 Dec 2025 07:17:31 +0300 Subject: [PATCH] feat: send xsrf_token header --- api/_build/openapi.yaml | 15 ++++-- api/parameters/xsrf_token_cookie.yaml | 2 +- api/paths/users-id-titles-id.yaml | 8 +++ api/paths/users-id.yaml | 5 +- auth/openapi-auth.yaml | 4 +- modules/frontend/package-lock.json | 53 ++++++++++++++++++- modules/frontend/package.json | 1 + modules/frontend/src/api/index.ts | 3 ++ .../frontend/src/api/models/accessToken.ts | 9 ++++ modules/frontend/src/api/models/csrfToken.ts | 11 ++++ .../src/api/models/csrfTokenHeader.ts | 10 ++++ .../src/api/services/DefaultService.ts | 21 ++++++++ .../frontend/src/auth/services/AuthService.ts | 17 +++--- .../TitleStatusControls.tsx | 9 +++- .../src/pages/LoginPage/LoginPage.tsx | 10 ++-- 15 files changed, 151 insertions(+), 27 deletions(-) create mode 100644 modules/frontend/src/api/models/accessToken.ts create mode 100644 modules/frontend/src/api/models/csrfToken.ts create mode 100644 modules/frontend/src/api/models/csrfTokenHeader.ts 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/parameters/xsrf_token_cookie.yaml b/api/parameters/xsrf_token_cookie.yaml index cf85999..37041e0 100644 --- a/api/parameters/xsrf_token_cookie.yaml +++ b/api/parameters/xsrf_token_cookie.yaml @@ -1,4 +1,4 @@ -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/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/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) {