Merge branch 'dev' into auth
This commit is contained in:
commit
e316617175
24 changed files with 378 additions and 26 deletions
|
|
@ -120,6 +120,8 @@ paths:
|
||||||
description: Title not found
|
description: Title not found
|
||||||
'500':
|
'500':
|
||||||
description: Unknown server error
|
description: Unknown server error
|
||||||
|
security:
|
||||||
|
- JwtAuthCookies: []
|
||||||
'/users/{user_id}':
|
'/users/{user_id}':
|
||||||
get:
|
get:
|
||||||
operationId: getUsersId
|
operationId: getUsersId
|
||||||
|
|
@ -148,6 +150,8 @@ paths:
|
||||||
description: User not found
|
description: User not found
|
||||||
'500':
|
'500':
|
||||||
description: Unknown server error
|
description: Unknown server error
|
||||||
|
security:
|
||||||
|
- JwtAuthCookies: []
|
||||||
patch:
|
patch:
|
||||||
operationId: updateUser
|
operationId: updateUser
|
||||||
summary: Partially update a user account
|
summary: Partially update a user account
|
||||||
|
|
@ -156,6 +160,7 @@ paths:
|
||||||
Password updates must be done via the dedicated auth-service (`/auth/`).
|
Password updates must be done via the dedicated auth-service (`/auth/`).
|
||||||
Fields not provided in the request body remain unchanged.
|
Fields not provided in the request body remain unchanged.
|
||||||
parameters:
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/csrfTokenHeader'
|
||||||
- name: user_id
|
- name: user_id
|
||||||
in: path
|
in: path
|
||||||
description: User ID (primary key)
|
description: User ID (primary key)
|
||||||
|
|
@ -223,6 +228,8 @@ paths:
|
||||||
description: 'Unprocessable Entity — semantic errors not caught by schema (e.g., invalid `avatar_id`)'
|
description: 'Unprocessable Entity — semantic errors not caught by schema (e.g., invalid `avatar_id`)'
|
||||||
'500':
|
'500':
|
||||||
description: Unknown server error
|
description: Unknown server error
|
||||||
|
security:
|
||||||
|
- JwtAuthCookies: []
|
||||||
'/users/{user_id}/titles':
|
'/users/{user_id}/titles':
|
||||||
get:
|
get:
|
||||||
operationId: getUserTitles
|
operationId: getUserTitles
|
||||||
|
|
@ -398,11 +405,14 @@ paths:
|
||||||
description: User or title not found
|
description: User or title not found
|
||||||
'500':
|
'500':
|
||||||
description: Unknown server error
|
description: Unknown server error
|
||||||
|
security:
|
||||||
|
- JwtAuthCookies: []
|
||||||
patch:
|
patch:
|
||||||
operationId: updateUserTitle
|
operationId: updateUserTitle
|
||||||
summary: Update a usertitle
|
summary: Update a usertitle
|
||||||
description: User updating title list of watched
|
description: User updating title list of watched
|
||||||
parameters:
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/csrfTokenHeader'
|
||||||
- name: user_id
|
- name: user_id
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
|
|
@ -444,11 +454,14 @@ paths:
|
||||||
description: User or Title not found
|
description: User or Title not found
|
||||||
'500':
|
'500':
|
||||||
description: Internal server error
|
description: Internal server error
|
||||||
|
security:
|
||||||
|
- JwtAuthCookies: []
|
||||||
delete:
|
delete:
|
||||||
operationId: deleteUserTitle
|
operationId: deleteUserTitle
|
||||||
summary: Delete a usertitle
|
summary: Delete a usertitle
|
||||||
description: User deleting title from list of watched
|
description: User deleting title from list of watched
|
||||||
parameters:
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/csrfTokenHeader'
|
||||||
- name: user_id
|
- name: user_id
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
|
|
@ -472,8 +485,43 @@ paths:
|
||||||
description: User or Title not found
|
description: User or Title not found
|
||||||
'500':
|
'500':
|
||||||
description: Internal server error
|
description: Internal server error
|
||||||
|
security:
|
||||||
|
- JwtAuthCookies: []
|
||||||
components:
|
components:
|
||||||
parameters:
|
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:
|
cursor:
|
||||||
in: query
|
in: query
|
||||||
name: cursor
|
name: cursor
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,10 @@ import (
|
||||||
openapi_types "github.com/oapi-codegen/runtime/types"
|
openapi_types "github.com/oapi-codegen/runtime/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
JwtAuthCookiesScopes = "JwtAuthCookies.Scopes"
|
||||||
|
)
|
||||||
|
|
||||||
// Defines values for ReleaseSeason.
|
// Defines values for ReleaseSeason.
|
||||||
const (
|
const (
|
||||||
Fall ReleaseSeason = "fall"
|
Fall ReleaseSeason = "fall"
|
||||||
|
|
@ -170,6 +174,12 @@ type UserTitleMini struct {
|
||||||
// UserTitleStatus User's title status
|
// UserTitleStatus User's title status
|
||||||
type UserTitleStatus string
|
type UserTitleStatus string
|
||||||
|
|
||||||
|
// AccessToken defines model for accessToken.
|
||||||
|
type AccessToken = string
|
||||||
|
|
||||||
|
// CsrfToken defines model for csrfToken.
|
||||||
|
type CsrfToken = string
|
||||||
|
|
||||||
// Cursor defines model for cursor.
|
// Cursor defines model for cursor.
|
||||||
type Cursor = string
|
type Cursor = string
|
||||||
|
|
||||||
|
|
@ -219,6 +229,17 @@ type UpdateUserJSONBody struct {
|
||||||
UserDesc *string `json:"user_desc,omitempty"`
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
// GetUserTitlesParams defines parameters for GetUserTitles.
|
// GetUserTitlesParams defines parameters for GetUserTitles.
|
||||||
type GetUserTitlesParams struct {
|
type GetUserTitlesParams struct {
|
||||||
Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"`
|
Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"`
|
||||||
|
|
@ -276,7 +297,7 @@ type ServerInterface interface {
|
||||||
GetUsersId(c *gin.Context, userId string, params GetUsersIdParams)
|
GetUsersId(c *gin.Context, userId string, params GetUsersIdParams)
|
||||||
// Partially update a user account
|
// Partially update a user account
|
||||||
// (PATCH /users/{user_id})
|
// (PATCH /users/{user_id})
|
||||||
UpdateUser(c *gin.Context, userId int64)
|
UpdateUser(c *gin.Context, userId int64, params UpdateUserParams)
|
||||||
// Get user titles
|
// Get user titles
|
||||||
// (GET /users/{user_id}/titles)
|
// (GET /users/{user_id}/titles)
|
||||||
GetUserTitles(c *gin.Context, userId string, params GetUserTitlesParams)
|
GetUserTitles(c *gin.Context, userId string, params GetUserTitlesParams)
|
||||||
|
|
@ -431,6 +452,8 @@ func (siw *ServerInterfaceWrapper) GetTitle(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.Set(JwtAuthCookiesScopes, []string{})
|
||||||
|
|
||||||
// Parameter object where we will unmarshal all parameters from the context
|
// Parameter object where we will unmarshal all parameters from the context
|
||||||
var params GetTitleParams
|
var params GetTitleParams
|
||||||
|
|
||||||
|
|
@ -501,6 +524,47 @@ func (siw *ServerInterfaceWrapper) UpdateUser(c *gin.Context) {
|
||||||
return
|
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)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var cookie string
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
} else {
|
||||||
|
siw.ErrorHandler(c, fmt.Errorf("Query argument XSRF-TOKEN is required, but not found"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, middleware := range siw.HandlerMiddlewares {
|
for _, middleware := range siw.HandlerMiddlewares {
|
||||||
middleware(c)
|
middleware(c)
|
||||||
if c.IsAborted() {
|
if c.IsAborted() {
|
||||||
|
|
@ -508,7 +572,7 @@ func (siw *ServerInterfaceWrapper) UpdateUser(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
siw.Handler.UpdateUser(c, userId)
|
siw.Handler.UpdateUser(c, userId, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserTitles operation middleware
|
// GetUserTitles operation middleware
|
||||||
|
|
@ -935,6 +999,7 @@ func (response GetUsersId500Response) VisitGetUsersIdResponse(w http.ResponseWri
|
||||||
|
|
||||||
type UpdateUserRequestObject struct {
|
type UpdateUserRequestObject struct {
|
||||||
UserId int64 `json:"user_id"`
|
UserId int64 `json:"user_id"`
|
||||||
|
Params UpdateUserParams
|
||||||
Body *UpdateUserJSONRequestBody
|
Body *UpdateUserJSONRequestBody
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1411,10 +1476,11 @@ func (sh *strictHandler) GetUsersId(ctx *gin.Context, userId string, params GetU
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUser operation middleware
|
// UpdateUser operation middleware
|
||||||
func (sh *strictHandler) UpdateUser(ctx *gin.Context, userId int64) {
|
func (sh *strictHandler) UpdateUser(ctx *gin.Context, userId int64, params UpdateUserParams) {
|
||||||
var request UpdateUserRequestObject
|
var request UpdateUserRequestObject
|
||||||
|
|
||||||
request.UserId = userId
|
request.UserId = userId
|
||||||
|
request.Params = params
|
||||||
|
|
||||||
var body UpdateUserJSONRequestBody
|
var body UpdateUserJSONRequestBody
|
||||||
if err := ctx.ShouldBindJSON(&body); err != nil {
|
if err := ctx.ShouldBindJSON(&body); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
cursor:
|
cursor:
|
||||||
$ref: "./cursor.yaml"
|
$ref: "./cursor.yaml"
|
||||||
title_sort:
|
title_sort:
|
||||||
$ref: "./title_sort.yaml"
|
$ref: "./title_sort.yaml"
|
||||||
|
accessToken:
|
||||||
|
$ref: "./access_token.yaml"
|
||||||
|
csrfToken:
|
||||||
|
$ref: "./xsrf_token_cookie.yaml"
|
||||||
|
csrfTokenHeader:
|
||||||
|
$ref: "./xsrf_token_header.yaml"
|
||||||
9
api/parameters/access_token.yaml
Normal file
9
api/parameters/access_token.yaml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
name: access_token
|
||||||
|
in: cookie
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: jwt
|
||||||
|
example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.x.y"
|
||||||
|
description: |
|
||||||
|
JWT access token.
|
||||||
11
api/parameters/xsrf_token_cookie.yaml
Normal file
11
api/parameters/xsrf_token_cookie.yaml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
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).
|
||||||
10
api/parameters/xsrf_token_header.yaml
Normal file
10
api/parameters/xsrf_token_header.yaml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
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"
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
get:
|
get:
|
||||||
summary: Get title description
|
summary: Get title description
|
||||||
|
security:
|
||||||
|
- JwtAuthCookies: []
|
||||||
operationId: getTitle
|
operationId: getTitle
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
get:
|
get:
|
||||||
summary: Get user title
|
summary: Get user title
|
||||||
operationId: getUserTitle
|
operationId: getUserTitle
|
||||||
|
security:
|
||||||
|
- JwtAuthCookies: []
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: user_id
|
name: user_id
|
||||||
|
|
@ -34,7 +36,10 @@ patch:
|
||||||
summary: Update a usertitle
|
summary: Update a usertitle
|
||||||
description: User updating title list of watched
|
description: User updating title list of watched
|
||||||
operationId: updateUserTitle
|
operationId: updateUserTitle
|
||||||
|
security:
|
||||||
|
- JwtAuthCookies: []
|
||||||
parameters:
|
parameters:
|
||||||
|
- $ref: '../parameters/xsrf_token_header.yaml'
|
||||||
- in: path
|
- in: path
|
||||||
name: user_id
|
name: user_id
|
||||||
required: true
|
required: true
|
||||||
|
|
@ -81,7 +86,10 @@ delete:
|
||||||
summary: Delete a usertitle
|
summary: Delete a usertitle
|
||||||
description: User deleting title from list of watched
|
description: User deleting title from list of watched
|
||||||
operationId: deleteUserTitle
|
operationId: deleteUserTitle
|
||||||
|
security:
|
||||||
|
- JwtAuthCookies: []
|
||||||
parameters:
|
parameters:
|
||||||
|
- $ref: '../parameters/xsrf_token_header.yaml'
|
||||||
- in: path
|
- in: path
|
||||||
name: user_id
|
name: user_id
|
||||||
required: true
|
required: true
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
get:
|
get:
|
||||||
summary: Get user info
|
summary: Get user info
|
||||||
operationId: getUsersId
|
operationId: getUsersId
|
||||||
|
security:
|
||||||
|
- JwtAuthCookies: []
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: user_id
|
name: user_id
|
||||||
|
|
@ -28,12 +30,15 @@ get:
|
||||||
|
|
||||||
patch:
|
patch:
|
||||||
summary: Partially update a user account
|
summary: Partially update a user account
|
||||||
|
security:
|
||||||
|
- JwtAuthCookies: []
|
||||||
description: |
|
description: |
|
||||||
Update selected user profile fields (excluding password).
|
Update selected user profile fields (excluding password).
|
||||||
Password updates must be done via the dedicated auth-service (`/auth/`).
|
Password updates must be done via the dedicated auth-service (`/auth/`).
|
||||||
Fields not provided in the request body remain unchanged.
|
Fields not provided in the request body remain unchanged.
|
||||||
operationId: updateUser
|
operationId: updateUser
|
||||||
parameters:
|
parameters:
|
||||||
|
- $ref: '../parameters/xsrf_token_header.yaml'
|
||||||
- name: user_id
|
- name: user_id
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
|
|
|
||||||
7
api/schemas/JWTAuth.yaml
Normal file
7
api/schemas/JWTAuth.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# type: apiKey
|
||||||
|
# in: cookie
|
||||||
|
# name: access_token
|
||||||
|
# scheme: bearer
|
||||||
|
# bearerFormat: JWT
|
||||||
|
# description: |
|
||||||
|
# JWT access token sent in `Cookie: access_token=...`.
|
||||||
|
|
@ -24,3 +24,5 @@ User:
|
||||||
$ref: "./User.yaml"
|
$ref: "./User.yaml"
|
||||||
UserTitle:
|
UserTitle:
|
||||||
$ref: "./UserTitle.yaml"
|
$ref: "./UserTitle.yaml"
|
||||||
|
# JwtAuth:
|
||||||
|
# $ref: "./JWTAuth.yaml"
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ servers:
|
||||||
- url: /auth
|
- url: /auth
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
/auth/sign-up:
|
/sign-up:
|
||||||
post:
|
post:
|
||||||
summary: Sign up a new user
|
summary: Sign up a new user
|
||||||
tags: [Auth]
|
tags: [Auth]
|
||||||
|
|
@ -38,7 +38,7 @@ paths:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
|
|
||||||
/auth/sign-in:
|
/sign-in:
|
||||||
post:
|
post:
|
||||||
summary: Sign in a user and return JWT
|
summary: Sign in a user and return JWT
|
||||||
tags: [Auth]
|
tags: [Auth]
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
|
|
||||||
oapi "nyanimedb/api"
|
oapi "nyanimedb/api"
|
||||||
handlers "nyanimedb/modules/backend/handlers"
|
handlers "nyanimedb/modules/backend/handlers"
|
||||||
|
middleware "nyanimedb/modules/backend/middlewares"
|
||||||
"nyanimedb/modules/backend/rmq"
|
"nyanimedb/modules/backend/rmq"
|
||||||
|
|
||||||
"github.com/gin-contrib/cors"
|
"github.com/gin-contrib/cors"
|
||||||
|
|
@ -45,6 +46,8 @@ func main() {
|
||||||
|
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
|
r.Use(middleware.CSRFMiddleware())
|
||||||
|
// jwt middle will be here
|
||||||
queries := sqlc.New(pool)
|
queries := sqlc.New(pool)
|
||||||
|
|
||||||
// === RabbitMQ setup ===
|
// === RabbitMQ setup ===
|
||||||
|
|
@ -63,7 +66,6 @@ func main() {
|
||||||
rpcClient := rmq.NewRPCClient(rmqConn, 30*time.Second)
|
rpcClient := rmq.NewRPCClient(rmqConn, 30*time.Second)
|
||||||
|
|
||||||
server := handlers.NewServer(queries, publisher, rpcClient)
|
server := handlers.NewServer(queries, publisher, rpcClient)
|
||||||
// r.LoadHTMLGlob("templates/*")
|
|
||||||
|
|
||||||
r.Use(cors.New(cors.Config{
|
r.Use(cors.New(cors.Config{
|
||||||
AllowOrigins: []string{"*"}, // allow all origins, change to specific domains in production
|
AllowOrigins: []string{"*"}, // allow all origins, change to specific domains in production
|
||||||
|
|
|
||||||
70
modules/backend/middlewares/csrf.go
Normal file
70
modules/backend/middlewares/csrf.go
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
53
modules/frontend/package-lock.json
generated
53
modules/frontend/package-lock.json
generated
|
|
@ -13,6 +13,7 @@
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
|
"react-cookie": "^8.0.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-router-dom": "^7.9.4",
|
"react-router-dom": "^7.9.4",
|
||||||
"tailwindcss": "^4.1.17"
|
"tailwindcss": "^4.1.17"
|
||||||
|
|
@ -1868,6 +1869,18 @@
|
||||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
|
|
@ -1890,7 +1903,6 @@
|
||||||
"version": "19.2.2",
|
"version": "19.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -2524,7 +2536,6 @@
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
|
|
@ -3260,6 +3271,15 @@
|
||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
|
|
@ -4068,6 +4088,20 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/react-dom": {
|
||||||
"version": "19.2.0",
|
"version": "19.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||||
|
|
@ -4081,6 +4115,12 @@
|
||||||
"react": "^19.2.0"
|
"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": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.17.0",
|
"version": "0.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||||
|
|
@ -4481,6 +4521,15 @@
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/universalify": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
|
"react-cookie": "^8.0.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-router-dom": "^7.9.4",
|
"react-router-dom": "^7.9.4",
|
||||||
"tailwindcss": "^4.1.17"
|
"tailwindcss": "^4.1.17"
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ export { CancelablePromise, CancelError } from './core/CancelablePromise';
|
||||||
export { OpenAPI } from './core/OpenAPI';
|
export { OpenAPI } from './core/OpenAPI';
|
||||||
export type { OpenAPIConfig } 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 { cursor } from './models/cursor';
|
||||||
export type { CursorObj } from './models/CursorObj';
|
export type { CursorObj } from './models/CursorObj';
|
||||||
export type { Image } from './models/Image';
|
export type { Image } from './models/Image';
|
||||||
|
|
|
||||||
9
modules/frontend/src/api/models/accessToken.ts
Normal file
9
modules/frontend/src/api/models/accessToken.ts
Normal file
|
|
@ -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;
|
||||||
11
modules/frontend/src/api/models/csrfToken.ts
Normal file
11
modules/frontend/src/api/models/csrfToken.ts
Normal file
|
|
@ -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;
|
||||||
10
modules/frontend/src/api/models/csrfTokenHeader.ts
Normal file
10
modules/frontend/src/api/models/csrfTokenHeader.ts
Normal file
|
|
@ -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;
|
||||||
|
|
@ -135,12 +135,16 @@ export class DefaultService {
|
||||||
* Password updates must be done via the dedicated auth-service (`/auth/`).
|
* Password updates must be done via the dedicated auth-service (`/auth/`).
|
||||||
* Fields not provided in the request body remain unchanged.
|
* 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 userId User ID (primary key)
|
||||||
* @param requestBody
|
* @param requestBody
|
||||||
* @returns User User updated successfully. Returns updated user representation (excluding sensitive fields).
|
* @returns User User updated successfully. Returns updated user representation (excluding sensitive fields).
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public static updateUser(
|
public static updateUser(
|
||||||
|
xXsrfToken: string,
|
||||||
userId: number,
|
userId: number,
|
||||||
requestBody: {
|
requestBody: {
|
||||||
/**
|
/**
|
||||||
|
|
@ -171,6 +175,9 @@ export class DefaultService {
|
||||||
path: {
|
path: {
|
||||||
'user_id': userId,
|
'user_id': userId,
|
||||||
},
|
},
|
||||||
|
headers: {
|
||||||
|
'X-XSRF-TOKEN': xXsrfToken,
|
||||||
|
},
|
||||||
body: requestBody,
|
body: requestBody,
|
||||||
mediaType: 'application/json',
|
mediaType: 'application/json',
|
||||||
errors: {
|
errors: {
|
||||||
|
|
@ -309,6 +316,9 @@ export class DefaultService {
|
||||||
/**
|
/**
|
||||||
* Update a usertitle
|
* Update a usertitle
|
||||||
* User updating title list of watched
|
* 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 userId
|
||||||
* @param titleId
|
* @param titleId
|
||||||
* @param requestBody
|
* @param requestBody
|
||||||
|
|
@ -316,6 +326,7 @@ export class DefaultService {
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public static updateUserTitle(
|
public static updateUserTitle(
|
||||||
|
xXsrfToken: string,
|
||||||
userId: number,
|
userId: number,
|
||||||
titleId: number,
|
titleId: number,
|
||||||
requestBody: {
|
requestBody: {
|
||||||
|
|
@ -330,6 +341,9 @@ export class DefaultService {
|
||||||
'user_id': userId,
|
'user_id': userId,
|
||||||
'title_id': titleId,
|
'title_id': titleId,
|
||||||
},
|
},
|
||||||
|
headers: {
|
||||||
|
'X-XSRF-TOKEN': xXsrfToken,
|
||||||
|
},
|
||||||
body: requestBody,
|
body: requestBody,
|
||||||
mediaType: 'application/json',
|
mediaType: 'application/json',
|
||||||
errors: {
|
errors: {
|
||||||
|
|
@ -344,12 +358,16 @@ export class DefaultService {
|
||||||
/**
|
/**
|
||||||
* Delete a usertitle
|
* Delete a usertitle
|
||||||
* User deleting title from list of watched
|
* 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 userId
|
||||||
* @param titleId
|
* @param titleId
|
||||||
* @returns any Title successfully deleted
|
* @returns any Title successfully deleted
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public static deleteUserTitle(
|
public static deleteUserTitle(
|
||||||
|
xXsrfToken: string,
|
||||||
userId: number,
|
userId: number,
|
||||||
titleId: number,
|
titleId: number,
|
||||||
): CancelablePromise<any> {
|
): CancelablePromise<any> {
|
||||||
|
|
@ -360,6 +378,9 @@ export class DefaultService {
|
||||||
'user_id': userId,
|
'user_id': userId,
|
||||||
'title_id': titleId,
|
'title_id': titleId,
|
||||||
},
|
},
|
||||||
|
headers: {
|
||||||
|
'X-XSRF-TOKEN': xXsrfToken,
|
||||||
|
},
|
||||||
errors: {
|
errors: {
|
||||||
401: `Unauthorized — missing or invalid auth token`,
|
401: `Unauthorized — missing or invalid auth token`,
|
||||||
403: `Forbidden — user not allowed to delete title`,
|
403: `Forbidden — user not allowed to delete title`,
|
||||||
|
|
|
||||||
|
|
@ -12,19 +12,17 @@ export class AuthService {
|
||||||
* @returns any Sign-up result
|
* @returns any Sign-up result
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public static postAuthSignUp(
|
public static postSignUp(
|
||||||
requestBody: {
|
requestBody: {
|
||||||
nickname: string;
|
nickname: string;
|
||||||
pass: string;
|
pass: string;
|
||||||
},
|
},
|
||||||
): CancelablePromise<{
|
): CancelablePromise<{
|
||||||
success?: boolean;
|
user_id: number;
|
||||||
error?: string | null;
|
|
||||||
user_id?: string | null;
|
|
||||||
}> {
|
}> {
|
||||||
return __request(OpenAPI, {
|
return __request(OpenAPI, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/auth/sign-up',
|
url: '/sign-up',
|
||||||
body: requestBody,
|
body: requestBody,
|
||||||
mediaType: 'application/json',
|
mediaType: 'application/json',
|
||||||
});
|
});
|
||||||
|
|
@ -35,19 +33,18 @@ export class AuthService {
|
||||||
* @returns any Sign-in result with JWT
|
* @returns any Sign-in result with JWT
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public static postAuthSignIn(
|
public static postSignIn(
|
||||||
requestBody: {
|
requestBody: {
|
||||||
nickname: string;
|
nickname: string;
|
||||||
pass: string;
|
pass: string;
|
||||||
},
|
},
|
||||||
): CancelablePromise<{
|
): CancelablePromise<{
|
||||||
error?: string | null;
|
user_id: number;
|
||||||
user_id?: string | null;
|
user_name: string;
|
||||||
user_name?: string | null;
|
|
||||||
}> {
|
}> {
|
||||||
return __request(OpenAPI, {
|
return __request(OpenAPI, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/auth/sign-in',
|
url: '/sign-in',
|
||||||
body: requestBody,
|
body: requestBody,
|
||||||
mediaType: 'application/json',
|
mediaType: 'application/json',
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { DefaultService } from "../../api";
|
import { DefaultService } from "../../api";
|
||||||
import type { UserTitleStatus } from "../../api";
|
import type { UserTitleStatus } from "../../api";
|
||||||
|
import { useCookies } from 'react-cookie';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ClockIcon,
|
ClockIcon,
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
|
|
@ -17,6 +19,9 @@ const STATUS_BUTTONS: { status: UserTitleStatus; icon: React.ReactNode; label: s
|
||||||
];
|
];
|
||||||
|
|
||||||
export function TitleStatusControls({ titleId }: { titleId: number }) {
|
export function TitleStatusControls({ titleId }: { titleId: number }) {
|
||||||
|
const [cookies] = useCookies(['xsrf_token']);
|
||||||
|
const xsrfToken = cookies['xsrf_token'] || null;
|
||||||
|
|
||||||
const [currentStatus, setCurrentStatus] = useState<UserTitleStatus | null>(null);
|
const [currentStatus, setCurrentStatus] = useState<UserTitleStatus | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
|
@ -41,7 +46,7 @@ export function TitleStatusControls({ titleId }: { titleId: number }) {
|
||||||
try {
|
try {
|
||||||
// 1) Если кликнули на текущий статус — DELETE
|
// 1) Если кликнули на текущий статус — DELETE
|
||||||
if (currentStatus === status) {
|
if (currentStatus === status) {
|
||||||
await DefaultService.deleteUserTitle(userId, titleId);
|
await DefaultService.deleteUserTitle(xsrfToken, userId, titleId);
|
||||||
setCurrentStatus(null);
|
setCurrentStatus(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -56,7 +61,7 @@ export function TitleStatusControls({ titleId }: { titleId: number }) {
|
||||||
setCurrentStatus(added.status);
|
setCurrentStatus(added.status);
|
||||||
} else {
|
} else {
|
||||||
// уже есть запись — PATCH
|
// уже есть запись — PATCH
|
||||||
const updated = await DefaultService.updateUserTitle(userId, titleId, { status });
|
const updated = await DefaultService.updateUserTitle(xsrfToken, userId, titleId, { status });
|
||||||
setCurrentStatus(updated.status);
|
setCurrentStatus(updated.status);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -17,23 +17,23 @@ export const LoginPage: React.FC = () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isLogin) {
|
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) {
|
if (res.user_id && res.user_name) {
|
||||||
// Сохраняем user_id и username в localStorage
|
// Сохраняем user_id и username в localStorage
|
||||||
localStorage.setItem("userId", res.user_id);
|
localStorage.setItem("userId", res.user_id.toString());
|
||||||
localStorage.setItem("username", res.user_name);
|
localStorage.setItem("username", res.user_name);
|
||||||
|
|
||||||
navigate("/profile"); // редирект на профиль
|
navigate("/profile"); // редирект на профиль
|
||||||
} else {
|
} else {
|
||||||
setError(res.error || "Login failed");
|
setError("Login failed");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// SignUp оставляем без сохранения данных
|
// SignUp оставляем без сохранения данных
|
||||||
const res = await AuthService.postAuthSignUp({ nickname, pass: password });
|
const res = await AuthService.postSignUp({ nickname, pass: password });
|
||||||
if (res.user_id) {
|
if (res.user_id) {
|
||||||
setIsLogin(true); // переключаемся на login после регистрации
|
setIsLogin(true); // переключаемся на login после регистрации
|
||||||
} else {
|
} else {
|
||||||
setError(res.error || "Sign up failed");
|
setError("Sign up failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue