feat(debug): expose nextEligibleAt and structured send-wakeup results
Extend authenticated debug endpoints for local iOS notification testing: add nextEligibleAt (23h prod / 10m test) to device lookup, return success and failureReason from send-wakeup with masked tokens only, reuse resolveOwnedDevice for ownership checks, and standardize [DebugEndpoint] logs.
This commit is contained in:
+49
-15
@@ -1,10 +1,13 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { db } from "../db/fcmTokens.js";
|
import { db } from "../db/fcmTokens.js";
|
||||||
import { requireAuth } from "../middleware/auth.js";
|
import { requireAuth } from "../middleware/auth.js";
|
||||||
import { sendPushToDevice } from "../services/pushService.js";
|
import {
|
||||||
|
computeNextEligibleAt,
|
||||||
|
sendPushToDevice,
|
||||||
|
} from "../services/pushService.js";
|
||||||
import { maskToken } from "../util/maskToken.js";
|
import { maskToken } from "../util/maskToken.js";
|
||||||
|
|
||||||
// TODO: Restrict further before production deployment
|
// TODO: Protect this endpoint before production deployment
|
||||||
export const debugRouter = Router();
|
export const debugRouter = Router();
|
||||||
|
|
||||||
debugRouter.use(requireAuth);
|
debugRouter.use(requireAuth);
|
||||||
@@ -23,15 +26,26 @@ function deviceDebugPayload(row: {
|
|||||||
id: row.id,
|
id: row.id,
|
||||||
deviceId: row.deviceId,
|
deviceId: row.deviceId,
|
||||||
platform: row.platform,
|
platform: row.platform,
|
||||||
testMode: row.testMode,
|
testMode: row.testMode ?? false,
|
||||||
createdAt: row.createdAt,
|
createdAt: row.createdAt,
|
||||||
updatedAt: row.updatedAt,
|
updatedAt: row.updatedAt,
|
||||||
lastNotifiedAt: row.lastNotifiedAt,
|
lastNotifiedAt: row.lastNotifiedAt,
|
||||||
|
nextEligibleAt: computeNextEligibleAt(row),
|
||||||
fcmTokenSuffix: maskToken(row.fcmToken),
|
fcmTokenSuffix: maskToken(row.fcmToken),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Restrict further before production deployment
|
function sendWakeupFailureReason(
|
||||||
|
result: "sent" | "skipped" | "failed"
|
||||||
|
): string | undefined {
|
||||||
|
if (result === "sent") return undefined;
|
||||||
|
if (result === "skipped") {
|
||||||
|
return "Device was notified within the eligibility threshold";
|
||||||
|
}
|
||||||
|
return "FCM send failed";
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Protect this endpoint before production deployment
|
||||||
debugRouter.get("/device/:token", async (req, res) => {
|
debugRouter.get("/device/:token", async (req, res) => {
|
||||||
const userId = req.did;
|
const userId = req.did;
|
||||||
if (userId === undefined) {
|
if (userId === undefined) {
|
||||||
@@ -40,10 +54,10 @@ debugRouter.get("/device/:token", async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fcmToken = decodeURIComponent(req.params.token);
|
const fcmToken = decodeURIComponent(req.params.token);
|
||||||
const row = await db.getByFcmTokenForUser(userId, fcmToken);
|
const row = await db.resolveOwnedDevice(userId, { fcmToken });
|
||||||
if (row === undefined) {
|
if (row === undefined) {
|
||||||
console.log(
|
console.log(
|
||||||
"[Debug] Device lookup not found for user, token suffix:",
|
"[DebugEndpoint] Device lookup not found for user, token suffix:",
|
||||||
maskToken(fcmToken)
|
maskToken(fcmToken)
|
||||||
);
|
);
|
||||||
res.status(404).json({ error: "Device not found" });
|
res.status(404).json({ error: "Device not found" });
|
||||||
@@ -51,13 +65,13 @@ debugRouter.get("/device/:token", async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"[Debug] Device lookup for user, token suffix:",
|
"[DebugEndpoint] Device lookup for user, token suffix:",
|
||||||
maskToken(fcmToken)
|
maskToken(fcmToken)
|
||||||
);
|
);
|
||||||
res.json(deviceDebugPayload(row));
|
res.json(deviceDebugPayload(row));
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Restrict further before production deployment
|
// TODO: Protect this endpoint before production deployment
|
||||||
debugRouter.post("/send-wakeup", async (req, res) => {
|
debugRouter.post("/send-wakeup", async (req, res) => {
|
||||||
const userId = req.did;
|
const userId = req.did;
|
||||||
if (userId === undefined) {
|
if (userId === undefined) {
|
||||||
@@ -67,25 +81,45 @@ debugRouter.post("/send-wakeup", async (req, res) => {
|
|||||||
|
|
||||||
const { fcmToken } = req.body as { fcmToken?: unknown };
|
const { fcmToken } = req.body as { fcmToken?: unknown };
|
||||||
if (typeof fcmToken !== "string" || fcmToken.length === 0) {
|
if (typeof fcmToken !== "string" || fcmToken.length === 0) {
|
||||||
res.status(400).json({ error: "fcmToken is required" });
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
failureReason: "fcmToken is required",
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const row = await db.getByFcmTokenForUser(userId, fcmToken);
|
const row = await db.resolveOwnedDevice(userId, { fcmToken });
|
||||||
if (row === undefined) {
|
if (row === undefined) {
|
||||||
console.log(
|
console.log(
|
||||||
"[Debug] Send-wakeup rejected, token suffix:",
|
"[DebugEndpoint] Send-wakeup rejected, token suffix:",
|
||||||
maskToken(fcmToken)
|
maskToken(fcmToken)
|
||||||
);
|
);
|
||||||
res.status(404).json({ error: "Device not found" });
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
failureReason: "Device not found",
|
||||||
|
fcmTokenSuffix: maskToken(fcmToken),
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[Debug] Send-wakeup for token suffix:", maskToken(fcmToken));
|
console.log(
|
||||||
|
"[DebugEndpoint] Send-wakeup for token suffix:",
|
||||||
|
maskToken(fcmToken)
|
||||||
|
);
|
||||||
const result = await sendPushToDevice(fcmToken);
|
const result = await sendPushToDevice(fcmToken);
|
||||||
|
const success = result === "sent";
|
||||||
|
const failureReason = sendWakeupFailureReason(result);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[DebugEndpoint] Send-wakeup result:",
|
||||||
|
success ? "success" : result,
|
||||||
|
"token suffix:",
|
||||||
|
maskToken(fcmToken)
|
||||||
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
result,
|
success,
|
||||||
|
...(failureReason !== undefined ? { failureReason } : {}),
|
||||||
fcmTokenSuffix: maskToken(fcmToken),
|
fcmTokenSuffix: maskToken(fcmToken),
|
||||||
deviceId: row.deviceId,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,10 +5,22 @@ import { messaging } from "./firebase.js";
|
|||||||
const MS_PRODUCTION = 23 * 60 * 60 * 1000;
|
const MS_PRODUCTION = 23 * 60 * 60 * 1000;
|
||||||
const MS_TEST = 10 * 60 * 1000;
|
const MS_TEST = 10 * 60 * 1000;
|
||||||
|
|
||||||
function notifyThresholdMs(testMode?: boolean): number {
|
export function notifyThresholdMs(testMode?: boolean): number {
|
||||||
return testMode === true ? MS_TEST : MS_PRODUCTION;
|
return testMode === true ? MS_TEST : MS_PRODUCTION;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Epoch ms when the device may receive another push (diagnostics only). */
|
||||||
|
export function computeNextEligibleAt(row: {
|
||||||
|
lastNotifiedAt?: number;
|
||||||
|
testMode?: boolean;
|
||||||
|
}): number {
|
||||||
|
const threshold = notifyThresholdMs(row.testMode);
|
||||||
|
if (row.lastNotifiedAt === undefined) {
|
||||||
|
return Date.now();
|
||||||
|
}
|
||||||
|
return row.lastNotifiedAt + threshold;
|
||||||
|
}
|
||||||
|
|
||||||
function lastNotifiedMs(row: StoredRow | undefined): number | undefined {
|
function lastNotifiedMs(row: StoredRow | undefined): number | undefined {
|
||||||
const v = row?.lastNotifiedAt;
|
const v = row?.lastNotifiedAt;
|
||||||
if (v === undefined) return undefined;
|
if (v === undefined) return undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user