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:
@@ -1,10 +1,13 @@
|
||||
import { Router } from "express";
|
||||
import { db } from "../db/fcmTokens.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";
|
||||
|
||||
// TODO: Restrict further before production deployment
|
||||
// TODO: Protect this endpoint before production deployment
|
||||
export const debugRouter = Router();
|
||||
|
||||
debugRouter.use(requireAuth);
|
||||
@@ -23,15 +26,26 @@ function deviceDebugPayload(row: {
|
||||
id: row.id,
|
||||
deviceId: row.deviceId,
|
||||
platform: row.platform,
|
||||
testMode: row.testMode,
|
||||
testMode: row.testMode ?? false,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
lastNotifiedAt: row.lastNotifiedAt,
|
||||
nextEligibleAt: computeNextEligibleAt(row),
|
||||
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) => {
|
||||
const userId = req.did;
|
||||
if (userId === undefined) {
|
||||
@@ -40,10 +54,10 @@ debugRouter.get("/device/:token", async (req, res) => {
|
||||
}
|
||||
|
||||
const fcmToken = decodeURIComponent(req.params.token);
|
||||
const row = await db.getByFcmTokenForUser(userId, fcmToken);
|
||||
const row = await db.resolveOwnedDevice(userId, { fcmToken });
|
||||
if (row === undefined) {
|
||||
console.log(
|
||||
"[Debug] Device lookup not found for user, token suffix:",
|
||||
"[DebugEndpoint] Device lookup not found for user, token suffix:",
|
||||
maskToken(fcmToken)
|
||||
);
|
||||
res.status(404).json({ error: "Device not found" });
|
||||
@@ -51,13 +65,13 @@ debugRouter.get("/device/:token", async (req, res) => {
|
||||
}
|
||||
|
||||
console.log(
|
||||
"[Debug] Device lookup for user, token suffix:",
|
||||
"[DebugEndpoint] Device lookup for user, token suffix:",
|
||||
maskToken(fcmToken)
|
||||
);
|
||||
res.json(deviceDebugPayload(row));
|
||||
});
|
||||
|
||||
// TODO: Restrict further before production deployment
|
||||
// TODO: Protect this endpoint before production deployment
|
||||
debugRouter.post("/send-wakeup", async (req, res) => {
|
||||
const userId = req.did;
|
||||
if (userId === undefined) {
|
||||
@@ -67,25 +81,45 @@ debugRouter.post("/send-wakeup", async (req, res) => {
|
||||
|
||||
const { fcmToken } = req.body as { fcmToken?: unknown };
|
||||
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;
|
||||
}
|
||||
|
||||
const row = await db.getByFcmTokenForUser(userId, fcmToken);
|
||||
const row = await db.resolveOwnedDevice(userId, { fcmToken });
|
||||
if (row === undefined) {
|
||||
console.log(
|
||||
"[Debug] Send-wakeup rejected, token suffix:",
|
||||
"[DebugEndpoint] Send-wakeup rejected, token suffix:",
|
||||
maskToken(fcmToken)
|
||||
);
|
||||
res.status(404).json({ error: "Device not found" });
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
failureReason: "Device not found",
|
||||
fcmTokenSuffix: maskToken(fcmToken),
|
||||
});
|
||||
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 success = result === "sent";
|
||||
const failureReason = sendWakeupFailureReason(result);
|
||||
|
||||
console.log(
|
||||
"[DebugEndpoint] Send-wakeup result:",
|
||||
success ? "success" : result,
|
||||
"token suffix:",
|
||||
maskToken(fcmToken)
|
||||
);
|
||||
|
||||
res.json({
|
||||
result,
|
||||
success,
|
||||
...(failureReason !== undefined ? { failureReason } : {}),
|
||||
fcmTokenSuffix: maskToken(fcmToken),
|
||||
deviceId: row.deviceId,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,10 +5,22 @@ import { messaging } from "./firebase.js";
|
||||
const MS_PRODUCTION = 23 * 60 * 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;
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
const v = row?.lastNotifiedAt;
|
||||
if (v === undefined) return undefined;
|
||||
|
||||
Reference in New Issue
Block a user