diff --git a/api/parameters/xsrf_token_cookie.yaml b/api/parameters/xsrf_token_cookie.yaml index 37041e0..cf85999 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/deploy/docker-compose.yml b/deploy/docker-compose.yml index 0ae97c6..79ad2f5 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -62,8 +62,6 @@ services: environment: LOG_LEVEL: ${LOG_LEVEL} DATABASE_URL: ${DATABASE_URL} - SERVICE_ADDRESS: ${SERVICE_ADDRESS} - JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY} ports: - "8082:8082" depends_on: diff --git a/modules/auth/handlers/handlers.go b/modules/auth/handlers/handlers.go index 6fee512..261826c 100644 --- a/modules/auth/handlers/handlers.go +++ b/modules/auth/handlers/handlers.go @@ -2,8 +2,6 @@ package handlers import ( "context" - "crypto/rand" - "encoding/base64" "fmt" "net/http" auth "nyanimedb/auth" @@ -17,13 +15,15 @@ import ( log "github.com/sirupsen/logrus" ) +var accessSecret = []byte("my_access_secret_key") +var refreshSecret = []byte("my_refresh_secret_key") + type Server struct { - db *sqlc.Queries - JwtPrivateKey string + db *sqlc.Queries } -func NewServer(db *sqlc.Queries, JwtPrivatekey string) Server { - return Server{db: db, JwtPrivateKey: JwtPrivatekey} +func NewServer(db *sqlc.Queries) Server { + return Server{db: db} } func parseInt64(s string) (int32, error) { @@ -47,15 +47,15 @@ func CheckPassword(password, hash string) (bool, error) { return argon2id.ComparePasswordAndHash(password, hash) } -func (s Server) generateTokens(userID string) (accessToken string, refreshToken string, csrfToken string, err error) { +func generateTokens(userID string) (accessToken string, refreshToken string, err error) { accessClaims := jwt.MapClaims{ "user_id": userID, "exp": time.Now().Add(15 * time.Minute).Unix(), } at := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims) - accessToken, err = at.SignedString(s.JwtPrivateKey) + accessToken, err = at.SignedString(accessSecret) if err != nil { - return "", "", "", err + return "", "", err } refreshClaims := jwt.MapClaims{ @@ -63,19 +63,12 @@ func (s Server) generateTokens(userID string) (accessToken string, refreshToken "exp": time.Now().Add(7 * 24 * time.Hour).Unix(), } rt := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims) - refreshToken, err = rt.SignedString(s.JwtPrivateKey) + refreshToken, err = rt.SignedString(refreshSecret) if err != nil { - return "", "", "", err + return "", "", err } - csrfBytes := make([]byte, 32) - _, err = rand.Read(csrfBytes) - if err != nil { - return "", "", "", err - } - csrfToken = base64.RawURLEncoding.EncodeToString(csrfBytes) - - return accessToken, refreshToken, csrfToken, nil + return accessToken, refreshToken, nil } func (s Server) PostAuthSignUp(ctx context.Context, req auth.PostAuthSignUpRequestObject) (auth.PostAuthSignUpResponseObject, error) { @@ -125,7 +118,7 @@ func (s Server) PostAuthSignIn(ctx context.Context, req auth.PostAuthSignInReque }, nil } - accessToken, refreshToken, csrfToken, err := s.generateTokens(req.Body.Nickname) + accessToken, refreshToken, err := generateTokens(req.Body.Nickname) if err != nil { log.Errorf("failed to generate tokens for user %s: %v", req.Body.Nickname, err) // TODO: return 500 @@ -133,9 +126,8 @@ func (s Server) PostAuthSignIn(ctx context.Context, req auth.PostAuthSignInReque // TODO: check cookie settings carefully ginCtx.SetSameSite(http.SameSiteStrictMode) - ginCtx.SetCookie("access_token", accessToken, 900, "/api", "", false, true) - ginCtx.SetCookie("refresh_token", refreshToken, 1209600, "/auth", "", false, true) - ginCtx.SetCookie("xsrf_token", csrfToken, 1209600, "/api", "", false, false) + ginCtx.SetCookie("access_token", accessToken, 604800, "/auth", "", false, true) + ginCtx.SetCookie("refresh_token", refreshToken, 604800, "/api", "", false, true) result := auth.PostAuthSignIn200JSONResponse{ UserId: user.ID, diff --git a/modules/auth/helpers.go b/modules/auth/helpers.go deleted file mode 100644 index 9c3ab36..0000000 --- a/modules/auth/helpers.go +++ /dev/null @@ -1,33 +0,0 @@ -package main - -import ( - "fmt" - "reflect" -) - -func setField(obj interface{}, name string, value interface{}) error { - v := reflect.ValueOf(obj) - - if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { - return fmt.Errorf("expected pointer to a struct") - } - - v = v.Elem() - field := v.FieldByName(name) - - if !field.IsValid() { - return fmt.Errorf("no such field: %s", name) - } - if !field.CanSet() { - return fmt.Errorf("cannot set field: %s", name) - } - - val := reflect.ValueOf(value) - - if field.Type() != val.Type() { - return fmt.Errorf("provided value type (%s) doesn't match field type (%s)", val.Type(), field.Type()) - } - - field.Set(val) - return nil -} diff --git a/modules/auth/main.go b/modules/auth/main.go index ef9b977..7554f42 100644 --- a/modules/auth/main.go +++ b/modules/auth/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "os" - "reflect" "time" auth "nyanimedb/auth" @@ -14,24 +13,12 @@ import ( "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/jackc/pgx/v5/pgxpool" - "github.com/pelletier/go-toml/v2" - log "github.com/sirupsen/logrus" ) var AppConfig Config func main() { - if len(os.Args) != 2 { - AppConfig.Mode = "env" - } else { - AppConfig.Mode = "argv" - } - - err := InitConfig() - if err != nil { - log.Fatalf("Failed to init config: %v\n", err) - } - + // TODO: env args r := gin.Default() pool, err := pgxpool.New(context.Background(), os.Getenv("DATABASE_URL")) @@ -42,10 +29,10 @@ func main() { var queries *sqlc.Queries = sqlc.New(pool) - server := handlers.NewServer(queries, AppConfig.JwtPrivateKey) + server := handlers.NewServer(queries) r.Use(cors.New(cors.Config{ - AllowOrigins: []string{AppConfig.ServiceAddress}, + AllowOrigins: []string{"*"}, // allow all origins, change to specific domains in production AllowMethods: []string{"GET", "POST", "PUT", "DELETE"}, AllowHeaders: []string{"Origin", "Content-Type", "Accept"}, ExposeHeaders: []string{"Content-Length"}, @@ -60,41 +47,3 @@ func main() { r.Run(":8082") } - -func InitConfig() error { - if AppConfig.Mode == "argv" { - content, err := os.ReadFile(os.Args[1]) - if err != nil { - return err - } - - toml.Unmarshal(content, &AppConfig) - - fmt.Printf("%+v\n", AppConfig) - - return nil - } else if AppConfig.Mode == "env" { - f := reflect.ValueOf(AppConfig) - - for i := 0; i < f.NumField(); i++ { - field := f.Type().Field(i) - tag := field.Tag - env_var := tag.Get("env") - fmt.Printf("Field: %v.\nEnvironment variable: %v.\n", field.Name, env_var) - if env_var != "" { - env_value, exists := os.LookupEnv(env_var) - if !exists { - return fmt.Errorf("there is no env variable %s", env_var) - } - err := setField(&AppConfig, field.Name, env_value) - if err != nil { - return fmt.Errorf("failed to set config field %s: %v", field.Name, err) - } - } - } - - return nil - } else { - return fmt.Errorf("incorrect config mode") - } -} diff --git a/modules/auth/types.go b/modules/auth/types.go index 694843e..038b179 100644 --- a/modules/auth/types.go +++ b/modules/auth/types.go @@ -1,9 +1,6 @@ package main type Config struct { - Mode string - ServiceAddress string `toml:"ServiceAddress" env:"SERVICE_ADDRESS"` - DdUrl string `toml:"DbUrl" env:"DATABASE_URL"` - JwtPrivateKey string `toml:"JwtPrivateKey" env:"JWT_PRIVATE_KEY"` - LogLevel string `toml:"LogLevel" env:"LOG_LEVEL"` + JwtPrivateKey string + LogLevel string `toml:"LogLevel" env:"LOG_LEVEL"` } diff --git a/modules/backend/main.go b/modules/backend/main.go index 0cffdcf..aab1287 100644 --- a/modules/backend/main.go +++ b/modules/backend/main.go @@ -25,18 +25,18 @@ import ( var AppConfig Config func main() { - if len(os.Args) != 2 { - AppConfig.Mode = "env" - } else { - AppConfig.Mode = "argv" - } + // if len(os.Args) != 2 { + // AppConfig.Mode = "env" + // } else { + // AppConfig.Mode = "argv" + // } - err := InitConfig() - if err != nil { - log.Fatalf("Failed to init config: %v\n", err) - } + // err := InitConfig() + // if err != nil { + // log.Fatalf("Failed to init config: %v\n", err) + // } - pool, err := pgxpool.New(context.Background(), AppConfig.DdUrl) + pool, err := pgxpool.New(context.Background(), os.Getenv("DATABASE_URL")) if err != nil { fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err) os.Exit(1) @@ -47,11 +47,16 @@ func main() { r := gin.Default() r.Use(middleware.CSRFMiddleware()) - r.Use(middleware.JWTAuthMiddleware(AppConfig.JwtPrivateKey)) - + // jwt middle will be here queries := sqlc.New(pool) - rmqConn, err := amqp091.Dial(AppConfig.rmqURL) + // === RabbitMQ setup === + rmqURL := os.Getenv("RABBITMQ_URL") + if rmqURL == "" { + rmqURL = "amqp://guest:guest@rabbitmq:5672/" + } + + rmqConn, err := amqp091.Dial(rmqURL) if err != nil { log.Fatalf("Failed to connect to RabbitMQ: %v", err) } @@ -63,7 +68,7 @@ func main() { server := handlers.NewServer(queries, publisher, rpcClient) r.Use(cors.New(cors.Config{ - AllowOrigins: []string{AppConfig.ServiceAddress}, + AllowOrigins: []string{"*"}, // allow all origins, change to specific domains in production AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "PATCH"}, AllowHeaders: []string{"Origin", "Content-Type", "Accept"}, ExposeHeaders: []string{"Content-Length"}, @@ -73,7 +78,7 @@ func main() { oapi.RegisterHandlers(r, oapi.NewStrictHandler( server, - + // сюда можно добавить middlewares, если нужно []oapi.StrictMiddlewareFunc{}, )) diff --git a/modules/backend/middlewares/access.go b/modules/backend/middlewares/access.go deleted file mode 100644 index 73200e8..0000000 --- a/modules/backend/middlewares/access.go +++ /dev/null @@ -1,109 +0,0 @@ -package middleware - -import ( - "context" - "errors" - "net/http" - - "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v5" -) - -// ctxKey — приватный тип для ключа контекста -type ctxKey struct{} - -// ginContextKey — уникальный ключ для хранения *gin.Context -var ginContextKey = &ctxKey{} - -// GinContextToContext сохраняет *gin.Context в context.Context запроса -func GinContextToContext(c *gin.Context) { - ctx := context.WithValue(c.Request.Context(), ginContextKey, c) - c.Request = c.Request.WithContext(ctx) -} - -// GinContextFromContext извлекает *gin.Context из context.Context -func GinContextFromContext(ctx context.Context) (*gin.Context, bool) { - ginCtx, ok := ctx.Value(ginContextKey).(*gin.Context) - return ginCtx, ok -} - -func JWTAuthMiddleware(secret string) gin.HandlerFunc { - return func(c *gin.Context) { - // 1. Получаем access_token из cookie - tokenStr, err := c.Cookie("access_token") - if err != nil { - abortWithJSON(c, http.StatusUnauthorized, "missing access_token cookie") - return - } - - // 2. Парсим токен с MapClaims - token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) { - if t.Method != jwt.SigningMethodHS256 { - return nil, errors.New("unexpected signing method: " + t.Method.Alg()) - } - return []byte(secret), nil // ← конвертируем string → []byte - }) - if err != nil { - abortWithJSON(c, http.StatusUnauthorized, "invalid token: "+err.Error()) - return - } - - // 3. Проверяем валидность - if !token.Valid { - abortWithJSON(c, http.StatusUnauthorized, "token is invalid") - return - } - - // 4. Извлекаем user_id из claims - claims, ok := token.Claims.(jwt.MapClaims) - if !ok { - abortWithJSON(c, http.StatusUnauthorized, "invalid claims format") - return - } - - userID, ok := claims["user_id"].(string) - if !ok || userID == "" { - abortWithJSON(c, http.StatusUnauthorized, "user_id claim missing or invalid") - return - } - - // 5. Сохраняем в контексте - c.Set("user_id", userID) - - // 6. Для oapi-codegen — кладём gin.Context в request context - GinContextToContext(c) - - c.Next() - } -} - -// Вспомогательные функции (без изменений) -func UserIDFromGin(c *gin.Context) (string, bool) { - id, exists := c.Get("user_id") - if !exists { - return "", false - } - if s, ok := id.(string); ok { - return s, true - } - return "", false -} - -func UserIDFromContext(ctx context.Context) (string, error) { - ginCtx, ok := GinContextFromContext(ctx) - if !ok { - return "", errors.New("gin context not found") - } - userID, ok := UserIDFromGin(ginCtx) - if !ok { - return "", errors.New("user_id not found in context") - } - return userID, nil -} - -func abortWithJSON(c *gin.Context, code int, message string) { - c.AbortWithStatusJSON(code, gin.H{ - "error": "unauthorized", - "message": message, - }) -} diff --git a/modules/backend/types.go b/modules/backend/types.go index c4f70ed..20d3158 100644 --- a/modules/backend/types.go +++ b/modules/backend/types.go @@ -1,10 +1,12 @@ package main type Config struct { - Mode string - ServiceAddress string `toml:"ServiceAddress" env:"SERVICE_ADDRESS"` - DdUrl string `toml:"DbUrl" env:"DATABASE_URL"` - JwtPrivateKey string `toml:"JwtPrivateKey" env:"JWT_PRIVATE_KEY"` - LogLevel string `toml:"LogLevel" env:"LOG_LEVEL"` - rmqURL string `toml:"RabbitMQUrl" env:"RABBITMQ_URL"` + Mode string + LogLevel string `toml:"LogLevel" env:"LOG_LEVEL"` +} + +type Item struct { + ID int `json:"id"` + Title string `json:"title"` + Description string `json:"description"` } diff --git a/modules/frontend/src/api/services/DefaultService.ts b/modules/frontend/src/api/services/DefaultService.ts index 6898c46..218b461 100644 --- a/modules/frontend/src/api/services/DefaultService.ts +++ b/modules/frontend/src/api/services/DefaultService.ts @@ -20,7 +20,6 @@ export class DefaultService { * @param cursor * @param sort * @param sortForward - * @param extSearch * @param word * @param status List of title statuses to filter * @param rating @@ -36,7 +35,6 @@ export class DefaultService { cursor?: string, sort?: TitleSort, sortForward: boolean = true, - extSearch: boolean = false, word?: string, status?: Array, rating?: number, @@ -59,7 +57,6 @@ export class DefaultService { 'cursor': cursor, 'sort': sort, 'sort_forward': sortForward, - 'ext_search': extSearch, 'word': word, 'status': status, 'rating': rating, diff --git a/modules/frontend/src/components/TitlesFilterPanel/TitlesFilterPanel.tsx b/modules/frontend/src/components/TitlesFilterPanel/TitlesFilterPanel.tsx deleted file mode 100644 index 3cfef69..0000000 --- a/modules/frontend/src/components/TitlesFilterPanel/TitlesFilterPanel.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { useState } from "react"; -import type { TitleStatus, ReleaseSeason } from "../../api"; -import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/solid"; - -export type TitlesFilter = { - extSearch: boolean; - status: TitleStatus | ""; - rating: number | ""; - releaseYear: number | ""; - releaseSeason: ReleaseSeason | ""; -}; - -type TitlesFilterPanelProps = { - filters: TitlesFilter; - setFilters: (filters: TitlesFilter) => void; -}; - -const STATUS_OPTIONS: (TitleStatus | "")[] = ["", "planned", "finished", "ongoing"]; -const SEASON_OPTIONS: (ReleaseSeason | "")[] = ["", "winter", "spring", "summer", "fall"]; -const RATING_OPTIONS = ["", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - -export function TitlesFilterPanel({ filters, setFilters }: TitlesFilterPanelProps) { - const [open, setOpen] = useState(false); - - const handleChange = (field: keyof TitlesFilter, value: any) => { - setFilters({ ...filters, [field]: value }); - }; - - return ( -
-
- {/* Заголовок панели */} -
setOpen((prev) => !prev)} - > -

Filters

- {open ? : } -
- - {/* Контент панели */} - {open && ( -
- {/* Extended Search */} -
- handleChange("extSearch", e.target.checked)} - className="w-4 h-4" - /> - -
- - {/* Status */} -
- - -
- - {/* Rating */} -
- - -
- - {/* Release Year */} -
- - - handleChange("releaseYear", e.target.value ? Number(e.target.value) : "") - } - className="border rounded px-2 py-1" - placeholder="Any" - /> -
- - {/* Release Season */} -
- - -
-
- )} -
-
- ); -} diff --git a/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx b/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx index ed55d8d..c9911b9 100644 --- a/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx +++ b/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx @@ -8,7 +8,6 @@ import { TitleCardHorizontal } from "../../components/cards/TitleCardHorizontal" import type { CursorObj, Title, TitleSort } from "../../api"; import { LayoutSwitch } from "../../components/LayoutSwitch/LayoutSwitch"; import { Link } from "react-router-dom"; -import { type TitlesFilter, TitlesFilterPanel } from "../../components/TitlesFilterPanel/TitlesFilterPanel"; const PAGE_SIZE = 10; @@ -23,14 +22,6 @@ export default function TitlesPage() { const [sortForward, setSortForward] = useState(true); const [layout, setLayout] = useState<"square" | "horizontal">("square"); - const [filters, setFilters] = useState({ - extSearch: false, - status: "", - rating: "", - releaseYear: "", - releaseSeason: "", - }); - const fetchPage = async (cursorObj: CursorObj | null) => { const cursorStr = cursorObj ? btoa(JSON.stringify(cursorObj)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') : ""; @@ -39,14 +30,13 @@ export default function TitlesPage() { cursorStr, sort, sortForward, - filters.extSearch, search.trim() || undefined, - filters.status ? [filters.status] : undefined, - filters.rating || undefined, - filters.releaseYear || undefined, - filters.releaseSeason || undefined, - PAGE_SIZE, + undefined, + undefined, + undefined, + undefined, PAGE_SIZE, + undefined, "all" ); @@ -83,7 +73,7 @@ export default function TitlesPage() { }; initLoad(); - }, [search, sort, sortForward, filters]); + }, [search, sort, sortForward]); const handleLoadMore = async () => { @@ -131,7 +121,6 @@ const handleLoadMore = async () => { setSortForward={setSortForward} /> - {loading &&
Loading...
}