import express from 'express'; import { createClient } from '@supabase/supabase-js'; import jwt from 'jsonwebtoken'; import { v4 as uuidv4 } from 'uuid'; import cors from 'cors'; const app = express(); const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE_KEY); app.use(cors()); app.use(express.json()); app.get('/', (req, res) => res.send('Gateway Active')); const tempKeys = new Map(); // key -> { uid, projectId, createdAt } // --- MIDDLEWARE: AUTHENTICATION --- const verifySupabaseSession = async (req, res, next) => { const authHeader = req.headers.authorization; if (!authHeader?.startsWith('Bearer ')) return res.status(401).json({ error: 'Unauthorized' }); const idToken = authHeader.split(' ')[1]; try { const { data: { user }, error } = await supabase.auth.getUser(idToken); if (error || !user) throw new Error("Invalid Session"); req.user = user; next(); } catch (err) { return res.status(403).json({ error: 'Session Expired', details: err.message }); } }; // 1. GENERATE TEMP KEY (Called by Auth Page) app.post('/key', verifySupabaseSession, (req, res) => { const { projectId } = req.body; const key = `key_${uuidv4().replace(/-/g, '')}`; // Store key with 5-minute expiry tempKeys.set(key, { uid: req.user.id, projectId: projectId || 'default', createdAt: Date.now() }); res.json({ key, expiresIn: 300 }); }); // 2. REDEEM KEY (Called by Local CLI) app.post('/redeem', async (req, res) => { const { key, deviceName } = req.body; if (!tempKeys.has(key)) return res.status(404).json({ error: 'Key invalid' }); const data = tempKeys.get(key); if (Date.now() - data.createdAt > 300000) { tempKeys.delete(key); return res.status(410).json({ error: 'Key expired' }); } const sessionSecret = uuidv4(); // Create persistent session in DB const { data: session, error } = await supabase .from('user_sessions') .insert({ user_id: data.uid, project_id: data.projectId, session_secret: sessionSecret, device_name: deviceName || 'Unknown Device' }) .select() .single(); if (error) return res.status(500).json({ error: "Failed to create session" }); // Sign JWT using the unique session secret // Payload contains the session ID so Gateway can verify DB existence const token = jwt.sign( { uid: data.uid, projectId: data.projectId, sid: session.id }, sessionSecret, { expiresIn: '30d' } ); tempKeys.delete(key); res.json({ token }); }); // 3. LIST ACTIVE SESSIONS (Called by Web Console) app.get('/sessions', verifySupabaseSession, async (req, res) => { const { data, error } = await supabase .from('user_sessions') .select('id, project_id, device_name, created_at, last_used_at') .eq('user_id', req.user.id); if (error) return res.status(500).json({ error: error.message }); res.json(data); }); // 4. REVOKE SESSION (Called by Web Console) app.post('/revoke', verifySupabaseSession, async (req, res) => { const { sessionId } = req.body; if (!sessionId) return res.status(400).json({ error: "Session ID required" }); // RLS ensures the user can only delete their own, // but we add an explicit check for safety. const { error } = await supabase .from('user_sessions') .delete() .eq('id', sessionId) .eq('user_id', req.user.id); if (error) return res.status(500).json({ error: "Revocation failed" }); console.log(`🗑️ Session ${sessionId} revoked by user ${req.user.id}`); res.json({ success: true }); }); app.listen(7860, () => console.log("🚀 Auth Proxy: Secure & Revocable"));