1
0
forked from wrenn/wrenn

Fix API key cleanup on user deactivation and build archive race condition

Delete all API keys created by a user when their account is disabled,
deleted, or soft-deleted. Store build archives before enqueuing to Redis
so workers never dequeue a build with missing files.
This commit is contained in:
2026-04-16 05:29:02 +06:00
parent 451d0819cc
commit e91109d69c
5 changed files with 28 additions and 6 deletions

View File

@ -25,3 +25,6 @@ UPDATE team_api_keys SET last_used = NOW() WHERE id = $1;
-- name: DeleteAPIKeysByTeam :exec -- name: DeleteAPIKeysByTeam :exec
DELETE FROM team_api_keys WHERE team_id = $1; DELETE FROM team_api_keys WHERE team_id = $1;
-- name: DeleteAPIKeysByCreator :exec
DELETE FROM team_api_keys WHERE created_by = $1;

View File

@ -524,6 +524,10 @@ func (h *meHandler) DeleteAccount(w http.ResponseWriter, r *http.Request) {
} }
} }
if err := h.db.DeleteAPIKeysByCreator(ctx, ac.UserID); err != nil {
slog.Warn("account delete: failed to delete user's API keys", "error", err)
}
if err := h.db.SoftDeleteUser(ctx, ac.UserID); err != nil { if err := h.db.SoftDeleteUser(ctx, ac.UserID); err != nil {
writeError(w, http.StatusInternalServerError, "db_error", "failed to delete account") writeError(w, http.StatusInternalServerError, "db_error", "failed to delete account")
return return

View File

@ -25,6 +25,15 @@ func (q *Queries) DeleteAPIKey(ctx context.Context, arg DeleteAPIKeyParams) erro
return err return err
} }
const deleteAPIKeysByCreator = `-- name: DeleteAPIKeysByCreator :exec
DELETE FROM team_api_keys WHERE created_by = $1
`
func (q *Queries) DeleteAPIKeysByCreator(ctx context.Context, createdBy pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteAPIKeysByCreator, createdBy)
return err
}
const deleteAPIKeysByTeam = `-- name: DeleteAPIKeysByTeam :exec const deleteAPIKeysByTeam = `-- name: DeleteAPIKeysByTeam :exec
DELETE FROM team_api_keys WHERE team_id = $1 DELETE FROM team_api_keys WHERE team_id = $1
` `

View File

@ -139,16 +139,16 @@ func (s *BuildService) Create(ctx context.Context, p BuildCreateParams) (db.Temp
return db.TemplateBuild{}, fmt.Errorf("insert build: %w", err) return db.TemplateBuild{}, fmt.Errorf("insert build: %w", err)
} }
// Enqueue build ID (as formatted string) to Redis for workers to pick up. // Store archive before enqueue so the worker never dequeues without files.
if err := s.Redis.RPush(ctx, buildQueueKey, buildIDStr).Err(); err != nil {
return db.TemplateBuild{}, fmt.Errorf("enqueue build: %w", err)
}
// Store archive for the worker if provided.
if len(p.Archive) > 0 { if len(p.Archive) > 0 {
s.storeArchive(buildIDStr, p.Archive) s.storeArchive(buildIDStr, p.Archive)
} }
if err := s.Redis.RPush(ctx, buildQueueKey, buildIDStr).Err(); err != nil {
s.takeArchive(buildIDStr) // clean up on enqueue failure
return db.TemplateBuild{}, fmt.Errorf("enqueue build: %w", err)
}
return build, nil return build, nil
} }

View File

@ -3,6 +3,7 @@ package service
import ( import (
"context" "context"
"fmt" "fmt"
"log/slog"
"time" "time"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
@ -66,5 +67,10 @@ func (s *UserService) SetUserStatus(ctx context.Context, userID pgtype.UUID, sta
}); err != nil { }); err != nil {
return fmt.Errorf("set user status: %w", err) return fmt.Errorf("set user status: %w", err)
} }
if status == "disabled" || status == "deleted" {
if err := s.DB.DeleteAPIKeysByCreator(ctx, userID); err != nil {
slog.Warn("failed to delete API keys for deactivated user", "user_id", userID, "error", err)
}
}
return nil return nil
} }