diff --git a/src/services/notifications/NativeNotificationService.ts b/src/services/notifications/NativeNotificationService.ts index d75f87f8..1d066a89 100644 --- a/src/services/notifications/NativeNotificationService.ts +++ b/src/services/notifications/NativeNotificationService.ts @@ -27,6 +27,12 @@ import { logRefreshSuccess, logScheduleReplacement, } from "./notificationLog"; +import { + getNotificationApiHeaders, + httpAuthErrorMessage, + logNotificationAuthFailure, + logNotificationRequestAuthenticated, +} from "./notificationApiAuth"; import { logNotification } from "./NotificationDebugEvents"; /** @@ -585,10 +591,22 @@ export async function refreshNotificationsWithDiagnostics(options?: { } try { + const auth = await getNotificationApiHeaders(); + if (!auth.ok) { + logNotificationAuthFailure("refresh", auth.message); + logRefreshFailure(startedAt, auth.message); + return { + ok: false, + scheduledCount: 0, + errorMessage: auth.message, + }; + } + logNotificationRequestAuthenticated("refresh"); + const baseUrl = getNotificationApiBaseUrl(); const res = await fetch(`${baseUrl}/notifications/refresh`, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: auth.headers, body: JSON.stringify({ platform: Capacitor.getPlatform(), testMode: getTestMode(), @@ -596,10 +614,14 @@ export async function refreshNotificationsWithDiagnostics(options?: { }); if (!res.ok) { - const errorMessage = res.statusText || `HTTP ${res.status}`; + const errorMessage = + res.status === 401 || res.status === 403 + ? httpAuthErrorMessage(res.status) + : res.statusText || `HTTP ${res.status}`; logger.warn("[NativeNotificationService] refreshNotifications failed", { status: res.status, statusText: res.statusText, + errorMessage, }); logRefreshFailure(startedAt, errorMessage, res.status); return { diff --git a/src/services/notifications/NotificationService.ts b/src/services/notifications/NotificationService.ts index 5b6bde98..782c30b7 100644 --- a/src/services/notifications/NotificationService.ts +++ b/src/services/notifications/NotificationService.ts @@ -20,6 +20,12 @@ import { getNotificationApiBaseUrl, getTestMode, } from "./NotificationDebugConfig"; +import { + getNotificationApiHeaders, + httpAuthErrorMessage, + logNotificationAuthFailure, + logNotificationRequestAuthenticated, +} from "./notificationApiAuth"; import { logTokenRegistrationFailure, logTokenRegistrationStarted, @@ -36,9 +42,16 @@ export async function registerToken(fcmToken: string): Promise { const deviceId = await getOrCreateDeviceId(); const baseUrl = getNotificationApiBaseUrl(); try { + const auth = await getNotificationApiHeaders(); + if (!auth.ok) { + logNotificationAuthFailure("register", auth.message); + throw new Error(`registerToken auth unavailable: ${auth.message}`); + } + logNotificationRequestAuthenticated("register"); + const res = await fetch(`${baseUrl}/notifications/register`, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: auth.headers, body: JSON.stringify({ deviceId, fcmToken, @@ -47,11 +60,16 @@ export async function registerToken(fcmToken: string): Promise { }), }); if (!res.ok) { + const authDetail = + res.status === 401 || res.status === 403 + ? httpAuthErrorMessage(res.status) + : `HTTP ${res.status}`; logger.warn("[NotificationService] registerToken failed", { status: res.status, statusText: res.statusText, + authDetail, }); - throw new Error(`registerToken failed: HTTP ${res.status}`); + throw new Error(`registerToken failed: ${authDetail}`); } logTokenRegistrationSuccess(fcmToken); } catch (err) { diff --git a/src/services/notifications/notificationApiAuth.ts b/src/services/notifications/notificationApiAuth.ts new file mode 100644 index 00000000..0a061cce --- /dev/null +++ b/src/services/notifications/notificationApiAuth.ts @@ -0,0 +1,104 @@ +/** + * Authenticated headers for notification backend API calls (`/notifications/*`). + * Uses the same `getHeaders` + active DID flow as the rest of the app. + */ + +import { getHeaders } from "@/libs/endorserServer"; +import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; +import { logger } from "@/utils/logger"; +import { logNotification } from "./NotificationDebugEvents"; + +export type NotificationRequestKind = "register" | "refresh"; + +export type NotificationApiHeadersResult = + | { ok: true; headers: { "Content-Type": string; Authorization: string } } + | { + ok: false; + reason: "no_active_did" | "missing_token"; + message: string; + }; + +async function resolveActiveDid(): Promise { + try { + const service = PlatformServiceFactory.getInstance(); + const row = await service.dbGetOneRow( + "SELECT activeDid FROM active_identity WHERE id = 1", + ); + if (!row?.[0]) { + return null; + } + const did = String(row[0]).trim(); + return did || null; + } catch (err) { + logger.warn("[notificationApiAuth] Failed to read active DID", err); + return null; + } +} + +function hasBearerToken(headers: { + Authorization?: string; +}): headers is { Authorization: string } { + const auth = headers.Authorization; + return ( + typeof auth === "string" && + auth.startsWith("Bearer ") && + auth.length > "Bearer ".length + ); +} + +/** Resolve `Authorization: Bearer …` headers for notification API requests. */ +export async function getNotificationApiHeaders(): Promise { + const did = await resolveActiveDid(); + if (!did) { + return { + ok: false, + reason: "no_active_did", + message: "no active identity (cannot authenticate)", + }; + } + + const headers = await getHeaders(did); + if (!hasBearerToken(headers)) { + return { + ok: false, + reason: "missing_token", + message: "missing or empty Authorization token", + }; + } + + return { + ok: true, + headers: { + "Content-Type": headers["Content-Type"], + Authorization: headers.Authorization, + }, + }; +} + +export function logNotificationRequestAuthenticated( + kind: NotificationRequestKind, +): void { + logNotification( + kind === "register" + ? "Register request authenticated" + : "Refresh request authenticated", + ); +} + +export function logNotificationAuthFailure( + kind: NotificationRequestKind, + message: string, +): void { + const verb = kind === "register" ? "Register" : "Refresh"; + logNotification(`${verb} auth unavailable: ${message}`); +} + +export function httpAuthErrorMessage(status: number): string { + if (status === 401) { + return "unauthorized (expired or invalid auth)"; + } + if (status === 403) { + return "forbidden (not authorized)"; + } + return `HTTP ${status}`; +}