Files
neta/supabase/schema.sql
T

342 lines
12 KiB
PL/PgSQL

-- 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]
)
);