WIP: new-activity notification #231
@@ -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) {
|
||||
|
||||
104
src/services/notifications/notificationApiAuth.ts
Normal file
104
src/services/notifications/notificationApiAuth.ts
Normal file
@@ -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