-- ========================================== -- Switchboard Core — 002 Teams & Access Control -- ========================================== CREATE TABLE IF NOT EXISTS teams ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(200) NOT NULL UNIQUE, description TEXT DEFAULT '', created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT, is_active BOOLEAN DEFAULT true, settings JSONB DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_teams_active ON teams(is_active) WHERE is_active = true; DROP TRIGGER IF EXISTS teams_updated_at ON teams; CREATE TRIGGER teams_updated_at BEFORE UPDATE ON teams FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TABLE IF NOT EXISTS team_members ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, role VARCHAR(50) NOT NULL DEFAULT 'member', joined_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(team_id, user_id) ); CREATE INDEX IF NOT EXISTS idx_team_members_team ON team_members(team_id); CREATE INDEX IF NOT EXISTS idx_team_members_user ON team_members(user_id); -- ── Groups ────────────────────────────────── CREATE TABLE IF NOT EXISTS groups ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(100) NOT NULL, description TEXT NOT NULL DEFAULT '', scope VARCHAR(20) NOT NULL DEFAULT 'global' CHECK (scope IN ('global', 'team')), team_id UUID REFERENCES teams(id) ON DELETE CASCADE, created_by UUID REFERENCES users(id), source VARCHAR(20) NOT NULL DEFAULT 'manual' CHECK (source IN ('manual', 'oidc', 'system')), permissions JSONB NOT NULL DEFAULT '[]'::jsonb, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT groups_scope_team CHECK ( (scope = 'global' AND team_id IS NULL) OR (scope = 'team' AND team_id IS NOT NULL) ) ); CREATE UNIQUE INDEX IF NOT EXISTS idx_groups_name_scope ON groups(name, COALESCE(team_id, '00000000-0000-0000-0000-000000000000')); DROP TRIGGER IF EXISTS groups_updated_at ON groups; CREATE TRIGGER groups_updated_at BEFORE UPDATE ON groups FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TABLE IF NOT EXISTS group_members ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, added_by UUID NOT NULL REFERENCES users(id), added_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(group_id, user_id) ); CREATE INDEX IF NOT EXISTS idx_group_members_group ON group_members(group_id); CREATE INDEX IF NOT EXISTS idx_group_members_user ON group_members(user_id); -- Seed Everyone group INSERT INTO groups (id, name, description, scope, created_by, source, permissions) VALUES ( '00000000-0000-0000-0000-000000000001', 'Everyone', 'Implicit group — all authenticated users receive these permissions.', 'global', NULL, 'system', '["extension.use","workflow.submit"]'::jsonb ) ON CONFLICT (id) DO NOTHING; -- Seed Admins group — members receive full platform permissions. -- Replaces the legacy users.role = 'admin' authorization check. -- Membership is managed by bootstrap/seed logic at runtime. INSERT INTO groups (id, name, description, scope, created_by, source, permissions) VALUES ( '00000000-0000-0000-0000-000000000002', 'Admins', 'Full platform access — replaces legacy admin role.', 'global', NULL, 'system', '["surface.admin.access","extension.use","extension.install","workflow.create","workflow.submit","admin.view","token.unlimited"]'::jsonb ) ON CONFLICT (id) DO NOTHING;