feat(debug): harden debug routes with auth and user-scoped token access

Add GET /debug/device/:token and POST /debug/send-wakeup behind requireAuth,
scope lookups to the authenticated user (404 otherwise), and mask FCM tokens
in logs via maskToken. Mark routes for further restriction before production.
This commit is contained in:
Jose Olarte III
2026-05-19 19:42:22 +08:00
parent 8e502a2335
commit afbc2e9a57
5 changed files with 112 additions and 7 deletions

View File

@@ -224,6 +224,16 @@ export const db = {
return Object.values(all).find((r) => r.fcmToken === fcmToken);
},
async getByFcmTokenForUser(
userId: string,
fcmToken: string
): Promise<StoredRow | undefined> {
const all = await load();
return Object.values(all).find(
(r) => r.userId === userId && r.fcmToken === fcmToken
);
},
async update(id: string, patch: { lastNotifiedAt: number }): Promise<void> {
const all = await load();
const found = Object.entries(all).find(([, r]) => r.id === id);

View File

@@ -1,5 +1,6 @@
import express from "express";
import "./services/firebase.js";
import { debugRouter } from "./routes/debug.js";
import { notificationsRouter } from "./routes/notifications.js";
import { startScheduler } from "./scheduler.js";
@@ -13,6 +14,7 @@ app.get("/health", (_req, res) => {
});
app.use("/notifications", notificationsRouter);
app.use("/debug", debugRouter);
startScheduler();

91
src/routes/debug.ts Normal file
View File

@@ -0,0 +1,91 @@
import { Router } from "express";
import { db } from "../db/fcmTokens.js";
import { requireAuth } from "../middleware/auth.js";
import { sendPushToDevice } from "../services/pushService.js";
import { maskToken } from "../util/maskToken.js";
// TODO: Restrict further before production deployment
export const debugRouter = Router();
debugRouter.use(requireAuth);
function deviceDebugPayload(row: {
id: string;
deviceId: string;
platform: string;
testMode?: boolean;
createdAt: string;
updatedAt: string;
lastNotifiedAt?: number;
fcmToken: string;
}) {
return {
id: row.id,
deviceId: row.deviceId,
platform: row.platform,
testMode: row.testMode,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
lastNotifiedAt: row.lastNotifiedAt,
fcmTokenSuffix: maskToken(row.fcmToken),
};
}
// TODO: Restrict further before production deployment
debugRouter.get("/device/:token", async (req, res) => {
const userId = req.did;
if (userId === undefined) {
res.status(401).json({ success: false, message: "Unauthorized" });
return;
}
const fcmToken = decodeURIComponent(req.params.token);
const row = await db.getByFcmTokenForUser(userId, fcmToken);
if (row === undefined) {
console.log(
"[Debug] Device lookup not found for user, token suffix:",
maskToken(fcmToken)
);
res.status(404).json({ error: "Device not found" });
return;
}
console.log(
"[Debug] Device lookup for user, token suffix:",
maskToken(fcmToken)
);
res.json(deviceDebugPayload(row));
});
// TODO: Restrict further before production deployment
debugRouter.post("/send-wakeup", async (req, res) => {
const userId = req.did;
if (userId === undefined) {
res.status(401).json({ success: false, message: "Unauthorized" });
return;
}
const { fcmToken } = req.body as { fcmToken?: unknown };
if (typeof fcmToken !== "string" || fcmToken.length === 0) {
res.status(400).json({ error: "fcmToken is required" });
return;
}
const row = await db.getByFcmTokenForUser(userId, fcmToken);
if (row === undefined) {
console.log(
"[Debug] Send-wakeup rejected, token suffix:",
maskToken(fcmToken)
);
res.status(404).json({ error: "Device not found" });
return;
}
console.log("[Debug] Send-wakeup for token suffix:", maskToken(fcmToken));
const result = await sendPushToDevice(fcmToken);
res.json({
result,
fcmTokenSuffix: maskToken(fcmToken),
deviceId: row.deviceId,
});
});

View File

@@ -1,4 +1,5 @@
import { db, type StoredRow } from "../db/fcmTokens.js";
import { maskToken } from "../util/maskToken.js";
import { messaging } from "./firebase.js";
const MS_PRODUCTION = 23 * 60 * 60 * 1000;
@@ -15,12 +16,6 @@ function lastNotifiedMs(row: StoredRow | undefined): number | undefined {
return undefined;
}
/** Short token fingerprint for logs (not the full FCM token). */
function tokenHint(token: string): string {
if (token.length <= 16) return token;
return `${token.slice(0, 8)}${token.slice(-4)}`;
}
function stringifyData(
payload: Record<string, unknown>
): Record<string, string> {
@@ -59,7 +54,7 @@ export async function sendPushToDevice(
type: "WAKEUP_PING",
};
const token = tokenHint(fcmToken);
const token = maskToken(fcmToken);
console.log("[Push] Sending to:", token);
await messaging.send({

7
src/util/maskToken.ts Normal file
View File

@@ -0,0 +1,7 @@
/** Last 6 characters only — safe for logs and debug responses. */
export function maskToken(token: string): string {
if (token.length <= 6) {
return "******";
}
return token.slice(-6);
}