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:
@@ -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;
|
||||
Reference in New Issue
Block a user