forked from wrenn/wrenn
Add team management endpoints
- Three-role model (owner/admin/member) with owner protection invariants - Team CRUD: create, rename (admin+), soft-delete with VM cleanup (owner only) - Member management: add by email, remove, role updates (admin+), leave - Switch-team endpoint re-issues JWT after DB membership verification - User email prefix search for add-member UI autocomplete - JWT carries role as a hint; all authorization decisions verified from DB - Team slug: immutable 12-char hex (e.g. a1b2c3-d1e2f3), reserved on soft-delete - Migration adds slug + deleted_at to teams; backfills existing rows
This commit is contained in:
17
db/migrations/20260324071453_team_management.sql
Normal file
17
db/migrations/20260324071453_team_management.sql
Normal file
@ -0,0 +1,17 @@
|
||||
-- +goose Up
|
||||
|
||||
ALTER TABLE teams ADD COLUMN slug TEXT;
|
||||
ALTER TABLE teams ADD COLUMN deleted_at TIMESTAMPTZ;
|
||||
|
||||
-- Backfill slugs for existing teams using MD5 of their ID.
|
||||
-- MD5 returns 32 hex chars; take chars 1-6 and 7-12 to form a 6-6 slug.
|
||||
UPDATE teams SET slug = LEFT(MD5(id), 6) || '-' || SUBSTRING(MD5(id), 7, 6);
|
||||
|
||||
ALTER TABLE teams ALTER COLUMN slug SET NOT NULL;
|
||||
CREATE UNIQUE INDEX idx_teams_slug ON teams(slug);
|
||||
|
||||
-- +goose Down
|
||||
|
||||
DROP INDEX idx_teams_slug;
|
||||
ALTER TABLE teams DROP COLUMN deleted_at;
|
||||
ALTER TABLE teams DROP COLUMN slug;
|
||||
@ -51,3 +51,8 @@ UPDATE sandboxes
|
||||
SET status = $2,
|
||||
last_updated = NOW()
|
||||
WHERE id = ANY($1::text[]);
|
||||
|
||||
-- name: ListActiveSandboxesByTeam :many
|
||||
SELECT * FROM sandboxes
|
||||
WHERE team_id = $1 AND status IN ('running', 'paused', 'starting')
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
-- name: InsertTeam :one
|
||||
INSERT INTO teams (id, name)
|
||||
VALUES ($1, $2)
|
||||
INSERT INTO teams (id, name, slug)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetTeam :one
|
||||
@ -24,3 +24,32 @@ SELECT * FROM teams WHERE is_byoc = TRUE ORDER BY created_at;
|
||||
|
||||
-- name: GetTeamMembership :one
|
||||
SELECT * FROM users_teams WHERE user_id = $1 AND team_id = $2;
|
||||
|
||||
-- name: UpdateTeamName :exec
|
||||
UPDATE teams SET name = $2 WHERE id = $1 AND deleted_at IS NULL;
|
||||
|
||||
-- name: SoftDeleteTeam :exec
|
||||
UPDATE teams SET deleted_at = NOW() WHERE id = $1;
|
||||
|
||||
-- name: GetTeamBySlug :one
|
||||
SELECT * FROM teams WHERE slug = $1 AND deleted_at IS NULL;
|
||||
|
||||
-- name: GetTeamsForUser :many
|
||||
SELECT t.id, t.name, t.slug, t.is_byoc, t.created_at, t.deleted_at, ut.role
|
||||
FROM teams t
|
||||
JOIN users_teams ut ON ut.team_id = t.id
|
||||
WHERE ut.user_id = $1 AND t.deleted_at IS NULL
|
||||
ORDER BY ut.created_at;
|
||||
|
||||
-- name: GetTeamMembers :many
|
||||
SELECT u.id, u.email, ut.role, ut.created_at AS joined_at
|
||||
FROM users_teams ut
|
||||
JOIN users u ON u.id = ut.user_id
|
||||
WHERE ut.team_id = $1
|
||||
ORDER BY ut.created_at;
|
||||
|
||||
-- name: UpdateMemberRole :exec
|
||||
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;
|
||||
|
||||
@ -34,3 +34,6 @@ SELECT * FROM admin_permissions WHERE user_id = $1 ORDER BY permission;
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM admin_permissions WHERE user_id = $1 AND permission = $2
|
||||
) AS has_permission;
|
||||
|
||||
-- name: SearchUsersByEmailPrefix :many
|
||||
SELECT id, email FROM users WHERE email LIKE $1 || '%' ORDER BY email LIMIT 10;
|
||||
|
||||
Reference in New Issue
Block a user