feat(notifications): authenticate register and refresh API calls

Use getHeaders(activeDid) for POST /notifications/register and
/notifications/refresh so requests include Authorization: Bearer tokens
like the rest of the app. Add notificationApiAuth helper for shared header
resolution, auth logging, and graceful handling when identity or token
is missing or the server returns 401/403.
This commit is contained in:
Jose Olarte III
2026-05-20 15:45:46 +08:00
parent 8864a2049b
commit 8cd8727a84
3 changed files with 148 additions and 4 deletions
@@ -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 {
@@ -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<void> {
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<void> {
}),
});
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) {
@@ -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<string | null> {
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<NotificationApiHeadersResult> {
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}`;
}