Add PM extension (Projects, Work Logs, Comments) + Quill rich-text UI
Projects: Brick-backed model with list/detail screens and nav entry. Work Logs: per-contributor time ledger, assignee running-clock + helper logged minutes with Hours/Minutes input; pause-after-save and helper-deduct prompts. Comments: threaded comments with file attachments and Quill composer. Detail screen: tabbed layout (Work Log / Comments / Activity), editable title + description via edit dialog, Quill-rendered descriptions with legacy plain-text fallback. Create dialog: fixed-width (480px max), Quill description editor. Shared QuillFieldEditor + QuillReadOnly widgets extracted for reuse. RLS: hardened activity-log actor binding, immutable creator_id/ created_at triggers, SECURITY INVOKER visibility helper, append-only work-log and scoped comment policies. Brick migration for project_id FK on programmer_tasks. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
-- Hardens the programmer_tasks RLS shipped in 20260927120000, per the commit
|
||||
-- security review (two MEDIUM findings). The base migration is already applied
|
||||
-- remotely, so this is a corrective follow-up.
|
||||
|
||||
-- 1) audit-log-forgery: the activity-log INSERT policy did not bind actor_id to
|
||||
-- the caller, so an authorized user could attribute a log entry to someone
|
||||
-- else. Require actor_id to be the caller (or null for system rows).
|
||||
drop policy if exists "programmer_task_activity_logs_insert" on programmer_task_activity_logs;
|
||||
create policy "programmer_task_activity_logs_insert" on programmer_task_activity_logs
|
||||
for insert to authenticated
|
||||
with check (
|
||||
(actor_id = auth.uid() or actor_id is null)
|
||||
and exists (
|
||||
select 1 from programmer_tasks t
|
||||
where t.id = task_id
|
||||
and (
|
||||
t.assignee_id = auth.uid()
|
||||
or t.creator_id = auth.uid()
|
||||
or exists (
|
||||
select 1 from profiles p where p.id = auth.uid() and p.role in ('admin', 'programmer')
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
-- 2) column-immutability: the UPDATE policy allowed an assignee to mutate any
|
||||
-- column, including creator_id/created_at (the DELETE policy keys off
|
||||
-- creator_id). Make those columns immutable via a trigger.
|
||||
create or replace function programmer_tasks_guard_immutable()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
set search_path = ''
|
||||
as $$
|
||||
begin
|
||||
if new.creator_id is distinct from old.creator_id then
|
||||
raise exception 'creator_id is immutable';
|
||||
end if;
|
||||
if new.created_at is distinct from old.created_at then
|
||||
raise exception 'created_at is immutable';
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists trg_programmer_tasks_guard_immutable on programmer_tasks;
|
||||
create trigger trg_programmer_tasks_guard_immutable
|
||||
before update on programmer_tasks
|
||||
for each row execute function programmer_tasks_guard_immutable();
|
||||
|
||||
-- Note: activity-logs UPDATE/DELETE remain denied (no such policies exist),
|
||||
-- keeping the audit trail append-only. New log/ledger tables follow the same shape.
|
||||
@@ -0,0 +1,151 @@
|
||||
-- Programmer Tasks PM extension: projects, work logs (per-contributor time
|
||||
-- ledger), and comments with attachments. All tables start hardened (author
|
||||
-- bound to auth.uid(); logs/work-logs append-only; immutable creator/created_at).
|
||||
|
||||
-- Visibility helper: true iff the caller can see the task. SECURITY INVOKER so
|
||||
-- it runs under the caller's own programmer_tasks RLS (no SECURITY DEFINER
|
||||
-- exposure); reused by work-log and comment policies.
|
||||
create or replace function programmer_task_visible(t_id uuid)
|
||||
returns boolean
|
||||
language sql
|
||||
stable
|
||||
security invoker
|
||||
set search_path = public
|
||||
as $$
|
||||
select exists (select 1 from programmer_tasks where id = t_id);
|
||||
$$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Projects
|
||||
-- ---------------------------------------------------------------------------
|
||||
create table if not exists programmer_projects (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
name text not null,
|
||||
description text,
|
||||
status text not null default 'active', -- active | archived
|
||||
creator_id uuid references profiles(id),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
create index if not exists idx_programmer_projects_status on programmer_projects(status);
|
||||
|
||||
-- Reuse the table-agnostic updated_at + immutability trigger functions from the
|
||||
-- base/harden migrations.
|
||||
drop trigger if exists trg_programmer_projects_updated_at on programmer_projects;
|
||||
create trigger trg_programmer_projects_updated_at
|
||||
before update on programmer_projects
|
||||
for each row execute function set_programmer_tasks_updated_at();
|
||||
|
||||
drop trigger if exists trg_programmer_projects_guard_immutable on programmer_projects;
|
||||
create trigger trg_programmer_projects_guard_immutable
|
||||
before update on programmer_projects
|
||||
for each row execute function programmer_tasks_guard_immutable();
|
||||
|
||||
alter table programmer_projects enable row level security;
|
||||
|
||||
drop policy if exists "programmer_projects_select" on programmer_projects;
|
||||
create policy "programmer_projects_select" on programmer_projects
|
||||
for select to authenticated
|
||||
using (
|
||||
creator_id = auth.uid()
|
||||
or exists (select 1 from profiles p where p.id = auth.uid() and p.role in ('admin', 'programmer'))
|
||||
);
|
||||
|
||||
drop policy if exists "programmer_projects_insert" on programmer_projects;
|
||||
create policy "programmer_projects_insert" on programmer_projects
|
||||
for insert to authenticated
|
||||
with check (
|
||||
creator_id = auth.uid()
|
||||
and exists (select 1 from profiles p where p.id = auth.uid() and p.role in ('admin', 'programmer'))
|
||||
);
|
||||
|
||||
drop policy if exists "programmer_projects_update" on programmer_projects;
|
||||
create policy "programmer_projects_update" on programmer_projects
|
||||
for update to authenticated
|
||||
using (
|
||||
creator_id = auth.uid()
|
||||
or exists (select 1 from profiles p where p.id = auth.uid() and p.role in ('admin', 'programmer'))
|
||||
)
|
||||
with check (
|
||||
creator_id = auth.uid()
|
||||
or exists (select 1 from profiles p where p.id = auth.uid() and p.role in ('admin', 'programmer'))
|
||||
);
|
||||
|
||||
drop policy if exists "programmer_projects_delete" on programmer_projects;
|
||||
create policy "programmer_projects_delete" on programmer_projects
|
||||
for delete to authenticated
|
||||
using (
|
||||
creator_id = auth.uid()
|
||||
or exists (select 1 from profiles p where p.id = auth.uid() and p.role in ('admin', 'programmer'))
|
||||
);
|
||||
|
||||
-- Link tasks to a project (optional; unset on project delete).
|
||||
alter table programmer_tasks add column if not exists project_id uuid
|
||||
references programmer_projects(id) on delete set null;
|
||||
create index if not exists idx_programmer_tasks_project_id on programmer_tasks(project_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Work logs (per-contributor time ledger)
|
||||
-- ---------------------------------------------------------------------------
|
||||
create table if not exists programmer_task_work_logs (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
task_id uuid not null references programmer_tasks(id) on delete cascade,
|
||||
author_id uuid not null references profiles(id),
|
||||
description text not null,
|
||||
minutes int check (minutes is null or minutes > 0), -- null = assignee progress note
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
create index if not exists idx_programmer_task_work_logs_task_id on programmer_task_work_logs(task_id);
|
||||
|
||||
alter table programmer_task_work_logs enable row level security;
|
||||
|
||||
-- Append-only: select + insert only (no update/delete policies).
|
||||
drop policy if exists "programmer_task_work_logs_select" on programmer_task_work_logs;
|
||||
create policy "programmer_task_work_logs_select" on programmer_task_work_logs
|
||||
for select to authenticated
|
||||
using (programmer_task_visible(task_id));
|
||||
|
||||
drop policy if exists "programmer_task_work_logs_insert" on programmer_task_work_logs;
|
||||
create policy "programmer_task_work_logs_insert" on programmer_task_work_logs
|
||||
for insert to authenticated
|
||||
with check (author_id = auth.uid() and programmer_task_visible(task_id));
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Comments (+ attachments stored in the task_attachments bucket)
|
||||
-- ---------------------------------------------------------------------------
|
||||
create table if not exists programmer_task_comments (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
task_id uuid not null references programmer_tasks(id) on delete cascade,
|
||||
author_id uuid not null references profiles(id),
|
||||
body text not null default '',
|
||||
attachments jsonb not null default '[]'::jsonb, -- [{path, name}]
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
create index if not exists idx_programmer_task_comments_task_id on programmer_task_comments(task_id);
|
||||
|
||||
alter table programmer_task_comments enable row level security;
|
||||
|
||||
drop policy if exists "programmer_task_comments_select" on programmer_task_comments;
|
||||
create policy "programmer_task_comments_select" on programmer_task_comments
|
||||
for select to authenticated
|
||||
using (programmer_task_visible(task_id));
|
||||
|
||||
drop policy if exists "programmer_task_comments_insert" on programmer_task_comments;
|
||||
create policy "programmer_task_comments_insert" on programmer_task_comments
|
||||
for insert to authenticated
|
||||
with check (author_id = auth.uid() and programmer_task_visible(task_id));
|
||||
|
||||
drop policy if exists "programmer_task_comments_delete" on programmer_task_comments;
|
||||
create policy "programmer_task_comments_delete" on programmer_task_comments
|
||||
for delete to authenticated
|
||||
using (
|
||||
author_id = auth.uid()
|
||||
or exists (select 1 from profiles p where p.id = auth.uid() and p.role in ('admin', 'programmer'))
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Realtime
|
||||
-- ---------------------------------------------------------------------------
|
||||
alter publication supabase_realtime add table programmer_projects;
|
||||
alter publication supabase_realtime add table programmer_task_work_logs;
|
||||
alter publication supabase_realtime add table programmer_task_comments;
|
||||
Reference in New Issue
Block a user