feat(notifications): persist FCM tokens on POST /register

Add JSON-backed upsert store (data/fcm-tokens.json, optional
FCM_TOKEN_DATA_DIR), validate body fields, and gitignore data/.
This commit is contained in:
Jose Olarte III
2026-05-11 14:52:09 +08:00
parent d311b6a504
commit 64ea7d2f98
3 changed files with 87 additions and 0 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@ node_modules/
dist/
.env
*.log
data/

55
src/db/fcmTokens.ts Normal file
View File

@@ -0,0 +1,55 @@
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
import path from "node:path";
const dataDir =
process.env.FCM_TOKEN_DATA_DIR ?? path.join(process.cwd(), "data");
const dataFile = path.join(dataDir, "fcm-tokens.json");
type StoredRow = {
fcmToken: string;
platform: string;
testMode?: boolean;
createdAt: string;
updatedAt: string;
};
async function load(): Promise<Record<string, StoredRow>> {
try {
const raw = await readFile(dataFile, "utf8");
return JSON.parse(raw) as Record<string, StoredRow>;
} catch (e: unknown) {
const code = (e as NodeJS.ErrnoException).code;
if (code === "ENOENT") return {};
throw e;
}
}
async function save(records: Record<string, StoredRow>): Promise<void> {
await mkdir(dataDir, { recursive: true });
const tmp = path.join(dataDir, `.fcm-tokens.${process.pid}.tmp`);
const payload = JSON.stringify(records, null, 2);
await writeFile(tmp, payload, "utf8");
await rename(tmp, dataFile);
}
export const db = {
async upsert(row: {
fcmToken: string;
platform: string;
testMode?: boolean;
updatedAt: Date;
}): Promise<void> {
const all = await load();
const key = row.fcmToken;
const prev = all[key];
const now = row.updatedAt.toISOString();
all[key] = {
fcmToken: row.fcmToken,
platform: row.platform,
testMode: row.testMode,
updatedAt: now,
createdAt: prev?.createdAt ?? now,
};
await save(all);
},
};

View File

@@ -1,7 +1,38 @@
import { Router } from "express";
import { db } from "../db/fcmTokens.js";
export const notificationsRouter = Router();
notificationsRouter.get("/", (_req, res) => {
res.json({ ok: true, resource: "notifications" });
});
notificationsRouter.post("/register", async (req, res) => {
const { fcmToken, platform, testMode } = req.body as {
fcmToken?: unknown;
platform?: unknown;
testMode?: unknown;
};
if (typeof fcmToken !== "string" || fcmToken.length === 0) {
res.status(400).json({ error: "fcmToken is required" });
return;
}
if (typeof platform !== "string" || platform.length === 0) {
res.status(400).json({ error: "platform is required" });
return;
}
try {
await db.upsert({
fcmToken,
platform,
testMode: typeof testMode === "boolean" ? testMode : undefined,
updatedAt: new Date(),
});
res.sendStatus(200);
} catch (err) {
console.error("register failed", err);
res.sendStatus(500);
}
});