Establish project-scoped account and collaboration schema
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
BEGIN;
|
||||
|
||||
-- Keep existing Google accounts and sessions valid during the expansion.
|
||||
ALTER TABLE project_accounts ALTER COLUMN google_subject DROP NOT NULL;
|
||||
ALTER TABLE project_accounts
|
||||
ADD COLUMN username text,
|
||||
ADD COLUMN display_name text NOT NULL DEFAULT '',
|
||||
ADD COLUMN avatar_url text,
|
||||
ADD COLUMN created_at timestamptz NOT NULL DEFAULT now(),
|
||||
ADD COLUMN updated_at timestamptz NOT NULL DEFAULT now();
|
||||
CREATE UNIQUE INDEX project_account_username ON project_accounts(project, lower(username))
|
||||
WHERE username IS NOT NULL;
|
||||
|
||||
CREATE TABLE account_identities (
|
||||
project text NOT NULL,
|
||||
provider text NOT NULL CHECK (provider IN ('google', 'slack')),
|
||||
subject text NOT NULL,
|
||||
account_id uuid NOT NULL,
|
||||
PRIMARY KEY (project, provider, subject),
|
||||
UNIQUE (project, provider, account_id),
|
||||
FOREIGN KEY (project, account_id) REFERENCES project_accounts ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO account_identities(project, provider, subject, account_id)
|
||||
SELECT project, 'google', google_subject, id FROM project_accounts WHERE google_subject IS NOT NULL;
|
||||
|
||||
CREATE TABLE email_credentials (
|
||||
project text NOT NULL,
|
||||
email text NOT NULL CHECK (email = lower(email)),
|
||||
account_id uuid NOT NULL,
|
||||
password_hash text NOT NULL,
|
||||
verified_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (project, email),
|
||||
UNIQUE (project, account_id),
|
||||
FOREIGN KEY (project, account_id) REFERENCES project_accounts ON DELETE CASCADE
|
||||
);
|
||||
-- Registration is promoted to an account only after the single-use email check.
|
||||
CREATE TABLE email_registrations (
|
||||
project text NOT NULL,
|
||||
email text NOT NULL,
|
||||
password_hash text NOT NULL,
|
||||
token_hash text NOT NULL UNIQUE,
|
||||
expires_at timestamptz NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (project, email)
|
||||
);
|
||||
CREATE TABLE email_outbox (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project text NOT NULL,
|
||||
recipient text NOT NULL,
|
||||
subject text NOT NULL,
|
||||
body text NOT NULL,
|
||||
attempts integer NOT NULL DEFAULT 0,
|
||||
available_at timestamptz NOT NULL DEFAULT now(),
|
||||
lease_until timestamptz,
|
||||
sent_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX email_outbox_pending ON email_outbox(available_at) WHERE sent_at IS NULL;
|
||||
CREATE TABLE auth_rate_limits (
|
||||
key_hash text PRIMARY KEY,
|
||||
attempts integer NOT NULL CHECK (attempts > 0),
|
||||
expires_at timestamptz NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE onboarding_configs (
|
||||
project text PRIMARY KEY,
|
||||
require_workspace boolean NOT NULL DEFAULT false,
|
||||
questions jsonb NOT NULL DEFAULT '[]' CHECK (jsonb_typeof(questions) = 'array'),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE TABLE onboarding_responses (
|
||||
project text NOT NULL,
|
||||
account_id uuid NOT NULL,
|
||||
answers jsonb NOT NULL DEFAULT '{}' CHECK (jsonb_typeof(answers) = 'object'),
|
||||
completed_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (project, account_id),
|
||||
FOREIGN KEY (project, account_id) REFERENCES project_accounts ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE workspaces (
|
||||
project text NOT NULL,
|
||||
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||
name text NOT NULL CHECK (length(trim(name)) BETWEEN 1 AND 160),
|
||||
owner_id uuid NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (project, id),
|
||||
FOREIGN KEY (project, owner_id) REFERENCES project_accounts
|
||||
);
|
||||
CREATE TABLE workspace_members (
|
||||
project text NOT NULL,
|
||||
workspace_id uuid NOT NULL,
|
||||
account_id uuid NOT NULL,
|
||||
role text NOT NULL CHECK (role IN ('admin', 'editor', 'viewer')),
|
||||
joined_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (project, workspace_id, account_id),
|
||||
FOREIGN KEY (project, workspace_id) REFERENCES workspaces ON DELETE CASCADE,
|
||||
FOREIGN KEY (project, account_id) REFERENCES project_accounts ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX workspace_members_account ON workspace_members(project, account_id);
|
||||
|
||||
CREATE TABLE materials (
|
||||
project text NOT NULL,
|
||||
workspace_id uuid NOT NULL,
|
||||
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||
kind text NOT NULL CHECK (kind IN ('text', 'article', 'image', 'board', 'card')),
|
||||
title text NOT NULL CHECK (length(title) <= 300),
|
||||
description text NOT NULL DEFAULT '' CHECK (length(description) <= 10000),
|
||||
content jsonb NOT NULL DEFAULT '{}' CHECK (jsonb_typeof(content) = 'object'),
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
|
||||
created_by uuid NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
modified_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (project, workspace_id, id),
|
||||
FOREIGN KEY (project, workspace_id) REFERENCES workspaces ON DELETE CASCADE,
|
||||
FOREIGN KEY (project, created_by) REFERENCES project_accounts
|
||||
);
|
||||
CREATE INDEX material_modified ON materials(project, workspace_id, modified_at DESC, id);
|
||||
CREATE INDEX material_created ON materials(project, workspace_id, created_at DESC, id);
|
||||
CREATE INDEX material_alphabetical ON materials(project, workspace_id, lower(title), id);
|
||||
CREATE TABLE material_opens (
|
||||
project text NOT NULL,
|
||||
workspace_id uuid NOT NULL,
|
||||
material_id uuid NOT NULL,
|
||||
account_id uuid NOT NULL,
|
||||
opened_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (project, workspace_id, material_id, account_id),
|
||||
FOREIGN KEY (project, workspace_id, material_id) REFERENCES materials ON DELETE CASCADE,
|
||||
FOREIGN KEY (project, account_id) REFERENCES project_accounts ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX material_opens_account ON material_opens(project, workspace_id, account_id, opened_at DESC);
|
||||
|
||||
CREATE TABLE tags (
|
||||
project text NOT NULL,
|
||||
workspace_id uuid NOT NULL,
|
||||
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||
name text NOT NULL CHECK (length(trim(name)) BETWEEN 1 AND 80),
|
||||
color text NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (project, workspace_id, id),
|
||||
FOREIGN KEY (project, workspace_id) REFERENCES workspaces ON DELETE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX tag_name ON tags(project, workspace_id, lower(name));
|
||||
CREATE TABLE material_tags (
|
||||
project text NOT NULL,
|
||||
workspace_id uuid NOT NULL,
|
||||
material_id uuid NOT NULL,
|
||||
tag_id uuid NOT NULL,
|
||||
PRIMARY KEY (project, workspace_id, material_id, tag_id),
|
||||
FOREIGN KEY (project, workspace_id, material_id) REFERENCES materials ON DELETE CASCADE,
|
||||
FOREIGN KEY (project, workspace_id, tag_id) REFERENCES tags ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX material_tags_filter ON material_tags(project, workspace_id, tag_id, material_id);
|
||||
|
||||
-- A board placement references a reusable material, independently of its content.
|
||||
CREATE TABLE board_layouts (
|
||||
project text NOT NULL,
|
||||
workspace_id uuid NOT NULL,
|
||||
board_id uuid NOT NULL,
|
||||
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||
material_id uuid NOT NULL,
|
||||
x double precision NOT NULL CHECK (x BETWEEN -1000000 AND 1000000),
|
||||
y double precision NOT NULL CHECK (y BETWEEN -1000000 AND 1000000),
|
||||
width double precision NOT NULL CHECK (width BETWEEN 1 AND 100000),
|
||||
height double precision NOT NULL CHECK (height BETWEEN 1 AND 100000),
|
||||
z_index integer NOT NULL DEFAULT 0,
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
|
||||
PRIMARY KEY (project, workspace_id, board_id, id),
|
||||
FOREIGN KEY (project, workspace_id, board_id) REFERENCES materials ON DELETE CASCADE,
|
||||
FOREIGN KEY (project, workspace_id, material_id) REFERENCES materials ON DELETE CASCADE,
|
||||
CHECK (board_id <> material_id)
|
||||
);
|
||||
CREATE TABLE material_grants (
|
||||
project text NOT NULL,
|
||||
workspace_id uuid NOT NULL,
|
||||
material_id uuid NOT NULL,
|
||||
account_id uuid NOT NULL,
|
||||
role text NOT NULL CHECK (role IN ('editor', 'viewer')),
|
||||
PRIMARY KEY (project, workspace_id, material_id, account_id),
|
||||
FOREIGN KEY (project, workspace_id, material_id) REFERENCES materials ON DELETE CASCADE,
|
||||
FOREIGN KEY (project, account_id) REFERENCES project_accounts ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE share_links (
|
||||
project text NOT NULL,
|
||||
workspace_id uuid NOT NULL,
|
||||
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||
material_id uuid,
|
||||
token_hash text NOT NULL UNIQUE,
|
||||
role text NOT NULL CHECK (role IN ('admin', 'editor', 'viewer')),
|
||||
created_by uuid NOT NULL,
|
||||
expires_at timestamptz NOT NULL,
|
||||
revoked_at timestamptz,
|
||||
max_uses integer CHECK (max_uses BETWEEN 1 AND 100000),
|
||||
use_count integer NOT NULL DEFAULT 0 CHECK (use_count >= 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (project, workspace_id, id),
|
||||
FOREIGN KEY (project, workspace_id) REFERENCES workspaces ON DELETE CASCADE,
|
||||
FOREIGN KEY (project, workspace_id, material_id) REFERENCES materials ON DELETE CASCADE,
|
||||
FOREIGN KEY (project, created_by) REFERENCES project_accounts,
|
||||
CHECK (material_id IS NULL OR role <> 'admin')
|
||||
);
|
||||
CREATE TABLE share_redemptions (
|
||||
project text NOT NULL,
|
||||
workspace_id uuid NOT NULL,
|
||||
link_id uuid NOT NULL,
|
||||
account_id uuid NOT NULL,
|
||||
redeemed_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (project, workspace_id, link_id, account_id),
|
||||
FOREIGN KEY (project, workspace_id, link_id) REFERENCES share_links ON DELETE CASCADE,
|
||||
FOREIGN KEY (project, account_id) REFERENCES project_accounts ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE link_clicks (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
project text NOT NULL,
|
||||
workspace_id uuid NOT NULL,
|
||||
link_id uuid NOT NULL,
|
||||
clicked_at timestamptz NOT NULL DEFAULT now(),
|
||||
-- Store the referring origin only, without URL paths, queries or user tokens.
|
||||
referrer_origin text NOT NULL DEFAULT '',
|
||||
FOREIGN KEY (project, workspace_id, link_id) REFERENCES share_links ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX link_clicks_summary ON link_clicks(project, workspace_id, link_id, clicked_at);
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user