Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | 5x 5x 5x 5x 5x 5x 23x 14x 14x 1x 13x 13x 13x 13x 13x 14x 11x 11x 1x 10x 10x 10x 10x 10x 10x 17x 17x 2x 15x 15x 15x 14x 14x 14x 14x 7x 7x 4x 10x 10x 1x 1x 28x 3x 3x 25x 16x 16x 5x | /**
* Firebase Auth middleware for Express.
*
* Verifies Firebase ID tokens and resolves Firebase UID → uniqueId
* via the identity system. Sets req.auth = { uid, uniqueId, token }.
*/
const { auth, db } = require('../utils/firebase');
const log = require('../utils/log');
// ─── In-memory caches ────────────────────────────────────────────
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
const MAX_CACHE_SIZE = 500;
// uid → { uniqueId, expiresAt }
const uniqueIdCache = new Map();
// uniqueId → { isSuspended, expiresAt }
const suspensionCache = new Map();
function evictOldest(cache) {
Iif (cache.size > MAX_CACHE_SIZE) {
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
}
// ─── UniqueId resolution ─────────────────────────────────────────
/**
* Resolves a Firebase UID to the user's stable uniqueId by querying
* the users collection for a doc where firebaseUid matches.
* Returns null if no user doc is found (new user or cross-project).
*/
async function resolveUniqueId(uid) {
const cached = uniqueIdCache.get(uid);
if (cached && Date.now() < cached.expiresAt) {
return cached.uniqueId;
}
const snap = await db.collection('users').where('firebaseUid', '==', uid).limit(1).get();
const uniqueId = snap.empty ? null : (snap.docs[0].data().uniqueId ?? null);
uniqueIdCache.set(uid, { uniqueId, expiresAt: Date.now() + CACHE_TTL });
evictOldest(uniqueIdCache);
return uniqueId;
}
// ─── Suspension check ────────────────────────────────────────────
/**
* Checks if a user is suspended by reading their user doc.
* Uses uniqueId-based doc path: users/{uniqueId}.
*/
async function checkSuspension(uniqueId) {
if (uniqueId === null || uniqueId === undefined) return false;
const cached = suspensionCache.get(uniqueId);
if (cached && Date.now() < cached.expiresAt) {
return cached.isSuspended;
}
const snap = await db.doc(`users/${uniqueId}`).get();
const user = snap.exists ? snap.data() : null;
const isSuspended = !!(user?.isSuspended || user?.is_suspended);
suspensionCache.set(uniqueId, { isSuspended, expiresAt: Date.now() + CACHE_TTL });
evictOldest(suspensionCache);
return isSuspended;
}
// ─── Middleware ───────────────────────────────────────────────────
/**
* Express middleware: verifies Firebase ID token, resolves uniqueId,
* checks suspension. Sets req.auth = { uid, uniqueId, token }.
*/
async function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or invalid Authorization header' });
}
const idToken = authHeader.slice(7);
try {
const decoded = await auth.verifyIdToken(idToken);
const uid = decoded.uid;
// Resolve Firebase UID → stable uniqueId
const uniqueId = await resolveUniqueId(uid);
// Check suspension (only if user exists)
const isSuspended = await checkSuspension(uniqueId);
if (isSuspended) {
const isSuspensionExempt =
/^\/users\/[^/]+\/appeal$/.test(req.path) ||
/^\/users\/[^/]+\/lift-suspension$/.test(req.path) ||
/^\/users\/[^/]+\/delete$/.test(req.path) ||
/^\/users\/[^/]+\/cancel-delete$/.test(req.path) ||
/^\/users\/[^/]+\/deletion-status$/.test(req.path) ||
/^\/users\/[^/]+\/data-export/.test(req.path) ||
(req.method === 'POST' && req.path === '/appeals');
if (!isSuspensionExempt) {
return res.status(403).json({ error: 'Account suspended' });
}
}
req.auth = { uid, uniqueId, token: decoded };
next();
} catch (err) {
log.error('auth', 'Authentication failed', { error: err.message });
return res.status(401).json({ error: 'Authentication failed' });
}
}
// ─── Helpers ─────────────────────────────────────────────────────
/**
* Admin guard — call at the top of admin route handlers.
* Returns true if blocked (response already sent), false if admin.
*/
function requireAdmin(req, res) {
if (!req.auth?.token.admin) {
res.status(403).json({ error: 'Admin access required' });
return true;
}
return false;
}
function clearSuspensionCache(uniqueId) {
suspensionCache.delete(uniqueId);
}
/** Clear uniqueId cache entry — call after firebaseUid is updated. */
function clearUniqueIdCache(uid) {
Iif (uid) {
uniqueIdCache.delete(uid);
} else {
uniqueIdCache.clear();
}
}
/** Update uniqueId cache — call after sign-in resolves a new mapping. */
function updateUniqueIdCache(uid, uniqueId) {
uniqueIdCache.set(uid, { uniqueId, expiresAt: Date.now() + CACHE_TTL });
evictOldest(uniqueIdCache);
}
module.exports = {
authMiddleware,
requireAdmin,
clearSuspensionCache,
clearUniqueIdCache,
updateUniqueIdCache,
};
|