diff --git a/VERSION b/VERSION index 2a5310b..185fbd9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.28.2 +3.28.3 diff --git a/play-life-backend/main.go b/play-life-backend/main.go index 9ca98e9..44706db 100644 --- a/play-life-backend/main.go +++ b/play-life-backend/main.go @@ -1018,10 +1018,7 @@ func (a *App) refreshTokenHandler(w http.ResponseWriter, r *http.Request) { return } - // Delete old refresh token - a.DB.Exec("DELETE FROM refresh_tokens WHERE id = $1", foundTokenID) - - // Generate new tokens + // Generate new tokens FIRST before deleting old one to prevent race condition accessToken, err := a.generateAccessToken(user.ID) if err != nil { log.Printf("Error generating access token: %v", err) @@ -1036,12 +1033,21 @@ func (a *App) refreshTokenHandler(w http.ResponseWriter, r *http.Request) { return } - // Store new refresh token + // Store new refresh token FIRST refreshTokenHash, _ := hashPassword(refreshToken) - a.DB.Exec(` + _, err = a.DB.Exec(` INSERT INTO refresh_tokens (user_id, token_hash, expires_at) VALUES ($1, $2, $3) `, user.ID, refreshTokenHash, nil) + if err != nil { + log.Printf("Error storing new refresh token: %v", err) + sendErrorWithCORS(w, "Error generating token", http.StatusInternalServerError) + return + } + + // Delete old refresh token AFTER new one is successfully stored + // This prevents race condition where multiple refresh requests might use the same token + a.DB.Exec("DELETE FROM refresh_tokens WHERE id = $1", foundTokenID) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(TokenResponse{ @@ -2818,6 +2824,29 @@ func (a *App) initAuthDB() error { a.DB.Exec("CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user_id ON refresh_tokens(user_id)") a.DB.Exec("CREATE INDEX IF NOT EXISTS idx_refresh_tokens_token_hash ON refresh_tokens(token_hash)") + // Apply migration 014: Make refresh tokens permanent (expires_at nullable) + // This allows refresh tokens to never expire + var isNullable string + err = a.DB.QueryRow(` + SELECT is_nullable + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'refresh_tokens' + AND column_name = 'expires_at' + `).Scan(&isNullable) + if err == nil && isNullable == "NO" { + // Column is NOT NULL, need to make it nullable + if _, err := a.DB.Exec("ALTER TABLE refresh_tokens ALTER COLUMN expires_at DROP NOT NULL"); err != nil { + log.Printf("Warning: Failed to apply migration 014 (make expires_at nullable): %v", err) + } else { + log.Printf("Migration 014 applied: refresh_tokens.expires_at is now nullable") + } + } else if err == nil && isNullable == "YES" { + // Migration already applied + log.Printf("Migration 014 already applied (expires_at is nullable), skipping") + } + // If err != nil, column might not exist yet (shouldn't happen, but ignore) + // Add user_id column to all tables if not exists tables := []string{"projects", "entries", "nodes", "dictionaries", "words", "progress", "configs", "telegram_integrations", "weekly_goals", "tasks"} for _, table := range tables { diff --git a/play-life-web/package.json b/play-life-web/package.json index 5701a4c..1bfda06 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "3.28.2", + "version": "3.28.3", "type": "module", "scripts": { "dev": "vite",