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,661 @@
|
||||
# padhle — Implementation Plan
|
||||
## Rate Limiting + Supabase Integration + Session Bug Fix
|
||||
|
||||
**Date:** 2026-08-19
|
||||
**Total Estimated Time:** ~3 hours
|
||||
**Status:** Ready to implement
|
||||
|
||||
---
|
||||
|
||||
## Issues to Address
|
||||
|
||||
### 🔴 Critical: Session Data Leak (User A ↔ User B)
|
||||
**Symptoms:**
|
||||
- User A logs in → chats with AI → logs out
|
||||
- User B logs in → sidebar empty (correct)
|
||||
- BUT User B sees User A's conversation on main screen ❌
|
||||
|
||||
**Root Cause:**
|
||||
- Frontend `activeChat` state not cleared on sign-out
|
||||
- In-memory backend store allows session ID guessing
|
||||
- Stale `messages` state renders after User A logs out
|
||||
|
||||
---
|
||||
|
||||
### 🟡 Security: No Rate Limiting
|
||||
- Auth endpoints (`/signup`, `/signin`) vulnerable to brute-force
|
||||
- No per-IP throttling
|
||||
|
||||
---
|
||||
|
||||
### 🟠 Architecture: In-Memory Sessions
|
||||
- Sessions lost on server restart
|
||||
- Not tied to database — only scoped via cookies
|
||||
- Need Supabase Postgres for persistence
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### **PHASE 1: Rate Limiting by IP (30 min)**
|
||||
|
||||
**Goal:** Prevent brute-force attacks on auth endpoints
|
||||
|
||||
#### 1.1 Install dependency
|
||||
```bash
|
||||
cd backend
|
||||
npm install express-rate-limit
|
||||
```
|
||||
|
||||
#### 1.2 Create middleware file
|
||||
**File:** `backend/src/middleware/rateLimiter.js` (NEW)
|
||||
|
||||
```js
|
||||
import rateLimit from 'express-rate-limit';
|
||||
|
||||
export const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 5, // 5 requests per window per IP
|
||||
message: { error: 'Too many auth attempts. Try again later.' },
|
||||
standardHeaders: false,
|
||||
skip: (req) => process.env.NODE_ENV !== 'production' && req.ip === '::1',
|
||||
});
|
||||
|
||||
export const chatLimiter = rateLimit({
|
||||
windowMs: 60 * 1000, // 1 minute
|
||||
max: 30, // 30 requests per minute
|
||||
message: { error: 'Rate limit exceeded. Try again later.' },
|
||||
standardHeaders: false,
|
||||
});
|
||||
|
||||
export const sessionsLimiter = rateLimit({
|
||||
windowMs: 60 * 1000, // 1 minute
|
||||
max: 20, // 20 requests per minute
|
||||
message: { error: 'Rate limit exceeded. Try again later.' },
|
||||
standardHeaders: false,
|
||||
});
|
||||
```
|
||||
|
||||
#### 1.3 Apply to auth routes
|
||||
**File:** `backend/src/routes/auth.js` (MODIFY top of file)
|
||||
|
||||
Add import:
|
||||
```js
|
||||
import { authLimiter } from '../middleware/rateLimiter.js';
|
||||
```
|
||||
|
||||
Then apply to each route:
|
||||
```js
|
||||
router.post('/signup', authLimiter, async (req, res) => { ... });
|
||||
router.post('/signin', authLimiter, async (req, res) => { ... });
|
||||
router.post('/signout', authLimiter, async (req, res) => { ... });
|
||||
```
|
||||
|
||||
#### 1.4 Apply to other routes
|
||||
**File:** `backend/src/index.js` (MODIFY)
|
||||
|
||||
Add imports:
|
||||
```js
|
||||
import { chatLimiter, sessionsLimiter } from './middleware/rateLimiter.js';
|
||||
```
|
||||
|
||||
Update middleware order:
|
||||
```js
|
||||
app.use('/api/chat', chatLimiter, supabaseAuth, chatRoutes);
|
||||
app.use('/api/sessions', sessionsLimiter, supabaseAuth, sessionRoutes);
|
||||
```
|
||||
|
||||
#### 1.5 Test
|
||||
```bash
|
||||
cd backend && npm start
|
||||
|
||||
# Test rate limiting (6th request should fail)
|
||||
for i in {1..7}; do
|
||||
curl -X POST http://localhost:3001/api/auth/signup \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"email\": \"test$i@example.com\", \"password\": \"test\"}"
|
||||
echo ""
|
||||
done
|
||||
# Expect 429 on 6th attempt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **PHASE 2: Supabase Database Integration (90 min)**
|
||||
|
||||
**Goal:** Move sessions & messages from in-memory to Supabase Postgres with RLS
|
||||
|
||||
#### 2.1 Create database migration
|
||||
**File:** `backend/supabase/migrations/{timestamp}_create_sessions_messages.sql` (NEW)
|
||||
|
||||
```sql
|
||||
-- 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()
|
||||
);
|
||||
|
||||
-- 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()
|
||||
);
|
||||
|
||||
-- profiles table (for future use)
|
||||
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()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
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: sessions
|
||||
CREATE POLICY "Users view own sessions"
|
||||
ON public.sessions FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users create sessions"
|
||||
ON public.sessions FOR INSERT
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users update own sessions"
|
||||
ON public.sessions FOR UPDATE
|
||||
USING (auth.uid() = user_id)
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users delete own sessions"
|
||||
ON public.sessions FOR DELETE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- RLS: messages
|
||||
CREATE POLICY "Users view own messages"
|
||||
ON public.messages FOR SELECT
|
||||
USING (session_id IN (SELECT id FROM public.sessions WHERE user_id = auth.uid()));
|
||||
|
||||
CREATE POLICY "Users insert own messages"
|
||||
ON public.messages FOR INSERT
|
||||
WITH CHECK (session_id IN (SELECT id FROM public.sessions WHERE user_id = auth.uid()));
|
||||
|
||||
CREATE POLICY "Users delete own messages"
|
||||
ON public.messages FOR DELETE
|
||||
USING (session_id IN (SELECT id FROM public.sessions WHERE user_id = auth.uid()));
|
||||
|
||||
-- RLS: profiles
|
||||
CREATE POLICY "Users view own profile"
|
||||
ON public.profiles FOR SELECT
|
||||
USING (auth.uid() = id);
|
||||
|
||||
CREATE POLICY "Users update own profile"
|
||||
ON public.profiles FOR UPDATE
|
||||
USING (auth.uid() = id)
|
||||
WITH CHECK (auth.uid() = id);
|
||||
```
|
||||
|
||||
#### 2.2 Apply migration
|
||||
```bash
|
||||
cd /path/to/padhle
|
||||
npx supabase db push
|
||||
```
|
||||
|
||||
Verify in Supabase Studio (http://localhost:54323):
|
||||
- Tables exist: `sessions`, `messages`, `profiles`
|
||||
- RLS enabled on all three
|
||||
- Policies listed under each table
|
||||
|
||||
#### 2.3 Create database service
|
||||
**File:** `backend/src/services/db.js` (NEW)
|
||||
|
||||
```js
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const SUPABASE_URL = process.env.SUPABASE_URL;
|
||||
const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY);
|
||||
|
||||
export async function createSession(userId, grade, subject, chapter) {
|
||||
const { data, error } = await supabase
|
||||
.from('sessions')
|
||||
.insert({
|
||||
user_id: userId,
|
||||
grade: grade || 'General',
|
||||
subject: subject || 'General',
|
||||
chapter: chapter || 'General',
|
||||
preview: 'New Chat',
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw new Error(`Failed to create session: ${error.message}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getSession(sessionId) {
|
||||
const { data, error } = await supabase
|
||||
.from('sessions')
|
||||
.select('*')
|
||||
.eq('id', sessionId)
|
||||
.single();
|
||||
|
||||
if (error && error.code === 'PGRST116') return null;
|
||||
if (error) throw new Error(`Failed to get session: ${error.message}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function listSessions(userId) {
|
||||
const { data, error } = await supabase
|
||||
.from('sessions')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.order('updated_at', { ascending: false });
|
||||
|
||||
if (error) throw new Error(`Failed to list sessions: ${error.message}`);
|
||||
return data || [];
|
||||
}
|
||||
|
||||
export async function getMessages(sessionId) {
|
||||
const { data, error } = await supabase
|
||||
.from('messages')
|
||||
.select('*')
|
||||
.eq('session_id', sessionId)
|
||||
.order('created_at', { ascending: true });
|
||||
|
||||
if (error) throw new Error(`Failed to get messages: ${error.message}`);
|
||||
return data || [];
|
||||
}
|
||||
|
||||
export async function addMessage(sessionId, role, text) {
|
||||
const { data: messageData, error: msgError } = await supabase
|
||||
.from('messages')
|
||||
.insert({ session_id: sessionId, role, text })
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (msgError) throw new Error(`Failed to add message: ${msgError.message}`);
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
.from('sessions')
|
||||
.update({
|
||||
updated_at: new Date().toISOString(),
|
||||
preview: role === 'user' ? text.substring(0, 60) + (text.length > 60 ? '...' : '') : undefined,
|
||||
})
|
||||
.eq('id', sessionId);
|
||||
|
||||
if (updateError) console.error('Failed to update session:', updateError);
|
||||
return messageData;
|
||||
}
|
||||
|
||||
export async function deleteSession(sessionId) {
|
||||
const { error } = await supabase
|
||||
.from('sessions')
|
||||
.delete()
|
||||
.eq('id', sessionId);
|
||||
|
||||
if (error) throw new Error(`Failed to delete session: ${error.message}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function clearMessages(sessionId) {
|
||||
const { error } = await supabase
|
||||
.from('messages')
|
||||
.delete()
|
||||
.eq('session_id', sessionId);
|
||||
|
||||
if (error) throw new Error(`Failed to clear messages: ${error.message}`);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.4 Install SDK
|
||||
```bash
|
||||
cd backend
|
||||
npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
#### 2.5 Update routes to use DB service
|
||||
**File:** `backend/src/routes/chat.js` (MODIFY)
|
||||
|
||||
Replace top imports:
|
||||
```js
|
||||
// OLD
|
||||
import { createSession, addMessage, getMessages, getSession } from "../stores/sessionStore.js";
|
||||
|
||||
// NEW
|
||||
import { createSession, addMessage, getMessages, getSession } from "../services/db.js";
|
||||
```
|
||||
|
||||
Update function calls (add `await`):
|
||||
```js
|
||||
// OLD: session = createSession(userId, grade, subject, chapter);
|
||||
// NEW: session = await createSession(userId, grade, subject, chapter);
|
||||
|
||||
// OLD: addMessage(currentChatId, userMsg);
|
||||
// NEW: await addMessage(currentChatId, userMsg.role, userMsg.text);
|
||||
|
||||
// OLD: getMessages(currentChatId)
|
||||
// NEW: const messages = await getMessages(currentChatId);
|
||||
|
||||
// OLD: addMessage(currentChatId, assistantMsg);
|
||||
// NEW: await addMessage(currentChatId, assistantMsg.role, assistantMsg.text);
|
||||
```
|
||||
|
||||
Wrap in try-catch:
|
||||
```js
|
||||
try {
|
||||
session = await createSession(...);
|
||||
// ... rest of logic
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: 'Database error' });
|
||||
}
|
||||
```
|
||||
|
||||
**File:** `backend/src/routes/sessions.js` (MODIFY)
|
||||
|
||||
Same replacements as chat.js:
|
||||
```js
|
||||
import { listSessions, getSession, deleteSession, clearMessages, createSession } from "../services/db.js";
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const sessions = await listSessions(req.user.uid);
|
||||
res.json(sessions);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Failed to load sessions' });
|
||||
}
|
||||
});
|
||||
|
||||
// ... same for POST, GET/:id, DELETE/:id, PATCH/:id/clear
|
||||
```
|
||||
|
||||
#### 2.6 Update .env
|
||||
**File:** `backend/.env` (ADD)
|
||||
|
||||
```
|
||||
SUPABASE_SERVICE_ROLE_KEY=sb_secret_N7UND0UgjKTVK-Uodkm0Hg_xSvEMPvz
|
||||
```
|
||||
|
||||
(Get key from `npx supabase status`)
|
||||
|
||||
#### 2.7 Test integration
|
||||
```bash
|
||||
cd backend && npm start
|
||||
|
||||
# Create a session
|
||||
curl -X POST http://localhost:3001/api/sessions \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Cookie: padhle.token=<valid_jwt>' \
|
||||
-d '{"grade": "Grade 10", "subject": "math", "chapter": "Chapter 1"}'
|
||||
|
||||
# Check Supabase Studio: http://localhost:54323
|
||||
# → Tables → sessions → should see new row
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **PHASE 3: Fix Session Leak Bug (45 min)**
|
||||
|
||||
**Goal:** Ensure User A's data not visible to User B
|
||||
|
||||
#### 3.1 Fix frontend state cleanup
|
||||
**File:** `frontend/src/App.jsx` (MODIFY)
|
||||
|
||||
Find `useEffect([isAuthenticated])` around line 85. Replace entire block:
|
||||
|
||||
```js
|
||||
useEffect(() => {
|
||||
if (!hasMountedRef.current) {
|
||||
hasMountedRef.current = true;
|
||||
} else if (isAuthenticated) {
|
||||
// User just signed in after sign-out
|
||||
setMessages([]);
|
||||
setActiveChat(null); // CRITICAL: clear before loading
|
||||
setActiveGrade(DEFAULTS.grade);
|
||||
setActiveSubject(DEFAULTS.subject);
|
||||
setActiveChapter(DEFAULTS.chapter);
|
||||
setIsTyping(false);
|
||||
setSelectorStep('grade');
|
||||
loadSessions(); // Load fresh sessions for current user
|
||||
} else {
|
||||
// User just signed out
|
||||
setMessages([]);
|
||||
setActiveChat(null);
|
||||
setActiveGrade(DEFAULTS.grade);
|
||||
setActiveSubject(DEFAULTS.subject);
|
||||
setActiveChapter(DEFAULTS.chapter);
|
||||
setIsTyping(false);
|
||||
setSelectorStep('grade');
|
||||
setSessions([]);
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
```
|
||||
|
||||
#### 3.2 Add safety check in ChatHistory
|
||||
**File:** `frontend/src/components/chat-history/ChatHistory.jsx` (MODIFY at top of component)
|
||||
|
||||
Add safety check before rendering:
|
||||
|
||||
```js
|
||||
function ChatHistory({ messages, sessions, activeChat, selectorStep, setMessages, setActiveChat, ...props }) {
|
||||
// Safety: verify activeChat session still exists
|
||||
const sessionExists = activeChat && sessions.some(s => s.id === activeChat);
|
||||
|
||||
if (selectorStep !== 'chat') {
|
||||
return <SelectorFlow {...props} />;
|
||||
}
|
||||
|
||||
if (!sessionExists && messages.length > 0) {
|
||||
// Stale state detected — clear
|
||||
setMessages([]);
|
||||
setActiveChat(null);
|
||||
return <WelcomeScreen />;
|
||||
}
|
||||
|
||||
if (messages.length === 0) {
|
||||
return <WelcomeScreen />;
|
||||
}
|
||||
|
||||
return <MessageList messages={messages} />;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3 Verify RLS on backend
|
||||
**File:** `backend/src/routes/sessions.js` (VERIFY ownership check)
|
||||
|
||||
```js
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.uid;
|
||||
const session = await getSession(req.params.id);
|
||||
|
||||
// Defense-in-depth: explicit ownership check
|
||||
if (!session || session.user_id !== userId) {
|
||||
return res.status(404).json({ error: 'Session not found' });
|
||||
}
|
||||
|
||||
res.json(session);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Failed to load session' });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### 3.4 E2E Test
|
||||
```
|
||||
1. Open Window 1 (Firefox)
|
||||
2. Sign in as User A
|
||||
3. Send message → create session
|
||||
4. Verify session in sidebar ✓
|
||||
5. Sign out
|
||||
6. Open Window 2 (Chrome) or incognito tab
|
||||
7. Sign in as User B
|
||||
8. Verify:
|
||||
- Sidebar empty ✓
|
||||
- Main screen shows welcome (NOT User A's chat) ✓
|
||||
9. Send a message → create new session
|
||||
10. Sign out User B
|
||||
11. In Window 1: Sign back in as User A
|
||||
12. Verify User A sees original session ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **PHASE 4: Error Handling & Validation (30 min)**
|
||||
|
||||
#### 4.1 Sanitize AI provider errors
|
||||
**File:** `backend/src/services/ai.js` (MODIFY error handlers)
|
||||
|
||||
In `streamAnthropic`:
|
||||
```js
|
||||
if (!response.ok) {
|
||||
const errText = await response.text();
|
||||
console.error('Anthropic error:', errText); // Log server-side only
|
||||
onError('AI service encountered an error. Please try again.');
|
||||
return '';
|
||||
}
|
||||
```
|
||||
|
||||
In `streamGoogle`:
|
||||
```js
|
||||
if (!response.ok) {
|
||||
const errText = await response.text();
|
||||
console.error('Google error:', errText); // Log server-side only
|
||||
onError('AI service encountered an error. Please try again.');
|
||||
return '';
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2 Fix Anthropic non-streaming endpoint
|
||||
**File:** `backend/src/services/ai.js` (MODIFY line ~138)
|
||||
|
||||
```js
|
||||
// OLD
|
||||
const res = await fetch(ANTHROPIC_BASE_URL, {
|
||||
|
||||
// NEW
|
||||
const res = await fetch(`${ANTHROPIC_BASE_URL}/messages`, {
|
||||
```
|
||||
|
||||
#### 4.3 Validate metadata
|
||||
**File:** `backend/src/routes/chat.js` (ADD at top)
|
||||
|
||||
```js
|
||||
const VALID_GRADES = ['Grade 6', 'Grade 7', 'Grade 8', 'Grade 9', 'Grade 10', 'Grade 11', 'Grade 12', 'General', 'Choose standard'];
|
||||
const VALID_SUBJECTS = ['math', 'science', 'history', 'language', 'english', 'geography', 'General', 'choose-subject'];
|
||||
```
|
||||
|
||||
Then in POST handler:
|
||||
```js
|
||||
if (grade && !VALID_GRADES.includes(grade)) {
|
||||
return res.status(400).json({ error: 'Invalid grade' });
|
||||
}
|
||||
if (subject && !VALID_SUBJECTS.includes(subject)) {
|
||||
return res.status(400).json({ error: 'Invalid subject' });
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.4 Test error paths
|
||||
```bash
|
||||
# Invalid grade
|
||||
curl -X POST http://localhost:3001/api/chat \
|
||||
-H 'Cookie: padhle.token=<jwt>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"text": "hello", "grade": "invalid"}'
|
||||
# Expect 400
|
||||
|
||||
# Message too long
|
||||
curl -X POST http://localhost:3001/api/chat \
|
||||
-H 'Cookie: padhle.token=<jwt>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"text": "'$(printf 'x%.0s' {1..10001})'}'
|
||||
# Expect 400
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary Table
|
||||
|
||||
| Phase | Task | Time | Dependencies |
|
||||
|-------|------|------|--------------|
|
||||
| 1.1 | Install `express-rate-limit` | 2 min | — |
|
||||
| 1.2 | Create `rateLimiter.js` | 5 min | — |
|
||||
| 1.3 | Apply to auth.js | 5 min | 1.2 |
|
||||
| 1.4 | Apply to index.js | 5 min | 1.3 |
|
||||
| 1.5 | Test rate limiting | 13 min | 1.1–1.4 |
|
||||
| **Phase 1 Total** | **Rate Limiting** | **30 min** | — |
|
||||
| 2.1 | Create SQL migration | 10 min | — |
|
||||
| 2.2 | Apply migration | 5 min | 2.1 |
|
||||
| 2.3 | Create `db.js` | 20 min | 2.2 |
|
||||
| 2.4 | Install SDK | 3 min | — |
|
||||
| 2.5 | Update chat.js & sessions.js | 20 min | 2.3, 2.4 |
|
||||
| 2.6 | Update .env | 2 min | — |
|
||||
| 2.7 | Test integration | 15 min | 2.1–2.6 |
|
||||
| **Phase 2 Total** | **DB Integration** | **75 min** | Phase 1 (optional) |
|
||||
| 3.1 | Fix App.jsx state cleanup | 10 min | — |
|
||||
| 3.2 | Add ChatHistory safety check | 10 min | 3.1 |
|
||||
| 3.3 | Verify RLS in sessions.js | 5 min | 2.7 |
|
||||
| 3.4 | E2E test | 20 min | 3.1–3.3 |
|
||||
| **Phase 3 Total** | **Fix Session Leak** | **45 min** | Phase 2 |
|
||||
| 4.1 | Sanitize AI errors | 5 min | — |
|
||||
| 4.2 | Fix Anthropic endpoint | 2 min | — |
|
||||
| 4.3 | Validate metadata | 10 min | — |
|
||||
| 4.4 | Test error paths | 13 min | 4.1–4.3 |
|
||||
| **Phase 4 Total** | **Error Handling** | **30 min** | — |
|
||||
| | | | |
|
||||
| **GRAND TOTAL** | **All Phases** | **~180 min (3 hrs)** | |
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
Before marking complete, verify:
|
||||
|
||||
- [ ] Phase 1: Rate limiting blocks 6th auth attempt
|
||||
- [ ] Phase 2: Sessions stored in Supabase (check Studio)
|
||||
- [ ] Phase 2: User A's session NOT visible to User B via API
|
||||
- [ ] Phase 3: Frontend clears state on sign-out
|
||||
- [ ] Phase 3: User B sees welcome screen (not User A's chat) after sign-in
|
||||
- [ ] Phase 3: E2E test passes (User A → chat → signout → User B → fresh state)
|
||||
- [ ] Phase 4: Invalid grade returns 400
|
||||
- [ ] Phase 4: AI errors return generic message to client
|
||||
- [ ] Phase 4: Anthropic non-streaming calls work
|
||||
- [ ] All tests pass locally
|
||||
- [ ] No console errors in browser
|
||||
- [ ] No console errors in backend
|
||||
|
||||
---
|
||||
|
||||
## Next Steps After Implementation
|
||||
|
||||
1. **Staging deployment** — Test on staging server
|
||||
2. **Production safety** — Ensure `.env` secrets not exposed
|
||||
3. **Monitoring** — Log rate limit hits, DB errors
|
||||
4. **Performance** — Monitor Supabase query times
|
||||
5. **Backup** — Set up Supabase backup schedule
|
||||
6. **Documentation** — Update API docs with RLS details
|
||||
|
||||
---
|
||||
|
||||
Ready to start? Let me know which phase first, or proceed sequentially.
|
||||
Reference in New Issue
Block a user