feat(notifications): allow local debug register/refresh without JWT

When the Notification Debug Panel sends testMode: true and omits
Authorization, skip requireAuth on /notifications/register and /refresh
and scope devices under a synthetic local-test user id. Requests with
a Bearer token or without testMode still use full JWT auth unchanged.
This commit is contained in:
Jose Olarte III
2026-06-04 20:32:18 +08:00
parent dffb86007e
commit 6ba7d678c6

View File

@@ -1,181 +1,221 @@
import type { NextFunction, Request, Response } from "express";
import { Router } from "express";
import { db } from "../db/fcmTokens.js";
import { requireAuth } from "../middleware/auth.js";
import { errorMessage, formatElapsedMs } from "../util/formatElapsed.js";
import { maskToken } from "../util/maskToken.js";
/** Synthetic userId for unauthenticated local debug registrations (testMode). */
const LOCAL_TEST_USER_ID = "__notification_local_test__";
function isNotificationLocalTestBypass(req: Request): boolean {
if (req.headers.authorization?.startsWith("Bearer ")) {
return false;
}
const body = req.body;
return (
body !== null &&
typeof body === "object" &&
(body as { testMode?: unknown }).testMode === true
);
}
async function requireAuthOrNotificationLocalTest(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
if (isNotificationLocalTestBypass(req)) {
req.did = LOCAL_TEST_USER_ID;
console.log("[Auth] Local notification test bypass");
next();
return;
}
return requireAuth(req, res, next);
}
export const notificationsRouter = Router();
notificationsRouter.get("/", (_req, res) => {
res.json({ ok: true, resource: "notifications" });
});
notificationsRouter.post("/refresh", requireAuth, async (req, res) => {
const started = Date.now();
const userId = req.did;
if (userId === undefined) {
res.status(401).json({ success: false, message: "Unauthorized" });
return;
}
notificationsRouter.post(
"/refresh",
requireAuthOrNotificationLocalTest,
async (req, res) => {
const started = Date.now();
const userId = req.did;
if (userId === undefined) {
res.status(401).json({ success: false, message: "Unauthorized" });
return;
}
const { deviceId, fcmToken } = req.body as {
deviceId?: unknown;
fcmToken?: unknown;
};
const { deviceId, fcmToken } = req.body as {
deviceId?: unknown;
fcmToken?: unknown;
};
const canonicalDeviceId =
typeof deviceId === "string" ? deviceId.trim() : undefined;
const token =
typeof fcmToken === "string" && fcmToken.length > 0 ? fcmToken : undefined;
const canonicalDeviceId =
typeof deviceId === "string" ? deviceId.trim() : undefined;
const token =
typeof fcmToken === "string" && fcmToken.length > 0
? fcmToken
: undefined;
console.log(
"[Refresh] Request received",
canonicalDeviceId !== undefined ? `deviceId=${canonicalDeviceId}` : "",
token !== undefined ? `token suffix=${maskToken(token)}` : ""
);
if (
(canonicalDeviceId === undefined || canonicalDeviceId.length === 0) &&
token === undefined
) {
console.log(
"[Refresh] Rejected in",
formatElapsedMs(Date.now() - started) + ":",
"deviceId or fcmToken is required"
);
res.status(400).json({ error: "deviceId or fcmToken is required" });
return;
}
const device = await db.resolveOwnedDevice(userId, {
deviceId: canonicalDeviceId,
fcmToken: token,
});
if (device === undefined) {
console.log(
"[Refresh] Device not found in",
formatElapsedMs(Date.now() - started),
canonicalDeviceId !== undefined
? `deviceId=${canonicalDeviceId}`
: "",
"[Refresh] Request received",
canonicalDeviceId !== undefined ? `deviceId=${canonicalDeviceId}` : "",
token !== undefined ? `token suffix=${maskToken(token)}` : ""
);
res.status(404).json({ error: "Device not found" });
return;
}
const now = Date.now();
res.json({
shouldNotify: true,
nextNotifications: [{ timestamp: now + 600000 }],
});
console.log(
"[Refresh] Completed in",
formatElapsedMs(Date.now() - started) + ",",
"deviceId=" + device.deviceId + ",",
"token suffix=" + maskToken(device.fcmToken)
);
});
if (
(canonicalDeviceId === undefined || canonicalDeviceId.length === 0) &&
token === undefined
) {
console.log(
"[Refresh] Rejected in",
formatElapsedMs(Date.now() - started) + ":",
"deviceId or fcmToken is required"
);
res.status(400).json({ error: "deviceId or fcmToken is required" });
return;
}
notificationsRouter.post("/register", requireAuth, async (req, res) => {
const started = Date.now();
const userId = req.did;
if (userId === undefined) {
res.status(401).json({ success: false, message: "Unauthorized" });
return;
}
if (
req.body !== null &&
typeof req.body === "object" &&
"userId" in req.body
) {
console.log(
"[Register] Rejected in",
formatElapsedMs(Date.now() - started) + ":",
"userId must not be sent in the request body"
);
res.status(400).json({
error: "userId must not be sent in the request body",
});
return;
}
const { deviceId, fcmToken, platform, testMode } = req.body as {
deviceId?: unknown;
fcmToken?: unknown;
platform?: unknown;
testMode?: unknown;
};
if (typeof deviceId !== "string" || deviceId.trim().length === 0) {
console.log(
"[Register] Rejected in",
formatElapsedMs(Date.now() - started) + ":",
"deviceId is required"
);
res.status(400).json({ error: "deviceId is required" });
return;
}
if (typeof fcmToken !== "string" || fcmToken.length === 0) {
console.log(
"[Register] Rejected in",
formatElapsedMs(Date.now() - started) + ":",
"fcmToken is required"
);
res.status(400).json({ error: "fcmToken is required" });
return;
}
if (typeof platform !== "string" || platform.length === 0) {
console.log(
"[Register] Rejected in",
formatElapsedMs(Date.now() - started) + ":",
"platform is required"
);
res.status(400).json({ error: "platform is required" });
return;
}
const canonicalDeviceId = deviceId.trim();
console.log(
"[Register] Request received,",
"deviceId=" + canonicalDeviceId + ",",
"platform=" + platform + ",",
"token suffix=" + maskToken(fcmToken)
);
try {
const existing = await db.getByDeviceId(userId, canonicalDeviceId);
const action =
existing === undefined
? "create"
: existing.fcmToken !== fcmToken
? "update-token"
: "update";
await db.upsert({
userId,
const device = await db.resolveOwnedDevice(userId, {
deviceId: canonicalDeviceId,
fcmToken,
platform,
testMode: typeof testMode === "boolean" ? testMode : undefined,
updatedAt: new Date(),
fcmToken: token,
});
if (device === undefined) {
console.log(
"[Refresh] Device not found in",
formatElapsedMs(Date.now() - started),
canonicalDeviceId !== undefined
? `deviceId=${canonicalDeviceId}`
: "",
token !== undefined ? `token suffix=${maskToken(token)}` : ""
);
res.status(404).json({ error: "Device not found" });
return;
}
const now = Date.now();
res.json({
shouldNotify: true,
nextNotifications: [{ timestamp: now + 600000 }],
});
res.sendStatus(200);
console.log(
"[Register] Completed in",
"[Refresh] Completed in",
formatElapsedMs(Date.now() - started) + ",",
"deviceId=" + canonicalDeviceId + ",",
"action=" + action
"deviceId=" + device.deviceId + ",",
"token suffix=" + maskToken(device.fcmToken)
);
} catch (err) {
console.error(
"[Register] Failed in",
formatElapsedMs(Date.now() - started) + ",",
"deviceId=" + canonicalDeviceId + ":",
errorMessage(err)
);
res.sendStatus(500);
}
});
);
notificationsRouter.post(
"/register",
requireAuthOrNotificationLocalTest,
async (req, res) => {
const started = Date.now();
const userId = req.did;
if (userId === undefined) {
res.status(401).json({ success: false, message: "Unauthorized" });
return;
}
if (
req.body !== null &&
typeof req.body === "object" &&
"userId" in req.body
) {
console.log(
"[Register] Rejected in",
formatElapsedMs(Date.now() - started) + ":",
"userId must not be sent in the request body"
);
res.status(400).json({
error: "userId must not be sent in the request body",
});
return;
}
const { deviceId, fcmToken, platform, testMode } = req.body as {
deviceId?: unknown;
fcmToken?: unknown;
platform?: unknown;
testMode?: unknown;
};
if (typeof deviceId !== "string" || deviceId.trim().length === 0) {
console.log(
"[Register] Rejected in",
formatElapsedMs(Date.now() - started) + ":",
"deviceId is required"
);
res.status(400).json({ error: "deviceId is required" });
return;
}
if (typeof fcmToken !== "string" || fcmToken.length === 0) {
console.log(
"[Register] Rejected in",
formatElapsedMs(Date.now() - started) + ":",
"fcmToken is required"
);
res.status(400).json({ error: "fcmToken is required" });
return;
}
if (typeof platform !== "string" || platform.length === 0) {
console.log(
"[Register] Rejected in",
formatElapsedMs(Date.now() - started) + ":",
"platform is required"
);
res.status(400).json({ error: "platform is required" });
return;
}
const canonicalDeviceId = deviceId.trim();
console.log(
"[Register] Request received,",
"deviceId=" + canonicalDeviceId + ",",
"platform=" + platform + ",",
"token suffix=" + maskToken(fcmToken)
);
try {
const existing = await db.getByDeviceId(userId, canonicalDeviceId);
const action =
existing === undefined
? "create"
: existing.fcmToken !== fcmToken
? "update-token"
: "update";
await db.upsert({
userId,
deviceId: canonicalDeviceId,
fcmToken,
platform,
testMode: typeof testMode === "boolean" ? testMode : undefined,
updatedAt: new Date(),
});
res.sendStatus(200);
console.log(
"[Register] Completed in",
formatElapsedMs(Date.now() - started) + ",",
"deviceId=" + canonicalDeviceId + ",",
"action=" + action
);
} catch (err) {
console.error(
"[Register] Failed in",
formatElapsedMs(Date.now() - started) + ",",
"deviceId=" + canonicalDeviceId + ":",
errorMessage(err)
);
res.sendStatus(500);
}
}
);