Sfoglia il codice sorgente

fix: 429 rate limit + refreshSession ordering + export refreshAccessToken

- refreshSession: refreshAccessToken() first, then /auth/me (no more 401 spam)
- api.ts: export refreshAccessToken for useAuth
- api.ts request(): handle 429 with 1s retry
- main.go: write limiter in-memory fallback 10→60 req/min
- ratelimit_redis.go: Redis write limiter 10→60 req/min per user
neyrogovnarik 1 mese fa
parent
commit
12f6dcf9d7

+ 1 - 1
backend/cmd/api/main.go

@@ -146,7 +146,7 @@ func main() {
 		func(client *redis.Client, log *slog.Logger, fo bool) (rateLimiterInterface, error) {
 			return middleware.RateLimitAPIWrite(client, log, fo)
 		},
-		func() *middleware.RateLimiter { return middleware.NewRateLimiter(rate.Limit(10/60.0), 10) }, // 10 req/min
+		func() *middleware.RateLimiter { return middleware.NewRateLimiter(rate.Limit(60/60.0), 60) }, // 60 req/min
 	)
 	adminLimiter = newRateLimiterWithFallback(redisClient, redisAvailable, logger, failOpen,
 		func(client *redis.Client, log *slog.Logger, fo bool) (rateLimiterInterface, error) {

+ 1 - 1
backend/internal/middleware/ratelimit_redis.go

@@ -143,7 +143,7 @@ func RateLimitAPIRead(redisClient *redis.Client, logger *slog.Logger, failOpen b
 
 func RateLimitAPIWrite(redisClient *redis.Client, logger *slog.Logger, failOpen bool) (*RedisRateLimiter, error) {
 	return NewRedisRateLimiter(redisClient, RateLimitConfig{
-		Rate:     limiter.Rate{Period: time.Minute, Limit: 10},
+		Rate:     limiter.Rate{Period: time.Minute, Limit: 60},
 		KeyFunc:  KeyByUserID("api_write"),
 		FailOpen: failOpen,
 		Logger:   logger,

+ 3 - 1
frontend/src/hooks/useAuth.tsx

@@ -2,7 +2,7 @@
 
 import { createContext, useContext, useState, useCallback, useEffect, type ReactNode } from 'react'
 import type { User } from '@/types'
-import { api, setAccessToken } from '@/lib/api'
+import { api, setAccessToken, refreshAccessToken } from '@/lib/api'
 
 /**
  * Состояние аутентификации, доступное через контекст
@@ -35,6 +35,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
 
   const refreshSession = useCallback(async () => {
     try {
+      const newToken = await refreshAccessToken()
+      setAccessToken(newToken)
       const res = await api.get<User>('/auth/me')
       setUser(res)
     } catch {

+ 5 - 1
frontend/src/lib/api.ts

@@ -19,7 +19,7 @@ export function getAccessToken(): string | null {
  * @returns Новый access-токен
  * @throws Ошибка, если refresh-токен недействителен
  */
-async function refreshAccessToken(): Promise<string> {
+export async function refreshAccessToken(): Promise<string> {
   if (refreshPromise) return refreshPromise
 
   refreshPromise = (async () => {
@@ -69,6 +69,10 @@ async function request<T>(
   })
 
   const noRefreshPaths = ['/auth/login', '/auth/register', '/auth/refresh', '/auth/logout']
+  if (res.status === 429 && !isRetry) {
+    await new Promise(r => setTimeout(r, 1000))
+    return request<T>(path, options, true)
+  }
   if (res.status === 401 && !isRetry && !noRefreshPaths.includes(path)) {
     try {
       const newToken = await refreshAccessToken()