Compare commits
6 Commits
2dba6c3597
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcf3fb9aa0 | ||
| a99117dc98 | |||
| 85301135c7 | |||
|
|
7a2bb88207 | ||
|
|
6261f1baa0 | ||
| bb0927ad92 |
@@ -1,5 +1,5 @@
|
|||||||
# HTTP port (default: 3000)
|
# HTTP port (default: 3003)
|
||||||
PORT=3000
|
PORT=3003
|
||||||
|
|
||||||
# Firebase Admin: inline service account JSON (one line).
|
# Firebase Admin: inline service account JSON (one line).
|
||||||
# If unset, uses Application Default Credentials (e.g. GOOGLE_APPLICATION_CREDENTIALS).
|
# If unset, uses Application Default Credentials (e.g. GOOGLE_APPLICATION_CREDENTIALS).
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ dist/
|
|||||||
.env
|
.env
|
||||||
*.log
|
*.log
|
||||||
data/
|
data/
|
||||||
|
.DS_Store
|
||||||
|
|||||||
26
Dockerfile
26
Dockerfile
@@ -1,17 +1,31 @@
|
|||||||
FROM node:22-alpine
|
# ---- build stage: install everything, type-check + compile to JS ----
|
||||||
|
FROM node:22-alpine AS build
|
||||||
|
|
||||||
RUN corepack enable && corepack prepare pnpm@11.4.0 --activate
|
RUN corepack enable && corepack prepare pnpm@11.4.0 --activate
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
RUN pnpm install --prod
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
COPY src ./src
|
|
||||||
COPY tsconfig.json ./
|
COPY tsconfig.json ./
|
||||||
|
COPY src ./src
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
# ---- runtime stage: prod deps + compiled JS only, no tsx/esbuild ----
|
||||||
|
FROM node:22-alpine AS runtime
|
||||||
|
|
||||||
|
RUN corepack enable && corepack prepare pnpm@11.4.0 --activate
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
|
RUN pnpm install --prod --frozen-lockfile
|
||||||
|
|
||||||
|
COPY --from=build /app/dist ./dist
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV PORT=3000
|
ENV PORT=3003
|
||||||
EXPOSE 3000
|
EXPOSE 3003
|
||||||
|
|
||||||
CMD ["node", "--import", "tsx/esm", "src/index.ts"]
|
CMD ["node", "dist/index.js"]
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ pnpm install
|
|||||||
pnpm run dev
|
pnpm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
The server starts on `http://localhost:3000` (or the port in `PORT`). Hot-reloads on file changes.
|
The server starts on `http://localhost:3003` (or the port in `PORT`). Hot-reloads on file changes.
|
||||||
|
|
||||||
Set `NODE_ENV=test-local` in `.env` to bypass JWT expiry verification during local development.
|
Set `NODE_ENV=test-local` in `.env` to bypass JWT expiry verification during local development.
|
||||||
|
|
||||||
@@ -30,8 +30,8 @@ pnpm start
|
|||||||
Or with Docker:
|
Or with Docker:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -t notification-wakeup-service .
|
docker build --no-cache -t notify-wakeup-api:amd-$NOTIFY_WAKEUP_API_VERSION --platform linux/amd64 .
|
||||||
docker run -e FIREBASE_SERVICE_ACCOUNT_JSON='...' -p 3000:3000 notification-wakeup-service
|
docker run --env-file notify-wakeup-api.env -p 3003:3003 notify-wakeup-api
|
||||||
```
|
```
|
||||||
|
|
||||||
Required environment variables:
|
Required environment variables:
|
||||||
@@ -39,5 +39,5 @@ Required environment variables:
|
|||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `FIREBASE_SERVICE_ACCOUNT_JSON` | Inline service account JSON (one line). If unset, falls back to Application Default Credentials. |
|
| `FIREBASE_SERVICE_ACCOUNT_JSON` | Inline service account JSON (one line). If unset, falls back to Application Default Credentials. |
|
||||||
| `PORT` | HTTP port (default: `3000`). |
|
| `PORT` | HTTP port (default: `3003`). |
|
||||||
| `FCM_TOKEN_DATA_DIR` | Directory for persisting registered FCM tokens (default: `./data`). |
|
| `FCM_TOKEN_DATA_DIR` | Directory for persisting registered FCM tokens (default: `./data`). |
|
||||||
|
|||||||
68
package-lock.json
generated
68
package-lock.json
generated
@@ -8,21 +8,23 @@
|
|||||||
"name": "notification-wakeup-service",
|
"name": "notification-wakeup-service",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@peculiar/asn1-ecc": "^2.3.8",
|
"@peculiar/asn1-ecc": "^2.7.0",
|
||||||
"@peculiar/asn1-schema": "^2.3.8",
|
"@peculiar/asn1-schema": "^2.7.0",
|
||||||
"cbor-x": "^1.5.9",
|
"cbor-x": "^1.6.4",
|
||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"did-jwt": "^7.4.7",
|
"did-jwt": "^7.4.7",
|
||||||
"did-resolver": "^4.1.0",
|
"did-resolver": "^4.1.0",
|
||||||
"express": "^5.1.0",
|
"dotenv": "^16.6.1",
|
||||||
"firebase-admin": "^13.9.0"
|
"express": "^5.2.1",
|
||||||
|
"firebase-admin": "^13.10.0",
|
||||||
|
"tsx": "^4.22.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.6",
|
||||||
"@types/node": "^22.10.0",
|
"@types/node": "^22.19.19",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.2",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cbor-extract/cbor-extract-darwin-arm64": {
|
"node_modules/@cbor-extract/cbor-extract-darwin-arm64": {
|
||||||
@@ -1146,9 +1148,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.19.18",
|
"version": "22.19.21",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.18.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.21.tgz",
|
||||||
"integrity": "sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ==",
|
"integrity": "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
@@ -1631,6 +1633,18 @@
|
|||||||
"integrity": "sha512-S6fWHvCXkZg2IhS4RcVHxwuyVejPR7c+a4Go0xbQ9ps5kILa8viiYQgrM4gfTyeTjJ0ekgJH9gk/BawTpmkbZA==",
|
"integrity": "sha512-S6fWHvCXkZg2IhS4RcVHxwuyVejPR7c+a4Go0xbQ9ps5kILa8viiYQgrM4gfTyeTjJ0ekgJH9gk/BawTpmkbZA==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "16.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||||
|
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -1982,9 +1996,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/firebase-admin": {
|
"node_modules/firebase-admin": {
|
||||||
"version": "13.9.0",
|
"version": "13.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.10.0.tgz",
|
||||||
"integrity": "sha512-qiCVBBFH+kfLiCXuuE9eAbBQSckPuA43fbQ/MNvQfd9nZcHFQExmQICD/N0sZrNZDNy8FSywhjFzJJGVQzG5UA==",
|
"integrity": "sha512-rbuCrJvYRwqBqvbccMS8fj/x2zsaMisdf5RQbRzQzr14Rbq9r2UlpuBHqWAwrO6c9dIRF56xF/xoepXsD5yDuQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/busboy": "^3.0.0",
|
"@fastify/busboy": "^3.0.0",
|
||||||
@@ -1994,9 +2008,7 @@
|
|||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"google-auth-library": "^10.6.1",
|
"google-auth-library": "^10.6.1",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"jwks-rsa": "^3.1.0",
|
"jwks-rsa": "^3.1.0"
|
||||||
"node-forge": "^1.4.0",
|
|
||||||
"uuid": "^11.0.2"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -2906,15 +2918,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-forge": {
|
|
||||||
"version": "1.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz",
|
|
||||||
"integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==",
|
|
||||||
"license": "(BSD-3-Clause OR GPL-2.0)",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6.13.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/node-gyp-build-optional-packages": {
|
"node_modules/node-gyp-build-optional-packages": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz",
|
||||||
@@ -3622,19 +3625,6 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/uuid": {
|
|
||||||
"version": "11.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz",
|
|
||||||
"integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==",
|
|
||||||
"funding": [
|
|
||||||
"https://github.com/sponsors/broofa",
|
|
||||||
"https://github.com/sponsors/ctavan"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"uuid": "dist/esm/bin/uuid"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vary": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "notification-wakeup-service",
|
"name": "notification-wakeup-service",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@11.4.0",
|
"packageManager": "pnpm@11.4.0",
|
||||||
@@ -14,17 +14,17 @@
|
|||||||
"@peculiar/asn1-schema": "^2.7.0",
|
"@peculiar/asn1-schema": "^2.7.0",
|
||||||
"cbor-x": "^1.6.4",
|
"cbor-x": "^1.6.4",
|
||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
|
"dotenv": "^16.6.1",
|
||||||
"did-jwt": "^7.4.7",
|
"did-jwt": "^7.4.7",
|
||||||
"did-resolver": "^4.1.0",
|
"did-resolver": "^4.1.0",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"firebase-admin": "^13.10.0",
|
"firebase-admin": "^13.10.0"
|
||||||
"tsx": "^4.22.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
"@types/node": "^22.19.19",
|
"@types/node": "^22.19.19",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.22.3",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@@ -26,15 +26,15 @@ importers:
|
|||||||
did-resolver:
|
did-resolver:
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
|
dotenv:
|
||||||
|
specifier: ^16.6.1
|
||||||
|
version: 16.6.1
|
||||||
express:
|
express:
|
||||||
specifier: ^5.2.1
|
specifier: ^5.2.1
|
||||||
version: 5.2.1
|
version: 5.2.1
|
||||||
firebase-admin:
|
firebase-admin:
|
||||||
specifier: ^13.10.0
|
specifier: ^13.10.0
|
||||||
version: 13.10.0
|
version: 13.10.0
|
||||||
tsx:
|
|
||||||
specifier: ^4.22.3
|
|
||||||
version: 4.22.3
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/cors':
|
'@types/cors':
|
||||||
specifier: ^2.8.19
|
specifier: ^2.8.19
|
||||||
@@ -45,6 +45,9 @@ importers:
|
|||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.19.19
|
specifier: ^22.19.19
|
||||||
version: 22.19.19
|
version: 22.19.19
|
||||||
|
tsx:
|
||||||
|
specifier: ^4.22.3
|
||||||
|
version: 4.22.3
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.9.3
|
specifier: ^5.9.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@@ -574,6 +577,10 @@ packages:
|
|||||||
did-resolver@4.1.0:
|
did-resolver@4.1.0:
|
||||||
resolution: {integrity: sha512-S6fWHvCXkZg2IhS4RcVHxwuyVejPR7c+a4Go0xbQ9ps5kILa8viiYQgrM4gfTyeTjJ0ekgJH9gk/BawTpmkbZA==}
|
resolution: {integrity: sha512-S6fWHvCXkZg2IhS4RcVHxwuyVejPR7c+a4Go0xbQ9ps5kILa8viiYQgrM4gfTyeTjJ0ekgJH9gk/BawTpmkbZA==}
|
||||||
|
|
||||||
|
dotenv@16.6.1:
|
||||||
|
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1711,6 +1718,8 @@ snapshots:
|
|||||||
|
|
||||||
did-resolver@4.1.0: {}
|
did-resolver@4.1.0: {}
|
||||||
|
|
||||||
|
dotenv@16.6.1: {}
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind-apply-helpers: 1.0.2
|
call-bind-apply-helpers: 1.0.2
|
||||||
|
|||||||
3
src/env.ts
Normal file
3
src/env.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { config } from "dotenv";
|
||||||
|
|
||||||
|
config();
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import "./env.js";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import "./services/firebase.js";
|
import "./services/firebase.js";
|
||||||
@@ -6,7 +7,7 @@ import { notificationsRouter } from "./routes/notifications.js";
|
|||||||
import { startScheduler } from "./scheduler.js";
|
import { startScheduler } from "./scheduler.js";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = Number(process.env.PORT) || 3000;
|
const port = Number(process.env.PORT) || 3003;
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
|
|||||||
@@ -13,6 +13,21 @@ type ClientErrorBody = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function clientErrorMessage(err: unknown): string | undefined {
|
function clientErrorMessage(err: unknown): string | undefined {
|
||||||
if (err && typeof err === "object" && "clientError" in err) {
|
if (err && typeof err === "object" && "clientError" in err) {
|
||||||
const message = (err as ClientErrorBody).clientError?.message;
|
const message = (err as ClientErrorBody).clientError?.message;
|
||||||
@@ -86,3 +101,17 @@ export async function requireAuth(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export 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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { Router } from "express";
|
import express, { Router } from "express";
|
||||||
import { db } from "../db/fcmTokens.js";
|
import { db } from "../db/fcmTokens.js";
|
||||||
import { requireAuth } from "../middleware/auth.js";
|
import {
|
||||||
|
requireAuth,
|
||||||
|
requireAuthOrNotificationLocalTest,
|
||||||
|
} from "../middleware/auth.js";
|
||||||
import {
|
import {
|
||||||
computeNextEligibleAt,
|
computeNextEligibleAt,
|
||||||
sendPushToDevice,
|
sendPushToDevice,
|
||||||
@@ -9,9 +12,7 @@ import { formatElapsedMs } from "../util/formatElapsed.js";
|
|||||||
import { maskToken } from "../util/maskToken.js";
|
import { maskToken } from "../util/maskToken.js";
|
||||||
|
|
||||||
// TODO: Protect this endpoint before production deployment
|
// TODO: Protect this endpoint before production deployment
|
||||||
export const debugRouter = Router();
|
export const debugRouter: express.Router = Router();
|
||||||
|
|
||||||
debugRouter.use(requireAuth);
|
|
||||||
|
|
||||||
function deviceDebugPayload(row: {
|
function deviceDebugPayload(row: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -47,7 +48,7 @@ function sendWakeupFailureReason(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Protect this endpoint before production deployment
|
// TODO: Protect this endpoint before production deployment
|
||||||
debugRouter.get("/device/:token", async (req, res) => {
|
debugRouter.get("/device/:token", requireAuth, async (req, res) => {
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
const userId = req.did;
|
const userId = req.did;
|
||||||
if (userId === undefined) {
|
if (userId === undefined) {
|
||||||
@@ -55,7 +56,10 @@ debugRouter.get("/device/:token", async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fcmToken = decodeURIComponent(req.params.token);
|
const tokenParam = req.params.token;
|
||||||
|
const fcmToken = decodeURIComponent(
|
||||||
|
Array.isArray(tokenParam) ? tokenParam[0] : tokenParam
|
||||||
|
);
|
||||||
const suffix = maskToken(fcmToken);
|
const suffix = maskToken(fcmToken);
|
||||||
console.log("[DebugEndpoint] Device lookup request, token suffix:", suffix);
|
console.log("[DebugEndpoint] Device lookup request, token suffix:", suffix);
|
||||||
|
|
||||||
@@ -81,7 +85,7 @@ debugRouter.get("/device/:token", async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Protect this endpoint before production deployment
|
// TODO: Protect this endpoint before production deployment
|
||||||
debugRouter.post("/send-wakeup", async (req, res) => {
|
debugRouter.post("/send-wakeup", requireAuthOrNotificationLocalTest, async (req, res) => {
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
const userId = req.did;
|
const userId = req.did;
|
||||||
if (userId === undefined) {
|
if (userId === undefined) {
|
||||||
|
|||||||
@@ -1,40 +1,10 @@
|
|||||||
import type { NextFunction, Request, Response } from "express";
|
import express, { Router } from "express";
|
||||||
import { Router } from "express";
|
|
||||||
import { db } from "../db/fcmTokens.js";
|
import { db } from "../db/fcmTokens.js";
|
||||||
import { requireAuth } from "../middleware/auth.js";
|
import { requireAuthOrNotificationLocalTest } from "../middleware/auth.js";
|
||||||
import { errorMessage, formatElapsedMs } from "../util/formatElapsed.js";
|
import { errorMessage, formatElapsedMs } from "../util/formatElapsed.js";
|
||||||
import { maskToken } from "../util/maskToken.js";
|
import { maskToken } from "../util/maskToken.js";
|
||||||
|
|
||||||
/** Synthetic userId for unauthenticated local debug registrations (testMode). */
|
export const notificationsRouter: express.Router = Router();
|
||||||
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) => {
|
notificationsRouter.get("/", (_req, res) => {
|
||||||
res.json({ ok: true, resource: "notifications" });
|
res.json({ ok: true, resource: "notifications" });
|
||||||
|
|||||||
@@ -2,11 +2,45 @@ import admin from "firebase-admin";
|
|||||||
import type { ServiceAccount } from "firebase-admin/app";
|
import type { ServiceAccount } from "firebase-admin/app";
|
||||||
import type { Messaging } from "firebase-admin/messaging";
|
import type { Messaging } from "firebase-admin/messaging";
|
||||||
|
|
||||||
|
type ServiceAccountJson = ServiceAccount & { project_id?: string };
|
||||||
|
|
||||||
|
function serviceAccountProjectId(account: ServiceAccountJson): string | undefined {
|
||||||
|
if (typeof account.projectId === "string" && account.projectId.length > 0) {
|
||||||
|
return account.projectId;
|
||||||
|
}
|
||||||
|
if (typeof account.project_id === "string" && account.project_id.length > 0) {
|
||||||
|
return account.project_id;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveCredential(): admin.credential.Credential {
|
function resolveCredential(): admin.credential.Credential {
|
||||||
const json = process.env.FIREBASE_SERVICE_ACCOUNT_JSON;
|
const json = process.env.FIREBASE_SERVICE_ACCOUNT_JSON;
|
||||||
if (json !== undefined && json.trim() !== "") {
|
if (json !== undefined && json.trim() !== "") {
|
||||||
return admin.credential.cert(JSON.parse(json) as ServiceAccount);
|
let account: ServiceAccountJson;
|
||||||
|
try {
|
||||||
|
account = JSON.parse(json) as ServiceAccountJson;
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
console.error(
|
||||||
|
"[Firebase] FIREBASE_SERVICE_ACCOUNT_JSON parse failed:",
|
||||||
|
message
|
||||||
|
);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
const projectId = serviceAccountProjectId(account);
|
||||||
|
console.log(
|
||||||
|
"[Firebase] Credential: FIREBASE_SERVICE_ACCOUNT_JSON (parsed successfully)"
|
||||||
|
);
|
||||||
|
if (projectId !== undefined) {
|
||||||
|
console.log("[Firebase] project_id:", projectId);
|
||||||
|
} else {
|
||||||
|
console.log("[Firebase] project_id: (not found in service account JSON)");
|
||||||
|
}
|
||||||
|
return admin.credential.cert(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("[Firebase] Credential: Application Default Credentials");
|
||||||
return admin.credential.applicationDefault();
|
return admin.credential.applicationDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user