WIP: new-activity notification #231

Draft
trentlarson wants to merge 80 commits from notify-api into master
3 changed files with 148 additions and 4 deletions
Showing only changes of commit 8cd8727a84 - Show all commits

View File

@@ -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 {

View File

@@ -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) {

View 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}`;
}