1
0
forked from wrenn/wrenn

Merge branch 'dev' into chore/hardening

This commit is contained in:
2026-04-16 12:58:48 +00:00
52 changed files with 795 additions and 708 deletions

View File

@ -25,6 +25,15 @@ func (q *Queries) DeleteAPIKey(ctx context.Context, arg DeleteAPIKeyParams) erro
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
DELETE FROM team_api_keys WHERE team_id = $1
`

View File

@ -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
`

View File

@ -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
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
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
`

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)
}
// Enqueue build ID (as formatted string) to Redis for workers to pick up.
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.
// Store archive before enqueue so the worker never dequeues without files.
if len(p.Archive) > 0 {
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
}

View File

@ -3,6 +3,7 @@ package service
import (
"context"
"fmt"
"log/slog"
"time"
"github.com/jackc/pgx/v5/pgtype"
@ -12,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.
@ -66,5 +68,40 @@ func (s *UserService) SetUserStatus(ctx context.Context, userID pgtype.UUID, sta
}); err != nil {
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)
}
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)
}
}
}
}