File size: 9,872 Bytes
dd935c0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
-- Vion Training β€” Supabase schema (Phase 1)
-- Run this in the Supabase SQL Editor (Dashboard β†’ SQL Editor β†’ New query β†’ paste β†’ Run).
-- Safe to re-run: uses IF NOT EXISTS / OR REPLACE throughout.
--
-- Auth itself (email/password, password reset, JWT) is handled by Supabase's
-- built-in `auth.users` table. Everything below is OUR application data, keyed
-- to that user id. Row Level Security (RLS) ensures a user can only read their
-- own rows, while admins can read everyone's.

-- ──────────────────────────────────────────────────────────────────────────
-- 1. Profiles β€” one row per auth user, holding app-level fields + role.
-- ──────────────────────────────────────────────────────────────────────────
create table if not exists public.profiles (
  id                   uuid primary key references auth.users(id) on delete cascade,
  email                text,
  full_name            text,
  role                 text not null default 'user' check (role in ('user', 'admin')),
  language_preference  text not null default 'en' check (language_preference in ('en', 'de')),
  theme                text not null default 'system' check (theme in ('light', 'dark', 'system')),
  notifications_enabled boolean not null default true,
  created_at           timestamptz not null default now()
);

-- Auto-create a profile row whenever a new auth user signs up.
create or replace function public.handle_new_user()
returns trigger
language plpgsql
security definer set search_path = public
as $$
begin
  insert into public.profiles (id, email, full_name)
  values (new.id, new.email, new.raw_user_meta_data->>'full_name')
  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 function public.handle_new_user();

-- Helper: is the current request from an admin? (security definer avoids RLS recursion)
create or replace function public.is_admin()
returns boolean
language sql
security definer set search_path = public
stable
as $$
  select exists (
    select 1 from public.profiles
    where id = auth.uid() and role = 'admin'
  );
$$;

-- ──────────────────────────────────────────────────────────────────────────
-- 2. Sessions β€” one row per training call.
-- ──────────────────────────────────────────────────────────────────────────
create table if not exists public.sessions (
  id           uuid primary key default gen_random_uuid(),
  user_id      uuid not null references public.profiles(id) on delete cascade,
  scenario_id  text not null,
  persona_id   text,
  language     text not null default 'en',
  status       text not null default 'created'
                 check (status in ('created', 'active', 'ended', 'error')),
  transcript   jsonb not null default '[]'::jsonb,
  user_notes   text,                          -- the trainee's own notes on this call
  started_at   timestamptz,
  ended_at     timestamptz,
  created_at   timestamptz not null default now()
);
create index if not exists sessions_user_id_idx  on public.sessions(user_id);
create index if not exists sessions_created_at_idx on public.sessions(created_at desc);

-- ──────────────────────────────────────────────────────────────────────────
-- 3. Evaluations β€” the LLM-scored result of a call (1:1 with a session).
-- ──────────────────────────────────────────────────────────────────────────
create table if not exists public.evaluations (
  id            uuid primary key default gen_random_uuid(),
  session_id    uuid not null unique references public.sessions(id) on delete cascade,
  user_id       uuid not null references public.profiles(id) on delete cascade,
  score         int  check (score between 0 and 100),
  passed        boolean,
  summary       text,
  good_points   jsonb not null default '[]'::jsonb,   -- ["...", "..."]
  improvements  jsonb not null default '[]'::jsonb,   -- ["...", "..."]
  created_at    timestamptz not null default now()
);
create index if not exists evaluations_user_id_idx on public.evaluations(user_id);

-- ──────────────────────────────────────────────────────────────────────────
-- 4. Feedback β€” free-text app feedback from the "Feedback" dashboard item.
-- ──────────────────────────────────────────────────────────────────────────
create table if not exists public.feedback (
  id         uuid primary key default gen_random_uuid(),
  user_id    uuid not null references public.profiles(id) on delete cascade,
  body       text not null,
  created_at timestamptz not null default now()
);

-- ──────────────────────────────────────────────────────────────────────────
-- 5. Row Level Security β€” lock every table to its owner; admins see all.
-- ──────────────────────────────────────────────────────────────────────────
alter table public.profiles    enable row level security;
alter table public.sessions    enable row level security;
alter table public.evaluations enable row level security;
alter table public.feedback    enable row level security;

-- Profiles: a user reads/updates their own row; admins read all.
drop policy if exists profiles_select_own on public.profiles;
create policy profiles_select_own on public.profiles
  for select using (id = auth.uid() or public.is_admin());

drop policy if exists profiles_update_own on public.profiles;
create policy profiles_update_own on public.profiles
  for update using (id = auth.uid());

-- Sessions: owner full access; admins read-only.
drop policy if exists sessions_owner_all on public.sessions;
create policy sessions_owner_all on public.sessions
  for all using (user_id = auth.uid()) with check (user_id = auth.uid());

drop policy if exists sessions_admin_read on public.sessions;
create policy sessions_admin_read on public.sessions
  for select using (public.is_admin());

-- Evaluations: owner reads; admins read. (Writes happen server-side via service key.)
drop policy if exists evaluations_owner_read on public.evaluations;
create policy evaluations_owner_read on public.evaluations
  for select using (user_id = auth.uid() or public.is_admin());

-- Feedback: owner inserts/reads own; admins read all.
drop policy if exists feedback_owner on public.feedback;
create policy feedback_owner on public.feedback
  for all using (user_id = auth.uid()) with check (user_id = auth.uid());

drop policy if exists feedback_admin_read on public.feedback;
create policy feedback_admin_read on public.feedback
  for select using (public.is_admin());

-- NOTE: the FastAPI backend uses the SERVICE ROLE key, which bypasses RLS, to
-- write sessions/evaluations on the user's behalf. The anon key (frontend) is
-- always subject to the policies above.

-- ──────────────────────────────────────────────────────────────────────────
-- 6. Grants β€” table-level privileges for the API roles.
-- Postgres requires BOTH a table privilege AND (for anon/authenticated) a
-- passing RLS policy. RLS alone is not enough; without these grants every
-- request 403s with "permission denied for table". Supabase normally adds
-- these automatically, but tables created via raw SQL can miss them.
-- ──────────────────────────────────────────────────────────────────────────
grant usage on schema public to anon, authenticated, service_role;

-- service_role bypasses RLS and does all server-side writes β€” full access.
grant all privileges on all tables    in schema public to service_role;
grant all privileges on all sequences in schema public to service_role;
grant all privileges on all functions in schema public to service_role;

-- Logged-in users act as `authenticated`; RLS still restricts them to own rows.
grant select, insert, update, delete on all tables in schema public to authenticated;
grant usage, select on all sequences in schema public to authenticated;

-- Make the same grants apply to any tables we add later.
alter default privileges in schema public
  grant all privileges on tables to service_role;
alter default privileges in schema public
  grant select, insert, update, delete on tables to authenticated;