T-HUB/apps/api/migrations/046_file_folders.sql
glass beadian b24ebc5862 fix: logic-audit P0/P1 wave — CORS PUT, chunked cap, presign checksums, logout await, refresh rotation race
From the logic audit (HEAD 04f5f7e):
- P0 CORS: enableCors methods lacked PUT — browsers could not chunk-upload
  at all (cross-origin preflight refused)
- P0 chunked: validateUpload enforced the 2 GiB whole-body cap on chunked
  completes, so every >2 GiB chunked upload failed after full upload
- P1 presignPut: the SDK's flexible-checksum default added an
  x-amz-checksum-crc32 header browsers cannot reproduce; S3Client now uses
  requestChecksumCalculation WHEN_REQUIRED
- P1 refresh rotation race: concurrent refreshes both succeeded (consume
  ignored rowCount) forking the token family; consume now returns whether
  it won and the loser revokes the chain
- P2 logout: awaits the revocation and ignores missing/forged bearers
honestly
2026-10-18 10:18:02 +00:00

65 lines
3.5 KiB
PL/PgSQL

-- Folder trees and personal files are metadata over the existing payload keys.
CREATE TABLE file_folders (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
group_id uuid NULL REFERENCES groups(id) ON DELETE CASCADE,
owner_id uuid NULL REFERENCES users(id) ON DELETE CASCADE,
parent_id uuid NULL REFERENCES file_folders(id) ON DELETE CASCADE,
name text NOT NULL CHECK (btrim(name) <> '' AND char_length(name) <= 255),
created_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT file_folders_scope CHECK ((group_id IS NULL) <> (owner_id IS NULL)),
UNIQUE (id, workspace_id),
FOREIGN KEY (group_id, workspace_id) REFERENCES groups(id, workspace_id) ON DELETE CASCADE
);
CREATE TRIGGER trg_file_folders_ws_immutable BEFORE UPDATE ON file_folders
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
ALTER TABLE file_folders ENABLE ROW LEVEL SECURITY;
ALTER TABLE file_folders FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON file_folders
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 file_folders TO sp_app;
ALTER TABLE chat_media_archive
ALTER COLUMN group_id DROP NOT NULL,
ADD COLUMN folder_id uuid NULL REFERENCES file_folders(id) ON DELETE SET NULL,
ADD COLUMN created_by uuid NULL,
-- Required even at the personal root, which has no folder from which to infer ownership.
ADD COLUMN owner_id uuid NULL REFERENCES users(id) ON DELETE CASCADE,
ADD CONSTRAINT chat_media_archive_scope CHECK ((group_id IS NULL) <> (owner_id IS NULL));
-- 045 may already have introduced this setting.
ALTER TABLE groups ADD COLUMN IF NOT EXISTS member_delete_own boolean NOT NULL DEFAULT true;
-- Only exact, known identities are mapped. New authenticated writes supply
-- created_by; Matrix ingest also resolves the configured homeserver in code.
UPDATE chat_media_archive a SET created_by = (
SELECT u.id FROM users u
WHERE (a.sender_mxid = '@' || u.username || ':tmbh.local' OR a.sender_mxid = u.external_id)
AND EXISTS (SELECT 1 FROM memberships m WHERE m.workspace_id = a.workspace_id AND m.user_id = u.id)
ORDER BY (a.sender_mxid = u.external_id) DESC, u.id LIMIT 1
) WHERE a.created_by IS NULL;
-- Other ingest clients (including archive-bot) write this table directly.
-- Keep exact known sender mappings attributed on those inserts too. Unknown
-- remote identities stay NULL, so they never acquire member delete rights.
CREATE FUNCTION archive_file_creator() RETURNS trigger
LANGUAGE plpgsql SET search_path = public AS $$
BEGIN
IF NEW.created_by IS NULL THEN
SELECT u.id INTO NEW.created_by FROM users u
WHERE (NEW.sender_mxid = '@' || u.username || ':tmbh.local' OR NEW.sender_mxid = u.external_id)
AND EXISTS (SELECT 1 FROM memberships m WHERE m.workspace_id = NEW.workspace_id AND m.user_id = u.id)
ORDER BY (NEW.sender_mxid = u.external_id) DESC, u.id LIMIT 1;
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_chat_media_archive_creator BEFORE INSERT ON chat_media_archive
FOR EACH ROW EXECUTE FUNCTION archive_file_creator();
CREATE INDEX file_folders_scope_parent_idx ON file_folders (workspace_id, group_id, owner_id, parent_id);
CREATE INDEX chat_media_archive_folder_idx ON chat_media_archive (workspace_id, folder_id);
CREATE INDEX chat_media_archive_personal_idx ON chat_media_archive (workspace_id, owner_id, created_at DESC, id DESC)
WHERE group_id IS NULL;