feat(push): native device-token registration + FCM/APNs senders
- /api/v1/devices (register) + /api/v1/devices/remove — auth-required, validates platform (ios|android), upserts by token; e2e covers register/validation/auth/remove. - db device_tokens table + deviceTokens repo. - push.js: FCM HTTP v1 (Android) and APNs token-based over HTTP/2 (iOS) folded into the single push.sendToUser path alongside Web Push; each transport independently config-gated and a silent no-op without creds. Dead tokens pruned on 404/410. - docs: CLIENTS.md Phase B updated; DEPLOY.md env table adds FCM/APNs vars. e2e 117/117. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -297,6 +297,25 @@ route('POST', '/api/push/unsubscribe', async (req, res) => {
|
||||
json(res, 200, { ok: true });
|
||||
});
|
||||
|
||||
// --- Native device tokens (mobile app): FCM (Android) / APNs (iOS). Registration is always
|
||||
// accepted and stored; delivery is a no-op until FCM/APNs creds are configured. ---
|
||||
route('POST', '/api/devices', async (req, res) => {
|
||||
const u = currentUser(req);
|
||||
if (!u) return json(res, 401, { error: 'unauthorized' });
|
||||
const { platform, token } = await readBody(req);
|
||||
if (!token || typeof token !== 'string') return json(res, 400, { error: 'token required' });
|
||||
if (platform !== 'ios' && platform !== 'android') return json(res, 400, { error: 'platform must be ios or android' });
|
||||
try { R.deviceTokens.register({ id: A.id(), userId: u.id, tenantId: u.team_id, platform, token }); } catch (_) {}
|
||||
json(res, 200, { ok: true });
|
||||
});
|
||||
route('POST', '/api/devices/remove', async (req, res) => {
|
||||
const u = currentUser(req);
|
||||
if (!u) return json(res, 401, { error: 'unauthorized' });
|
||||
const { token } = await readBody(req);
|
||||
if (token) { try { R.deviceTokens.removeByToken(token); } catch (_) {} }
|
||||
json(res, 200, { ok: true });
|
||||
});
|
||||
|
||||
// ---------- BizGaze SSO: agent arrives already logged in ----------
|
||||
route('GET', '/sso', async (req, res) => {
|
||||
if (!process.env.SSO_SECRET) { res.writeHead(503); return res.end('SSO not configured'); }
|
||||
|
||||
Reference in New Issue
Block a user