everydaycats commited on
Commit
cf87afd
·
verified ·
1 Parent(s): d47f50b

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +86 -214
app.js CHANGED
@@ -1,243 +1,115 @@
1
- const express = require('express');
2
- const { createClient } = require('@supabase/supabase-js');
3
- const jwt = require('jsonwebtoken');
4
- const { v4: uuidv4 } = require('uuid');
5
- const axios = require('axios');
6
- const bodyParser = require('body-parser');
7
- const cors = require('cors');
8
 
9
  const app = express();
10
- app.use(cors());
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
- let supabase = null;
 
25
 
26
- try {
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
- const verifySupabaseUser = async (req, res, next) => {
43
- const debugMode = process.env.DEBUG_NO_AUTH === 'true';
44
- if (debugMode) { req.user = { id: "user_dev_01" }; return next(); }
45
  const authHeader = req.headers.authorization;
46
- if (!authHeader || !authHeader.startsWith('Bearer ')) return res.status(401).json({ error: 'Missing Bearer token' });
47
- const idToken = authHeader.split('Bearer ')[1];
 
48
  try {
49
- if (supabase) {
50
- const { data: { user }, error } = await supabase.auth.getUser(idToken);
51
- if (error || !user) throw new Error("Invalid Token");
52
- req.user = user;
53
- next();
54
- } else { req.user = { id: "memory_user" }; next(); }
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
- return null;
75
- }
76
 
77
- app.post('/key', verifySupabaseUser, (req, res) => {
 
78
  const { projectId } = req.body;
79
- if (!projectId) return res.status(400).json({ error: 'projectId required' });
80
  const key = `key_${uuidv4().replace(/-/g, '')}`;
81
- tempKeys.set(key, { uid: req.user.id, projectId: projectId, createdAt: Date.now() });
82
- console.log(`🔑 Generated Key for user ${req.user.id}: ${key}`);
 
 
 
 
 
 
83
  res.json({ key, expiresIn: 300 });
84
  });
85
 
 
86
  app.post('/redeem', async (req, res) => {
87
- const { key } = req.body;
88
- if (!key || !tempKeys.has(key)) return res.status(404).json({ error: 'Invalid or expired key' });
 
89
  const data = tempKeys.get(key);
90
- const sessionSecret = uuidv4();
91
- const token = jwt.sign({ uid: data.uid, projectId: data.projectId }, sessionSecret, { expiresIn: '3d' });
92
- const cacheKey = `${data.uid}:${data.projectId}`;
93
- activeSessions.set(cacheKey, { secret: sessionSecret, lastAccessed: Date.now() });
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
- if (!projectId) return res.status(400).json({ error: 'Missing projectId' });
147
 
148
- if (supabase) {
149
- const { data, error } = await supabase.from('projects').select('id, user_id, info').eq('id', projectId).single();
150
- if (error || !data || data.user_id !== userId) return res.status(403).json({ error: 'Unauthorized' });
151
-
152
- const targetUrl = EXTERNAL_SERVER_URL.replace(/\/$/, '') + '/project/ping';
153
- try {
154
- const response = await axios.post(targetUrl, { projectId, userId, isFrontend: true });
155
- return res.json({ ...response.data, dbStatus: data.info?.status || 'idle' });
156
- } catch (e) { return res.status(502).json({ error: "AI Server Unreachable" }); }
157
- }
158
- });
 
 
 
 
 
 
 
 
 
 
159
 
160
- /*
161
- app.post('/poll', async (req, res) => {
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
- app.post('/poll', async (req, res) => {
178
- const { token } = req.body;
179
- if (!token) return res.status(400).json({ error: 'Token required' });
180
-
181
- const decoded = jwt.decode(token);
182
- if (!decoded) return res.status(401).json({ error: 'Malformed token' });
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
- // FIX: We do NOT send isFrontend: true.
192
- // We act as the Plugin (Executor), but the Main Server will still give us the snapshot now.
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
- app.post('/project/delete', verifySupabaseUser, async (req, res) => {
204
- const { projectId } = req.body;
205
- const userId = req.user.id;
206
- if (!projectId) return res.status(400).json({ error: "Missing Project ID" });
207
- try {
208
- const { data: project, error: fetchError } = await supabase.from('projects').select('user_id').eq('id', projectId).single();
209
- if (fetchError || !project || project.user_id !== userId) return res.status(403).json({ error: "Unauthorized" });
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
- app.post('/nullify', verifySupabaseUser, async (req, res) => {
233
- const { projectId } = req.body;
234
- if (!projectId) return res.status(400).json({ error: 'projectId required' });
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.get('/', (req, res) => { res.send('Plugin Auth Proxy Running (Supabase Edition)'); });
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"));