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);
|
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> {
|
async update(id: string, patch: { lastNotifiedAt: number }): Promise<void> {
|
||||||
const all = await load();
|
const all = await load();
|
||||||
const found = Object.entries(all).find(([, r]) => r.id === id);
|
const found = Object.entries(all).find(([, r]) => r.id === id);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import "./services/firebase.js";
|
import "./services/firebase.js";
|
||||||
|
import { debugRouter } from "./routes/debug.js";
|
||||||
import { notificationsRouter } from "./routes/notifications.js";
|
import { notificationsRouter } from "./routes/notifications.js";
|
||||||
import { startScheduler } from "./scheduler.js";
|
import { startScheduler } from "./scheduler.js";
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ app.get("/health", (_req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.use("/notifications", notificationsRouter);
|
app.use("/notifications", notificationsRouter);
|
||||||
|
app.use("/debug", debugRouter);
|
||||||
|
|
||||||
startScheduler();
|
startScheduler();
|
||||||
|
|
||||||
|
|||||||
@@ -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 { db, type StoredRow } from "../db/fcmTokens.js";
|
||||||
|
import { maskToken } from "../util/maskToken.js";
|
||||||
import { messaging } from "./firebase.js";
|
import { messaging } from "./firebase.js";
|
||||||
|
|
||||||
const MS_PRODUCTION = 23 * 60 * 60 * 1000;
|
const MS_PRODUCTION = 23 * 60 * 60 * 1000;
|
||||||
@@ -15,12 +16,6 @@ function lastNotifiedMs(row: StoredRow | undefined): number | undefined {
|
|||||||
return 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(
|
function stringifyData(
|
||||||
payload: Record<string, unknown>
|
payload: Record<string, unknown>
|
||||||
): Record<string, string> {
|
): Record<string, string> {
|
||||||
@@ -59,7 +54,7 @@ export async function sendPushToDevice(
|
|||||||
type: "WAKEUP_PING",
|
type: "WAKEUP_PING",
|
||||||
};
|
};
|
||||||
|
|
||||||
const token = tokenHint(fcmToken);
|
const token = maskToken(fcmToken);
|
||||||
console.log("[Push] Sending to:", token);
|
console.log("[Push] Sending to:", token);
|
||||||
|
|
||||||
await messaging.send({
|
await messaging.send({
|
||||||
|
|||||||
@@ -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