const express = require('express'); const admin = require('firebase-admin'); 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' })); // --------------------------------------------------------- // 1. STATE MANAGEMENT // --------------------------------------------------------- const tempKeys = new Map(); const activeSessions = new Map(); // --- GLOBAL FIREBASE SERVICES --- let db = null; let firestore = null; // Added let storage = null; // Added // --------------------------------------------------------- // 2. FIREBASE INITIALIZATION // --------------------------------------------------------- try { if (process.env.FIREBASE_SERVICE_ACCOUNT_JSON) { const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_JSON); // Define your bucket name here (or via ENV). // Based on your previous context, it is likely: const bucketName = process.env.FIREBASE_STORAGE_BUCKET; if (admin.apps.length === 0) { admin.initializeApp({ credential: admin.credential.cert(serviceAccount), databaseURL: process.env.FIREBASE_DB_URL, storageBucket: bucketName // Required for Storage deletion }); } // Initialize ALL services db = admin.database(); firestore = admin.firestore(); storage = admin.storage(); console.log("🔥 Firebase Connected (RTDB, Firestore, Storage)"); } else { console.warn("⚠️ Memory-Only mode (Firebase credentials missing)."); } } catch (e) { console.error("Firebase Init Error:", e); } // --------------------------------------------------------- // 3. MIDDLEWARE // --------------------------------------------------------- const verifyFirebaseUser = async (req, res, next) => { const debugMode = process.env.DEBUG_NO_AUTH === 'true'; if (debugMode) { req.user = { uid: "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 (admin.apps.length > 0) { const decodedToken = await admin.auth().verifyIdToken(idToken); req.user = decodedToken; next(); } else { req.user = { uid: "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 (db) { try { const snapshot = await db.ref(`plugin_oauth/${uid}/${projectId}`).once('value'); if (snapshot.exists()) { const secret = snapshot.val(); activeSessions.set(cacheKey, { secret, lastAccessed: Date.now() }); console.log(`💧 Hydrated secret for ${cacheKey} from DB`); return secret; } } catch (err) { console.error("DB Read Error:", err); } } return null; } // --------------------------------------------------------- // 4. ENDPOINTS // --------------------------------------------------------- app.post('/key', verifyFirebaseUser, (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.uid, projectId: projectId, createdAt: Date.now() }); console.log(`🔑 Generated Key for user ${req.user.uid}: ${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 (db) { await db.ref(`plugin_oauth/${data.uid}/${data.projectId}`).set(sessionSecret); } 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, error: 'Token required' }); const decoded = jwt.decode(token); if (!decoded || !decoded.uid || !decoded.projectId) { return res.status(401).json({ valid: false, error: 'Malformed token' }); } const secret = await getSessionSecret(decoded.uid, decoded.projectId); if (!secret) { return res.status(401).json({ valid: false, error: 'Session revoked' }); } try { jwt.verify(token, secret); const threeDaysInSeconds = 3 * 24 * 60 * 60; const nowInSeconds = Math.floor(Date.now() / 1000); if (decoded.iat && (nowInSeconds - decoded.iat > threeDaysInSeconds)) { return res.status(403).json({ valid: false, error: 'Expired' }); } return res.json({ valid: true }); } catch (err) { return res.status(403).json({ valid: false, error: 'Invalid signature' }); } }); // --------------------------------------------------------- // PROXY ENDPOINTS // --------------------------------------------------------- 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 || !decoded.uid || !decoded.projectId) { 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 externalBase = process.env.EXTERNAL_SERVER_URL || 'http://localhost:7860'; const targetUrl = externalBase.replace(/\/$/, '') + '/project/feedback'; console.log(`📨 Forwarding PLUGIN feedback for ${decoded.projectId} (${decoded.uid})`); const response = await axios.post(targetUrl, { userId: decoded.uid, projectId: decoded.projectId, ...pluginPayload }); return res.json({ success: true, externalResponse: response.data }); } catch (err) { console.error("Feedback Forward Error:", err.message); if (err.response) { return res.status(err.response.status).json(err.response.data); } return res.status(502).json({ error: 'Failed to forward feedback to Main AI server' }); } }); app.post('/feedback2', verifyFirebaseUser, async (req, res) => { const { projectId, prompt, images, ...otherPayload } = req.body; const userId = req.user.uid; if (!projectId || !prompt) { return res.status(400).json({ error: 'Missing projectId or prompt' }); } if (images && images.length > 0) { console.log(`📸 Received ${images.length} image(s) from Dashboard.`); } const externalBase = process.env.EXTERNAL_SERVER_URL || 'http://localhost:7860'; const targetUrl = externalBase.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) { console.error("Forward Error:", err.message); return res.status(502).json({ error: 'Failed to forward' }); } }); 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 || !decoded.uid || !decoded.projectId) { 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 or not found' }); try { const verifiedData = jwt.verify(token, secret); const threeDaysInSeconds = 3 * 24 * 60 * 60; const nowInSeconds = Math.floor(Date.now() / 1000); if (verifiedData.iat && (nowInSeconds - verifiedData.iat > threeDaysInSeconds)) { return res.status(403).json({ error: 'Token expired (older than 3 days)' }); } const externalBase = process.env.EXTERNAL_SERVER_URL || 'http://localhost:7860'; const targetUrl = externalBase.replace(/\/$/, '') + '/project/ping'; try { const response = await axios.post(targetUrl, { projectId: verifiedData.projectId, userId: verifiedData.uid }); return res.json(response.data); } catch (extError) { console.error("Poll Forward Error:", extError.message); return res.status(502).json({ error: 'External server error' }); } } catch (err) { if (err.name === 'TokenExpiredError') { return res.status(403).json({ error: 'Token has expired' }); } return res.status(403).json({ error: 'Invalid Token Signature' }); } }); // --------------------------------------------------------- // MANAGEMENT ENDPOINTS // --------------------------------------------------------- app.post('/project/delete', verifyFirebaseUser, async (req, res) => { const { projectId } = req.body; const userId = req.user.uid; if (!projectId) return res.status(400).json({ error: "Missing Project ID" }); console.log(`🗑️ Deleting Project: ${projectId} requested by ${userId}`); try { // 1. Verify Ownership // We check if the project info exists for this user const projectRef = db.ref(`projects/${projectId}/info`); const snapshot = await projectRef.once('value'); if (snapshot.exists()) { const data = snapshot.val(); if (data.userId !== userId) { return res.status(403).json({ error: "Unauthorized" }); } } const promises = []; // 2. Delete from Realtime Database promises.push(db.ref(`projects/${projectId}`).remove()); promises.push(db.ref(`plugin_oauth/${userId}/${projectId}`).remove()); // 3. Delete from Firestore if (firestore) { promises.push(firestore.collection('projects').doc(projectId).delete()); } else { console.warn("Skipping Firestore delete (not initialized)"); } // 4. Delete from Storage (Recursive) if (storage) { const bucket = storage.bucket(); promises.push(bucket.deleteFiles({ prefix: `${projectId}/` })); } else { console.warn("Skipping Storage delete (not initialized)"); } // 5. Clear from Memory (Manually, since StateManager isn't imported here) activeSessions.delete(`${userId}:${projectId}`); // Also iterate tempKeys if needed, though they expire quickly anyway for (const [key, val] of tempKeys.entries()) { if (val.projectId === projectId) tempKeys.delete(key); } await Promise.all(promises); console.log(`✅ Project ${projectId} deleted successfully.`); res.json({ success: true }); } catch (err) { console.error("Delete Error:", err); res.status(500).json({ error: "Failed to delete project resources" }); } }); app.get('/cleanup', (req, res) => { 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', verifyFirebaseUser, async (req, res) => { const { projectId } = req.body; if (!projectId) return res.status(400).json({ error: 'projectId required' }); const cacheKey = `${req.user.uid}:${projectId}`; const existedInMemory = activeSessions.delete(cacheKey); let deletedTempKeys = 0; for (const [tKey, tData] of tempKeys.entries()) { if (tData.uid === req.user.uid && tData.projectId === projectId) { tempKeys.delete(tKey); deletedTempKeys++; } } if (db) { try { await db.ref(`plugin_oauth/${req.user.uid}/${projectId}`).remove(); } catch (e) { return res.status(500).json({ error: 'Database error during nullify' }); } } console.log(`☢️ NULLIFIED session for ${cacheKey}.`); res.json({ success: true, message: 'Session purged.', wasCached: existedInMemory, tempKeysRemoved: deletedTempKeys }); }); app.get('/', (req, res) => { res.send('Plugin Auth Proxy Running'); }); const PORT = process.env.PORT || 7860; app.listen(PORT, () => { console.log(`🚀 Auth Proxy running on http://localhost:${PORT}`); });