forked from wrenn/wrenn
Destroy owned sandboxes on user disable and fix OAuth login resilience
When an admin disables a user, all active sandboxes (running, paused, hibernated) for teams they own are now destroyed and their API keys are deleted. User queries now filter by status column instead of deleted_at, so re-enabling a user always works. OAuth login paths use ensureDefaultTeam to auto-create a team if the user has none, matching the email/password login behavior.
This commit is contained in:
@ -90,6 +90,35 @@ func (q *Queries) GetDefaultTeamForUser(ctx context.Context, userID pgtype.UUID)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getOwnedTeamIDs = `-- name: GetOwnedTeamIDs :many
|
||||
SELECT t.id FROM teams t
|
||||
JOIN users_teams ut ON ut.team_id = t.id
|
||||
WHERE ut.user_id = $1
|
||||
AND ut.role = 'owner'
|
||||
AND t.deleted_at IS NULL
|
||||
`
|
||||
|
||||
// Returns team IDs where the given user has the 'owner' role.
|
||||
func (q *Queries) GetOwnedTeamIDs(ctx context.Context, userID pgtype.UUID) ([]pgtype.UUID, error) {
|
||||
rows, err := q.db.Query(ctx, getOwnedTeamIDs, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []pgtype.UUID
|
||||
for rows.Next() {
|
||||
var id pgtype.UUID
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, id)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getTeam = `-- name: GetTeam :one
|
||||
SELECT id, name, slug, is_byoc, created_at, deleted_at FROM teams WHERE id = $1
|
||||
`
|
||||
|
||||
@ -54,7 +54,7 @@ func (q *Queries) CountUsers(ctx context.Context) (int64, error) {
|
||||
const countUsersAdmin = `-- name: CountUsersAdmin :one
|
||||
SELECT COUNT(*)::int AS total
|
||||
FROM users
|
||||
WHERE deleted_at IS NULL
|
||||
WHERE status != 'deleted'
|
||||
`
|
||||
|
||||
func (q *Queries) CountUsersAdmin(ctx context.Context) (int32, error) {
|
||||
@ -142,7 +142,7 @@ func (q *Queries) GetAdminUsers(ctx context.Context) ([]User, error) {
|
||||
}
|
||||
|
||||
const getUserByEmail = `-- name: GetUserByEmail :one
|
||||
SELECT id, email, password_hash, name, is_admin, created_at, updated_at, deleted_at, status FROM users WHERE email = $1 AND deleted_at IS NULL
|
||||
SELECT id, email, password_hash, name, is_admin, created_at, updated_at, deleted_at, status FROM users WHERE email = $1 AND status != 'deleted'
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) {
|
||||
@ -163,7 +163,7 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error
|
||||
}
|
||||
|
||||
const getUserByID = `-- name: GetUserByID :one
|
||||
SELECT id, email, password_hash, name, is_admin, created_at, updated_at, deleted_at, status FROM users WHERE id = $1 AND deleted_at IS NULL
|
||||
SELECT id, email, password_hash, name, is_admin, created_at, updated_at, deleted_at, status FROM users WHERE id = $1 AND status != 'deleted'
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByID(ctx context.Context, id pgtype.UUID) (User, error) {
|
||||
@ -345,7 +345,7 @@ SELECT
|
||||
(SELECT COUNT(*) FROM users_teams ut WHERE ut.user_id = u.id)::int AS teams_joined,
|
||||
(SELECT COUNT(*) FROM users_teams ut WHERE ut.user_id = u.id AND ut.role = 'owner')::int AS teams_owned
|
||||
FROM users u
|
||||
WHERE u.deleted_at IS NULL
|
||||
WHERE u.status != 'deleted'
|
||||
ORDER BY u.created_at DESC
|
||||
LIMIT $1 OFFSET $2
|
||||
`
|
||||
|
||||
@ -13,7 +13,8 @@ import (
|
||||
|
||||
// UserService provides user management operations.
|
||||
type UserService struct {
|
||||
DB *db.Queries
|
||||
DB *db.Queries
|
||||
SandboxSvc *SandboxService
|
||||
}
|
||||
|
||||
// AdminUserRow is the shape returned by AdminListUsers.
|
||||
@ -71,6 +72,36 @@ func (s *UserService) SetUserStatus(ctx context.Context, userID pgtype.UUID, sta
|
||||
if err := s.DB.DeleteAPIKeysByCreator(ctx, userID); err != nil {
|
||||
slog.Warn("failed to delete API keys for deactivated user", "user_id", userID, "error", err)
|
||||
}
|
||||
s.destroySandboxesForOwnedTeams(ctx, userID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// destroySandboxesForOwnedTeams destroys all active sandboxes (running, paused,
|
||||
// hibernated, starting) for every team the user owns. Best-effort: errors are
|
||||
// logged but do not prevent the user from being disabled.
|
||||
func (s *UserService) destroySandboxesForOwnedTeams(ctx context.Context, userID pgtype.UUID) {
|
||||
if s.SandboxSvc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
teamIDs, err := s.DB.GetOwnedTeamIDs(ctx, userID)
|
||||
if err != nil {
|
||||
slog.Warn("failed to list owned teams for sandbox cleanup", "user_id", userID, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, teamID := range teamIDs {
|
||||
sandboxes, err := s.DB.ListActiveSandboxesByTeam(ctx, teamID)
|
||||
if err != nil {
|
||||
slog.Warn("failed to list active sandboxes for team", "team_id", teamID, "user_id", userID, "error", err)
|
||||
continue
|
||||
}
|
||||
for _, sb := range sandboxes {
|
||||
if err := s.SandboxSvc.Destroy(ctx, sb.ID, teamID); err != nil {
|
||||
slog.Warn("failed to destroy sandbox during user disable",
|
||||
"sandbox_id", sb.ID, "team_id", teamID, "user_id", userID, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user