Files
fourge-portal/supabase/schema.sql
T
Krao Hasanee eee0885811 Fix file sharing load speed and move error; misc updates
- Remove recursive directory size calculations (single Seafile API call per list)
- Remove 'Used in this location' usage display
- Fix move using v2 per-type endpoints instead of broken batch endpoint
- Send entry type from frontend for correct move routing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 14:20:38 -04:00

655 lines
27 KiB
PL/PgSQL
Executable File

-- ============================================================
-- Fourge Branding Portal — Database Schema v2
-- Paste this entire file into Supabase → SQL Editor → Run
-- ============================================================
-- Companies (client companies — created and managed by team)
create table public.companies (
id uuid default gen_random_uuid() primary key,
name text not null,
phone text default '',
address text default '',
contact_name text,
contact_email text,
contact_phone text,
client_logo_url text,
created_at timestamptz default now() not null
);
alter table public.companies enable row level security;
-- Profiles (extends auth.users — auto-created on signup)
create table public.profiles (
id uuid references auth.users on delete cascade primary key,
name text not null default '',
email text default '',
role text not null check (role in ('team', 'client', 'external')) default 'client',
company_id uuid references public.companies(id) on delete set null,
created_at timestamptz default now() not null
);
alter table public.profiles enable row level security;
-- Projects (belong to a company)
create table public.projects (
id uuid default gen_random_uuid() primary key,
company_id uuid references public.companies(id) on delete cascade not null,
name text not null,
constraint projects_name_not_blank check (length(trim(name)) > 0),
description text default '',
status text not null check (status in ('active', 'completed')) default 'active',
created_at timestamptz default now() not null
);
alter table public.projects enable row level security;
-- Tasks (belong to a project)
create table public.tasks (
id uuid default gen_random_uuid() primary key,
project_id uuid references public.projects(id) on delete cascade not null,
request_key uuid,
title text not null,
assigned_to uuid references public.profiles(id) on delete set null,
assigned_name text,
status text not null check (status in ('not_started', 'in_progress', 'on_hold', 'client_review', 'client_approved')) default 'not_started',
current_version integer default 0 not null,
invoiced boolean default false,
submitted_at timestamptz default now() not null,
completed_at timestamptz
);
alter table public.tasks enable row level security;
-- Submissions (each version of a request)
create table public.submissions (
id uuid default gen_random_uuid() primary key,
task_id uuid references public.tasks(id) on delete cascade not null,
request_key uuid,
version_number integer not null,
type text not null check (type in ('initial', 'revision', 'amendment')) default 'initial',
revision_type text check (revision_type in ('fourge_error', 'client_revision')),
invoiced boolean not null default false,
is_hot boolean not null default false,
service_type text default '',
deadline date,
description text default '',
submitted_by uuid references public.profiles(id) on delete set null,
submitted_by_name text default '',
submitted_at timestamptz default now() not null
);
alter table public.submissions enable row level security;
create unique index if not exists tasks_request_key_key
on public.tasks (request_key)
where request_key is not null;
create unique index if not exists submissions_request_key_key
on public.submissions (request_key)
where request_key is not null;
create unique index if not exists projects_company_normalized_name_key
on public.projects (company_id, lower(btrim(name)));
-- Submission Files
create table public.submission_files (
id uuid default gen_random_uuid() primary key,
submission_id uuid references public.submissions(id) on delete cascade not null,
name text not null,
storage_path text,
size bigint default 0
);
alter table public.submission_files enable row level security;
-- Deliveries (work sent to client)
create table public.deliveries (
id uuid default gen_random_uuid() primary key,
submission_id uuid references public.submissions(id) on delete cascade not null unique,
sent_at timestamptz default now() not null,
sent_by text default '',
message text default ''
);
alter table public.deliveries enable row level security;
-- Delivery Files
create table public.delivery_files (
id uuid default gen_random_uuid() primary key,
delivery_id uuid references public.deliveries(id) on delete cascade not null,
name text not null,
storage_path text,
size bigint default 0
);
alter table public.delivery_files enable row level security;
-- Project Members (external users assigned to specific projects)
create table public.project_members (
id uuid default gen_random_uuid() primary key,
project_id uuid references public.projects(id) on delete cascade not null,
profile_id uuid references public.profiles(id) on delete cascade not null,
created_at timestamptz default now() not null,
unique(project_id, profile_id)
);
alter table public.project_members enable row level security;
-- Company Prices (per service type, per company, per price type)
create table public.company_prices (
id uuid default gen_random_uuid() primary key,
company_id uuid references public.companies(id) on delete cascade not null,
service_type text not null,
price numeric(10,2) default 0,
price_type text not null default 'new' check (price_type in ('new', 'revision')),
unique(company_id, service_type, price_type)
);
alter table public.company_prices enable row level security;
-- Invoices
create table public.invoices (
id uuid default gen_random_uuid() primary key,
company_id uuid references public.companies(id) on delete cascade not null,
invoice_number text not null,
status text not null check (status in ('draft', 'sent', 'paid')) default 'draft',
invoice_date date default current_date,
due_date date,
total numeric(10,2) default 0,
notes text default '',
bill_to text,
invoice_email text,
stripe_fee numeric(10,2),
created_by uuid references public.profiles(id) on delete set null,
created_at timestamptz default now() not null
);
alter table public.invoices enable row level security;
-- Expenses
create table public.expenses (
id uuid default gen_random_uuid() primary key,
date date default current_date not null,
description text not null,
category text not null default 'Other',
amount numeric(10,2) not null,
notes text default '',
receipt_path text,
receipt_name text,
created_by uuid references public.profiles(id) on delete set null,
created_at timestamptz default now() not null
);
alter table public.expenses enable row level security;
create policy "Team all expenses" on public.expenses for all using (get_my_role() = 'team');
-- Subcontractor Payments (tracked separately, included as contractor expenses)
create table public.subcontractor_payments (
id uuid default gen_random_uuid() primary key,
profile_id uuid references public.profiles(id) on delete set null,
date date default current_date not null,
description text not null,
amount numeric(10,2) not null,
status text not null default 'pending' check (status in ('pending', 'paid')),
paid_at date,
notes text default '',
created_by uuid references public.profiles(id) on delete set null,
created_at timestamptz default now() not null
);
alter table public.subcontractor_payments enable row level security;
create policy "Team all subcontractor_payments" on public.subcontractor_payments
for all using (get_my_role() = 'team') with check (get_my_role() = 'team');
-- Meeting Notes
create table public.meeting_notes (
id uuid default gen_random_uuid() primary key,
meeting_at timestamptz default now() not null,
title text not null,
attendees text default '',
notes text not null default '',
created_by uuid references public.profiles(id) on delete set null,
created_at timestamptz default now() not null,
updated_at timestamptz default now() not null
);
alter table public.meeting_notes enable row level security;
create policy "Team all meeting_notes" on public.meeting_notes
for all using (get_my_role() = 'team') with check (get_my_role() = 'team');
-- Invoice Items
create table public.invoice_items (
id uuid default gen_random_uuid() primary key,
invoice_id uuid references public.invoices(id) on delete cascade not null,
task_id uuid references public.tasks(id) on delete set null,
submission_id uuid references public.submissions(id) on delete set null,
description text not null,
quantity numeric(10,2) default 1,
unit_price numeric(10,2) default 0,
created_at timestamptz default now() not null
);
alter table public.invoice_items enable row level security;
-- Brand Books (sign survey / brand book records, team only)
create table public.brand_books (
id uuid default gen_random_uuid() primary key,
client_id uuid references public.companies(id) on delete set null,
client_name text not null default '',
project_name text default '',
site_address text default '',
book_date date,
prepared_by text default '',
revision text default '',
template text default 'fourge',
site_map_path text,
inventory_map_path text,
signs jsonb default '[]',
survey_photo_paths text[] default '{}',
updated_at timestamptz default now() not null,
-- Cover page fields
project_logo_path text,
creation_date date,
revision_date date,
customer_name text,
customer_address text,
client_logo_url text,
client_contact_name text,
client_contact_email text,
client_contact_phone text,
approved_date date,
approval_notes text
);
alter table public.brand_books enable row level security;
-- Server Status Overrides (team-managed usage inputs for metrics without live APIs)
create table public.server_status_overrides (
id boolean primary key default true,
supabase_egress_bytes bigint,
vercel_fast_data_transfer_bytes bigint,
vercel_edge_requests bigint,
vercel_function_invocations bigint,
vercel_active_cpu_hours numeric(10,4),
updated_at timestamptz not null default now()
);
alter table public.server_status_overrides enable row level security;
create table public.fourge_passwords (
id uuid default gen_random_uuid() primary key,
service_name text not null,
service_url text default '',
username text not null default '',
encrypted_password text not null,
password_iv text not null,
notes text default '',
created_by uuid references public.profiles(id) on delete set null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
alter table public.fourge_passwords enable row level security;
-- ============================================================
-- Helpers
-- ============================================================
create or replace function public.get_my_role()
returns text as $$
select role from public.profiles where id = auth.uid();
$$ language sql security definer stable;
create or replace function public.is_external()
returns boolean as $$
select get_my_role() = 'external';
$$ language sql security definer stable;
create or replace function public.get_my_company_id()
returns uuid as $$
select company_id from public.profiles where id = auth.uid();
$$ language sql security definer stable;
create or replace function public.get_database_size_bytes()
returns bigint as $$
select pg_database_size(current_database());
$$ language sql security definer stable;
-- Prevents clients and externals from modifying protected task fields.
-- Fires before UPDATE so WITH CHECK sees the already-corrected new row.
create or replace function public.guard_task_update()
returns trigger as $$
declare
caller_role text;
begin
select role into caller_role from public.profiles where id = auth.uid();
if caller_role = 'client' then
new.project_id := old.project_id;
new.invoiced := old.invoiced;
if not (
new.status = 'not_started'
and coalesce(new.current_version, 0) > coalesce(old.current_version, 0)
and new.assigned_to is null
and new.assigned_name is null
) then
new.assigned_to := old.assigned_to;
new.assigned_name := old.assigned_name;
end if;
elsif caller_role = 'external' then
new.project_id := old.project_id;
new.invoiced := old.invoiced;
end if;
return new;
end;
$$ language plpgsql security definer;
create trigger guard_task_update
before update on public.tasks
for each row execute function public.guard_task_update();
-- ============================================================
-- RLS Policies
-- ============================================================
-- Companies
create policy "Team all companies" on public.companies for all using (get_my_role() = 'team');
create policy "Client reads own company" on public.companies for select using (id = get_my_company_id());
create policy "Client updates own company" on public.companies
for update using (id = get_my_company_id()) with check (id = get_my_company_id());
-- Project Members
create policy "Team all project_members" on public.project_members for all using (get_my_role() = 'team');
create policy "External reads own memberships" on public.project_members for select using (profile_id = auth.uid());
-- Profiles
create policy "Own profile select" on public.profiles for select using (id = auth.uid());
create policy "Team reads all profiles" on public.profiles for select using (get_my_role() = 'team');
create policy "Own profile update" on public.profiles
for update
using (id = auth.uid())
with check (
id = auth.uid()
and role = (select role from public.profiles where id = auth.uid())
and company_id is not distinct from (select company_id from public.profiles where id = auth.uid())
);
create policy "Team update profiles" on public.profiles for update using (get_my_role() = 'team');
-- Projects
create policy "Team all projects" on public.projects for all using (get_my_role() = 'team');
create policy "Client reads company projects" on public.projects for select using (company_id = get_my_company_id());
create policy "Client inserts company projects" on public.projects for insert with check (company_id = get_my_company_id());
create policy "Client updates own company projects" on public.projects
for update using (company_id = get_my_company_id()) with check (company_id = get_my_company_id());
create policy "External reads assigned projects" on public.projects for select using (
get_my_role() = 'external' and
id in (select project_id from public.project_members where profile_id = auth.uid())
);
-- Tasks
create policy "Team all tasks" on public.tasks for all using (get_my_role() = 'team');
create policy "Client reads company tasks" on public.tasks for select using (
project_id in (select id from public.projects where company_id = get_my_company_id())
);
create policy "Client insert task" on public.tasks for insert with check (
project_id in (select id from public.projects where company_id = get_my_company_id())
);
create policy "Client updates company tasks" on public.tasks
for update
using (
get_my_role() = 'client'
and project_id in (select id from public.projects where company_id = get_my_company_id())
)
with check (
get_my_role() = 'client'
and project_id in (select id from public.projects where company_id = get_my_company_id())
);
create policy "External reads assigned tasks" on public.tasks for select using (
get_my_role() = 'external' and
project_id in (select project_id from public.project_members where profile_id = auth.uid())
);
create policy "External updates assigned tasks" on public.tasks
for update
using (
get_my_role() = 'external'
and project_id in (select project_id from public.project_members where profile_id = auth.uid())
)
with check (
get_my_role() = 'external'
and project_id in (select project_id from public.project_members where profile_id = auth.uid())
);
-- Submissions
create policy "Team all submissions" on public.submissions for all using (get_my_role() = 'team');
create policy "Client reads company submissions" on public.submissions for select using (
task_id in (
select t.id from public.tasks t
join public.projects p on p.id = t.project_id
where p.company_id = get_my_company_id()
)
);
create policy "Client inserts submissions" on public.submissions for insert with check (
get_my_role() = 'client'
and submitted_by = auth.uid()
and task_id in (
select t.id from public.tasks t
join public.projects p on p.id = t.project_id
where p.company_id = get_my_company_id()
)
);
create policy "External reads assigned submissions" on public.submissions for select using (
get_my_role() = 'external' and
task_id in (
select t.id from public.tasks t
join public.project_members pm on pm.project_id = t.project_id
where pm.profile_id = auth.uid()
)
);
create policy "External inserts submissions" on public.submissions for insert with check (
get_my_role() = 'external'
and submitted_by = auth.uid()
and task_id in (
select t.id from public.tasks t
join public.project_members pm on pm.project_id = t.project_id
where pm.profile_id = auth.uid()
)
);
-- Submission Files
create policy "Team all submission_files" on public.submission_files for all using (get_my_role() = 'team');
create policy "Client reads company submission_files" on public.submission_files for select using (
submission_id in (
select s.id from public.submissions s
join public.tasks t on t.id = s.task_id
join public.projects p on p.id = t.project_id
where p.company_id = get_my_company_id()
)
);
create policy "Client inserts submission_files" on public.submission_files for insert with check (
get_my_role() = 'client'
and submission_id in (
select s.id from public.submissions s
join public.tasks t on t.id = s.task_id
join public.projects p on p.id = t.project_id
where p.company_id = get_my_company_id()
and s.submitted_by = auth.uid()
)
);
create policy "External reads assigned submission_files" on public.submission_files for select using (
get_my_role() = 'external' and
submission_id in (
select s.id from public.submissions s
join public.tasks t on t.id = s.task_id
join public.project_members pm on pm.project_id = t.project_id
where pm.profile_id = auth.uid()
)
);
create policy "External inserts submission_files" on public.submission_files for insert with check (
get_my_role() = 'external'
and submission_id in (
select s.id from public.submissions s
join public.tasks t on t.id = s.task_id
join public.project_members pm on pm.project_id = t.project_id
where pm.profile_id = auth.uid()
and s.submitted_by = auth.uid()
)
);
-- Deliveries
create policy "Team all deliveries" on public.deliveries for all using (get_my_role() = 'team');
create policy "Client reads company deliveries" on public.deliveries for select using (
submission_id in (
select s.id from public.submissions s
join public.tasks t on t.id = s.task_id
join public.projects p on p.id = t.project_id
where p.company_id = get_my_company_id()
)
);
create policy "External reads assigned deliveries" on public.deliveries for select using (
get_my_role() = 'external' and
submission_id in (
select s.id from public.submissions s
join public.tasks t on t.id = s.task_id
join public.project_members pm on pm.project_id = t.project_id
where pm.profile_id = auth.uid()
)
);
-- Delivery Files
create policy "Team all delivery_files" on public.delivery_files for all using (get_my_role() = 'team');
create policy "Client reads company delivery_files" on public.delivery_files for select using (
delivery_id in (
select d.id from public.deliveries d
join public.submissions s on s.id = d.submission_id
join public.tasks t on t.id = s.task_id
join public.projects p on p.id = t.project_id
where p.company_id = get_my_company_id()
)
);
create policy "External reads assigned delivery_files" on public.delivery_files for select using (
get_my_role() = 'external' and
delivery_id in (
select d.id from public.deliveries d
join public.submissions s on s.id = d.submission_id
join public.tasks t on t.id = s.task_id
join public.project_members pm on pm.project_id = t.project_id
where pm.profile_id = auth.uid()
)
);
-- Company Prices
create policy "Team all company_prices" on public.company_prices for all using (get_my_role() = 'team');
create policy "Client reads own company prices" on public.company_prices for select using (company_id = get_my_company_id());
-- Invoices
create policy "Team all invoices" on public.invoices for all using (get_my_role() = 'team');
create policy "Client reads company invoices" on public.invoices for select using (company_id = get_my_company_id());
-- Invoice Items
create policy "Team all invoice_items" on public.invoice_items for all using (get_my_role() = 'team');
create policy "Client reads company invoice_items" on public.invoice_items for select using (
invoice_id in (select id from public.invoices where company_id = get_my_company_id())
);
-- Brand Books
create policy "Team all brand_books" on public.brand_books for all using (get_my_role() = 'team');
create policy "Team all server_status_overrides" on public.server_status_overrides for all using (get_my_role() = 'team') with check (get_my_role() = 'team');
create policy "Team all fourge_passwords" on public.fourge_passwords for all using (get_my_role() = 'team') with check (get_my_role() = 'team');
-- ============================================================
-- Storage Buckets
-- ============================================================
insert into storage.buckets (id, name, public) values ('submissions', 'submissions', false);
insert into storage.buckets (id, name, public) values ('deliveries', 'deliveries', false);
insert into storage.buckets (id, name, public) values ('company-logos', 'company-logos', true);
insert into storage.buckets (id, name, public) values ('fourge-files', 'fourge-files', false);
insert into storage.buckets (id, name, public) values ('expense-receipts', 'expense-receipts', false);
-- Company Logos (public bucket — team manages, public can read for embedded URLs in PDFs)
create policy "Team manages company logos" on storage.objects
for all to authenticated
using (bucket_id = 'company-logos' and get_my_role() = 'team')
with check (bucket_id = 'company-logos' and get_my_role() = 'team');
create policy "Public can read company logos" on storage.objects
for select using (bucket_id = 'company-logos');
-- Submissions: SELECT
create policy "Team reads submissions storage" on storage.objects
for select to authenticated using (bucket_id = 'submissions' and get_my_role() = 'team');
create policy "Client reads submissions storage" on storage.objects
for select to authenticated using (
bucket_id = 'submissions' and get_my_role() = 'client'
and split_part(name, '/', 1) in (select t.id::text from public.tasks t join public.projects p on p.id = t.project_id where p.company_id = get_my_company_id())
);
create policy "External reads submissions storage" on storage.objects
for select to authenticated using (
bucket_id = 'submissions' and get_my_role() = 'external'
and split_part(name, '/', 1) in (select t.id::text from public.tasks t join public.project_members pm on pm.project_id = t.project_id where pm.profile_id = auth.uid())
);
-- Submissions: INSERT
create policy "Team inserts submissions storage" on storage.objects
for insert to authenticated with check (bucket_id = 'submissions' and get_my_role() = 'team');
create policy "Client inserts submissions storage" on storage.objects
for insert to authenticated with check (
bucket_id = 'submissions' and get_my_role() = 'client'
and split_part(name, '/', 1) in (select t.id::text from public.tasks t join public.projects p on p.id = t.project_id where p.company_id = get_my_company_id())
);
create policy "External inserts submissions storage" on storage.objects
for insert to authenticated with check (
bucket_id = 'submissions' and get_my_role() = 'external'
and split_part(name, '/', 1) in (select t.id::text from public.tasks t join public.project_members pm on pm.project_id = t.project_id where pm.profile_id = auth.uid())
);
-- Submissions: DELETE (team only)
create policy "Team deletes submissions storage" on storage.objects
for delete to authenticated using (bucket_id = 'submissions' and get_my_role() = 'team');
-- Deliveries: SELECT
create policy "Team reads deliveries storage" on storage.objects
for select to authenticated using (bucket_id = 'deliveries' and get_my_role() = 'team');
create policy "Client reads deliveries storage" on storage.objects
for select to authenticated using (
bucket_id = 'deliveries' and get_my_role() = 'client'
and split_part(name, '/', 1) in (select t.id::text from public.tasks t join public.projects p on p.id = t.project_id where p.company_id = get_my_company_id())
);
create policy "External reads deliveries storage" on storage.objects
for select to authenticated using (
bucket_id = 'deliveries' and get_my_role() = 'external'
and split_part(name, '/', 1) in (select t.id::text from public.tasks t join public.project_members pm on pm.project_id = t.project_id where pm.profile_id = auth.uid())
);
-- Deliveries: INSERT + DELETE (team only)
create policy "Team inserts deliveries storage" on storage.objects
for insert to authenticated with check (bucket_id = 'deliveries' and get_my_role() = 'team');
create policy "Team deletes deliveries storage" on storage.objects
for delete to authenticated using (bucket_id = 'deliveries' and get_my_role() = 'team');
-- Fourge Files: internal team-only company documents
create policy "Team reads fourge files storage" on storage.objects
for select to authenticated using (bucket_id = 'fourge-files' and get_my_role() = 'team');
create policy "Team inserts fourge files storage" on storage.objects
for insert to authenticated with check (bucket_id = 'fourge-files' and get_my_role() = 'team');
create policy "Team updates fourge files storage" on storage.objects
for update to authenticated using (bucket_id = 'fourge-files' and get_my_role() = 'team')
with check (bucket_id = 'fourge-files' and get_my_role() = 'team');
create policy "Team deletes fourge files storage" on storage.objects
for delete to authenticated using (bucket_id = 'fourge-files' and get_my_role() = 'team');
-- Expense Receipts: team-only storage for uploaded expense receipts/photos
create policy "Team reads expense receipts storage" on storage.objects
for select to authenticated using (bucket_id = 'expense-receipts' and get_my_role() = 'team');
create policy "Team inserts expense receipts storage" on storage.objects
for insert to authenticated with check (bucket_id = 'expense-receipts' and get_my_role() = 'team');
create policy "Team updates expense receipts storage" on storage.objects
for update to authenticated using (bucket_id = 'expense-receipts' and get_my_role() = 'team')
with check (bucket_id = 'expense-receipts' and get_my_role() = 'team');
create policy "Team deletes expense receipts storage" on storage.objects
for delete to authenticated using (bucket_id = 'expense-receipts' and get_my_role() = 'team');
-- ============================================================
-- Trigger: auto-create profile on signup
-- Team assigns company_id later via the Companies page
-- ============================================================
create or replace function public.handle_new_user()
returns trigger as $$
begin
insert into public.profiles (id, name, email, role)
values (
new.id,
coalesce(new.raw_user_meta_data->>'name', ''),
new.email,
coalesce(new.raw_user_meta_data->>'role', 'client')
);
return new;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();