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