feat: auth, persistence, and AI integration

- Cookie-based auth via backend proxy (httpOnly JWTs)
- Supabase Postgres persistence for sessions/messages/profiles + RLS
- Fix cross-user session leak (token cache keyed by full token, not 50-char prefix)
- Fix missing table grants (42501) via migration; auto-provision profiles on user creation
- Chat validation, ownership checks, rate limiting, /api/chat/sessions route ordering
- Frontend auth-state reset + credentials include
- OpenRouter AI provider (OpenAI-compatible base URL, reasoning disabled)
- Tests: chatValidation, appState
This commit is contained in:
2026-08-19 09:41:40 -04:00
parent 93a35a7343
commit ff9da4d324
43 changed files with 4174 additions and 249 deletions
@@ -0,0 +1,80 @@
-- Create sessions table
CREATE TABLE public.sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
grade TEXT,
subject TEXT,
chapter TEXT,
preview TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Create messages table
CREATE TABLE public.messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id UUID NOT NULL REFERENCES public.sessions(id) ON DELETE CASCADE,
role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
text TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Create profiles table (optional, for future user metadata)
CREATE TABLE public.profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
email TEXT NOT NULL,
display_name TEXT,
avatar_url TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Create indexes for faster queries
CREATE INDEX idx_sessions_user_id ON public.sessions(user_id);
CREATE INDEX idx_messages_session_id ON public.messages(session_id);
-- Enable RLS
ALTER TABLE public.sessions ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.messages ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
-- RLS Policies for sessions: users can CRUD only their own sessions
CREATE POLICY "Users can view own sessions"
ON public.sessions FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY "Users can create sessions"
ON public.sessions FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own sessions"
ON public.sessions FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can delete own sessions"
ON public.sessions FOR DELETE
USING (auth.uid() = user_id);
-- RLS Policies for messages: users can CRUD only messages in their own sessions
CREATE POLICY "Users can view messages in own sessions"
ON public.messages FOR SELECT
USING (session_id IN (SELECT id FROM public.sessions WHERE user_id = auth.uid()));
CREATE POLICY "Users can insert messages in own sessions"
ON public.messages FOR INSERT
WITH CHECK (session_id IN (SELECT id FROM public.sessions WHERE user_id = auth.uid()));
CREATE POLICY "Users can delete own messages"
ON public.messages FOR DELETE
USING (session_id IN (SELECT id FROM public.sessions WHERE user_id = auth.uid()));
-- RLS Policies for profiles
CREATE POLICY "Users can view own profile"
ON public.profiles FOR SELECT
USING (auth.uid() = id);
CREATE POLICY "Users can update own profile"
ON public.profiles FOR UPDATE
USING (auth.uid() = id)
WITH CHECK (auth.uid() = id);
@@ -0,0 +1,24 @@
-- Grant table privileges to Supabase API roles.
--
-- The sessions/messages/profiles tables are owned by `postgres`, and the
-- default ACL for postgres-owned tables omits SELECT/INSERT/UPDATE for the
-- PostgREST API roles (anon/authenticated/service_role). This caused
-- PostgreSQL error 42501 ("permission denied for table ...") on every
-- database operation through the REST gateway.
--
-- RLS remains enabled and is the actual access control; these grants only
-- allow the roles to reach the tables through PostgREST. service_role also
-- carries BYPASSRLS, which is the backend's write path.
GRANT SELECT, INSERT, UPDATE, DELETE
ON public.profiles,
public.sessions,
public.messages
TO anon, authenticated, service_role;
-- Ensure future tables created in `public` receive the same grants, so the
-- "always-revoked" default (auto_expose_new_tables) does not silently break
-- new tables the same way.
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES
TO anon, authenticated, service_role;
@@ -0,0 +1,67 @@
-- Production-like user control: application-level user table (public.profiles)
-- auto-provisioned from auth.users, with admin-managed control fields.
-- 1. Control columns (inherit existing table-wide grants automatically)
ALTER TABLE public.profiles
ADD COLUMN IF NOT EXISTS role text NOT NULL DEFAULT 'user',
ADD COLUMN IF NOT EXISTS is_banned boolean NOT NULL DEFAULT false;
-- 2. Backfill profiles for existing auth users (e.g. the current test accounts)
INSERT INTO public.profiles (id, email)
SELECT id, email FROM auth.users
ON CONFLICT (id) DO NOTHING;
-- 3. Auto-provision a profile row whenever an auth user is created (signup/admin)
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
BEGIN
INSERT INTO public.profiles (id, email)
VALUES (new.id, new.email)
ON CONFLICT (id) DO NOTHING;
RETURN new;
END;
$$;
-- 4. Keep profile email in sync with auth.users email changes
CREATE OR REPLACE FUNCTION public.handle_user_email_change()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
BEGIN
IF new.email IS DISTINCT FROM old.email THEN
UPDATE public.profiles SET email = new.email WHERE id = new.id;
END IF;
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();
DROP TRIGGER IF EXISTS on_auth_user_email_change ON auth.users;
CREATE TRIGGER on_auth_user_email_change
AFTER UPDATE OF email ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_user_email_change();
-- 5. Users may update their own profile, but must NOT be able to escalate
-- their own role or unban themselves.
-- NOTE: a column-level REVOKE does NOT override a broad TABLE-level grant.
-- The earlier grant migration gave INSERT,UPDATE,DELETE on profiles to
-- anon/authenticated (table-wide => every column). So undo the table-level
-- write grants for user-facing roles, then re-grant UPDATE only on the
-- editable columns. Profiles are created via the trigger, so users need no
-- INSERT or DELETE. service_role (backend/admin) keeps full access and
-- bypasses RLS.
REVOKE INSERT, UPDATE, DELETE ON public.profiles FROM authenticated;
REVOKE INSERT, UPDATE, DELETE ON public.profiles FROM anon;
GRANT SELECT ON public.profiles TO anon, authenticated;
GRANT UPDATE (display_name, avatar_url) ON public.profiles TO authenticated;