diff --git a/CLAUDE.md b/CLAUDE.md index 4731aec..6c0c883 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -176,3 +176,40 @@ backgroundColor: 'rgba(18, 18, 18, 0.85)', The `BlurView` in `tabBarBackground` is unreliable and can fail during re-renders/transitions, causing a fully transparent tab bar. The solid rgba background acts as a fallback while still allowing the blur effect on top. **Location:** `apps/mobile/app/(app)/_layout.tsx` - `tabBarStyle` + +## E2EE (End-to-End Encryption) Implementation + +Chat messages (text, images, videos) are encrypted client-side using AES-256-GCM with triple-wrapped RSA keys. + +### Architecture +- **Full documentation:** See `docs/e2ee-implementation.md` for complete details +- **Encryption library:** `apps/mobile/src/lib/encryption/` +- **React hooks:** `apps/mobile/src/hooks/useEncryptionKeys.ts`, `useDecryptedMessage.ts` +- **Edge functions:** `handle-key-rotation`, `reencrypt-pending-messages` + +### Key Concepts +1. **Message versions:** v1 = plaintext (legacy), v2 = E2EE +2. **Triple-wrapped keys:** Each message's AES key is RSA-encrypted for sender, recipient, and admin +3. **Device recovery:** When user gets new device, admin key re-wraps messages for new user key + +### Database Tables +- `messages.version` - Encryption version (1 or 2) +- `messages.encrypted_content` - Base64 ciphertext +- `messages.encryption_iv` - Base64 IV for AES-GCM +- `messages.keys_metadata` - JSONB with wrapped keys +- `profiles.public_key_jwk` - User's RSA public key +- `master_keys` - Admin RSA public keys + +### Setup Required +1. Run `node scripts/generate-admin-keys.mjs` to generate admin keypair +2. Insert public key into `master_keys` table +3. Store private key in `ADMIN_PRIVATE_KEY_JWK` environment variable for edge functions +4. Install crypto dependency: `npm install react-native-quick-crypto@^0.7.x` +5. Run `npx expo prebuild --clean` after installation + +### Edge Functions for E2EE +| Function | verify_jwt | Purpose | +|----------|------------|---------| +| `handle-key-rotation` | false | Re-wraps message keys when user gets new device | +| `reencrypt-pending-messages` | false | Wraps keys for recipient after they generate keys | + diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index 1e294ed..a643261 100755 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -1,168 +1,177 @@ -import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; -import { AuthProvider, PERMISSION_KEYS } from '@/contexts/AuthContext'; -import { ProtectedRoute } from '@/components/ProtectedRoute'; -import { AppLayout } from '@/components/layout/AppLayout'; -import { LoginPage } from '@/pages/LoginPage'; -import { DashboardPage } from '@/pages/DashboardPage'; -import { CategoriesPage } from '@/pages/content/CategoriesPage'; -import { PacksPage } from '@/pages/content/PacksPage'; -import { QuestionsPage } from '@/pages/content/QuestionsPage'; -import { UsersPage } from '@/pages/users/UsersPage'; -import { UserDetailPage } from '@/pages/users/UserDetailPage'; -import { MatchChatPage } from '@/pages/users/MatchChatPage'; -import { AuditLogsPage } from '@/pages/AuditLogsPage'; -import { AdminsPage } from '@/pages/admins/AdminsPage'; -import { RedemptionCodesPage } from '@/pages/RedemptionCodesPage'; -import { FeedbackPage } from '@/pages/FeedbackPage'; -import { UsageInsightsPage } from '@/pages/UsageInsightsPage'; -import { UserActivityPage } from '@/pages/activity/UserActivityPage'; -import { AiSettingsPage } from '@/pages/AiSettingsPage'; -import { AppSettingsPage } from '@/pages/AppSettingsPage'; - -function App() { - return ( - - - - {/* Public routes */} - } /> - - {/* Protected routes */} - - - - } - > - {/* Dashboard */} - } /> - - {/* Content management */} - - - - } - /> - - - - } - /> - - - - } - /> - - {/* User management */} - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - {/* System */} - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - {/* Catch all - redirect to dashboard */} - } /> - - - - ); -} - -export default App; +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { AuthProvider, PERMISSION_KEYS } from '@/contexts/AuthContext'; +import { ProtectedRoute } from '@/components/ProtectedRoute'; +import { AppLayout } from '@/components/layout/AppLayout'; +import { LoginPage } from '@/pages/LoginPage'; +import { DashboardPage } from '@/pages/DashboardPage'; +import { CategoriesPage } from '@/pages/content/CategoriesPage'; +import { PacksPage } from '@/pages/content/PacksPage'; +import { QuestionsPage } from '@/pages/content/QuestionsPage'; +import { UsersPage } from '@/pages/users/UsersPage'; +import { UserDetailPage } from '@/pages/users/UserDetailPage'; +import { MatchChatPage } from '@/pages/users/MatchChatPage'; +import { AuditLogsPage } from '@/pages/AuditLogsPage'; +import { AdminsPage } from '@/pages/admins/AdminsPage'; +import { RedemptionCodesPage } from '@/pages/RedemptionCodesPage'; +import { FeedbackPage } from '@/pages/FeedbackPage'; +import { UsageInsightsPage } from '@/pages/UsageInsightsPage'; +import { UserActivityPage } from '@/pages/activity/UserActivityPage'; +import { AiSettingsPage } from '@/pages/AiSettingsPage'; +import { AppSettingsPage } from '@/pages/AppSettingsPage'; +import { FlaggedMessagesPage } from '@/pages/FlaggedMessagesPage'; + +function App() { + return ( + + + + {/* Public routes */} + } /> + + {/* Protected routes */} + + + + } + > + {/* Dashboard */} + } /> + + {/* Content management */} + + + + } + /> + + + + } + /> + + + + } + /> + + {/* User management */} + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + {/* System */} + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + {/* Catch all - redirect to dashboard */} + } /> + + + + ); +} + +export default App; diff --git a/apps/admin/src/components/layout/Sidebar.tsx b/apps/admin/src/components/layout/Sidebar.tsx index 68238b8..254dff4 100644 --- a/apps/admin/src/components/layout/Sidebar.tsx +++ b/apps/admin/src/components/layout/Sidebar.tsx @@ -17,6 +17,7 @@ import { X, Home, Activity, + AlertTriangle, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Separator } from '@/components/ui/separator'; @@ -82,6 +83,12 @@ const systemNav: NavItem[] = [ icon: , requiredPermission: PERMISSION_KEYS.MANAGE_ADMINS, }, + { + label: 'Flagged Messages', + href: '/flagged-messages', + icon: , + requiredPermission: PERMISSION_KEYS.MANAGE_AI_CONFIG, + }, { label: 'AI Settings', href: '/ai-settings', diff --git a/apps/admin/src/hooks/useAiConfig.ts b/apps/admin/src/hooks/useAiConfig.ts index 53e6cd0..01e6f6f 100644 --- a/apps/admin/src/hooks/useAiConfig.ts +++ b/apps/admin/src/hooks/useAiConfig.ts @@ -20,6 +20,9 @@ export interface AiConfig { council_reviewer_model: string | null; council_selection_mode: CouncilSelectionMode | null; // 'whole_set' or 'cherry_pick' cherry_pick_ensure_intensity_distribution: boolean | null; // Balance intensity levels in cherry-pick mode + classifier_enabled: boolean | null; + classifier_model: string | null; + classifier_prompt: string | null; updated_at: string | null; updated_by: string | null; } diff --git a/apps/admin/src/pages/AiSettingsPage.tsx b/apps/admin/src/pages/AiSettingsPage.tsx index 3876eb0..4456e48 100644 --- a/apps/admin/src/pages/AiSettingsPage.tsx +++ b/apps/admin/src/pages/AiSettingsPage.tsx @@ -9,7 +9,8 @@ import { Skeleton } from '@/components/ui/skeleton'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; -import { Save, Eye, EyeOff, AlertTriangle, Bot, Sparkles, RefreshCw, Plus, Trash2, Package, Cherry } from 'lucide-react'; +import { Textarea } from '@/components/ui/textarea'; +import { Save, Eye, EyeOff, AlertTriangle, Bot, Sparkles, RefreshCw, Plus, Trash2, Package, Cherry, Shield } from 'lucide-react'; import { toast } from 'sonner'; import { format } from 'date-fns'; import { ModelCombobox } from '@/components/ModelCombobox'; @@ -47,6 +48,9 @@ export function AiSettingsPage() { council_reviewer_model: config.council_reviewer_model || '', council_selection_mode: config.council_selection_mode || 'whole_set', cherry_pick_ensure_intensity_distribution: config.cherry_pick_ensure_intensity_distribution ?? true, + classifier_enabled: config.classifier_enabled ?? true, + classifier_model: config.classifier_model || 'openai/gpt-4o', + classifier_prompt: config.classifier_prompt || '', }); setHasChanges(false); } @@ -92,6 +96,9 @@ export function AiSettingsPage() { council_reviewer_model: config.council_reviewer_model || '', council_selection_mode: config.council_selection_mode || 'whole_set', cherry_pick_ensure_intensity_distribution: config.cherry_pick_ensure_intensity_distribution ?? true, + classifier_enabled: config.classifier_enabled ?? true, + classifier_model: config.classifier_model || 'openai/gpt-4o', + classifier_prompt: config.classifier_prompt || '', }); setHasChanges(false); } @@ -248,6 +255,59 @@ export function AiSettingsPage() { + {/* Moderation Settings */} + + + + + Moderation Settings + + + Configure automatic content moderation for messages + + + +
+
+ +

+ Automatically flag illegal/dangerous content +

+
+ handleChange('classifier_enabled', checked)} + /> +
+ +
+ + handleChange('classifier_model', value)} + placeholder="openai/gpt-4o" + /> +

+ Model used for classification (must support vision if image moderation is needed) +

+
+ +
+ +