Spaces:
Running
Running
Update app.js
Browse files
app.js
CHANGED
|
@@ -1,243 +1,115 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
const bodyParser = require('body-parser');
|
| 7 |
-
const cors = require('cors');
|
| 8 |
|
| 9 |
const app = express();
|
| 10 |
-
|
| 11 |
-
app.use(bodyParser.json({ limit: '50mb' }));
|
| 12 |
-
|
| 13 |
-
const tempKeys = new Map();
|
| 14 |
-
const activeSessions = new Map();
|
| 15 |
-
|
| 16 |
-
const {
|
| 17 |
-
SUPABASE_URL,
|
| 18 |
-
SUPABASE_SERVICE_ROLE_KEY,
|
| 19 |
-
EXTERNAL_SERVER_URL = 'http://localhost:7860',
|
| 20 |
-
STORAGE_BUCKET = 'project-assets',
|
| 21 |
-
PORT = 7860
|
| 22 |
-
} = process.env;
|
| 23 |
|
| 24 |
-
|
|
|
|
| 25 |
|
| 26 |
-
|
| 27 |
-
if (SUPABASE_URL && SUPABASE_SERVICE_ROLE_KEY) {
|
| 28 |
-
supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, {
|
| 29 |
-
auth: {
|
| 30 |
-
autoRefreshToken: false,
|
| 31 |
-
persistSession: false
|
| 32 |
-
}
|
| 33 |
-
});
|
| 34 |
-
console.log("⚡ Supabase Connected (Admin Context)");
|
| 35 |
-
} else {
|
| 36 |
-
console.warn("⚠️ Memory-Only mode (Supabase credentials missing).");
|
| 37 |
-
}
|
| 38 |
-
} catch (e) {
|
| 39 |
-
console.error("Supabase Init Error:", e);
|
| 40 |
-
}
|
| 41 |
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
if (debugMode) { req.user = { id: "user_dev_01" }; return next(); }
|
| 45 |
const authHeader = req.headers.authorization;
|
| 46 |
-
if (!authHeader
|
| 47 |
-
|
|
|
|
| 48 |
try {
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
} catch (error) { return res.status(403).json({ error: 'Unauthorized', details: error.message }); }
|
| 56 |
-
};
|
| 57 |
-
|
| 58 |
-
async function getSessionSecret(uid, projectId) {
|
| 59 |
-
const cacheKey = `${uid}:${projectId}`;
|
| 60 |
-
if (activeSessions.has(cacheKey)) {
|
| 61 |
-
const session = activeSessions.get(cacheKey);
|
| 62 |
-
session.lastAccessed = Date.now();
|
| 63 |
-
return session.secret;
|
| 64 |
-
}
|
| 65 |
-
if (supabase) {
|
| 66 |
-
try {
|
| 67 |
-
const { data } = await supabase.from('projects').select('plugin_secret').eq('id', projectId).eq('user_id', uid).single();
|
| 68 |
-
if (data && data.plugin_secret) {
|
| 69 |
-
activeSessions.set(cacheKey, { secret: data.plugin_secret, lastAccessed: Date.now() });
|
| 70 |
-
return data.plugin_secret;
|
| 71 |
-
}
|
| 72 |
-
} catch (err) { console.error("DB Read Error:", err); }
|
| 73 |
}
|
| 74 |
-
|
| 75 |
-
}
|
| 76 |
|
| 77 |
-
|
|
|
|
| 78 |
const { projectId } = req.body;
|
| 79 |
-
if (!projectId) return res.status(400).json({ error: 'projectId required' });
|
| 80 |
const key = `key_${uuidv4().replace(/-/g, '')}`;
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
res.json({ key, expiresIn: 300 });
|
| 84 |
});
|
| 85 |
|
|
|
|
| 86 |
app.post('/redeem', async (req, res) => {
|
| 87 |
-
const { key } = req.body;
|
| 88 |
-
if (!
|
|
|
|
| 89 |
const data = tempKeys.get(key);
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
if (supabase) await supabase.from('projects').update({ plugin_secret: sessionSecret }).eq('id', data.projectId).eq('user_id', data.uid);
|
| 95 |
-
tempKeys.delete(key);
|
| 96 |
-
console.log(`🚀 Redeemed JWT for ${cacheKey}`);
|
| 97 |
-
res.json({ token });
|
| 98 |
-
});
|
| 99 |
-
|
| 100 |
-
app.post('/verify', async (req, res) => {
|
| 101 |
-
const { token } = req.body;
|
| 102 |
-
if (!token) return res.status(400).json({ valid: false });
|
| 103 |
-
const decoded = jwt.decode(token);
|
| 104 |
-
if (!decoded || !decoded.uid || !decoded.projectId) return res.status(401).json({ valid: false });
|
| 105 |
-
const secret = await getSessionSecret(decoded.uid, decoded.projectId);
|
| 106 |
-
if (!secret) return res.status(401).json({ valid: false });
|
| 107 |
-
try { jwt.verify(token, secret); return res.json({ valid: true }); } catch (err) { return res.status(403).json({ valid: false }); }
|
| 108 |
-
});
|
| 109 |
-
|
| 110 |
-
app.post('/feedback', async (req, res) => {
|
| 111 |
-
const { token, ...pluginPayload } = req.body;
|
| 112 |
-
if (!token) return res.status(400).json({ error: 'Token required' });
|
| 113 |
-
const decoded = jwt.decode(token);
|
| 114 |
-
if (!decoded) return res.status(401).json({ error: 'Malformed token' });
|
| 115 |
-
const secret = await getSessionSecret(decoded.uid, decoded.projectId);
|
| 116 |
-
if (!secret) return res.status(404).json({ error: 'Session revoked' });
|
| 117 |
-
try {
|
| 118 |
-
jwt.verify(token, secret);
|
| 119 |
-
const targetUrl = EXTERNAL_SERVER_URL.replace(/\/$/, '') + '/project/feedback';
|
| 120 |
-
const response = await axios.post(targetUrl, { userId: decoded.uid, projectId: decoded.projectId, ...pluginPayload });
|
| 121 |
-
return res.json({ success: true, externalResponse: response.data });
|
| 122 |
-
} catch (err) { return res.status(502).json({ error: 'Failed to forward' }); }
|
| 123 |
-
});
|
| 124 |
-
|
| 125 |
-
app.post('/feedback2', verifySupabaseUser, async (req, res) => {
|
| 126 |
-
const { projectId, prompt, images, ...otherPayload } = req.body;
|
| 127 |
-
const userId = req.user.id;
|
| 128 |
-
if (!projectId || !prompt) return res.status(400).json({ error: 'Missing projectId or prompt' });
|
| 129 |
-
const targetUrl = EXTERNAL_SERVER_URL.replace(/\/$/, '') + '/project/feedback';
|
| 130 |
-
try {
|
| 131 |
-
const response = await axios.post(targetUrl, { userId: userId, projectId: projectId, prompt: prompt, images: images || [], ...otherPayload });
|
| 132 |
-
return res.json({ success: true, externalResponse: response.data });
|
| 133 |
-
} catch (err) { return res.status(502).json({ error: 'Failed to forward' }); }
|
| 134 |
-
});
|
| 135 |
-
|
| 136 |
-
// --- STREAM FEED (Optimized headers) ---
|
| 137 |
-
app.post('/stream-feed', verifySupabaseUser, async (req, res) => {
|
| 138 |
-
const { projectId } = req.body;
|
| 139 |
-
const userId = req.user.id;
|
| 140 |
-
|
| 141 |
-
// Headers to disable caching for poller
|
| 142 |
-
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
| 143 |
-
res.setHeader('Pragma', 'no-cache');
|
| 144 |
-
res.setHeader('Expires', '0');
|
| 145 |
|
| 146 |
-
|
| 147 |
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
const { token } = req.body;
|
| 163 |
-
if (!token) return res.status(400).json({ error: 'Token required' });
|
| 164 |
-
const decoded = jwt.decode(token);
|
| 165 |
-
if (!decoded) return res.status(401).json({ error: 'Malformed token' });
|
| 166 |
-
const secret = await getSessionSecret(decoded.uid, decoded.projectId);
|
| 167 |
-
if (!secret) return res.status(404).json({ error: 'Session revoked' });
|
| 168 |
-
try {
|
| 169 |
-
jwt.verify(token, secret);
|
| 170 |
-
const targetUrl = EXTERNAL_SERVER_URL.replace(/\/$/, '') + '/project/ping';
|
| 171 |
-
const response = await axios.post(targetUrl, { projectId: decoded.projectId, userId: decoded.uid });
|
| 172 |
-
return res.json(response.data);
|
| 173 |
-
} catch (err) { return res.status(403).json({ error: 'Invalid Token' }); }
|
| 174 |
});
|
| 175 |
-
*/
|
| 176 |
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
const secret = await getSessionSecret(decoded.uid, decoded.projectId);
|
| 185 |
-
if (!secret) return res.status(404).json({ error: 'Session revoked' });
|
| 186 |
-
|
| 187 |
-
try {
|
| 188 |
-
jwt.verify(token, secret);
|
| 189 |
-
const targetUrl = EXTERNAL_SERVER_URL.replace(/\/$/, '') + '/project/ping';
|
| 190 |
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
const response = await axios.post(targetUrl, {
|
| 194 |
-
projectId: decoded.projectId,
|
| 195 |
-
userId: decoded.uid
|
| 196 |
-
});
|
| 197 |
-
|
| 198 |
-
return res.json(response.data);
|
| 199 |
-
} catch (err) { return res.status(403).json({ error: 'Invalid Token' }); }
|
| 200 |
});
|
| 201 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
const
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
await supabase.from('message_chunks').delete().eq('project_id', projectId);
|
| 211 |
-
await supabase.from('projects').delete().eq('id', projectId);
|
| 212 |
-
if (STORAGE_BUCKET) {
|
| 213 |
-
const { data: files } = await supabase.storage.from(STORAGE_BUCKET).list(projectId);
|
| 214 |
-
if (files && files.length > 0) { const filesToRemove = files.map(f => `${projectId}/${f.name}`); await supabase.storage.from(STORAGE_BUCKET).remove(filesToRemove); }
|
| 215 |
-
}
|
| 216 |
-
activeSessions.delete(`${userId}:${projectId}`);
|
| 217 |
-
for (const [key, val] of tempKeys.entries()) { if (val.projectId === projectId) tempKeys.delete(key); }
|
| 218 |
-
res.json({ success: true });
|
| 219 |
-
} catch (err) { res.status(500).json({ error: "Delete failed" }); }
|
| 220 |
-
});
|
| 221 |
-
|
| 222 |
-
app.get('/cleanup', (req, res) => {
|
| 223 |
-
// ... (Standard cleanup) ...
|
| 224 |
-
const THRESHOLD = 1000 * 60 * 60;
|
| 225 |
-
const now = Date.now();
|
| 226 |
-
let cleanedCount = 0;
|
| 227 |
-
for (const [key, value] of activeSessions.entries()) { if (now - value.lastAccessed > THRESHOLD) { activeSessions.delete(key); cleanedCount++; } }
|
| 228 |
-
for (const [key, value] of tempKeys.entries()) { if (now - value.createdAt > (1000 * 60 * 4)) { tempKeys.delete(key); } }
|
| 229 |
-
res.json({ message: `Cleaned ${cleanedCount} cached sessions from memory.` });
|
| 230 |
-
});
|
| 231 |
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
const cacheKey = `${req.user.id}:${projectId}`;
|
| 236 |
-
activeSessions.delete(cacheKey);
|
| 237 |
-
if (supabase) await supabase.from('projects').update({ plugin_secret: null }).eq('id', projectId).eq('user_id', req.user.id);
|
| 238 |
res.json({ success: true });
|
| 239 |
});
|
| 240 |
|
| 241 |
-
app.
|
| 242 |
-
|
| 243 |
-
app.listen(PORT, () => { console.log(`🚀 Auth Proxy running on port ${PORT}`); });
|
|
|
|
| 1 |
+
import express from 'express';
|
| 2 |
+
import { createClient } from '@supabase/supabase-js';
|
| 3 |
+
import jwt from 'jsonwebtoken';
|
| 4 |
+
import { v4 as uuidv4 } from 'uuid';
|
| 5 |
+
import cors from 'cors';
|
|
|
|
|
|
|
| 6 |
|
| 7 |
const app = express();
|
| 8 |
+
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE_KEY);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
+
app.use(cors());
|
| 11 |
+
app.use(express.json());
|
| 12 |
|
| 13 |
+
const tempKeys = new Map(); // key -> { uid, projectId, createdAt }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
+
// --- MIDDLEWARE: AUTHENTICATION ---
|
| 16 |
+
const verifySupabaseSession = async (req, res, next) => {
|
|
|
|
| 17 |
const authHeader = req.headers.authorization;
|
| 18 |
+
if (!authHeader?.startsWith('Bearer ')) return res.status(401).json({ error: 'Unauthorized' });
|
| 19 |
+
|
| 20 |
+
const idToken = authHeader.split(' ')[1];
|
| 21 |
try {
|
| 22 |
+
const { data: { user }, error } = await supabase.auth.getUser(idToken);
|
| 23 |
+
if (error || !user) throw new Error("Invalid Session");
|
| 24 |
+
req.user = user;
|
| 25 |
+
next();
|
| 26 |
+
} catch (err) {
|
| 27 |
+
return res.status(403).json({ error: 'Session Expired', details: err.message });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
}
|
| 29 |
+
};
|
|
|
|
| 30 |
|
| 31 |
+
// 1. GENERATE TEMP KEY (Called by Auth Page)
|
| 32 |
+
app.post('/key', verifySupabaseSession, (req, res) => {
|
| 33 |
const { projectId } = req.body;
|
|
|
|
| 34 |
const key = `key_${uuidv4().replace(/-/g, '')}`;
|
| 35 |
+
|
| 36 |
+
// Store key with 5-minute expiry
|
| 37 |
+
tempKeys.set(key, {
|
| 38 |
+
uid: req.user.id,
|
| 39 |
+
projectId: projectId || 'default',
|
| 40 |
+
createdAt: Date.now()
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
res.json({ key, expiresIn: 300 });
|
| 44 |
});
|
| 45 |
|
| 46 |
+
// 2. REDEEM KEY (Called by Local CLI)
|
| 47 |
app.post('/redeem', async (req, res) => {
|
| 48 |
+
const { key, deviceName } = req.body;
|
| 49 |
+
if (!tempKeys.has(key)) return res.status(404).json({ error: 'Key invalid' });
|
| 50 |
+
|
| 51 |
const data = tempKeys.get(key);
|
| 52 |
+
if (Date.now() - data.createdAt > 300000) {
|
| 53 |
+
tempKeys.delete(key);
|
| 54 |
+
return res.status(410).json({ error: 'Key expired' });
|
| 55 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
+
const sessionSecret = uuidv4();
|
| 58 |
|
| 59 |
+
// Create persistent session in DB
|
| 60 |
+
const { data: session, error } = await supabase
|
| 61 |
+
.from('user_sessions')
|
| 62 |
+
.insert({
|
| 63 |
+
user_id: data.uid,
|
| 64 |
+
project_id: data.projectId,
|
| 65 |
+
session_secret: sessionSecret,
|
| 66 |
+
device_name: deviceName || 'Unknown Device'
|
| 67 |
+
})
|
| 68 |
+
.select()
|
| 69 |
+
.single();
|
| 70 |
+
|
| 71 |
+
if (error) return res.status(500).json({ error: "Failed to create session" });
|
| 72 |
+
|
| 73 |
+
// Sign JWT using the unique session secret
|
| 74 |
+
// Payload contains the session ID so Gateway can verify DB existence
|
| 75 |
+
const token = jwt.sign(
|
| 76 |
+
{ uid: data.uid, projectId: data.projectId, sid: session.id },
|
| 77 |
+
sessionSecret,
|
| 78 |
+
{ expiresIn: '30d' }
|
| 79 |
+
);
|
| 80 |
|
| 81 |
+
tempKeys.delete(key);
|
| 82 |
+
res.json({ token });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
});
|
|
|
|
| 84 |
|
| 85 |
+
// 3. LIST ACTIVE SESSIONS (Called by Web Console)
|
| 86 |
+
app.get('/sessions', verifySupabaseSession, async (req, res) => {
|
| 87 |
+
const { data, error } = await supabase
|
| 88 |
+
.from('user_sessions')
|
| 89 |
+
.select('id, project_id, device_name, created_at, last_used_at')
|
| 90 |
+
.eq('user_id', req.user.id);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
+
if (error) return res.status(500).json({ error: error.message });
|
| 93 |
+
res.json(data);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
});
|
| 95 |
|
| 96 |
+
// 4. REVOKE SESSION (Called by Web Console)
|
| 97 |
+
app.post('/revoke', verifySupabaseSession, async (req, res) => {
|
| 98 |
+
const { sessionId } = req.body;
|
| 99 |
+
if (!sessionId) return res.status(400).json({ error: "Session ID required" });
|
| 100 |
|
| 101 |
+
// RLS ensures the user can only delete their own,
|
| 102 |
+
// but we add an explicit check for safety.
|
| 103 |
+
const { error } = await supabase
|
| 104 |
+
.from('user_sessions')
|
| 105 |
+
.delete()
|
| 106 |
+
.eq('id', sessionId)
|
| 107 |
+
.eq('user_id', req.user.id);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
+
if (error) return res.status(500).json({ error: "Revocation failed" });
|
| 110 |
+
|
| 111 |
+
console.log(`🗑️ Session ${sessionId} revoked by user ${req.user.id}`);
|
|
|
|
|
|
|
|
|
|
| 112 |
res.json({ success: true });
|
| 113 |
});
|
| 114 |
|
| 115 |
+
app.listen(7861, () => console.log("🚀 Auth Proxy: Secure & Revocable"));
|
|
|
|
|
|