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:
@@ -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);
|
||||
|
||||
@@ -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
91
src/routes/debug.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
@@ -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
7
src/util/maskToken.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user