1
0
forked from wrenn/wrenn
This commit is contained in:
2026-04-16 19:24:25 +00:00
parent 172413e91e
commit 605ad666a0
239 changed files with 19966 additions and 3454 deletions

View File

@ -171,7 +171,7 @@ CREATE TABLE audit_logs (
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_audit_logs_team_time ON audit_logs(team_id, created_at DESC);
CREATE INDEX idx_audit_logs_team_time ON audit_logs(team_id, created_at DESC, id DESC);
CREATE INDEX idx_audit_logs_team_resource ON audit_logs(team_id, resource_type, created_at DESC);
-- sandbox_metrics_snapshots

View File

@ -0,0 +1,17 @@
-- +goose Up
ALTER TABLE templates
ADD COLUMN default_user TEXT NOT NULL DEFAULT 'root',
ADD COLUMN default_env JSONB NOT NULL DEFAULT '{}';
ALTER TABLE template_builds
ADD COLUMN default_user TEXT NOT NULL DEFAULT 'root',
ADD COLUMN default_env JSONB NOT NULL DEFAULT '{}';
-- +goose Down
ALTER TABLE template_builds
DROP COLUMN default_env,
DROP COLUMN default_user;
ALTER TABLE templates
DROP COLUMN default_env,
DROP COLUMN default_user;

View File

@ -0,0 +1,12 @@
-- +goose Up
-- Seed the platform team row. This is the sentinel team (all-zeros UUID) that
-- owns platform-wide resources: global templates, admin-created capsules, etc.
-- No user can become a member of this team — it exists solely to satisfy
-- foreign key constraints and to act as a namespace for platform resources.
INSERT INTO teams (id, name, slug)
VALUES ('00000000-0000-0000-0000-000000000000', 'Platform', 'platform')
ON CONFLICT (id) DO NOTHING;
-- +goose Down
DELETE FROM teams WHERE id = '00000000-0000-0000-0000-000000000000';

View File

@ -0,0 +1,7 @@
-- +goose Up
ALTER TABLE users ADD COLUMN is_active BOOLEAN NOT NULL DEFAULT TRUE;
ALTER TABLE users ADD COLUMN deleted_at TIMESTAMPTZ;
-- +goose Down
ALTER TABLE users DROP COLUMN deleted_at;
ALTER TABLE users DROP COLUMN is_active;

View File

@ -0,0 +1,9 @@
-- +goose Up
ALTER TABLE sandboxes ADD COLUMN metadata JSONB NOT NULL DEFAULT '{}';
ALTER TABLE templates ADD COLUMN metadata JSONB NOT NULL DEFAULT '{}';
ALTER TABLE template_builds ADD COLUMN metadata JSONB NOT NULL DEFAULT '{}';
-- +goose Down
ALTER TABLE sandboxes DROP COLUMN metadata;
ALTER TABLE templates DROP COLUMN metadata;
ALTER TABLE template_builds DROP COLUMN metadata;

View File

@ -0,0 +1,15 @@
-- +goose Up
ALTER TABLE users ADD COLUMN status TEXT NOT NULL DEFAULT 'active';
-- Backfill from existing columns.
UPDATE users SET status = 'deleted' WHERE deleted_at IS NOT NULL;
UPDATE users SET status = 'disabled' WHERE is_active = false AND deleted_at IS NULL;
ALTER TABLE users DROP COLUMN is_active;
-- +goose Down
ALTER TABLE users ADD COLUMN is_active BOOLEAN NOT NULL DEFAULT TRUE;
UPDATE users SET is_active = false WHERE status IN ('inactive', 'disabled', 'deleted');
ALTER TABLE users DROP COLUMN status;

View File

@ -0,0 +1,72 @@
-- +goose Up
-- users_teams: remove membership when user is deleted
ALTER TABLE users_teams DROP CONSTRAINT users_teams_user_id_fkey;
ALTER TABLE users_teams ADD CONSTRAINT users_teams_user_id_fkey
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
-- oauth_providers: remove auth links when user is deleted
ALTER TABLE oauth_providers DROP CONSTRAINT oauth_providers_user_id_fkey;
ALTER TABLE oauth_providers ADD CONSTRAINT oauth_providers_user_id_fkey
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
-- admin_permissions: remove permissions when user is deleted
ALTER TABLE admin_permissions DROP CONSTRAINT admin_permissions_user_id_fkey;
ALTER TABLE admin_permissions ADD CONSTRAINT admin_permissions_user_id_fkey
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
-- team_api_keys.created_by: make nullable, SET NULL on user delete
ALTER TABLE team_api_keys ALTER COLUMN created_by DROP NOT NULL;
ALTER TABLE team_api_keys DROP CONSTRAINT team_api_keys_created_by_fkey;
ALTER TABLE team_api_keys ADD CONSTRAINT team_api_keys_created_by_fkey
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL;
-- hosts.created_by: make nullable, SET NULL on user delete
ALTER TABLE hosts ALTER COLUMN created_by DROP NOT NULL;
ALTER TABLE hosts DROP CONSTRAINT hosts_created_by_fkey;
ALTER TABLE hosts ADD CONSTRAINT hosts_created_by_fkey
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL;
-- host_tokens.created_by: make nullable, SET NULL on user delete
ALTER TABLE host_tokens ALTER COLUMN created_by DROP NOT NULL;
ALTER TABLE host_tokens DROP CONSTRAINT host_tokens_created_by_fkey;
ALTER TABLE host_tokens ADD CONSTRAINT host_tokens_created_by_fkey
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL;
-- +goose Down
-- Revert host_tokens.created_by
ALTER TABLE host_tokens DROP CONSTRAINT host_tokens_created_by_fkey;
UPDATE host_tokens SET created_by = '00000000-0000-0000-0000-000000000000' WHERE created_by IS NULL;
ALTER TABLE host_tokens ALTER COLUMN created_by SET NOT NULL;
ALTER TABLE host_tokens ADD CONSTRAINT host_tokens_created_by_fkey
FOREIGN KEY (created_by) REFERENCES users(id);
-- Revert hosts.created_by
ALTER TABLE hosts DROP CONSTRAINT hosts_created_by_fkey;
UPDATE hosts SET created_by = '00000000-0000-0000-0000-000000000000' WHERE created_by IS NULL;
ALTER TABLE hosts ALTER COLUMN created_by SET NOT NULL;
ALTER TABLE hosts ADD CONSTRAINT hosts_created_by_fkey
FOREIGN KEY (created_by) REFERENCES users(id);
-- Revert team_api_keys.created_by
ALTER TABLE team_api_keys DROP CONSTRAINT team_api_keys_created_by_fkey;
UPDATE team_api_keys SET created_by = '00000000-0000-0000-0000-000000000000' WHERE created_by IS NULL;
ALTER TABLE team_api_keys ALTER COLUMN created_by SET NOT NULL;
ALTER TABLE team_api_keys ADD CONSTRAINT team_api_keys_created_by_fkey
FOREIGN KEY (created_by) REFERENCES users(id);
-- Revert admin_permissions
ALTER TABLE admin_permissions DROP CONSTRAINT admin_permissions_user_id_fkey;
ALTER TABLE admin_permissions ADD CONSTRAINT admin_permissions_user_id_fkey
FOREIGN KEY (user_id) REFERENCES users(id);
-- Revert oauth_providers
ALTER TABLE oauth_providers DROP CONSTRAINT oauth_providers_user_id_fkey;
ALTER TABLE oauth_providers ADD CONSTRAINT oauth_providers_user_id_fkey
FOREIGN KEY (user_id) REFERENCES users(id);
-- Revert users_teams
ALTER TABLE users_teams DROP CONSTRAINT users_teams_user_id_fkey;
ALTER TABLE users_teams ADD CONSTRAINT users_teams_user_id_fkey
FOREIGN KEY (user_id) REFERENCES users(id);

10
db/migrations/embed.go Normal file
View File

@ -0,0 +1,10 @@
// Package migrations embeds the SQL migration files so that external modules
// (such as the enterprise edition) can access them programmatically.
package migrations
import "embed"
// FS contains all SQL migration files.
//
//go:embed *.sql
var FS embed.FS

View File

@ -13,7 +13,7 @@ SELECT * FROM team_api_keys WHERE team_id = $1 ORDER BY created_at DESC;
SELECT k.id, k.team_id, k.name, k.key_hash, k.key_prefix, k.created_by, k.created_at, k.last_used,
u.email AS creator_email
FROM team_api_keys k
JOIN users u ON u.id = k.created_by
LEFT JOIN users u ON u.id = k.created_by
WHERE k.team_id = $1
ORDER BY k.created_at DESC;
@ -22,3 +22,9 @@ DELETE FROM team_api_keys WHERE id = $1 AND team_id = $2;
-- name: UpdateAPIKeyLastUsed :exec
UPDATE team_api_keys SET last_used = NOW() WHERE id = $1;
-- name: DeleteAPIKeysByTeam :exec
DELETE FROM team_api_keys WHERE team_id = $1;
-- name: DeleteAPIKeysByCreator :exec
DELETE FROM team_api_keys WHERE created_by = $1;

View File

@ -22,6 +22,9 @@ RETURNING *;
-- name: DeleteChannelByTeam :exec
DELETE FROM channels WHERE id = $1 AND team_id = $2;
-- name: DeleteAllChannelsByTeam :exec
DELETE FROM channels WHERE team_id = $1;
-- name: ListChannelsForEvent :many
SELECT * FROM channels
WHERE team_id = $1

View File

@ -81,6 +81,41 @@ SELECT * FROM hosts WHERE id = $1 AND team_id = $2;
-- Returns all hosts that have completed registration (not pending/offline).
SELECT * FROM hosts WHERE status NOT IN ('pending', 'offline') ORDER BY created_at;
-- name: GetHostsWithLoad :many
-- Returns all online hosts with raw per-host sandbox resource consumption.
-- Separates running and paused sandbox totals so the caller can apply its own formulas.
SELECT
h.id,
h.type,
h.team_id,
h.provider,
h.availability_zone,
h.arch,
h.cpu_cores,
h.memory_mb,
h.disk_gb,
h.address,
h.status,
h.last_heartbeat_at,
h.metadata,
h.created_by,
h.created_at,
h.updated_at,
h.cert_fingerprint,
h.cert_expires_at,
COALESCE(SUM(s.vcpus) FILTER (WHERE s.status IN ('running', 'starting', 'pending')), 0)::int AS running_vcpus,
COALESCE(SUM(s.memory_mb) FILTER (WHERE s.status IN ('running', 'starting', 'pending')), 0)::int AS running_memory_mb,
COALESCE(SUM(s.disk_size_mb) FILTER (WHERE s.status IN ('running', 'starting', 'pending')), 0)::int AS running_disk_mb,
COALESCE(SUM(s.memory_mb) FILTER (WHERE s.status = 'paused'), 0)::int AS paused_memory_mb,
COALESCE(SUM(s.disk_size_mb) FILTER (WHERE s.status = 'paused'), 0)::int AS paused_disk_mb
FROM hosts h
LEFT JOIN sandboxes s ON s.host_id = h.id
AND s.status IN ('running', 'paused', 'starting', 'pending')
WHERE h.status = 'online'
AND h.address != ''
GROUP BY h.id
ORDER BY h.created_at;
-- name: UpdateHostHeartbeatAndStatus :execrows
-- Updates last_heartbeat_at and transitions unreachable hosts back to online.
-- Returns 0 if no host was found (deleted), which the caller treats as 404.

View File

@ -51,6 +51,13 @@ WHERE sandbox_id = $1 AND tier = $2;
DELETE FROM sandbox_metric_points
WHERE ts < EXTRACT(EPOCH FROM NOW() - INTERVAL '30 days')::BIGINT;
-- name: DeleteMetricsSnapshotsByTeam :exec
DELETE FROM sandbox_metrics_snapshots WHERE team_id = $1;
-- name: DeleteMetricPointsByTeam :exec
DELETE FROM sandbox_metric_points
WHERE sandbox_id IN (SELECT id FROM sandboxes WHERE team_id = $1);
-- name: SampleSandboxMetrics :many
-- Aggregates per-team resource usage from the live sandboxes table.
-- Groups by all teams that have any sandbox row (including stopped) so that

View File

@ -5,3 +5,9 @@ VALUES ($1, $2, $3, $4);
-- name: GetOAuthProvider :one
SELECT * FROM oauth_providers
WHERE provider = $1 AND provider_id = $2;
-- name: GetOAuthProvidersByUserID :many
SELECT * FROM oauth_providers WHERE user_id = $1;
-- name: DeleteOAuthProvider :exec
DELETE FROM oauth_providers WHERE user_id = $1 AND provider = $2;

View File

@ -1,6 +1,6 @@
-- name: InsertSandbox :one
INSERT INTO sandboxes (id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, template_id, template_team_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
INSERT INTO sandboxes (id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, template_id, template_team_id, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
RETURNING *;
-- name: GetSandbox :one
@ -15,7 +15,7 @@ SELECT * FROM sandboxes WHERE id = $1 AND team_id = $2;
SELECT s.status, h.address AS host_address
FROM sandboxes s
JOIN hosts h ON h.id = s.host_id
WHERE s.id = $1 AND s.team_id = $2;
WHERE s.id = $1;
-- name: ListSandboxes :many
SELECT * FROM sandboxes ORDER BY created_at DESC;
@ -62,7 +62,7 @@ WHERE id = ANY($1::uuid[]);
-- name: ListActiveSandboxesByTeam :many
SELECT * FROM sandboxes
WHERE team_id = $1 AND status IN ('running', 'paused', 'starting')
WHERE team_id = $1 AND status IN ('running', 'paused', 'starting', 'hibernated')
ORDER BY created_at DESC;
-- name: MarkSandboxesMissingByHost :exec
@ -74,6 +74,12 @@ SET status = 'missing',
last_updated = NOW()
WHERE host_id = $1 AND status IN ('running', 'starting', 'pending');
-- name: UpdateSandboxMetadata :exec
UPDATE sandboxes
SET metadata = $2,
last_updated = NOW()
WHERE id = $1;
-- name: BulkRestoreRunning :exec
-- Called by the reconciler when a host comes back online and its sandboxes are
-- confirmed alive. Restores only sandboxes that are in 'missing' state.

View File

@ -53,3 +53,48 @@ UPDATE users_teams SET role = $3 WHERE team_id = $1 AND user_id = $2;
-- name: DeleteTeamMember :exec
DELETE FROM users_teams WHERE team_id = $1 AND user_id = $2;
-- name: ListTeamsAdmin :many
SELECT
t.id,
t.name,
t.slug,
t.is_byoc,
t.created_at,
t.deleted_at,
(SELECT COUNT(*) FROM users_teams ut WHERE ut.team_id = t.id)::int AS member_count,
COALESCE(owner_u.name, '') AS owner_name,
COALESCE(owner_u.email, '') AS owner_email,
(SELECT COUNT(*) FROM sandboxes s WHERE s.team_id = t.id AND s.status IN ('running', 'paused', 'starting'))::int AS active_sandbox_count,
(SELECT COUNT(*) FROM channels c WHERE c.team_id = t.id)::int AS channel_count
FROM teams t
LEFT JOIN users_teams owner_ut ON owner_ut.team_id = t.id AND owner_ut.role = 'owner'
LEFT JOIN users owner_u ON owner_u.id = owner_ut.user_id
WHERE t.id != '00000000-0000-0000-0000-000000000000'
ORDER BY t.deleted_at ASC NULLS FIRST, t.created_at DESC
LIMIT $1 OFFSET $2;
-- name: ListSoleOwnedTeams :many
-- Returns teams where the user is the owner and no other members exist.
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
AND NOT EXISTS (
SELECT 1 FROM users_teams ut2
WHERE ut2.team_id = t.id AND ut2.user_id <> $1
);
-- name: GetOwnedTeamIDs :many
-- Returns team IDs where the given user has the 'owner' role.
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;
-- name: CountTeamsAdmin :one
SELECT COUNT(*)::int AS total
FROM teams
WHERE id != '00000000-0000-0000-0000-000000000000';

View File

@ -31,3 +31,8 @@ WHERE id = $1;
UPDATE template_builds
SET error = $2, status = 'failed', completed_at = NOW()
WHERE id = $1;
-- name: UpdateBuildDefaults :exec
UPDATE template_builds
SET default_user = $2, default_env = $3, metadata = $4
WHERE id = $1;

View File

@ -1,6 +1,6 @@
-- name: InsertTemplate :one
INSERT INTO templates (id, name, type, vcpus, memory_mb, size_bytes, team_id)
VALUES ($1, $2, $3, $4, $5, $6, $7)
INSERT INTO templates (id, name, type, vcpus, memory_mb, size_bytes, team_id, default_user, default_env, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING *;
-- name: GetTemplate :one

View File

@ -4,16 +4,21 @@ VALUES ($1, $2, $3, $4)
RETURNING *;
-- name: GetUserByEmail :one
SELECT * FROM users WHERE email = $1;
SELECT * FROM users WHERE email = $1 AND status != 'deleted';
-- name: GetUserByID :one
SELECT * FROM users WHERE id = $1;
SELECT * FROM users WHERE id = $1 AND status != 'deleted';
-- name: InsertUserOAuth :one
INSERT INTO users (id, email, name)
VALUES ($1, $2, $3)
RETURNING *;
-- name: InsertUserInactive :one
INSERT INTO users (id, email, password_hash, name, status)
VALUES ($1, $2, $3, $4, 'inactive')
RETURNING *;
-- name: SetUserAdmin :exec
UPDATE users SET is_admin = $2, updated_at = NOW() WHERE id = $1;
@ -35,8 +40,59 @@ SELECT EXISTS(
SELECT 1 FROM admin_permissions WHERE user_id = $1 AND permission = $2
) AS has_permission;
-- name: CountUsers :one
SELECT COUNT(*) FROM users;
-- name: CountActiveUsers :one
SELECT COUNT(*) FROM users WHERE status = 'active';
-- name: SearchUsersByEmailPrefix :many
SELECT id, email FROM users WHERE email LIKE $1 || '%' ORDER BY email LIMIT 10;
-- name: UpdateUserName :exec
UPDATE users SET name = $2, updated_at = NOW() WHERE id = $1;
-- name: ListUsersAdmin :many
SELECT
u.id,
u.email,
u.name,
u.is_admin,
u.status,
u.created_at,
(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.status != 'deleted'
ORDER BY u.created_at DESC
LIMIT $1 OFFSET $2;
-- name: CountUsersAdmin :one
SELECT COUNT(*)::int AS total
FROM users
WHERE status != 'deleted';
-- name: SetUserStatus :exec
UPDATE users SET status = $2, updated_at = NOW() WHERE id = $1;
-- name: UpdateUserPassword :exec
UPDATE users SET password_hash = $2, updated_at = NOW() WHERE id = $1;
-- name: SoftDeleteUser :exec
UPDATE users SET deleted_at = NOW(), status = 'deleted', updated_at = NOW() WHERE id = $1;
-- name: CountUserOwnedTeamsWithOtherMembers :one
SELECT COUNT(DISTINCT ut.team_id)::int
FROM users_teams ut
WHERE ut.user_id = $1
AND ut.role = 'owner'
AND EXISTS (
SELECT 1 FROM users_teams ut2
WHERE ut2.team_id = ut.team_id AND ut2.user_id <> $1
);
-- name: HardDeleteExpiredUsers :exec
DELETE FROM users WHERE deleted_at IS NOT NULL AND deleted_at < NOW() - INTERVAL '15 days';
-- name: HardDeleteUser :exec
DELETE FROM users WHERE id = $1;