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:
@@ -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}`;
|
||||
}
|
||||
Reference in New Issue
Block a user