From the hacklab/T-HUB fork (diverged at a6843e9):
- 046_file_folders: folder trees (group + personal scope, RLS forced,
workspace-immutability trigger) + created_by on chat_media_archive
- Instagram connector wired into connect + publishing
- outreach contacts: per-collective DM tracking (todo/sent/replied/declined),
RLS forced, role-scoped permissions, audit without message text,
migration renumbered 041→047
- FORCE row-level security on media_assets
The J1 api agent's folder service/controller work continues separately.
51 lines
2.7 KiB
SQL
51 lines
2.7 KiB
SQL
-- 041: outreach — per-workspace list of public accounts to contact by hand.
|
|
--
|
|
-- The Hub never sends these messages. A person opens the contact's profile,
|
|
-- pastes the drafted text into the platform's own DM box and marks the row
|
|
-- sent; replies are marked the same way. Only public accounts belong here:
|
|
-- names and contact details of private persons stay out of the Hub.
|
|
|
|
CREATE TABLE outreach_contacts (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE ON UPDATE RESTRICT,
|
|
name text NOT NULL CHECK (char_length(name) BETWEEN 1 AND 200),
|
|
platform text NOT NULL CHECK (platform ~ '^[a-z0-9][a-z0-9-]{1,31}$'),
|
|
handle text NOT NULL CHECK (char_length(handle) BETWEEN 1 AND 200),
|
|
profile_url text NOT NULL CHECK (profile_url ~ '^https://[^\s]+$' AND char_length(profile_url) <= 500),
|
|
category text CHECK (category IS NULL OR char_length(category) <= 100),
|
|
language text CHECK (language IS NULL OR language ~ '^[a-z]{2,3}(-[A-Za-z0-9]{2,8})?$'),
|
|
message text NOT NULL DEFAULT '' CHECK (char_length(message) <= 4000),
|
|
notes text NOT NULL DEFAULT '' CHECK (char_length(notes) <= 2000),
|
|
status text NOT NULL DEFAULT 'todo' CHECK (status IN ('todo', 'sent', 'replied', 'declined')),
|
|
sent_at timestamptz,
|
|
sent_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
|
replied_at timestamptz,
|
|
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
UNIQUE (id, workspace_id)
|
|
);
|
|
|
|
CREATE UNIQUE INDEX outreach_contacts_account ON outreach_contacts (workspace_id, platform, lower(handle));
|
|
CREATE INDEX outreach_contacts_status ON outreach_contacts (workspace_id, status);
|
|
|
|
CREATE TRIGGER trg_outreach_contacts_ws_immutable BEFORE UPDATE ON outreach_contacts
|
|
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
|
|
|
|
ALTER TABLE outreach_contacts ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE outreach_contacts FORCE ROW LEVEL SECURITY;
|
|
CREATE POLICY tenant_isolation ON outreach_contacts
|
|
USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid)
|
|
WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid);
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON outreach_contacts TO sp_app;
|
|
|
|
-- Reads for anyone who already sees the inbox; edits for the roles that write drafts.
|
|
INSERT INTO role_permissions (role, permission)
|
|
SELECT 'Owner', 'outreach.view'
|
|
UNION ALL SELECT 'Admin', 'outreach.view'
|
|
UNION ALL SELECT 'Editor', 'outreach.view'
|
|
UNION ALL SELECT 'Moderator', 'outreach.view'
|
|
UNION ALL SELECT 'Owner', 'outreach.manage'
|
|
UNION ALL SELECT 'Admin', 'outreach.manage'
|
|
UNION ALL SELECT 'Editor', 'outreach.manage'
|
|
ON CONFLICT DO NOTHING;
|