127 lines
6.0 KiB
PL/PgSQL
127 lines
6.0 KiB
PL/PgSQL
CREATE TABLE collaboration_workspaces (
|
|
tenant_id bigint PRIMARY KEY REFERENCES tenants(id) ON DELETE CASCADE,
|
|
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
|
guests_enabled boolean NOT NULL DEFAULT true
|
|
);
|
|
|
|
CREATE TABLE collaboration_principals (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
kind text NOT NULL CHECK (kind IN ('user','guest','remote')),
|
|
tenant_id bigint REFERENCES tenants(id) ON DELETE CASCADE,
|
|
user_id uuid REFERENCES auth_users(id) ON DELETE CASCADE,
|
|
remote_origin text,
|
|
remote_actor_id uuid,
|
|
display_name text NOT NULL,
|
|
email text,
|
|
disabled boolean NOT NULL DEFAULT false,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
CHECK ((kind='user' AND tenant_id IS NOT NULL AND user_id IS NOT NULL AND remote_origin IS NULL)
|
|
OR (kind='guest' AND tenant_id IS NOT NULL AND user_id IS NULL AND email IS NOT NULL AND remote_origin IS NULL)
|
|
OR (kind='remote' AND tenant_id IS NULL AND user_id IS NULL AND remote_origin IS NOT NULL AND remote_actor_id IS NOT NULL))
|
|
);
|
|
CREATE UNIQUE INDEX collaboration_local_actor ON collaboration_principals(tenant_id,user_id) WHERE kind='user';
|
|
CREATE UNIQUE INDEX collaboration_guest_actor ON collaboration_principals(tenant_id,email) WHERE kind='guest';
|
|
CREATE UNIQUE INDEX collaboration_remote_actor ON collaboration_principals(remote_origin,remote_actor_id) WHERE kind='remote';
|
|
|
|
CREATE TABLE collaboration_rooms (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id bigint NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
project_id bigint REFERENCES projects(id) ON DELETE CASCADE,
|
|
name text NOT NULL CHECK (length(name) BETWEEN 1 AND 160),
|
|
topic text NOT NULL DEFAULT '',
|
|
shared_overview jsonb NOT NULL DEFAULT '{}',
|
|
status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','archived','blocked')),
|
|
sequence bigint NOT NULL DEFAULT 0,
|
|
created_by uuid NOT NULL REFERENCES auth_users(id),
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX collaboration_project_rooms ON collaboration_rooms(tenant_id,project_id);
|
|
|
|
CREATE TABLE collaboration_members (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
room_id uuid NOT NULL REFERENCES collaboration_rooms(id) ON DELETE CASCADE,
|
|
principal_id uuid NOT NULL REFERENCES collaboration_principals(id),
|
|
status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','revoked')),
|
|
role text NOT NULL DEFAULT 'participant' CHECK (role IN ('participant','reader')),
|
|
overview_allowed boolean NOT NULL DEFAULT false,
|
|
history_from_seq bigint NOT NULL,
|
|
last_read_seq bigint NOT NULL DEFAULT 0,
|
|
revision bigint NOT NULL DEFAULT 1,
|
|
expires_at timestamptz,
|
|
joined_at timestamptz NOT NULL DEFAULT now(),
|
|
revoked_at timestamptz,
|
|
UNIQUE(room_id,principal_id)
|
|
);
|
|
|
|
CREATE TABLE collaboration_messages (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
room_id uuid NOT NULL REFERENCES collaboration_rooms(id) ON DELETE CASCADE,
|
|
seq bigint NOT NULL,
|
|
principal_id uuid NOT NULL REFERENCES collaboration_principals(id),
|
|
client_id uuid NOT NULL,
|
|
body text NOT NULL CHECK (length(body) BETWEEN 1 AND 10000),
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
UNIQUE(room_id,seq),
|
|
UNIQUE(room_id,principal_id,client_id)
|
|
);
|
|
|
|
CREATE TABLE collaboration_invitations (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
room_id uuid NOT NULL REFERENCES collaboration_rooms(id) ON DELETE CASCADE,
|
|
target_principal_id uuid NOT NULL REFERENCES collaboration_principals(id),
|
|
token_hash text NOT NULL UNIQUE,
|
|
role text NOT NULL CHECK (role IN ('participant','reader')),
|
|
overview_allowed boolean NOT NULL DEFAULT false,
|
|
invited_by uuid NOT NULL REFERENCES auth_users(id),
|
|
status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','accepted','declined','revoked')),
|
|
expires_at timestamptz NOT NULL,
|
|
access_expires_at timestamptz,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
accepted_at timestamptz
|
|
);
|
|
CREATE INDEX collaboration_recipient_invitations ON collaboration_invitations(target_principal_id,status);
|
|
|
|
CREATE TABLE collaboration_guest_challenges (
|
|
id uuid PRIMARY KEY,
|
|
principal_id uuid NOT NULL REFERENCES collaboration_principals(id) ON DELETE CASCADE,
|
|
code_hash text NOT NULL,
|
|
attempts integer NOT NULL DEFAULT 0,
|
|
expires_at timestamptz NOT NULL,
|
|
consumed_at timestamptz
|
|
);
|
|
CREATE TABLE collaboration_guest_sessions (
|
|
token_hash text PRIMARY KEY,
|
|
principal_id uuid NOT NULL REFERENCES collaboration_principals(id) ON DELETE CASCADE,
|
|
expires_at timestamptz NOT NULL,
|
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX collaboration_guest_session_expiry ON collaboration_guest_sessions(expires_at);
|
|
CREATE TABLE collaboration_rate_limits (
|
|
key text PRIMARY KEY,
|
|
hits integer NOT NULL,
|
|
resets_at timestamptz NOT NULL
|
|
);
|
|
CREATE TABLE collaboration_audit (
|
|
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
tenant_id bigint NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
room_id uuid REFERENCES collaboration_rooms(id) ON DELETE SET NULL,
|
|
actor_id uuid REFERENCES collaboration_principals(id),
|
|
action text NOT NULL,
|
|
subject_id uuid,
|
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- This must also hold for direct SQL writes, not just the HTTP validation.
|
|
CREATE FUNCTION collaboration_check_project_tenant() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
IF NEW.project_id IS NOT NULL AND NOT EXISTS (
|
|
SELECT 1 FROM projects WHERE id=NEW.project_id AND tenant=NEW.tenant_id
|
|
) THEN RAISE EXCEPTION 'Project belongs to another tenant' USING ERRCODE='23514'; END IF;
|
|
IF TG_OP='UPDATE' AND (NEW.project_id IS DISTINCT FROM OLD.project_id OR NEW.tenant_id<>OLD.tenant_id)
|
|
THEN RAISE EXCEPTION 'Collaboration ownership is immutable' USING ERRCODE='23514'; END IF;
|
|
RETURN NEW;
|
|
END $$;
|
|
CREATE TRIGGER collaboration_project_tenant BEFORE INSERT OR UPDATE ON collaboration_rooms
|
|
FOR EACH ROW EXECUTE FUNCTION collaboration_check_project_tenant();
|