everydaycats's picture
Update app.js
c0b7bc4 verified
raw
history blame
14.8 kB
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 app = express();
app.use(bodyParser.json());
// ---------------------------------------------------------
// 1. STATE MANAGEMENT (In-Memory DB)
// ---------------------------------------------------------
// Store temporary "proj_" redemption keys.
// Structure: { "proj_xyz": { uid: "...", projectId: "...", expiresAt: timestamp } }
const tempKeys = new Map();
// Store JWT Secrets (Hydrated Cache).
// Structure: { "uid:projectId": { secret: "...", lastAccessed: timestamp } }
const activeSessions = new Map();
let db = null; // Firebase DB Reference
// ---------------------------------------------------------
// 2. FIREBASE INITIALIZATION
// ---------------------------------------------------------
try {
if (process.env.FIREBASE_SERVICE_ACCOUNT_JSON) {
const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_JSON);
if (admin.apps.length === 0) {
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: process.env.FIREBASE_DB_URL || "https://your-firebase-project.firebaseio.com"
});
}
db = admin.database();
console.log("🔥 Firebase Connected & StateManager Linked");
} else {
console.warn("⚠️ Memory-Only mode (No Firebase JSON provided).");
}
} catch (e) {
console.error("Firebase Init Error:", e);
}
// ---------------------------------------------------------
// 3. HELPER FUNCTIONS & MIDDLEWARE
// ---------------------------------------------------------
// Middleware to verify Firebase ID Token (for /key and /nullify)
// Includes a Debug Bypass variable
const verifyFirebaseUser = async (req, res, next) => {
const debugMode = process.env.DEBUG_NO_AUTH === 'true'; // Set to true in .env for testing without valid tokens
if (debugMode) {
req.user = { uid: "debug_user_001" };
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 {
// Fallback for memory-only mode without validation
req.user = { uid: "memory_user" };
next();
}
} catch (error) {
return res.status(403).json({ error: 'Unauthorized', details: error.message });
}
};
// Helper to fetch Secret (Memory -> DB -> 404)
async function getSessionSecret(uid, projectId) {
const cacheKey = `${uid}:${projectId}`;
// 1. Check Memory
if (activeSessions.has(cacheKey)) {
const session = activeSessions.get(cacheKey);
session.lastAccessed = Date.now(); // Update access time for cleanup
return session.secret;
}
// 2. Hydrate from DB
if (db) {
try {
const snapshot = await db.ref(`plugin_oauth/${uid}/${projectId}`).once('value');
if (snapshot.exists()) {
const secret = snapshot.val();
// Store in memory for next time
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);
}
}
// 3. Not found
return null;
}
// ---------------------------------------------------------
// 4. ENDPOINTS
// ---------------------------------------------------------
// Endpoint: /key
// Generates a temporary 'proj_' token. Expects Firebase Auth.
app.post('/key', verifyFirebaseUser, (req, res) => {
const { projectId } = req.body;
if (!projectId) return res.status(400).json({ error: 'projectId required' });
const key = `proj_${uuidv4().replace(/-/g, '')}`;
// Store in memory (valid for 5 minutes)
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 });
});
// Endpoint: /redeem
// Exchanges 'proj_' key for a JWT.
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);
// Generate a unique secret for this session/project
const sessionSecret = uuidv4();
// Create JWT
const token = jwt.sign(
{ uid: data.uid, projectId: data.projectId },
sessionSecret,
{ expiresIn: '7d' } // Plugin token validity
);
const cacheKey = `${data.uid}:${data.projectId}`;
// 1. Store in Memory
activeSessions.set(cacheKey, { secret: sessionSecret, lastAccessed: Date.now() });
// 2. Store in Firebase (Persist)
if (db) {
await db.ref(`plugin_oauth/${data.uid}/${data.projectId}`).set(sessionSecret);
}
// Burn the redemption key (One-time use)
tempKeys.delete(key);
console.log(`🚀 Redeemed JWT for ${cacheKey}`);
res.json({ token });
});
// Endpoint: /poll
// Verifies JWT using the stored secret and forwards to external server.
app.post('/poll', async (req, res) => {
const { token, payload: clientPayload } = req.body;
if (!token) return res.status(400).json({ error: 'Token required' });
// Decode without verification first to find WHO it is
const decoded = jwt.decode(token);
if (!decoded || !decoded.uid || !decoded.projectId) {
return res.status(401).json({ error: 'Malformed token' });
}
// Fetch secret (Memory -> DB -> 404)
const secret = await getSessionSecret(decoded.uid, decoded.projectId);
if (!secret) {
return res.status(404).json({ error: 'Session revoked or not found' });
}
// Verify signature
try {
jwt.verify(token, secret);
// If valid, post to external server
const externalUrl = process.env.EXTERNAL_SERVER_URL || 'https://httpbin.org/post'; // Default for testing
try {
const response = await axios.post(externalUrl, {
user: decoded.uid,
project: decoded.projectId,
data: clientPayload
});
return res.json({ status: 'success', externalResponse: response.data });
} catch (extError) {
return res.status(502).json({ error: 'External server error' });
}
} catch (err) {
return res.status(403).json({ error: 'Invalid Token Signature' });
}
});
// Endpoint: /cleanup
// Memory management: Clears old JWT secrets from RAM that haven't been used recently.
// Does NOT delete from Firebase.
app.post('/cleanup', (req, res) => {
const THRESHOLD = 1000 * 60 * 60; // 1 Hour
const now = Date.now();
let cleanedCount = 0;
for (const [key, value] of activeSessions.entries()) {
if (now - value.lastAccessed > THRESHOLD) {
activeSessions.delete(key);
cleanedCount++;
}
}
// Also clean old temp keys (older than 10 mins)
for (const [key, value] of tempKeys.entries()) {
if (now - value.createdAt > (1000 * 60 * 10)) {
tempKeys.delete(key);
}
}
console.log(`🧹 Garbage Collection: Removed ${cleanedCount} cached sessions.`);
res.json({ message: `Cleaned ${cleanedCount} cached sessions from memory.` });
});
// Endpoint: /nullify
// Security Nuke: Removes session from Memory AND Firebase.
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}`;
// 1. Remove from Memory
const existedInMemory = activeSessions.delete(cacheKey);
// 2. Remove from Firebase
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 secrets purged from memory and database.',
wasCached: existedInMemory
});
});
// ---------------------------------------------------------
// 5. EMBEDDED HTML DEBUGGER
// ---------------------------------------------------------
app.get('/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Roblox Plugin Auth Server</title>
<style>
body { font-family: monospace; background: #1e1e1e; color: #d4d4d4; padding: 20px; max-width: 800px; margin: 0 auto; }
.container { background: #252526; padding: 20px; border-radius: 8px; border: 1px solid #333; }
input, button { padding: 10px; margin: 5px 0; width: 100%; box-sizing: border-box; background: #3c3c3c; border: 1px solid #555; color: white; }
button { background: #0e639c; cursor: pointer; font-weight: bold; }
button:hover { background: #1177bb; }
button.danger { background: #a51d2d; }
label { display: block; margin-top: 10px; color: #9cdcfe; }
pre { background: #000; padding: 10px; border: 1px solid #444; overflow-x: auto; white-space: pre-wrap; }
.status { margin-top: 20px; padding: 10px; border-left: 4px solid #0e639c; background: #2d2d2d; }
</style>
</head>
<body>
<h1>🔌 Plugin Auth Debugger</h1>
<div class="container">
<h3>1. Configuration</h3>
<label>Firebase ID Token (leave empty if DEBUG_NO_AUTH=true)</label>
<input type="text" id="fbToken" placeholder="eyJhbG...">
<label>Project ID</label>
<input type="text" id="projId" value="roblox_world_1">
<hr style="border-color:#444">
<h3>2. Generate Key (/key)</h3>
<button onclick="generateKey()">Generate PROJ_ Key</button>
<pre id="keyResult">Waiting...</pre>
<h3>3. Redeem Token (/redeem)</h3>
<label>Key to Redeem</label>
<input type="text" id="redeemKeyInput">
<button onclick="redeemKey()">Redeem for JWT</button>
<pre id="jwtResult">Waiting...</pre>
<h3>4. Poll External (/poll)</h3>
<label>JWT Token</label>
<input type="text" id="jwtInput">
<button onclick="poll()">Poll External Server</button>
<pre id="pollResult">Waiting...</pre>
<h3>5. Management</h3>
<div style="display:flex; gap:10px;">
<button onclick="cleanup()">Memory Cleanup</button>
<button class="danger" onclick="nullify()">Nullify (Nuke)</button>
</div>
<pre id="mgmtResult">Waiting...</pre>
</div>
<script>
const baseUrl = '';
async function generateKey() {
const token = document.getElementById('fbToken').value;
const projectId = document.getElementById('projId').value;
const headers = {};
if(token) headers['Authorization'] = 'Bearer ' + token;
const res = await fetch(baseUrl + '/key', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...headers },
body: JSON.stringify({ projectId })
});
const data = await res.json();
document.getElementById('keyResult').innerText = JSON.stringify(data, null, 2);
if(data.key) document.getElementById('redeemKeyInput').value = data.key;
}
async function redeemKey() {
const key = document.getElementById('redeemKeyInput').value;
const res = await fetch(baseUrl + '/redeem', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key })
});
const data = await res.json();
document.getElementById('jwtResult').innerText = JSON.stringify(data, null, 2);
if(data.token) document.getElementById('jwtInput').value = data.token;
}
async function poll() {
const token = document.getElementById('jwtInput').value;
const res = await fetch(baseUrl + '/poll', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, payload: { msg: "Hello from Roblox" } })
});
const data = await res.json();
document.getElementById('pollResult').innerText = JSON.stringify(data, null, 2);
}
async function cleanup() {
const res = await fetch(baseUrl + '/cleanup', { method: 'POST' });
const data = await res.json();
document.getElementById('mgmtResult').innerText = JSON.stringify(data, null, 2);
}
async function nullify() {
const token = document.getElementById('fbToken').value;
const projectId = document.getElementById('projId').value;
const headers = {};
if(token) headers['Authorization'] = 'Bearer ' + token;
const res = await fetch(baseUrl + '/nullify', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...headers },
body: JSON.stringify({ projectId })
});
const data = await res.json();
document.getElementById('mgmtResult').innerText = JSON.stringify(data, null, 2);
}
</script>
</body>
</html>
`);
});
const PORT = process.env.PORT || 7860;
app.listen(PORT, () => {
console.log(`🚀 Server running on http://localhost:${PORT}`);
});