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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@ node_modules/
|
||||
dist/
|
||||
.env
|
||||
*.log
|
||||
data/
|
||||
|
||||
55
src/db/fcmTokens.ts
Normal file
55
src/db/fcmTokens.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user