Compare commits
No commits in common. "31e55c0539748624d0c59af9d74b6b7cc3301a66" and "4dd60f3b190ffa8b2ca91ade49d83c1b36ea295d" have entirely different histories.
31e55c0539
...
4dd60f3b19
12 changed files with 5 additions and 233 deletions
|
|
@ -120,8 +120,6 @@ 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
|
||||||
|
|
@ -158,8 +156,6 @@ 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/accessToken'
|
|
||||||
- $ref: '#/components/parameters/csrfToken'
|
|
||||||
- name: user_id
|
- name: user_id
|
||||||
in: path
|
in: path
|
||||||
description: User ID (primary key)
|
description: User ID (primary key)
|
||||||
|
|
@ -227,8 +223,6 @@ 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
|
||||||
|
|
@ -480,39 +474,6 @@ paths:
|
||||||
description: Internal server error
|
description: Internal server error
|
||||||
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,10 +16,6 @@ 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"
|
||||||
|
|
@ -174,12 +170,6 @@ 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
|
||||||
|
|
||||||
|
|
@ -229,17 +219,6 @@ 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"`
|
||||||
|
|
@ -297,7 +276,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, params UpdateUserParams)
|
UpdateUser(c *gin.Context, userId int64)
|
||||||
// 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)
|
||||||
|
|
@ -452,8 +431,6 @@ 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
|
||||||
|
|
||||||
|
|
@ -524,47 +501,6 @@ 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() {
|
||||||
|
|
@ -572,7 +508,7 @@ func (siw *ServerInterfaceWrapper) UpdateUser(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
siw.Handler.UpdateUser(c, userId, params)
|
siw.Handler.UpdateUser(c, userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserTitles operation middleware
|
// GetUserTitles operation middleware
|
||||||
|
|
@ -999,7 +935,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1476,11 +1411,10 @@ 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, params UpdateUserParams) {
|
func (sh *strictHandler) UpdateUser(ctx *gin.Context, userId int64) {
|
||||||
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,10 +1,4 @@
|
||||||
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"
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
name: access_token
|
|
||||||
in: cookie
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
format: jwt
|
|
||||||
example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.x.y"
|
|
||||||
description: |
|
|
||||||
JWT access token.
|
|
||||||
|
|
@ -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).
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
get:
|
get:
|
||||||
summary: Get title description
|
summary: Get title description
|
||||||
security:
|
|
||||||
- JwtAuthCookies: []
|
|
||||||
operationId: getTitle
|
operationId: getTitle
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
|
|
|
||||||
|
|
@ -28,16 +28,12 @@ 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/access_token.yaml' # ← для поля в UI и GoDoc
|
|
||||||
- $ref: '../parameters/xsrf_token_cookie.yaml' # ← для CSRF
|
|
||||||
- name: user_id
|
- name: user_id
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
# type: apiKey
|
|
||||||
# in: cookie
|
|
||||||
# name: access_token
|
|
||||||
# scheme: bearer
|
|
||||||
# bearerFormat: JWT
|
|
||||||
# description: |
|
|
||||||
# JWT access token sent in `Cookie: access_token=...`.
|
|
||||||
|
|
@ -24,5 +24,3 @@ User:
|
||||||
$ref: "./User.yaml"
|
$ref: "./User.yaml"
|
||||||
UserTitle:
|
UserTitle:
|
||||||
$ref: "./UserTitle.yaml"
|
$ref: "./UserTitle.yaml"
|
||||||
# JwtAuth:
|
|
||||||
# $ref: "./JWTAuth.yaml"
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ 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"
|
||||||
|
|
@ -46,8 +45,6 @@ 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 ===
|
||||||
|
|
@ -66,6 +63,7 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue