diff --git a/.gitignore b/.gitignore index aa0926a..caf428f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/ dist/ .env *.log +data/ diff --git a/src/db/fcmTokens.ts b/src/db/fcmTokens.ts new file mode 100644 index 0000000..0c84ec2 --- /dev/null +++ b/src/db/fcmTokens.ts @@ -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> { + try { + const raw = await readFile(dataFile, "utf8"); + return JSON.parse(raw) as Record; + } catch (e: unknown) { + const code = (e as NodeJS.ErrnoException).code; + if (code === "ENOENT") return {}; + throw e; + } +} + +async function save(records: Record): Promise { + 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 { + 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); + }, +}; diff --git a/src/routes/notifications.ts b/src/routes/notifications.ts index 2387eda..7cbedaf 100644 --- a/src/routes/notifications.ts +++ b/src/routes/notifications.ts @@ -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); + } +});