Spaces:
Running
Running
| 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")); |