1564 lines
64 KiB
PL/PgSQL
1564 lines
64 KiB
PL/PgSQL
-- Neta one-shot Supabase setup SQL
|
|
-- Run this once in a fresh Supabase project after Auth and Storage schemas are available.
|
|
-- This file is generated from supabase/schema.sql and supabase/migrations/0002..0011.
|
|
-- Demo seed data is intentionally not included.
|
|
|
|
|
|
-- -----------------------------------------------------------------------------
|
|
-- Source: supabase/schema.sql
|
|
-- -----------------------------------------------------------------------------
|
|
|
|
-- 0001: Initial Cognis baseline schema
|
|
-- This file is intentionally idempotent enough to be retried after a failed SQL Editor run.
|
|
|
|
-- Enable necessary extensions
|
|
create extension if not exists "uuid-ossp";
|
|
|
|
-- 1. Create journals table
|
|
create table if not exists public.journals (
|
|
id uuid default uuid_generate_v4() primary key,
|
|
user_id uuid references auth.users(id) on delete cascade not null,
|
|
date timestamp with time zone default timezone('utc'::text, now()) not null,
|
|
mood text not null,
|
|
energy smallint not null,
|
|
content text not null,
|
|
ai_tags text[] default array[]::text[],
|
|
ai_sentiment_score numeric,
|
|
ai_summary text,
|
|
ai_reflection text,
|
|
analysis_status text default 'pending'::text,
|
|
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
|
|
updated_at timestamp with time zone default timezone('utc'::text, now()) not null
|
|
);
|
|
|
|
-- 2. Create tasks table
|
|
create table if not exists public.tasks (
|
|
id uuid default uuid_generate_v4() primary key,
|
|
user_id uuid references auth.users(id) on delete cascade not null,
|
|
date timestamp with time zone default timezone('utc'::text, now()) not null,
|
|
title text not null,
|
|
description text,
|
|
status text default 'todo'::text,
|
|
ai_generated boolean default false,
|
|
source_journal_id uuid references public.journals(id) on delete set null,
|
|
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
|
|
updated_at timestamp with time zone default timezone('utc'::text, now()) not null
|
|
);
|
|
|
|
-- 3. Create chat_sessions table
|
|
create table if not exists public.chat_sessions (
|
|
id uuid default uuid_generate_v4() primary key,
|
|
user_id uuid references auth.users(id) on delete cascade not null,
|
|
title text not null,
|
|
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
|
|
updated_at timestamp with time zone default timezone('utc'::text, now()) not null
|
|
);
|
|
|
|
-- 4. Create chat_messages table
|
|
create table if not exists public.chat_messages (
|
|
id uuid default uuid_generate_v4() primary key,
|
|
session_id uuid references public.chat_sessions(id) on delete cascade not null,
|
|
role text not null,
|
|
content text not null,
|
|
context_journal_ids uuid[] default array[]::uuid[],
|
|
created_at timestamp with time zone default timezone('utc'::text, now()) not null
|
|
);
|
|
|
|
-- 5. Create profiles table
|
|
create table if not exists public.profiles (
|
|
id uuid references auth.users(id) on delete cascade primary key,
|
|
first_name text,
|
|
last_name text,
|
|
avatar_url text,
|
|
updated_at timestamp with time zone default timezone('utc'::text, now()) not null
|
|
);
|
|
|
|
-- RLS (Row Level Security)
|
|
alter table public.journals enable row level security;
|
|
alter table public.tasks enable row level security;
|
|
alter table public.chat_sessions enable row level security;
|
|
alter table public.chat_messages enable row level security;
|
|
alter table public.profiles enable row level security;
|
|
|
|
-- Journals policies
|
|
drop policy if exists "Users can view their own journals." on public.journals;
|
|
create policy "Users can view their own journals." on public.journals
|
|
for select using (auth.uid() = user_id);
|
|
|
|
drop policy if exists "Users can insert their own journals." on public.journals;
|
|
create policy "Users can insert their own journals." on public.journals
|
|
for insert with check (auth.uid() = user_id);
|
|
|
|
drop policy if exists "Users can update their own journals." on public.journals;
|
|
create policy "Users can update their own journals." on public.journals
|
|
for update using (auth.uid() = user_id);
|
|
|
|
drop policy if exists "Users can delete their own journals." on public.journals;
|
|
create policy "Users can delete their own journals." on public.journals
|
|
for delete using (auth.uid() = user_id);
|
|
|
|
-- Tasks policies
|
|
drop policy if exists "Users can view their own tasks." on public.tasks;
|
|
create policy "Users can view their own tasks." on public.tasks
|
|
for select using (auth.uid() = user_id);
|
|
|
|
drop policy if exists "Users can insert their own tasks." on public.tasks;
|
|
create policy "Users can insert their own tasks." on public.tasks
|
|
for insert with check (auth.uid() = user_id);
|
|
|
|
drop policy if exists "Users can update their own tasks." on public.tasks;
|
|
create policy "Users can update their own tasks." on public.tasks
|
|
for update using (auth.uid() = user_id);
|
|
|
|
drop policy if exists "Users can delete their own tasks." on public.tasks;
|
|
create policy "Users can delete their own tasks." on public.tasks
|
|
for delete using (auth.uid() = user_id);
|
|
|
|
-- Chat sessions policies
|
|
drop policy if exists "Users can view their own chat sessions." on public.chat_sessions;
|
|
create policy "Users can view their own chat sessions." on public.chat_sessions
|
|
for select using (auth.uid() = user_id);
|
|
|
|
drop policy if exists "Users can insert their own chat sessions." on public.chat_sessions;
|
|
create policy "Users can insert their own chat sessions." on public.chat_sessions
|
|
for insert with check (auth.uid() = user_id);
|
|
|
|
drop policy if exists "Users can update their own chat sessions." on public.chat_sessions;
|
|
create policy "Users can update their own chat sessions." on public.chat_sessions
|
|
for update using (auth.uid() = user_id);
|
|
|
|
drop policy if exists "Users can delete their own chat sessions." on public.chat_sessions;
|
|
create policy "Users can delete their own chat sessions." on public.chat_sessions
|
|
for delete using (auth.uid() = user_id);
|
|
|
|
-- Chat messages policies
|
|
drop policy if exists "Users can view their own chat messages." on public.chat_messages;
|
|
create policy "Users can view their own chat messages." on public.chat_messages
|
|
for select using (
|
|
exists (
|
|
select 1 from public.chat_sessions
|
|
where chat_sessions.id = chat_messages.session_id
|
|
and chat_sessions.user_id = auth.uid()
|
|
)
|
|
);
|
|
|
|
drop policy if exists "Users can insert their own chat messages." on public.chat_messages;
|
|
create policy "Users can insert their own chat messages." on public.chat_messages
|
|
for insert with check (
|
|
exists (
|
|
select 1 from public.chat_sessions
|
|
where chat_sessions.id = session_id
|
|
and chat_sessions.user_id = auth.uid()
|
|
)
|
|
);
|
|
|
|
drop policy if exists "Users can update their own chat messages." on public.chat_messages;
|
|
create policy "Users can update their own chat messages." on public.chat_messages
|
|
for update using (
|
|
exists (
|
|
select 1 from public.chat_sessions
|
|
where chat_sessions.id = session_id
|
|
and chat_sessions.user_id = auth.uid()
|
|
)
|
|
);
|
|
|
|
drop policy if exists "Users can delete their own chat messages." on public.chat_messages;
|
|
create policy "Users can delete their own chat messages." on public.chat_messages
|
|
for delete using (
|
|
exists (
|
|
select 1 from public.chat_sessions
|
|
where chat_sessions.id = session_id
|
|
and chat_sessions.user_id = auth.uid()
|
|
)
|
|
);
|
|
|
|
-- Profiles policies
|
|
drop policy if exists "Users can view their own profile." on public.profiles;
|
|
create policy "Users can view their own profile." on public.profiles
|
|
for select using (auth.uid() = id);
|
|
|
|
drop policy if exists "Users can insert their own profile." on public.profiles;
|
|
create policy "Users can insert their own profile." on public.profiles
|
|
for insert with check (auth.uid() = id);
|
|
|
|
drop policy if exists "Users can update their own profile." on public.profiles;
|
|
create policy "Users can update their own profile." on public.profiles
|
|
for update using (auth.uid() = id);
|
|
|
|
-- First admin setup guard for self-hosted installations
|
|
create or replace function public.is_first_admin_setup_available()
|
|
returns boolean
|
|
language sql
|
|
security definer
|
|
set search_path = public
|
|
as $$
|
|
select not exists (
|
|
select 1
|
|
from public.profiles
|
|
limit 1
|
|
);
|
|
$$;
|
|
|
|
revoke all on function public.is_first_admin_setup_available() from public;
|
|
grant execute on function public.is_first_admin_setup_available() to anon;
|
|
grant execute on function public.is_first_admin_setup_available() to authenticated;
|
|
|
|
create or replace function public.neta_current_jwt_role()
|
|
returns text
|
|
language sql
|
|
stable
|
|
as $$
|
|
select coalesce(
|
|
nullif(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'role', ''),
|
|
nullif(current_setting('request.jwt.claim.role', true), ''),
|
|
''
|
|
);
|
|
$$;
|
|
|
|
revoke all on function public.neta_current_jwt_role() from public;
|
|
grant execute on function public.neta_current_jwt_role() to anon;
|
|
grant execute on function public.neta_current_jwt_role() to authenticated;
|
|
grant execute on function public.neta_current_jwt_role() to service_role;
|
|
|
|
create schema if not exists neta_internal;
|
|
|
|
create table if not exists neta_internal.internal_auth_creations (
|
|
id uuid default uuid_generate_v4() primary key,
|
|
email text not null,
|
|
reason text default 'internal'::text not null,
|
|
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
|
|
expires_at timestamp with time zone default (timezone('utc'::text, now()) + interval '2 minutes') not null
|
|
);
|
|
|
|
create index if not exists internal_auth_creations_email_idx
|
|
on neta_internal.internal_auth_creations (lower(email));
|
|
|
|
revoke all on schema neta_internal from public;
|
|
revoke all on all tables in schema neta_internal from public;
|
|
|
|
create or replace function public.request_internal_auth_creation(
|
|
target_email text,
|
|
target_reason text default 'internal'
|
|
)
|
|
returns void
|
|
language plpgsql
|
|
security definer
|
|
set search_path = public, neta_internal
|
|
as $$
|
|
begin
|
|
if public.neta_current_jwt_role() <> 'service_role' then
|
|
raise exception 'Only service role can request internal auth creation.';
|
|
end if;
|
|
|
|
if target_email is null or btrim(target_email) = '' then
|
|
raise exception 'target_email is required.';
|
|
end if;
|
|
|
|
delete from neta_internal.internal_auth_creations
|
|
where expires_at <= timezone('utc'::text, now())
|
|
or lower(email) = lower(btrim(target_email));
|
|
|
|
insert into neta_internal.internal_auth_creations (email, reason)
|
|
values (btrim(target_email), coalesce(nullif(btrim(target_reason), ''), 'internal'));
|
|
end;
|
|
$$;
|
|
|
|
revoke all on function public.request_internal_auth_creation(text, text) from public;
|
|
grant execute on function public.request_internal_auth_creation(text, text) to service_role;
|
|
|
|
-- Function to handle new user signup
|
|
create or replace function public.handle_new_user()
|
|
returns trigger
|
|
language plpgsql
|
|
security definer
|
|
set search_path = public, neta_internal
|
|
as $$
|
|
declare
|
|
allowed_internal_creation boolean := false;
|
|
begin
|
|
delete from neta_internal.internal_auth_creations
|
|
where lower(email) = lower(new.email)
|
|
and expires_at > timezone('utc'::text, now())
|
|
returning true into allowed_internal_creation;
|
|
|
|
allowed_internal_creation := coalesce(allowed_internal_creation, false);
|
|
|
|
if exists (select 1 from public.profiles limit 1)
|
|
and coalesce(new.raw_app_meta_data->>'internal_created', 'false') <> 'true'
|
|
and not allowed_internal_creation then
|
|
raise exception 'Registration is closed. The first admin account already exists.';
|
|
end if;
|
|
|
|
insert into public.profiles (id, first_name, last_name, avatar_url)
|
|
values (new.id, '', '', '')
|
|
on conflict (id) do nothing;
|
|
|
|
return new;
|
|
end;
|
|
$$;
|
|
|
|
-- Trigger to automatically create profile on signup
|
|
drop trigger if exists on_auth_user_created on auth.users;
|
|
|
|
create trigger on_auth_user_created
|
|
after insert on auth.users
|
|
for each row execute procedure public.handle_new_user();
|
|
|
|
-- Setup storage bucket for avatars
|
|
insert into storage.buckets (id, name, public)
|
|
values ('avatars', 'avatars', true)
|
|
on conflict (id) do nothing;
|
|
|
|
drop policy if exists "Avatar images are publicly accessible." on storage.objects;
|
|
create policy "Avatar images are publicly accessible."
|
|
on storage.objects for select
|
|
using (
|
|
bucket_id = 'avatars'
|
|
or public.neta_current_jwt_role() = 'service_role'
|
|
);
|
|
|
|
drop policy if exists "Users can upload an avatar." on storage.objects;
|
|
create policy "Users can upload an avatar."
|
|
on storage.objects for insert
|
|
with check (
|
|
bucket_id = 'avatars'
|
|
and (
|
|
public.neta_current_jwt_role() = 'service_role'
|
|
or auth.uid()::text = (storage.foldername(name))[1]
|
|
)
|
|
);
|
|
|
|
drop policy if exists "Users can update their own avatar." on storage.objects;
|
|
create policy "Users can update their own avatar."
|
|
on storage.objects for update
|
|
using (
|
|
bucket_id = 'avatars'
|
|
and (
|
|
public.neta_current_jwt_role() = 'service_role'
|
|
or auth.uid()::text = (storage.foldername(name))[1]
|
|
)
|
|
);
|
|
|
|
drop policy if exists "Users can delete their own avatar." on storage.objects;
|
|
create policy "Users can delete their own avatar."
|
|
on storage.objects for delete
|
|
using (
|
|
bucket_id = 'avatars'
|
|
and (
|
|
public.neta_current_jwt_role() = 'service_role'
|
|
or auth.uid()::text = (storage.foldername(name))[1]
|
|
)
|
|
);
|
|
|
|
-- -----------------------------------------------------------------------------
|
|
-- Source: supabase/migrations/0002_add_freelancer_os_core_tables.sql
|
|
-- -----------------------------------------------------------------------------
|
|
|
|
-- 0002: Add Freelancer OS core tables
|
|
-- Run after: supabase/schema.sql
|
|
|
|
create extension if not exists "uuid-ossp";
|
|
|
|
create or replace function public.set_updated_at()
|
|
returns trigger as $$
|
|
begin
|
|
new.updated_at = timezone('utc'::text, now());
|
|
return new;
|
|
end;
|
|
$$ language plpgsql;
|
|
|
|
create table if not exists public.clients (
|
|
id uuid default uuid_generate_v4() primary key,
|
|
user_id uuid references auth.users(id) on delete cascade not null,
|
|
name text not null,
|
|
company_name text,
|
|
email text,
|
|
phone text,
|
|
website text,
|
|
status text default 'active'::text not null,
|
|
notes text,
|
|
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
|
|
updated_at timestamp with time zone default timezone('utc'::text, now()) not null
|
|
);
|
|
|
|
create table if not exists public.projects (
|
|
id uuid default uuid_generate_v4() primary key,
|
|
user_id uuid references auth.users(id) on delete cascade not null,
|
|
client_id uuid references public.clients(id) on delete set null,
|
|
name text not null,
|
|
type text default 'client_project'::text not null,
|
|
description text,
|
|
status text default 'planning'::text not null,
|
|
start_date date,
|
|
due_date date,
|
|
budget_amount numeric(12, 2),
|
|
currency text default 'USD'::text not null,
|
|
progress integer default 0 not null,
|
|
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
|
|
updated_at timestamp with time zone default timezone('utc'::text, now()) not null
|
|
);
|
|
|
|
create table if not exists public.calendar_events (
|
|
id uuid default uuid_generate_v4() primary key,
|
|
user_id uuid references auth.users(id) on delete cascade not null,
|
|
client_id uuid references public.clients(id) on delete set null,
|
|
project_id uuid references public.projects(id) on delete set null,
|
|
task_id uuid references public.tasks(id) on delete set null,
|
|
title text not null,
|
|
description text,
|
|
type text default 'focus'::text not null,
|
|
starts_at timestamp with time zone not null,
|
|
ends_at timestamp with time zone,
|
|
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
|
|
updated_at timestamp with time zone default timezone('utc'::text, now()) not null
|
|
);
|
|
|
|
create table if not exists public.finance_transactions (
|
|
id uuid default uuid_generate_v4() primary key,
|
|
user_id uuid references auth.users(id) on delete cascade not null,
|
|
client_id uuid references public.clients(id) on delete set null,
|
|
project_id uuid references public.projects(id) on delete set null,
|
|
type text not null,
|
|
amount numeric(12, 2) not null,
|
|
currency text default 'USD'::text not null,
|
|
transaction_date date default current_date not null,
|
|
category text,
|
|
payment_status text default 'planned'::text not null,
|
|
description text,
|
|
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
|
|
updated_at timestamp with time zone default timezone('utc'::text, now()) not null
|
|
);
|
|
|
|
create table if not exists public.daily_logs (
|
|
id uuid default uuid_generate_v4() primary key,
|
|
user_id uuid references auth.users(id) on delete cascade not null,
|
|
log_date date default current_date not null,
|
|
mood_score smallint not null,
|
|
energy_score smallint not null,
|
|
work_satisfaction_score smallint,
|
|
note text,
|
|
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
|
|
updated_at timestamp with time zone default timezone('utc'::text, now()) not null,
|
|
unique (user_id, log_date)
|
|
);
|
|
|
|
create table if not exists public.app_settings (
|
|
id uuid default uuid_generate_v4() primary key,
|
|
user_id uuid references auth.users(id) on delete cascade not null unique,
|
|
timezone text default 'UTC'::text not null,
|
|
currency text default 'USD'::text not null,
|
|
ai_provider text default 'ollama'::text not null,
|
|
ai_model text,
|
|
api_key text,
|
|
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
|
|
updated_at timestamp with time zone default timezone('utc'::text, now()) not null
|
|
);
|
|
|
|
alter table public.tasks
|
|
add column if not exists client_id uuid references public.clients(id) on delete set null,
|
|
add column if not exists project_id uuid references public.projects(id) on delete set null,
|
|
add column if not exists priority text default 'medium'::text not null,
|
|
add column if not exists due_at timestamp with time zone,
|
|
add column if not exists estimated_minutes integer,
|
|
add column if not exists actual_minutes integer;
|
|
|
|
do $$
|
|
begin
|
|
if not exists (select 1 from pg_constraint where conname = 'clients_status_check') then
|
|
alter table public.clients
|
|
add constraint clients_status_check
|
|
check (status in ('active', 'paused', 'archived'));
|
|
end if;
|
|
|
|
if not exists (select 1 from pg_constraint where conname = 'projects_type_check') then
|
|
alter table public.projects
|
|
add constraint projects_type_check
|
|
check (type in ('client_project', 'side_project'));
|
|
end if;
|
|
|
|
if not exists (select 1 from pg_constraint where conname = 'projects_status_check') then
|
|
alter table public.projects
|
|
add constraint projects_status_check
|
|
check (status in ('planning', 'active', 'paused', 'completed', 'cancelled'));
|
|
end if;
|
|
|
|
if not exists (select 1 from pg_constraint where conname = 'projects_progress_check') then
|
|
alter table public.projects
|
|
add constraint projects_progress_check
|
|
check (progress >= 0 and progress <= 100);
|
|
end if;
|
|
|
|
if not exists (select 1 from pg_constraint where conname = 'tasks_priority_check') then
|
|
alter table public.tasks
|
|
add constraint tasks_priority_check
|
|
check (priority in ('low', 'medium', 'high', 'urgent'));
|
|
end if;
|
|
|
|
if not exists (select 1 from pg_constraint where conname = 'calendar_events_type_check') then
|
|
alter table public.calendar_events
|
|
add constraint calendar_events_type_check
|
|
check (type in ('meeting', 'focus', 'deadline', 'personal', 'finance'));
|
|
end if;
|
|
|
|
if not exists (select 1 from pg_constraint where conname = 'calendar_events_time_check') then
|
|
alter table public.calendar_events
|
|
add constraint calendar_events_time_check
|
|
check (ends_at is null or ends_at >= starts_at);
|
|
end if;
|
|
|
|
if not exists (select 1 from pg_constraint where conname = 'finance_transactions_type_check') then
|
|
alter table public.finance_transactions
|
|
add constraint finance_transactions_type_check
|
|
check (type in ('income', 'expense'));
|
|
end if;
|
|
|
|
if not exists (select 1 from pg_constraint where conname = 'finance_transactions_amount_check') then
|
|
alter table public.finance_transactions
|
|
add constraint finance_transactions_amount_check
|
|
check (amount >= 0);
|
|
end if;
|
|
|
|
if not exists (select 1 from pg_constraint where conname = 'finance_transactions_payment_status_check') then
|
|
alter table public.finance_transactions
|
|
add constraint finance_transactions_payment_status_check
|
|
check (payment_status in ('planned', 'pending', 'paid', 'cancelled'));
|
|
end if;
|
|
|
|
if not exists (select 1 from pg_constraint where conname = 'daily_logs_mood_score_check') then
|
|
alter table public.daily_logs
|
|
add constraint daily_logs_mood_score_check
|
|
check (mood_score between 1 and 5);
|
|
end if;
|
|
|
|
if not exists (select 1 from pg_constraint where conname = 'daily_logs_energy_score_check') then
|
|
alter table public.daily_logs
|
|
add constraint daily_logs_energy_score_check
|
|
check (energy_score between 1 and 5);
|
|
end if;
|
|
|
|
if not exists (select 1 from pg_constraint where conname = 'daily_logs_work_satisfaction_score_check') then
|
|
alter table public.daily_logs
|
|
add constraint daily_logs_work_satisfaction_score_check
|
|
check (work_satisfaction_score is null or work_satisfaction_score between 1 and 5);
|
|
end if;
|
|
|
|
if not exists (select 1 from pg_constraint where conname = 'app_settings_ai_provider_check') then
|
|
alter table public.app_settings
|
|
add constraint app_settings_ai_provider_check
|
|
check (ai_provider in ('ollama', 'openai', 'gemini', 'groq'));
|
|
end if;
|
|
end $$;
|
|
|
|
create index if not exists clients_user_id_idx on public.clients(user_id);
|
|
create index if not exists clients_status_idx on public.clients(status);
|
|
|
|
create index if not exists projects_user_id_idx on public.projects(user_id);
|
|
create index if not exists projects_client_id_idx on public.projects(client_id);
|
|
create index if not exists projects_status_idx on public.projects(status);
|
|
create index if not exists projects_due_date_idx on public.projects(due_date);
|
|
|
|
create index if not exists tasks_client_id_idx on public.tasks(client_id);
|
|
create index if not exists tasks_project_id_idx on public.tasks(project_id);
|
|
create index if not exists tasks_due_at_idx on public.tasks(due_at);
|
|
create index if not exists tasks_priority_idx on public.tasks(priority);
|
|
|
|
create index if not exists calendar_events_user_id_idx on public.calendar_events(user_id);
|
|
create index if not exists calendar_events_starts_at_idx on public.calendar_events(starts_at);
|
|
create index if not exists calendar_events_project_id_idx on public.calendar_events(project_id);
|
|
create index if not exists calendar_events_task_id_idx on public.calendar_events(task_id);
|
|
|
|
create index if not exists finance_transactions_user_id_idx on public.finance_transactions(user_id);
|
|
create index if not exists finance_transactions_client_id_idx on public.finance_transactions(client_id);
|
|
create index if not exists finance_transactions_project_id_idx on public.finance_transactions(project_id);
|
|
create index if not exists finance_transactions_transaction_date_idx on public.finance_transactions(transaction_date);
|
|
create index if not exists finance_transactions_payment_status_idx on public.finance_transactions(payment_status);
|
|
|
|
create index if not exists daily_logs_user_id_idx on public.daily_logs(user_id);
|
|
create index if not exists daily_logs_log_date_idx on public.daily_logs(log_date);
|
|
|
|
alter table public.clients enable row level security;
|
|
alter table public.projects enable row level security;
|
|
alter table public.calendar_events enable row level security;
|
|
alter table public.finance_transactions enable row level security;
|
|
alter table public.daily_logs enable row level security;
|
|
alter table public.app_settings enable row level security;
|
|
|
|
drop policy if exists "Users can view their own clients." on public.clients;
|
|
create policy "Users can view their own clients." on public.clients
|
|
for select using (auth.uid() = user_id);
|
|
drop policy if exists "Users can insert their own clients." on public.clients;
|
|
create policy "Users can insert their own clients." on public.clients
|
|
for insert with check (auth.uid() = user_id);
|
|
drop policy if exists "Users can update their own clients." on public.clients;
|
|
create policy "Users can update their own clients." on public.clients
|
|
for update using (auth.uid() = user_id);
|
|
drop policy if exists "Users can delete their own clients." on public.clients;
|
|
create policy "Users can delete their own clients." on public.clients
|
|
for delete using (auth.uid() = user_id);
|
|
|
|
drop policy if exists "Users can view their own projects." on public.projects;
|
|
create policy "Users can view their own projects." on public.projects
|
|
for select using (auth.uid() = user_id);
|
|
drop policy if exists "Users can insert their own projects." on public.projects;
|
|
create policy "Users can insert their own projects." on public.projects
|
|
for insert with check (auth.uid() = user_id);
|
|
drop policy if exists "Users can update their own projects." on public.projects;
|
|
create policy "Users can update their own projects." on public.projects
|
|
for update using (auth.uid() = user_id);
|
|
drop policy if exists "Users can delete their own projects." on public.projects;
|
|
create policy "Users can delete their own projects." on public.projects
|
|
for delete using (auth.uid() = user_id);
|
|
|
|
drop policy if exists "Users can view their own calendar events." on public.calendar_events;
|
|
create policy "Users can view their own calendar events." on public.calendar_events
|
|
for select using (auth.uid() = user_id);
|
|
drop policy if exists "Users can insert their own calendar events." on public.calendar_events;
|
|
create policy "Users can insert their own calendar events." on public.calendar_events
|
|
for insert with check (auth.uid() = user_id);
|
|
drop policy if exists "Users can update their own calendar events." on public.calendar_events;
|
|
create policy "Users can update their own calendar events." on public.calendar_events
|
|
for update using (auth.uid() = user_id);
|
|
drop policy if exists "Users can delete their own calendar events." on public.calendar_events;
|
|
create policy "Users can delete their own calendar events." on public.calendar_events
|
|
for delete using (auth.uid() = user_id);
|
|
|
|
drop policy if exists "Users can view their own finance transactions." on public.finance_transactions;
|
|
create policy "Users can view their own finance transactions." on public.finance_transactions
|
|
for select using (auth.uid() = user_id);
|
|
drop policy if exists "Users can insert their own finance transactions." on public.finance_transactions;
|
|
create policy "Users can insert their own finance transactions." on public.finance_transactions
|
|
for insert with check (auth.uid() = user_id);
|
|
drop policy if exists "Users can update their own finance transactions." on public.finance_transactions;
|
|
create policy "Users can update their own finance transactions." on public.finance_transactions
|
|
for update using (auth.uid() = user_id);
|
|
drop policy if exists "Users can delete their own finance transactions." on public.finance_transactions;
|
|
create policy "Users can delete their own finance transactions." on public.finance_transactions
|
|
for delete using (auth.uid() = user_id);
|
|
|
|
drop policy if exists "Users can view their own daily logs." on public.daily_logs;
|
|
create policy "Users can view their own daily logs." on public.daily_logs
|
|
for select using (auth.uid() = user_id);
|
|
drop policy if exists "Users can insert their own daily logs." on public.daily_logs;
|
|
create policy "Users can insert their own daily logs." on public.daily_logs
|
|
for insert with check (auth.uid() = user_id);
|
|
drop policy if exists "Users can update their own daily logs." on public.daily_logs;
|
|
create policy "Users can update their own daily logs." on public.daily_logs
|
|
for update using (auth.uid() = user_id);
|
|
drop policy if exists "Users can delete their own daily logs." on public.daily_logs;
|
|
create policy "Users can delete their own daily logs." on public.daily_logs
|
|
for delete using (auth.uid() = user_id);
|
|
|
|
drop policy if exists "Users can view their own app settings." on public.app_settings;
|
|
create policy "Users can view their own app settings." on public.app_settings
|
|
for select using (auth.uid() = user_id);
|
|
drop policy if exists "Users can insert their own app settings." on public.app_settings;
|
|
create policy "Users can insert their own app settings." on public.app_settings
|
|
for insert with check (auth.uid() = user_id);
|
|
drop policy if exists "Users can update their own app settings." on public.app_settings;
|
|
create policy "Users can update their own app settings." on public.app_settings
|
|
for update using (auth.uid() = user_id);
|
|
drop policy if exists "Users can delete their own app settings." on public.app_settings;
|
|
create policy "Users can delete their own app settings." on public.app_settings
|
|
for delete using (auth.uid() = user_id);
|
|
|
|
drop trigger if exists set_clients_updated_at on public.clients;
|
|
create trigger set_clients_updated_at
|
|
before update on public.clients
|
|
for each row execute procedure public.set_updated_at();
|
|
|
|
drop trigger if exists set_projects_updated_at on public.projects;
|
|
create trigger set_projects_updated_at
|
|
before update on public.projects
|
|
for each row execute procedure public.set_updated_at();
|
|
|
|
drop trigger if exists set_calendar_events_updated_at on public.calendar_events;
|
|
create trigger set_calendar_events_updated_at
|
|
before update on public.calendar_events
|
|
for each row execute procedure public.set_updated_at();
|
|
|
|
drop trigger if exists set_finance_transactions_updated_at on public.finance_transactions;
|
|
create trigger set_finance_transactions_updated_at
|
|
before update on public.finance_transactions
|
|
for each row execute procedure public.set_updated_at();
|
|
|
|
drop trigger if exists set_daily_logs_updated_at on public.daily_logs;
|
|
create trigger set_daily_logs_updated_at
|
|
before update on public.daily_logs
|
|
for each row execute procedure public.set_updated_at();
|
|
|
|
drop trigger if exists set_app_settings_updated_at on public.app_settings;
|
|
create trigger set_app_settings_updated_at
|
|
before update on public.app_settings
|
|
for each row execute procedure public.set_updated_at();
|
|
|
|
-- -----------------------------------------------------------------------------
|
|
-- Source: supabase/migrations/0003_add_project_planning_assets.sql
|
|
-- -----------------------------------------------------------------------------
|
|
|
|
-- 0003: Add project planning sections and project visual storage
|
|
-- Run after: supabase/migrations/0002_add_freelancer_os_core_tables.sql
|
|
|
|
alter table public.projects
|
|
add column if not exists cover_image_path text,
|
|
add column if not exists cover_image_alt text;
|
|
|
|
create table if not exists public.project_planning_sections (
|
|
id uuid default uuid_generate_v4() primary key,
|
|
user_id uuid references auth.users(id) on delete cascade not null,
|
|
project_id uuid references public.projects(id) on delete cascade not null,
|
|
category text not null,
|
|
title text not null,
|
|
content text,
|
|
metadata jsonb default '{}'::jsonb not null,
|
|
sort_order integer default 0 not null,
|
|
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
|
|
updated_at timestamp with time zone default timezone('utc'::text, now()) not null
|
|
);
|
|
|
|
do $$
|
|
begin
|
|
if not exists (select 1 from pg_constraint where conname = 'project_planning_sections_category_check') then
|
|
alter table public.project_planning_sections
|
|
add constraint project_planning_sections_category_check
|
|
check (
|
|
category in (
|
|
'overview',
|
|
'problem',
|
|
'goal',
|
|
'audience',
|
|
'scope',
|
|
'design_system',
|
|
'color_palette',
|
|
'typography',
|
|
'assets',
|
|
'notes'
|
|
)
|
|
);
|
|
end if;
|
|
end $$;
|
|
|
|
create index if not exists project_planning_sections_user_id_idx
|
|
on public.project_planning_sections(user_id);
|
|
|
|
create index if not exists project_planning_sections_project_id_idx
|
|
on public.project_planning_sections(project_id);
|
|
|
|
create index if not exists project_planning_sections_category_idx
|
|
on public.project_planning_sections(category);
|
|
|
|
alter table public.project_planning_sections enable row level security;
|
|
|
|
drop policy if exists "Users can view their own project planning sections." on public.project_planning_sections;
|
|
create policy "Users can view their own project planning sections." on public.project_planning_sections
|
|
for select using (auth.uid() = user_id);
|
|
|
|
drop policy if exists "Users can insert their own project planning sections." on public.project_planning_sections;
|
|
create policy "Users can insert their own project planning sections." on public.project_planning_sections
|
|
for insert with check (auth.uid() = user_id);
|
|
|
|
drop policy if exists "Users can update their own project planning sections." on public.project_planning_sections;
|
|
create policy "Users can update their own project planning sections." on public.project_planning_sections
|
|
for update using (auth.uid() = user_id);
|
|
|
|
drop policy if exists "Users can delete their own project planning sections." on public.project_planning_sections;
|
|
create policy "Users can delete their own project planning sections." on public.project_planning_sections
|
|
for delete using (auth.uid() = user_id);
|
|
|
|
drop trigger if exists set_project_planning_sections_updated_at on public.project_planning_sections;
|
|
create trigger set_project_planning_sections_updated_at
|
|
before update on public.project_planning_sections
|
|
for each row execute procedure public.set_updated_at();
|
|
|
|
insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
|
values (
|
|
'project-assets',
|
|
'project-assets',
|
|
false,
|
|
5242880,
|
|
array['image/jpeg', 'image/png', 'image/webp', 'image/gif']
|
|
)
|
|
on conflict (id) do update set
|
|
public = excluded.public,
|
|
file_size_limit = excluded.file_size_limit,
|
|
allowed_mime_types = excluded.allowed_mime_types;
|
|
|
|
drop policy if exists "Users can view their own project assets." on storage.objects;
|
|
create policy "Users can view their own project assets." on storage.objects
|
|
for select using (
|
|
bucket_id = 'project-assets'
|
|
and (
|
|
public.neta_current_jwt_role() = 'service_role'
|
|
or auth.uid()::text = (storage.foldername(name))[1]
|
|
)
|
|
);
|
|
|
|
drop policy if exists "Users can upload their own project assets." on storage.objects;
|
|
create policy "Users can upload their own project assets." on storage.objects
|
|
for insert with check (
|
|
bucket_id = 'project-assets'
|
|
and (
|
|
public.neta_current_jwt_role() = 'service_role'
|
|
or auth.uid()::text = (storage.foldername(name))[1]
|
|
)
|
|
);
|
|
|
|
drop policy if exists "Users can update their own project assets." on storage.objects;
|
|
create policy "Users can update their own project assets." on storage.objects
|
|
for update using (
|
|
bucket_id = 'project-assets'
|
|
and (
|
|
public.neta_current_jwt_role() = 'service_role'
|
|
or auth.uid()::text = (storage.foldername(name))[1]
|
|
)
|
|
);
|
|
|
|
drop policy if exists "Users can delete their own project assets." on storage.objects;
|
|
create policy "Users can delete their own project assets." on storage.objects
|
|
for delete using (
|
|
bucket_id = 'project-assets'
|
|
and (
|
|
public.neta_current_jwt_role() = 'service_role'
|
|
or auth.uid()::text = (storage.foldername(name))[1]
|
|
)
|
|
);
|
|
|
|
-- -----------------------------------------------------------------------------
|
|
-- Source: supabase/migrations/0004_add_business_os_tables.sql
|
|
-- -----------------------------------------------------------------------------
|
|
|
|
-- 0004: Faz 6 - Freelancer Business OS Tables
|
|
|
|
-- 1. Proposals
|
|
create table if not exists public.proposals (
|
|
id uuid default uuid_generate_v4() primary key,
|
|
user_id uuid references auth.users(id) on delete cascade not null,
|
|
client_id uuid references public.clients(id) on delete set null,
|
|
project_id uuid references public.projects(id) on delete set null,
|
|
title text not null,
|
|
description text,
|
|
amount numeric(12,2) not null default 0,
|
|
currency text default 'TRY'::text,
|
|
status text default 'draft'::text check (status in ('draft', 'sent', 'accepted', 'rejected')),
|
|
valid_until timestamp with time zone,
|
|
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
|
|
updated_at timestamp with time zone default timezone('utc'::text, now()) not null
|
|
);
|
|
|
|
-- 2. Contracts
|
|
create table if not exists public.contracts (
|
|
id uuid default uuid_generate_v4() primary key,
|
|
user_id uuid references auth.users(id) on delete cascade not null,
|
|
proposal_id uuid references public.proposals(id) on delete set null,
|
|
client_id uuid references public.clients(id) on delete set null,
|
|
title text not null,
|
|
content text,
|
|
status text default 'draft'::text check (status in ('draft', 'active', 'completed', 'cancelled')),
|
|
signed_at timestamp with time zone,
|
|
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
|
|
updated_at timestamp with time zone default timezone('utc'::text, now()) not null
|
|
);
|
|
|
|
-- 3. Invoices
|
|
create table if not exists public.invoices (
|
|
id uuid default uuid_generate_v4() primary key,
|
|
user_id uuid references auth.users(id) on delete cascade not null,
|
|
client_id uuid references public.clients(id) on delete set null,
|
|
project_id uuid references public.projects(id) on delete set null,
|
|
invoice_number text not null,
|
|
amount numeric(12,2) not null default 0,
|
|
tax_rate numeric(5,2) default 0, -- percentage
|
|
currency text default 'TRY'::text,
|
|
status text default 'draft'::text check (status in ('draft', 'sent', 'paid', 'overdue', 'cancelled')),
|
|
issue_date timestamp with time zone default timezone('utc'::text, now()),
|
|
due_date timestamp with time zone,
|
|
paid_at timestamp with time zone,
|
|
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
|
|
updated_at timestamp with time zone default timezone('utc'::text, now()) not null
|
|
);
|
|
|
|
-- 4. Subscriptions
|
|
create table if not exists public.subscriptions (
|
|
id uuid default uuid_generate_v4() primary key,
|
|
user_id uuid references auth.users(id) on delete cascade not null,
|
|
name text not null,
|
|
amount numeric(12,2) not null default 0,
|
|
currency text default 'TRY'::text,
|
|
billing_cycle text default 'monthly'::text check (billing_cycle in ('monthly', 'yearly', 'weekly')),
|
|
next_billing_date timestamp with time zone,
|
|
status text default 'active'::text check (status in ('active', 'cancelled')),
|
|
category text, -- e.g., 'software', 'hosting', 'marketing'
|
|
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
|
|
updated_at timestamp with time zone default timezone('utc'::text, now()) not null
|
|
);
|
|
|
|
-- Enable RLS
|
|
alter table public.proposals enable row level security;
|
|
alter table public.contracts enable row level security;
|
|
alter table public.invoices enable row level security;
|
|
alter table public.subscriptions enable row level security;
|
|
|
|
-- Proposals RLS
|
|
drop policy if exists "Users can view their own proposals" on public.proposals;
|
|
create policy "Users can view their own proposals" on public.proposals for select using (auth.uid() = user_id);
|
|
drop policy if exists "Users can insert their own proposals" on public.proposals;
|
|
create policy "Users can insert their own proposals" on public.proposals for insert with check (auth.uid() = user_id);
|
|
drop policy if exists "Users can update their own proposals" on public.proposals;
|
|
create policy "Users can update their own proposals" on public.proposals for update using (auth.uid() = user_id);
|
|
drop policy if exists "Users can delete their own proposals" on public.proposals;
|
|
create policy "Users can delete their own proposals" on public.proposals for delete using (auth.uid() = user_id);
|
|
|
|
-- Contracts RLS
|
|
drop policy if exists "Users can view their own contracts" on public.contracts;
|
|
create policy "Users can view their own contracts" on public.contracts for select using (auth.uid() = user_id);
|
|
drop policy if exists "Users can insert their own contracts" on public.contracts;
|
|
create policy "Users can insert their own contracts" on public.contracts for insert with check (auth.uid() = user_id);
|
|
drop policy if exists "Users can update their own contracts" on public.contracts;
|
|
create policy "Users can update their own contracts" on public.contracts for update using (auth.uid() = user_id);
|
|
drop policy if exists "Users can delete their own contracts" on public.contracts;
|
|
create policy "Users can delete their own contracts" on public.contracts for delete using (auth.uid() = user_id);
|
|
|
|
-- Invoices RLS
|
|
drop policy if exists "Users can view their own invoices" on public.invoices;
|
|
create policy "Users can view their own invoices" on public.invoices for select using (auth.uid() = user_id);
|
|
drop policy if exists "Users can insert their own invoices" on public.invoices;
|
|
create policy "Users can insert their own invoices" on public.invoices for insert with check (auth.uid() = user_id);
|
|
drop policy if exists "Users can update their own invoices" on public.invoices;
|
|
create policy "Users can update their own invoices" on public.invoices for update using (auth.uid() = user_id);
|
|
drop policy if exists "Users can delete their own invoices" on public.invoices;
|
|
create policy "Users can delete their own invoices" on public.invoices for delete using (auth.uid() = user_id);
|
|
|
|
-- Subscriptions RLS
|
|
drop policy if exists "Users can view their own subscriptions" on public.subscriptions;
|
|
create policy "Users can view their own subscriptions" on public.subscriptions for select using (auth.uid() = user_id);
|
|
drop policy if exists "Users can insert their own subscriptions" on public.subscriptions;
|
|
create policy "Users can insert their own subscriptions" on public.subscriptions for insert with check (auth.uid() = user_id);
|
|
drop policy if exists "Users can update their own subscriptions" on public.subscriptions;
|
|
create policy "Users can update their own subscriptions" on public.subscriptions for update using (auth.uid() = user_id);
|
|
drop policy if exists "Users can delete their own subscriptions" on public.subscriptions;
|
|
create policy "Users can delete their own subscriptions" on public.subscriptions for delete using (auth.uid() = user_id);
|
|
|
|
-- -----------------------------------------------------------------------------
|
|
-- Source: supabase/migrations/0005_add_advanced_crm_tables.sql
|
|
-- -----------------------------------------------------------------------------
|
|
|
|
-- 0005: Faz 7 - Advanced CRM Tables
|
|
|
|
-- 1. Alter clients table to add CRM specific columns
|
|
alter table public.clients
|
|
add column if not exists pipeline_stage text default 'lead'::text check (pipeline_stage in ('lead', 'contacted', 'proposal_sent', 'won', 'lost')),
|
|
add column if not exists next_follow_up_date timestamp with time zone,
|
|
add column if not exists last_contact_date timestamp with time zone,
|
|
add column if not exists client_value_score numeric(5,2) default 0;
|
|
|
|
-- 2. Create client_activities table
|
|
create table if not exists public.client_activities (
|
|
id uuid default uuid_generate_v4() primary key,
|
|
user_id uuid references auth.users(id) on delete cascade not null,
|
|
client_id uuid references public.clients(id) on delete cascade not null,
|
|
type text not null check (type in ('note', 'call', 'meeting', 'email')),
|
|
title text not null,
|
|
content text,
|
|
activity_date timestamp with time zone default timezone('utc'::text, now()) not null,
|
|
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
|
|
updated_at timestamp with time zone default timezone('utc'::text, now()) not null
|
|
);
|
|
|
|
-- Enable RLS
|
|
alter table public.client_activities enable row level security;
|
|
|
|
-- Client Activities RLS
|
|
drop policy if exists "Users can view their own client activities" on public.client_activities;
|
|
create policy "Users can view their own client activities" on public.client_activities for select using (auth.uid() = user_id);
|
|
drop policy if exists "Users can insert their own client activities" on public.client_activities;
|
|
create policy "Users can insert their own client activities" on public.client_activities for insert with check (auth.uid() = user_id);
|
|
drop policy if exists "Users can update their own client activities" on public.client_activities;
|
|
create policy "Users can update their own client activities" on public.client_activities for update using (auth.uid() = user_id);
|
|
drop policy if exists "Users can delete their own client activities" on public.client_activities;
|
|
create policy "Users can delete their own client activities" on public.client_activities for delete using (auth.uid() = user_id);
|
|
|
|
-- -----------------------------------------------------------------------------
|
|
-- Source: supabase/migrations/0006_add_pgvector_and_embeddings.sql
|
|
-- -----------------------------------------------------------------------------
|
|
|
|
-- 0006: Faz 8 - pgvector & RAG Embeddings
|
|
|
|
-- Enable the pgvector extension to work with embedding vectors
|
|
create extension if not exists vector;
|
|
|
|
-- Create a table to store document embeddings for RAG
|
|
create table if not exists public.document_embeddings (
|
|
id uuid default uuid_generate_v4() primary key,
|
|
user_id uuid references auth.users(id) on delete cascade not null,
|
|
content text not null,
|
|
metadata jsonb, -- e.g. { "source_type": "note", "source_id": "123" }
|
|
embedding vector(1536), -- 1536 works for OpenAI text-embedding-3-small and text-embedding-ada-002
|
|
created_at timestamp with time zone default timezone('utc'::text, now()) not null
|
|
);
|
|
|
|
-- Enable RLS
|
|
alter table public.document_embeddings enable row level security;
|
|
|
|
drop policy if exists "Users can view their own embeddings" on public.document_embeddings;
|
|
create policy "Users can view their own embeddings" on public.document_embeddings for select using (auth.uid() = user_id);
|
|
drop policy if exists "Users can insert their own embeddings" on public.document_embeddings;
|
|
create policy "Users can insert their own embeddings" on public.document_embeddings for insert with check (auth.uid() = user_id);
|
|
drop policy if exists "Users can update their own embeddings" on public.document_embeddings;
|
|
create policy "Users can update their own embeddings" on public.document_embeddings for update using (auth.uid() = user_id);
|
|
drop policy if exists "Users can delete their own embeddings" on public.document_embeddings;
|
|
create policy "Users can delete their own embeddings" on public.document_embeddings for delete using (auth.uid() = user_id);
|
|
|
|
-- Create a function to similarity search for embeddings
|
|
create or replace function match_documents (
|
|
query_embedding vector(1536),
|
|
match_count int default null,
|
|
filter_user_id uuid default null
|
|
) returns table (
|
|
id uuid,
|
|
content text,
|
|
metadata jsonb,
|
|
similarity float
|
|
)
|
|
language plpgsql
|
|
as $$
|
|
#variable_conflict use_column
|
|
begin
|
|
return query
|
|
select
|
|
document_embeddings.id,
|
|
document_embeddings.content,
|
|
document_embeddings.metadata,
|
|
1 - (document_embeddings.embedding <=> query_embedding) as similarity
|
|
from document_embeddings
|
|
where document_embeddings.user_id = filter_user_id
|
|
order by document_embeddings.embedding <=> query_embedding
|
|
limit match_count;
|
|
end;
|
|
$$;
|
|
|
|
-- -----------------------------------------------------------------------------
|
|
-- Source: supabase/migrations/0007_add_client_portal_tables.sql
|
|
-- -----------------------------------------------------------------------------
|
|
|
|
-- 0007: Add Client Portal Tables and Roles
|
|
-- Run after: 0006_add_pgvector_and_embeddings.sql
|
|
|
|
-- 1. Update Profiles with Role
|
|
ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS role text DEFAULT 'freelancer'::text CHECK (role IN ('freelancer', 'client'));
|
|
|
|
-- 2. Update Clients to link to Client Auth ID
|
|
ALTER TABLE public.clients ADD COLUMN IF NOT EXISTS client_auth_id uuid REFERENCES auth.users(id) ON DELETE SET NULL;
|
|
|
|
-- 3. Update Tasks to add Client Visibility
|
|
ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS is_public_to_client boolean DEFAULT false;
|
|
|
|
-- 4. Create Project Revisions table
|
|
CREATE TABLE IF NOT EXISTS public.project_revisions (
|
|
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
|
|
project_id uuid REFERENCES public.projects(id) ON DELETE CASCADE NOT NULL,
|
|
client_id uuid REFERENCES public.clients(id) ON DELETE CASCADE NOT NULL,
|
|
requested_by uuid REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
|
|
description text NOT NULL,
|
|
status text DEFAULT 'pending'::text CHECK (status IN ('pending', 'in_progress', 'completed', 'rejected')),
|
|
created_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL,
|
|
updated_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL
|
|
);
|
|
|
|
-- Trigger to set updated_at
|
|
DROP TRIGGER IF EXISTS set_project_revisions_updated_at ON public.project_revisions;
|
|
CREATE TRIGGER set_project_revisions_updated_at
|
|
BEFORE UPDATE ON public.project_revisions
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.set_updated_at();
|
|
|
|
-- 5. Enable RLS
|
|
ALTER TABLE public.project_revisions ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- 6. Add RLS Policies for Client Access
|
|
|
|
-- PROFILES
|
|
-- Clients can read their own profile
|
|
DROP POLICY IF EXISTS "Clients can view own profile" ON public.profiles;
|
|
CREATE POLICY "Clients can view own profile"
|
|
ON public.profiles FOR SELECT
|
|
USING (auth.uid() = id);
|
|
|
|
-- CLIENTS
|
|
-- Clients can read their own client record
|
|
DROP POLICY IF EXISTS "Clients can view own client record" ON public.clients;
|
|
CREATE POLICY "Clients can view own client record"
|
|
ON public.clients FOR SELECT
|
|
USING (client_auth_id = auth.uid());
|
|
|
|
-- PROJECTS
|
|
-- Clients can view projects where client_id matches their client record
|
|
DROP POLICY IF EXISTS "Clients can view own projects" ON public.projects;
|
|
CREATE POLICY "Clients can view own projects"
|
|
ON public.projects FOR SELECT
|
|
USING (
|
|
client_id IN (
|
|
SELECT id FROM public.clients WHERE client_auth_id = auth.uid()
|
|
)
|
|
);
|
|
|
|
-- TASKS
|
|
-- Clients can view tasks linked to their projects IF is_public_to_client is true
|
|
DROP POLICY IF EXISTS "Clients can view public tasks of their projects" ON public.tasks;
|
|
CREATE POLICY "Clients can view public tasks of their projects"
|
|
ON public.tasks FOR SELECT
|
|
USING (
|
|
is_public_to_client = true
|
|
AND project_id IN (
|
|
SELECT id FROM public.projects WHERE client_id IN (
|
|
SELECT id FROM public.clients WHERE client_auth_id = auth.uid()
|
|
)
|
|
)
|
|
);
|
|
|
|
-- PROJECT PLANNING SECTIONS (Milestones, Roadmap, etc.)
|
|
-- Clients can view planning sections of their projects
|
|
DROP POLICY IF EXISTS "Clients can view planning sections of their projects" ON public.project_planning_sections;
|
|
CREATE POLICY "Clients can view planning sections of their projects"
|
|
ON public.project_planning_sections FOR SELECT
|
|
USING (
|
|
project_id IN (
|
|
SELECT id FROM public.projects WHERE client_id IN (
|
|
SELECT id FROM public.clients WHERE client_auth_id = auth.uid()
|
|
)
|
|
)
|
|
);
|
|
|
|
-- PROJECT REVISIONS
|
|
-- Freelancers can manage all revisions for their projects
|
|
DROP POLICY IF EXISTS "Freelancers can manage revisions" ON public.project_revisions;
|
|
CREATE POLICY "Freelancers can manage revisions"
|
|
ON public.project_revisions FOR ALL
|
|
USING (
|
|
project_id IN (
|
|
SELECT id FROM public.projects WHERE user_id = auth.uid()
|
|
)
|
|
);
|
|
|
|
-- Clients can insert revisions for their projects
|
|
DROP POLICY IF EXISTS "Clients can insert revisions" ON public.project_revisions;
|
|
CREATE POLICY "Clients can insert revisions"
|
|
ON public.project_revisions FOR INSERT
|
|
WITH CHECK (
|
|
client_id IN (
|
|
SELECT id FROM public.clients WHERE client_auth_id = auth.uid()
|
|
)
|
|
AND requested_by = auth.uid()
|
|
);
|
|
|
|
-- Clients can view their own revisions
|
|
DROP POLICY IF EXISTS "Clients can view own revisions" ON public.project_revisions;
|
|
CREATE POLICY "Clients can view own revisions"
|
|
ON public.project_revisions FOR SELECT
|
|
USING (
|
|
client_id IN (
|
|
SELECT id FROM public.clients WHERE client_auth_id = auth.uid()
|
|
)
|
|
);
|
|
|
|
-- -----------------------------------------------------------------------------
|
|
-- Source: supabase/migrations/0008_add_project_progress_and_quota.sql
|
|
-- -----------------------------------------------------------------------------
|
|
|
|
-- 0008: Add Project Progress Type and Revision Quota
|
|
|
|
-- Add columns to projects table
|
|
ALTER TABLE public.projects
|
|
ADD COLUMN IF NOT EXISTS progress_type text DEFAULT 'manual'::text CHECK (progress_type IN ('manual', 'auto')),
|
|
ADD COLUMN IF NOT EXISTS revision_quota integer DEFAULT 0;
|
|
|
|
-- Function to update project progress automatically if progress_type is 'auto'
|
|
CREATE OR REPLACE FUNCTION public.update_project_progress_on_task_change()
|
|
RETURNS TRIGGER AS $$
|
|
DECLARE
|
|
v_project_id uuid;
|
|
v_progress_type text;
|
|
v_total_tasks integer;
|
|
v_done_tasks integer;
|
|
v_new_progress integer;
|
|
BEGIN
|
|
IF TG_OP = 'DELETE' THEN
|
|
v_project_id := OLD.project_id;
|
|
ELSE
|
|
v_project_id := NEW.project_id;
|
|
END IF;
|
|
|
|
IF v_project_id IS NULL THEN
|
|
RETURN NULL;
|
|
END IF;
|
|
|
|
SELECT progress_type INTO v_progress_type FROM public.projects WHERE id = v_project_id;
|
|
|
|
IF v_progress_type = 'auto' THEN
|
|
SELECT count(*) INTO v_total_tasks FROM public.tasks WHERE project_id = v_project_id;
|
|
SELECT count(*) INTO v_done_tasks FROM public.tasks WHERE project_id = v_project_id AND status = 'done';
|
|
|
|
IF v_total_tasks > 0 THEN
|
|
v_new_progress := round((v_done_tasks::numeric / v_total_tasks::numeric) * 100);
|
|
ELSE
|
|
v_new_progress := 0;
|
|
END IF;
|
|
|
|
UPDATE public.projects SET progress = v_new_progress WHERE id = v_project_id;
|
|
END IF;
|
|
|
|
RETURN NULL;
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- Trigger to recalculate progress when tasks are modified
|
|
DROP TRIGGER IF EXISTS trigger_update_project_progress ON public.tasks;
|
|
CREATE TRIGGER trigger_update_project_progress
|
|
AFTER INSERT OR UPDATE OF status OR DELETE
|
|
ON public.tasks
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_project_progress_on_task_change();
|
|
|
|
-- Trigger to recalculate progress when a project's progress_type is changed to 'auto'
|
|
CREATE OR REPLACE FUNCTION public.update_project_progress_on_type_change()
|
|
RETURNS TRIGGER AS $$
|
|
DECLARE
|
|
v_total_tasks integer;
|
|
v_done_tasks integer;
|
|
v_new_progress integer;
|
|
BEGIN
|
|
IF NEW.progress_type = 'auto' AND (OLD.progress_type IS DISTINCT FROM NEW.progress_type) THEN
|
|
SELECT count(*) INTO v_total_tasks FROM public.tasks WHERE project_id = NEW.id;
|
|
SELECT count(*) INTO v_done_tasks FROM public.tasks WHERE project_id = NEW.id AND status = 'done';
|
|
|
|
IF v_total_tasks > 0 THEN
|
|
v_new_progress := round((v_done_tasks::numeric / v_total_tasks::numeric) * 100);
|
|
ELSE
|
|
v_new_progress := 0;
|
|
END IF;
|
|
|
|
NEW.progress := v_new_progress;
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
DROP TRIGGER IF EXISTS trigger_update_project_progress_type ON public.projects;
|
|
CREATE TRIGGER trigger_update_project_progress_type
|
|
BEFORE UPDATE OF progress_type
|
|
ON public.projects
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.update_project_progress_on_type_change();
|
|
|
|
-- -----------------------------------------------------------------------------
|
|
-- Source: supabase/migrations/0009_lock_registration_after_first_admin.sql
|
|
-- -----------------------------------------------------------------------------
|
|
|
|
-- 0009: Lock public registration after the first self-host admin account
|
|
-- Run after: supabase/migrations/0008_add_project_progress_and_quota.sql
|
|
|
|
create or replace function public.is_first_admin_setup_available()
|
|
returns boolean
|
|
language sql
|
|
security definer
|
|
set search_path = public
|
|
as $$
|
|
select not exists (
|
|
select 1
|
|
from public.profiles
|
|
limit 1
|
|
);
|
|
$$;
|
|
|
|
revoke all on function public.is_first_admin_setup_available() from public;
|
|
grant execute on function public.is_first_admin_setup_available() to anon;
|
|
grant execute on function public.is_first_admin_setup_available() to authenticated;
|
|
|
|
create or replace function public.handle_new_user()
|
|
returns trigger
|
|
language plpgsql
|
|
security definer
|
|
set search_path = public
|
|
as $$
|
|
begin
|
|
if exists (select 1 from public.profiles limit 1)
|
|
and coalesce(new.raw_app_meta_data->>'internal_created', 'false') <> 'true' then
|
|
raise exception 'Registration is closed. The first admin account already exists.';
|
|
end if;
|
|
|
|
insert into public.profiles (id, first_name, last_name, avatar_url)
|
|
values (new.id, '', '', '')
|
|
on conflict (id) do nothing;
|
|
|
|
return new;
|
|
end;
|
|
$$;
|
|
|
|
drop trigger if exists on_auth_user_created on auth.users;
|
|
|
|
create trigger on_auth_user_created
|
|
after insert on auth.users
|
|
for each row execute procedure public.handle_new_user();
|
|
|
|
notify pgrst, 'reload schema';
|
|
|
|
-- -----------------------------------------------------------------------------
|
|
-- Source: supabase/migrations/0010_allow_internal_auth_user_creation.sql
|
|
-- -----------------------------------------------------------------------------
|
|
|
|
-- 0010: Allow trusted server-side internal Auth user creation
|
|
-- Run after: supabase/migrations/0009_lock_registration_after_first_admin.sql
|
|
|
|
create schema if not exists neta_internal;
|
|
|
|
create table if not exists neta_internal.internal_auth_creations (
|
|
id uuid default uuid_generate_v4() primary key,
|
|
email text not null,
|
|
reason text default 'internal'::text not null,
|
|
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
|
|
expires_at timestamp with time zone default (timezone('utc'::text, now()) + interval '2 minutes') not null
|
|
);
|
|
|
|
create index if not exists internal_auth_creations_email_idx
|
|
on neta_internal.internal_auth_creations (lower(email));
|
|
|
|
revoke all on schema neta_internal from public;
|
|
revoke all on all tables in schema neta_internal from public;
|
|
|
|
create or replace function public.neta_current_jwt_role()
|
|
returns text
|
|
language sql
|
|
stable
|
|
as $$
|
|
select coalesce(
|
|
nullif(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'role', ''),
|
|
nullif(current_setting('request.jwt.claim.role', true), ''),
|
|
''
|
|
);
|
|
$$;
|
|
|
|
revoke all on function public.neta_current_jwt_role() from public;
|
|
grant execute on function public.neta_current_jwt_role() to anon;
|
|
grant execute on function public.neta_current_jwt_role() to authenticated;
|
|
grant execute on function public.neta_current_jwt_role() to service_role;
|
|
|
|
create or replace function public.request_internal_auth_creation(
|
|
target_email text,
|
|
target_reason text default 'internal'
|
|
)
|
|
returns void
|
|
language plpgsql
|
|
security definer
|
|
set search_path = public, neta_internal
|
|
as $$
|
|
begin
|
|
if public.neta_current_jwt_role() <> 'service_role' then
|
|
raise exception 'Only service role can request internal auth creation.';
|
|
end if;
|
|
|
|
if target_email is null or btrim(target_email) = '' then
|
|
raise exception 'target_email is required.';
|
|
end if;
|
|
|
|
delete from neta_internal.internal_auth_creations
|
|
where expires_at <= timezone('utc'::text, now())
|
|
or lower(email) = lower(btrim(target_email));
|
|
|
|
insert into neta_internal.internal_auth_creations (email, reason)
|
|
values (btrim(target_email), coalesce(nullif(btrim(target_reason), ''), 'internal'));
|
|
end;
|
|
$$;
|
|
|
|
revoke all on function public.request_internal_auth_creation(text, text) from public;
|
|
grant execute on function public.request_internal_auth_creation(text, text) to service_role;
|
|
|
|
create or replace function public.handle_new_user()
|
|
returns trigger
|
|
language plpgsql
|
|
security definer
|
|
set search_path = public, neta_internal
|
|
as $$
|
|
declare
|
|
allowed_internal_creation boolean := false;
|
|
begin
|
|
delete from neta_internal.internal_auth_creations
|
|
where lower(email) = lower(new.email)
|
|
and expires_at > timezone('utc'::text, now())
|
|
returning true into allowed_internal_creation;
|
|
|
|
allowed_internal_creation := coalesce(allowed_internal_creation, false);
|
|
|
|
if exists (select 1 from public.profiles limit 1)
|
|
and coalesce(new.raw_app_meta_data->>'internal_created', 'false') <> 'true'
|
|
and not allowed_internal_creation then
|
|
raise exception 'Registration is closed. The first admin account already exists.';
|
|
end if;
|
|
|
|
insert into public.profiles (id, first_name, last_name, avatar_url)
|
|
values (new.id, '', '', '')
|
|
on conflict (id) do nothing;
|
|
|
|
return new;
|
|
end;
|
|
$$;
|
|
|
|
drop trigger if exists on_auth_user_created on auth.users;
|
|
|
|
create trigger on_auth_user_created
|
|
after insert on auth.users
|
|
for each row execute procedure public.handle_new_user();
|
|
|
|
notify pgrst, 'reload schema';
|
|
|
|
-- -----------------------------------------------------------------------------
|
|
-- Source: supabase/migrations/0011_fix_service_role_claims_and_storage_policies.sql
|
|
-- -----------------------------------------------------------------------------
|
|
|
|
-- 0011: Fix service-role JWT claim handling and storage policies
|
|
-- Run after: supabase/migrations/0010_allow_internal_auth_user_creation.sql
|
|
|
|
create schema if not exists neta_internal;
|
|
|
|
create table if not exists neta_internal.internal_auth_creations (
|
|
id uuid default uuid_generate_v4() primary key,
|
|
email text not null,
|
|
reason text default 'internal'::text not null,
|
|
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
|
|
expires_at timestamp with time zone default (timezone('utc'::text, now()) + interval '2 minutes') not null
|
|
);
|
|
|
|
create index if not exists internal_auth_creations_email_idx
|
|
on neta_internal.internal_auth_creations (lower(email));
|
|
|
|
revoke all on schema neta_internal from public;
|
|
revoke all on all tables in schema neta_internal from public;
|
|
|
|
create or replace function public.neta_current_jwt_role()
|
|
returns text
|
|
language sql
|
|
stable
|
|
as $$
|
|
select coalesce(
|
|
nullif(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'role', ''),
|
|
nullif(current_setting('request.jwt.claim.role', true), ''),
|
|
''
|
|
);
|
|
$$;
|
|
|
|
revoke all on function public.neta_current_jwt_role() from public;
|
|
grant execute on function public.neta_current_jwt_role() to anon;
|
|
grant execute on function public.neta_current_jwt_role() to authenticated;
|
|
grant execute on function public.neta_current_jwt_role() to service_role;
|
|
|
|
create or replace function public.request_internal_auth_creation(
|
|
target_email text,
|
|
target_reason text default 'internal'
|
|
)
|
|
returns void
|
|
language plpgsql
|
|
security definer
|
|
set search_path = public, neta_internal
|
|
as $$
|
|
begin
|
|
if public.neta_current_jwt_role() <> 'service_role' then
|
|
raise exception 'Only service role can request internal auth creation.';
|
|
end if;
|
|
|
|
if target_email is null or btrim(target_email) = '' then
|
|
raise exception 'target_email is required.';
|
|
end if;
|
|
|
|
delete from neta_internal.internal_auth_creations
|
|
where expires_at <= timezone('utc'::text, now())
|
|
or lower(email) = lower(btrim(target_email));
|
|
|
|
insert into neta_internal.internal_auth_creations (email, reason)
|
|
values (btrim(target_email), coalesce(nullif(btrim(target_reason), ''), 'internal'));
|
|
end;
|
|
$$;
|
|
|
|
revoke all on function public.request_internal_auth_creation(text, text) from public;
|
|
grant execute on function public.request_internal_auth_creation(text, text) to service_role;
|
|
|
|
drop policy if exists "Avatar images are publicly accessible." on storage.objects;
|
|
create policy "Avatar images are publicly accessible."
|
|
on storage.objects for select
|
|
using (
|
|
bucket_id = 'avatars'
|
|
or public.neta_current_jwt_role() = 'service_role'
|
|
);
|
|
|
|
drop policy if exists "Users can upload an avatar." on storage.objects;
|
|
create policy "Users can upload an avatar."
|
|
on storage.objects for insert
|
|
with check (
|
|
bucket_id = 'avatars'
|
|
and (
|
|
public.neta_current_jwt_role() = 'service_role'
|
|
or auth.uid()::text = (storage.foldername(name))[1]
|
|
)
|
|
);
|
|
|
|
drop policy if exists "Users can update their own avatar." on storage.objects;
|
|
create policy "Users can update their own avatar."
|
|
on storage.objects for update
|
|
using (
|
|
bucket_id = 'avatars'
|
|
and (
|
|
public.neta_current_jwt_role() = 'service_role'
|
|
or auth.uid()::text = (storage.foldername(name))[1]
|
|
)
|
|
);
|
|
|
|
drop policy if exists "Users can delete their own avatar." on storage.objects;
|
|
create policy "Users can delete their own avatar."
|
|
on storage.objects for delete
|
|
using (
|
|
bucket_id = 'avatars'
|
|
and (
|
|
public.neta_current_jwt_role() = 'service_role'
|
|
or auth.uid()::text = (storage.foldername(name))[1]
|
|
)
|
|
);
|
|
|
|
drop policy if exists "Users can view their own project assets." on storage.objects;
|
|
create policy "Users can view their own project assets." on storage.objects
|
|
for select using (
|
|
bucket_id = 'project-assets'
|
|
and (
|
|
public.neta_current_jwt_role() = 'service_role'
|
|
or auth.uid()::text = (storage.foldername(name))[1]
|
|
)
|
|
);
|
|
|
|
drop policy if exists "Users can upload their own project assets." on storage.objects;
|
|
create policy "Users can upload their own project assets." on storage.objects
|
|
for insert with check (
|
|
bucket_id = 'project-assets'
|
|
and (
|
|
public.neta_current_jwt_role() = 'service_role'
|
|
or auth.uid()::text = (storage.foldername(name))[1]
|
|
)
|
|
);
|
|
|
|
drop policy if exists "Users can update their own project assets." on storage.objects;
|
|
create policy "Users can update their own project assets." on storage.objects
|
|
for update using (
|
|
bucket_id = 'project-assets'
|
|
and (
|
|
public.neta_current_jwt_role() = 'service_role'
|
|
or auth.uid()::text = (storage.foldername(name))[1]
|
|
)
|
|
);
|
|
|
|
drop policy if exists "Users can delete their own project assets." on storage.objects;
|
|
create policy "Users can delete their own project assets." on storage.objects
|
|
for delete using (
|
|
bucket_id = 'project-assets'
|
|
and (
|
|
public.neta_current_jwt_role() = 'service_role'
|
|
or auth.uid()::text = (storage.foldername(name))[1]
|
|
)
|
|
);
|
|
|
|
notify pgrst, 'reload schema';
|