everydaycats commited on
Commit
bed4e30
·
verified ·
1 Parent(s): c13a5c0

Create app.js

Browse files
Files changed (1) hide show
  1. app.js +434 -0
app.js ADDED
@@ -0,0 +1,434 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ // ---------------------------------------------------------
14
+ // 1. STATE MANAGEMENT
15
+ // ---------------------------------------------------------
16
+ const tempKeys = new Map();
17
+ const activeSessions = new Map();
18
+
19
+ // ---------------------------------------------------------
20
+ // 2. SUPABASE INITIALIZATION
21
+ // ---------------------------------------------------------
22
+ const {
23
+ SUPABASE_URL,
24
+ SUPABASE_SERVICE_ROLE_KEY,
25
+ EXTERNAL_SERVER_URL = 'http://localhost:7860',
26
+ STORAGE_BUCKET = 'project-assets', // Default bucket name
27
+ PORT = 7860
28
+ } = process.env;
29
+
30
+ let supabase = null;
31
+
32
+ try {
33
+ if (SUPABASE_URL && SUPABASE_SERVICE_ROLE_KEY) {
34
+ // Use Service Role Key for Admin privileges (bypass RLS for management)
35
+ supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, {
36
+ auth: {
37
+ autoRefreshToken: false,
38
+ persistSession: false
39
+ }
40
+ });
41
+ console.log("⚡ Supabase Connected (Admin Context)");
42
+ } else {
43
+ console.warn("⚠️ Memory-Only mode (Supabase credentials missing).");
44
+ }
45
+ } catch (e) {
46
+ console.error("Supabase Init Error:", e);
47
+ }
48
+
49
+ // ---------------------------------------------------------
50
+ // 3. MIDDLEWARE
51
+ // ---------------------------------------------------------
52
+ const verifySupabaseUser = async (req, res, next) => {
53
+ const debugMode = process.env.DEBUG_NO_AUTH === 'true';
54
+
55
+ if (debugMode) {
56
+ req.user = { id: "user_dev_01" };
57
+ return next();
58
+ }
59
+
60
+ const authHeader = req.headers.authorization;
61
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
62
+ return res.status(401).json({ error: 'Missing Bearer token' });
63
+ }
64
+
65
+ const idToken = authHeader.split('Bearer ')[1];
66
+
67
+ try {
68
+ if (supabase) {
69
+ const { data: { user }, error } = await supabase.auth.getUser(idToken);
70
+ if (error || !user) throw new Error("Invalid Token");
71
+ req.user = user;
72
+ next();
73
+ } else {
74
+ req.user = { id: "memory_user" };
75
+ next();
76
+ }
77
+ } catch (error) {
78
+ return res.status(403).json({ error: 'Unauthorized', details: error.message });
79
+ }
80
+ };
81
+
82
+ async function getSessionSecret(uid, projectId) {
83
+ const cacheKey = `${uid}:${projectId}`;
84
+ if (activeSessions.has(cacheKey)) {
85
+ const session = activeSessions.get(cacheKey);
86
+ session.lastAccessed = Date.now();
87
+ return session.secret;
88
+ }
89
+
90
+ if (supabase) {
91
+ try {
92
+ // Retrieve plugin_secret from projects table
93
+ const { data, error } = await supabase
94
+ .from('projects')
95
+ .select('plugin_secret')
96
+ .eq('id', projectId)
97
+ .eq('user_id', uid)
98
+ .single();
99
+
100
+ if (data && data.plugin_secret) {
101
+ const secret = data.plugin_secret;
102
+ activeSessions.set(cacheKey, { secret, lastAccessed: Date.now() });
103
+ console.log(`💧 Hydrated secret for ${cacheKey} from DB`);
104
+ return secret;
105
+ }
106
+ } catch (err) {
107
+ console.error("DB Read Error:", err);
108
+ }
109
+ }
110
+ return null;
111
+ }
112
+
113
+ // ---------------------------------------------------------
114
+ // 4. ENDPOINTS
115
+ // ---------------------------------------------------------
116
+
117
+ app.post('/key', verifySupabaseUser, (req, res) => {
118
+ const { projectId } = req.body;
119
+ if (!projectId) return res.status(400).json({ error: 'projectId required' });
120
+
121
+ const key = `key_${uuidv4().replace(/-/g, '')}`;
122
+
123
+ tempKeys.set(key, {
124
+ uid: req.user.id, // Supabase UUID
125
+ projectId: projectId,
126
+ createdAt: Date.now()
127
+ });
128
+
129
+ console.log(`🔑 Generated Key for user ${req.user.id}: ${key}`);
130
+ res.json({ key, expiresIn: 300 });
131
+ });
132
+
133
+ app.post('/redeem', async (req, res) => {
134
+ const { key } = req.body;
135
+
136
+ if (!key || !tempKeys.has(key)) {
137
+ return res.status(404).json({ error: 'Invalid or expired key' });
138
+ }
139
+
140
+ const data = tempKeys.get(key);
141
+ const sessionSecret = uuidv4();
142
+
143
+ const token = jwt.sign(
144
+ { uid: data.uid, projectId: data.projectId },
145
+ sessionSecret,
146
+ { expiresIn: '3d' }
147
+ );
148
+
149
+ const cacheKey = `${data.uid}:${data.projectId}`;
150
+
151
+ activeSessions.set(cacheKey, { secret: sessionSecret, lastAccessed: Date.now() });
152
+
153
+ if (supabase) {
154
+ // Store secret in the projects table
155
+ await supabase
156
+ .from('projects')
157
+ .update({ plugin_secret: sessionSecret })
158
+ .eq('id', data.projectId)
159
+ .eq('user_id', data.uid);
160
+ }
161
+
162
+ tempKeys.delete(key);
163
+ console.log(`🚀 Redeemed JWT for ${cacheKey}`);
164
+ res.json({ token });
165
+ });
166
+
167
+ app.post('/verify', async (req, res) => {
168
+ const { token } = req.body;
169
+ if (!token) return res.status(400).json({ valid: false, error: 'Token required' });
170
+
171
+ const decoded = jwt.decode(token);
172
+ if (!decoded || !decoded.uid || !decoded.projectId) {
173
+ return res.status(401).json({ valid: false, error: 'Malformed token' });
174
+ }
175
+
176
+ const secret = await getSessionSecret(decoded.uid, decoded.projectId);
177
+
178
+ if (!secret) {
179
+ return res.status(401).json({ valid: false, error: 'Session revoked' });
180
+ }
181
+
182
+ try {
183
+ jwt.verify(token, secret);
184
+ const threeDaysInSeconds = 3 * 24 * 60 * 60;
185
+ const nowInSeconds = Math.floor(Date.now() / 1000);
186
+ if (decoded.iat && (nowInSeconds - decoded.iat > threeDaysInSeconds)) {
187
+ return res.status(403).json({ valid: false, error: 'Expired' });
188
+ }
189
+
190
+ return res.json({ valid: true });
191
+ } catch (err) {
192
+ return res.status(403).json({ valid: false, error: 'Invalid signature' });
193
+ }
194
+ });
195
+
196
+ // ---------------------------------------------------------
197
+ // PROXY ENDPOINTS
198
+ // ---------------------------------------------------------
199
+
200
+ app.post('/feedback', async (req, res) => {
201
+ const { token, ...pluginPayload } = req.body;
202
+
203
+ if (!token) return res.status(400).json({ error: 'Token required' });
204
+
205
+ const decoded = jwt.decode(token);
206
+ if (!decoded || !decoded.uid || !decoded.projectId) {
207
+ return res.status(401).json({ error: 'Malformed token' });
208
+ }
209
+
210
+ const secret = await getSessionSecret(decoded.uid, decoded.projectId);
211
+ if (!secret) return res.status(404).json({ error: 'Session revoked' });
212
+
213
+ try {
214
+ jwt.verify(token, secret);
215
+
216
+ const targetUrl = EXTERNAL_SERVER_URL.replace(/\/$/, '') + '/project/feedback';
217
+
218
+ console.log(`📨 Forwarding PLUGIN feedback for ${decoded.projectId} (${decoded.uid})`);
219
+
220
+ const response = await axios.post(targetUrl, {
221
+ userId: decoded.uid,
222
+ projectId: decoded.projectId,
223
+ ...pluginPayload
224
+ });
225
+
226
+ return res.json({ success: true, externalResponse: response.data });
227
+
228
+ } catch (err) {
229
+ console.error("Feedback Forward Error:", err.message);
230
+ if (err.response) {
231
+ return res.status(err.response.status).json(err.response.data);
232
+ }
233
+ return res.status(502).json({ error: 'Failed to forward feedback to Main AI server' });
234
+ }
235
+ });
236
+
237
+ app.post('/feedback2', verifySupabaseUser, async (req, res) => {
238
+ const { projectId, prompt, images, ...otherPayload } = req.body;
239
+ const userId = req.user.id;
240
+
241
+ if (!projectId || !prompt) {
242
+ return res.status(400).json({ error: 'Missing projectId or prompt' });
243
+ }
244
+
245
+ const targetUrl = EXTERNAL_SERVER_URL.replace(/\/$/, '') + '/project/feedback';
246
+
247
+ try {
248
+ const response = await axios.post(targetUrl, {
249
+ userId: userId,
250
+ projectId: projectId,
251
+ prompt: prompt,
252
+ images: images || [],
253
+ ...otherPayload
254
+ });
255
+
256
+ return res.json({ success: true, externalResponse: response.data });
257
+ } catch (err) {
258
+ console.error("Forward Error:", err.message);
259
+ return res.status(502).json({ error: 'Failed to forward' });
260
+ }
261
+ });
262
+
263
+ app.post('/poll', async (req, res) => {
264
+ const { token } = req.body;
265
+
266
+ if (!token) return res.status(400).json({ error: 'Token required' });
267
+
268
+ const decoded = jwt.decode(token);
269
+ if (!decoded || !decoded.uid || !decoded.projectId) {
270
+ return res.status(401).json({ error: 'Malformed token' });
271
+ }
272
+
273
+ const secret = await getSessionSecret(decoded.uid, decoded.projectId);
274
+ if (!secret) return res.status(404).json({ error: 'Session revoked or not found' });
275
+
276
+ try {
277
+ const verifiedData = jwt.verify(token, secret);
278
+
279
+ const threeDaysInSeconds = 3 * 24 * 60 * 60;
280
+ const nowInSeconds = Math.floor(Date.now() / 1000);
281
+ if (verifiedData.iat && (nowInSeconds - verifiedData.iat > threeDaysInSeconds)) {
282
+ return res.status(403).json({ error: 'Token expired (older than 3 days)' });
283
+ }
284
+
285
+ const targetUrl = EXTERNAL_SERVER_URL.replace(/\/$/, '') + '/project/ping';
286
+
287
+ try {
288
+ const response = await axios.post(targetUrl, {
289
+ projectId: verifiedData.projectId,
290
+ userId: verifiedData.uid
291
+ });
292
+
293
+ return res.json(response.data);
294
+ } catch (extError) {
295
+ return res.status(502).json({ error: 'External server error' });
296
+ }
297
+
298
+ } catch (err) {
299
+ return res.status(403).json({ error: 'Invalid Token Signature' });
300
+ }
301
+ });
302
+
303
+ // ---------------------------------------------------------
304
+ // MANAGEMENT ENDPOINTS
305
+ // ---------------------------------------------------------
306
+
307
+ app.post('/project/delete', verifySupabaseUser, async (req, res) => {
308
+ const { projectId } = req.body;
309
+ const userId = req.user.id;
310
+
311
+ if (!projectId) return res.status(400).json({ error: "Missing Project ID" });
312
+
313
+ console.log(`🗑️ Deleting Project: ${projectId} requested by ${userId}`);
314
+
315
+ try {
316
+ // 1. Verify Ownership
317
+ const { data: project, error: fetchError } = await supabase
318
+ .from('projects')
319
+ .select('user_id')
320
+ .eq('id', projectId)
321
+ .single();
322
+
323
+ if (fetchError || !project || project.user_id !== userId) {
324
+ return res.status(403).json({ error: "Unauthorized" });
325
+ }
326
+
327
+ // 2. Explicitly Delete Message Chunks
328
+ // Safe to run even if ON DELETE CASCADE exists
329
+ const { error: chunkError } = await supabase
330
+ .from('message_chunks')
331
+ .delete()
332
+ .eq('project_id', projectId);
333
+
334
+ if (chunkError) {
335
+ console.warn(`Warning: Failed to delete message chunks: ${chunkError.message}`);
336
+ // We continue, hoping foreign key cascade picks it up, or it might be partial delete
337
+ }
338
+
339
+ // 3. Delete Project
340
+ const { error: dbError } = await supabase
341
+ .from('projects')
342
+ .delete()
343
+ .eq('id', projectId);
344
+
345
+ if (dbError) throw dbError;
346
+
347
+ // 4. Delete from Supabase Storage
348
+ // Supabase storage doesn't have a "delete folder" command, so we list -> delete files
349
+ if (STORAGE_BUCKET) {
350
+ const { data: files } = await supabase.storage.from(STORAGE_BUCKET).list(projectId);
351
+
352
+ if (files && files.length > 0) {
353
+ const filesToRemove = files.map(f => `${projectId}/${f.name}`);
354
+ await supabase.storage.from(STORAGE_BUCKET).remove(filesToRemove);
355
+ }
356
+ }
357
+
358
+ // 5. Clear from Memory
359
+ activeSessions.delete(`${userId}:${projectId}`);
360
+ for (const [key, val] of tempKeys.entries()) {
361
+ if (val.projectId === projectId) tempKeys.delete(key);
362
+ }
363
+
364
+ console.log(`✅ Project ${projectId} deleted successfully.`);
365
+ res.json({ success: true });
366
+
367
+ } catch (err) {
368
+ console.error("Delete Error:", err);
369
+ res.status(500).json({ error: "Failed to delete project resources" });
370
+ }
371
+ });
372
+
373
+ app.get('/cleanup', (req, res) => {
374
+ const THRESHOLD = 1000 * 60 * 60;
375
+ const now = Date.now();
376
+ let cleanedCount = 0;
377
+
378
+ for (const [key, value] of activeSessions.entries()) {
379
+ if (now - value.lastAccessed > THRESHOLD) {
380
+ activeSessions.delete(key);
381
+ cleanedCount++;
382
+ }
383
+ }
384
+ for (const [key, value] of tempKeys.entries()) {
385
+ if (now - value.createdAt > (1000 * 60 * 4)) {
386
+ tempKeys.delete(key);
387
+ }
388
+ }
389
+ res.json({ message: `Cleaned ${cleanedCount} cached sessions from memory.` });
390
+ });
391
+
392
+ app.post('/nullify', verifySupabaseUser, async (req, res) => {
393
+ const { projectId } = req.body;
394
+ if (!projectId) return res.status(400).json({ error: 'projectId required' });
395
+
396
+ const cacheKey = `${req.user.id}:${projectId}`;
397
+ const existedInMemory = activeSessions.delete(cacheKey);
398
+
399
+ let deletedTempKeys = 0;
400
+ for (const [tKey, tData] of tempKeys.entries()) {
401
+ if (tData.uid === req.user.id && tData.projectId === projectId) {
402
+ tempKeys.delete(tKey);
403
+ deletedTempKeys++;
404
+ }
405
+ }
406
+
407
+ if (supabase) {
408
+ try {
409
+ await supabase
410
+ .from('projects')
411
+ .update({ plugin_secret: null })
412
+ .eq('id', projectId)
413
+ .eq('user_id', req.user.id);
414
+ } catch (e) {
415
+ return res.status(500).json({ error: 'Database error during nullify' });
416
+ }
417
+ }
418
+
419
+ console.log(`☢️ NULLIFIED session for ${cacheKey}.`);
420
+ res.json({
421
+ success: true,
422
+ message: 'Session purged.',
423
+ wasCached: existedInMemory,
424
+ tempKeysRemoved: deletedTempKeys
425
+ });
426
+ });
427
+
428
+ app.get('/', (req, res) => {
429
+ res.send('Plugin Auth Proxy Running (Supabase Edition)');
430
+ });
431
+
432
+ app.listen(PORT, () => {
433
+ console.log(`🚀 Auth Proxy running on port ${PORT}`);
434
+ });