everydaycats's picture
Update app.js
d798d35 verified
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}`); });