Spaces:
Running
Running
| const express = require('express'); | |
| const { createClient } = require('@supabase/supabase-js'); | |
| const jwt = require('jsonwebtoken'); | |
| const { v4: uuidv4 } = require('uuid'); | |
| const axios = require('axios'); | |
| const bodyParser = require('body-parser'); | |
| const cors = require('cors'); | |
| const app = express(); | |
| app.use(cors()); | |
| app.use(bodyParser.json({ limit: '50mb' })); | |
| const tempKeys = new Map(); | |
| const activeSessions = new Map(); | |
| const { | |
| SUPABASE_URL, | |
| SUPABASE_SERVICE_ROLE_KEY, | |
| EXTERNAL_SERVER_URL = 'http://localhost:7860', | |
| STORAGE_BUCKET = 'project-assets', | |
| PORT = 7860 | |
| } = process.env; | |
| let supabase = null; | |
| try { | |
| if (SUPABASE_URL && SUPABASE_SERVICE_ROLE_KEY) { | |
| supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { | |
| auth: { | |
| autoRefreshToken: false, | |
| persistSession: false | |
| } | |
| }); | |
| console.log("⚡ Supabase Connected (Admin Context)"); | |
| } else { | |
| console.warn("⚠️ Memory-Only mode (Supabase credentials missing)."); | |
| } | |
| } catch (e) { | |
| console.error("Supabase Init Error:", e); | |
| } | |
| const verifySupabaseUser = async (req, res, next) => { | |
| const debugMode = process.env.DEBUG_NO_AUTH === 'true'; | |
| if (debugMode) { req.user = { id: "user_dev_01" }; return next(); } | |
| const authHeader = req.headers.authorization; | |
| if (!authHeader || !authHeader.startsWith('Bearer ')) return res.status(401).json({ error: 'Missing Bearer token' }); | |
| const idToken = authHeader.split('Bearer ')[1]; | |
| try { | |
| if (supabase) { | |
| const { data: { user }, error } = await supabase.auth.getUser(idToken); | |
| if (error || !user) throw new Error("Invalid Token"); | |
| req.user = user; | |
| next(); | |
| } else { req.user = { id: "memory_user" }; next(); } | |
| } catch (error) { return res.status(403).json({ error: 'Unauthorized', details: error.message }); } | |
| }; | |
| async function getSessionSecret(uid, projectId) { | |
| const cacheKey = `${uid}:${projectId}`; | |
| if (activeSessions.has(cacheKey)) { | |
| const session = activeSessions.get(cacheKey); | |
| session.lastAccessed = Date.now(); | |
| return session.secret; | |
| } | |
| if (supabase) { | |
| try { | |
| const { data } = await supabase.from('projects').select('plugin_secret').eq('id', projectId).eq('user_id', uid).single(); | |
| if (data && data.plugin_secret) { | |
| activeSessions.set(cacheKey, { secret: data.plugin_secret, lastAccessed: Date.now() }); | |
| return data.plugin_secret; | |
| } | |
| } catch (err) { console.error("DB Read Error:", err); } | |
| } | |
| return null; | |
| } | |
| app.post('/key', verifySupabaseUser, (req, res) => { | |
| const { projectId } = req.body; | |
| if (!projectId) return res.status(400).json({ error: 'projectId required' }); | |
| const key = `key_${uuidv4().replace(/-/g, '')}`; | |
| tempKeys.set(key, { uid: req.user.id, projectId: projectId, createdAt: Date.now() }); | |
| console.log(`🔑 Generated Key for user ${req.user.id}: ${key}`); | |
| res.json({ key, expiresIn: 300 }); | |
| }); | |
| app.post('/redeem', async (req, res) => { | |
| const { key } = req.body; | |
| if (!key || !tempKeys.has(key)) return res.status(404).json({ error: 'Invalid or expired key' }); | |
| const data = tempKeys.get(key); | |
| const sessionSecret = uuidv4(); | |
| const token = jwt.sign({ uid: data.uid, projectId: data.projectId }, sessionSecret, { expiresIn: '3d' }); | |
| const cacheKey = `${data.uid}:${data.projectId}`; | |
| activeSessions.set(cacheKey, { secret: sessionSecret, lastAccessed: Date.now() }); | |
| if (supabase) await supabase.from('projects').update({ plugin_secret: sessionSecret }).eq('id', data.projectId).eq('user_id', data.uid); | |
| tempKeys.delete(key); | |
| console.log(`🚀 Redeemed JWT for ${cacheKey}`); | |
| res.json({ token }); | |
| }); | |
| app.post('/verify', async (req, res) => { | |
| const { token } = req.body; | |
| if (!token) return res.status(400).json({ valid: false }); | |
| const decoded = jwt.decode(token); | |
| if (!decoded || !decoded.uid || !decoded.projectId) return res.status(401).json({ valid: false }); | |
| const secret = await getSessionSecret(decoded.uid, decoded.projectId); | |
| if (!secret) return res.status(401).json({ valid: false }); | |
| try { jwt.verify(token, secret); return res.json({ valid: true }); } catch (err) { return res.status(403).json({ valid: false }); } | |
| }); | |
| app.post('/feedback', async (req, res) => { | |
| const { token, ...pluginPayload } = req.body; | |
| if (!token) return res.status(400).json({ error: 'Token required' }); | |
| const decoded = jwt.decode(token); | |
| if (!decoded) return res.status(401).json({ error: 'Malformed token' }); | |
| const secret = await getSessionSecret(decoded.uid, decoded.projectId); | |
| if (!secret) return res.status(404).json({ error: 'Session revoked' }); | |
| try { | |
| jwt.verify(token, secret); | |
| const targetUrl = EXTERNAL_SERVER_URL.replace(/\/$/, '') + '/project/feedback'; | |
| const response = await axios.post(targetUrl, { userId: decoded.uid, projectId: decoded.projectId, ...pluginPayload }); | |
| return res.json({ success: true, externalResponse: response.data }); | |
| } catch (err) { return res.status(502).json({ error: 'Failed to forward' }); } | |
| }); | |
| app.post('/feedback2', verifySupabaseUser, async (req, res) => { | |
| const { projectId, prompt, images, ...otherPayload } = req.body; | |
| const userId = req.user.id; | |
| if (!projectId || !prompt) return res.status(400).json({ error: 'Missing projectId or prompt' }); | |
| const targetUrl = EXTERNAL_SERVER_URL.replace(/\/$/, '') + '/project/feedback'; | |
| try { | |
| const response = await axios.post(targetUrl, { userId: userId, projectId: projectId, prompt: prompt, images: images || [], ...otherPayload }); | |
| return res.json({ success: true, externalResponse: response.data }); | |
| } catch (err) { return res.status(502).json({ error: 'Failed to forward' }); } | |
| }); | |
| // --- STREAM FEED (Optimized headers) --- | |
| app.post('/stream-feed', verifySupabaseUser, async (req, res) => { | |
| const { projectId } = req.body; | |
| const userId = req.user.id; | |
| // Headers to disable caching for poller | |
| res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); | |
| res.setHeader('Pragma', 'no-cache'); | |
| res.setHeader('Expires', '0'); | |
| if (!projectId) return res.status(400).json({ error: 'Missing projectId' }); | |
| if (supabase) { | |
| const { data, error } = await supabase.from('projects').select('id, user_id, info').eq('id', projectId).single(); | |
| if (error || !data || data.user_id !== userId) return res.status(403).json({ error: 'Unauthorized' }); | |
| const targetUrl = EXTERNAL_SERVER_URL.replace(/\/$/, '') + '/project/ping'; | |
| try { | |
| const response = await axios.post(targetUrl, { projectId, userId, isFrontend: true }); | |
| return res.json({ ...response.data, dbStatus: data.info?.status || 'idle' }); | |
| } catch (e) { return res.status(502).json({ error: "AI Server Unreachable" }); } | |
| } | |
| }); | |
| /* | |
| app.post('/poll', async (req, res) => { | |
| const { token } = req.body; | |
| if (!token) return res.status(400).json({ error: 'Token required' }); | |
| const decoded = jwt.decode(token); | |
| if (!decoded) return res.status(401).json({ error: 'Malformed token' }); | |
| const secret = await getSessionSecret(decoded.uid, decoded.projectId); | |
| if (!secret) return res.status(404).json({ error: 'Session revoked' }); | |
| try { | |
| jwt.verify(token, secret); | |
| const targetUrl = EXTERNAL_SERVER_URL.replace(/\/$/, '') + '/project/ping'; | |
| const response = await axios.post(targetUrl, { projectId: decoded.projectId, userId: decoded.uid }); | |
| return res.json(response.data); | |
| } catch (err) { return res.status(403).json({ error: 'Invalid Token' }); } | |
| }); | |
| */ | |
| app.post('/poll', async (req, res) => { | |
| const { token } = req.body; | |
| if (!token) return res.status(400).json({ error: 'Token required' }); | |
| const decoded = jwt.decode(token); | |
| if (!decoded) return res.status(401).json({ error: 'Malformed token' }); | |
| const secret = await getSessionSecret(decoded.uid, decoded.projectId); | |
| if (!secret) return res.status(404).json({ error: 'Session revoked' }); | |
| try { | |
| jwt.verify(token, secret); | |
| const targetUrl = EXTERNAL_SERVER_URL.replace(/\/$/, '') + '/project/ping'; | |
| // FIX: We do NOT send isFrontend: true. | |
| // We act as the Plugin (Executor), but the Main Server will still give us the snapshot now. | |
| const response = await axios.post(targetUrl, { | |
| projectId: decoded.projectId, | |
| userId: decoded.uid | |
| }); | |
| return res.json(response.data); | |
| } catch (err) { return res.status(403).json({ error: 'Invalid Token' }); } | |
| }); | |
| app.post('/project/delete', verifySupabaseUser, async (req, res) => { | |
| const { projectId } = req.body; | |
| const userId = req.user.id; | |
| if (!projectId) return res.status(400).json({ error: "Missing Project ID" }); | |
| try { | |
| const { data: project, error: fetchError } = await supabase.from('projects').select('user_id').eq('id', projectId).single(); | |
| if (fetchError || !project || project.user_id !== userId) return res.status(403).json({ error: "Unauthorized" }); | |
| await supabase.from('message_chunks').delete().eq('project_id', projectId); | |
| await supabase.from('projects').delete().eq('id', projectId); | |
| if (STORAGE_BUCKET) { | |
| const { data: files } = await supabase.storage.from(STORAGE_BUCKET).list(projectId); | |
| if (files && files.length > 0) { const filesToRemove = files.map(f => `${projectId}/${f.name}`); await supabase.storage.from(STORAGE_BUCKET).remove(filesToRemove); } | |
| } | |
| activeSessions.delete(`${userId}:${projectId}`); | |
| for (const [key, val] of tempKeys.entries()) { if (val.projectId === projectId) tempKeys.delete(key); } | |
| res.json({ success: true }); | |
| } catch (err) { res.status(500).json({ error: "Delete failed" }); } | |
| }); | |
| app.get('/cleanup', (req, res) => { | |
| // ... (Standard cleanup) ... | |
| const THRESHOLD = 1000 * 60 * 60; | |
| const now = Date.now(); | |
| let cleanedCount = 0; | |
| for (const [key, value] of activeSessions.entries()) { if (now - value.lastAccessed > THRESHOLD) { activeSessions.delete(key); cleanedCount++; } } | |
| for (const [key, value] of tempKeys.entries()) { if (now - value.createdAt > (1000 * 60 * 4)) { tempKeys.delete(key); } } | |
| res.json({ message: `Cleaned ${cleanedCount} cached sessions from memory.` }); | |
| }); | |
| app.post('/nullify', verifySupabaseUser, async (req, res) => { | |
| const { projectId } = req.body; | |
| if (!projectId) return res.status(400).json({ error: 'projectId required' }); | |
| const cacheKey = `${req.user.id}:${projectId}`; | |
| activeSessions.delete(cacheKey); | |
| if (supabase) await supabase.from('projects').update({ plugin_secret: null }).eq('id', projectId).eq('user_id', req.user.id); | |
| res.json({ success: true }); | |
| }); | |
| app.get('/', (req, res) => { res.send('Plugin Auth Proxy Running (Supabase Edition)'); }); | |
| app.listen(PORT, () => { console.log(`🚀 Auth Proxy running on port ${PORT}`); }); |