Skip to content

Commit 3d61412

Browse files
authored
Fix/bad notif token + system push notifs (#925)
* fix: push notification thing * fix: push notification token cycling * fix: system message title * chore: add debug logs * fix: remove bad error messges which can mess up token states
1 parent 7ce20f5 commit 3d61412

4 files changed

Lines changed: 154 additions & 29 deletions

File tree

infrastructure/evault-core/src/controllers/NotificationController.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ export class NotificationController {
1111
constructor() {
1212
this.notificationService = new NotificationService(
1313
AppDataSource.getRepository("Verification"),
14-
AppDataSource.getRepository("Notification")
14+
AppDataSource.getRepository("Notification"),
15+
AppDataSource.getRepository(DeviceToken),
1516
);
1617
this.deviceTokenService = new DeviceTokenService(
1718
AppDataSource.getRepository(DeviceToken)
@@ -76,8 +77,15 @@ export class NotificationController {
7677

7778
const token = typeof pushToken === "string" ? pushToken.trim() : undefined;
7879

80+
// Look up old tokens for this device so we can clean them from DeviceToken table
7981
if (token) {
80-
await this.deviceTokenService.register(eName, token);
82+
const existing = await AppDataSource.getRepository("Verification").findOne({
83+
where: { linkedEName: eName, deviceId },
84+
}) as { pushTokens?: string[] } | null;
85+
const oldTokens = (existing?.pushTokens ?? []).filter((t) => t !== token);
86+
for (const stale of oldTokens) {
87+
await this.deviceTokenService.unregister(eName, stale);
88+
}
8189
}
8290

8391
const verification = await this.notificationService.registerDevice({
@@ -88,6 +96,10 @@ export class NotificationController {
8896
registrationTime: new Date(),
8997
});
9098

99+
if (token) {
100+
await this.deviceTokenService.register(eName, token);
101+
}
102+
91103
res.json({
92104
success: true,
93105
message: "Device registered successfully",

infrastructure/evault-core/src/core/protocol/graphql-server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { exampleQueries } from "./examples/examples";
1313
import { typeDefs } from "./typedefs";
1414
import { VaultAccessGuard, type VaultContext } from "./vault-access-guard";
1515
import { MessageNotificationService } from "../../services/MessageNotificationService";
16+
import { DeviceToken } from "../../entities/DeviceToken";
1617
import { AppDataSource } from "../../config/database";
1718

1819
export class GraphQLServer {
@@ -54,6 +55,7 @@ export class GraphQLServer {
5455
AppDataSource.getRepository("Verification"),
5556
AppDataSource.getRepository("Notification"),
5657
this.db,
58+
AppDataSource.getRepository(DeviceToken),
5759
);
5860
}
5961
return this.messageNotificationService;

infrastructure/evault-core/src/services/MessageNotificationService.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Repository } from "typeorm";
22
import { Notification } from "../entities/Notification";
33
import { Verification } from "../entities/Verification";
4+
import { DeviceToken } from "../entities/DeviceToken";
45
import { NotificationService } from "./NotificationService";
56
import type { DbService } from "../core/db/db.service";
67
import { deserializeValue } from "../core/db/schema";
@@ -23,11 +24,13 @@ export class MessageNotificationService {
2324
constructor(
2425
verificationRepository: Repository<Verification>,
2526
notificationRepository: Repository<Notification>,
26-
db: DbService
27+
db: DbService,
28+
deviceTokenRepository?: Repository<DeviceToken>,
2729
) {
2830
this.notificationService = new NotificationService(
2931
verificationRepository,
30-
notificationRepository
32+
notificationRepository,
33+
deviceTokenRepository,
3134
);
3235
this.db = db;
3336
}
@@ -74,7 +77,11 @@ export class MessageNotificationService {
7477
const recipients = [...allENames];
7578
if (recipients.length === 0) return;
7679

77-
const messageText = payload.content || payload.text || "";
80+
const rawText: string = payload.content || payload.text || "";
81+
const isSystemMessage = rawText.startsWith("$$system-message$$");
82+
const messageText = isSystemMessage
83+
? rawText.replace("$$system-message$$", "").trim()
84+
: rawText;
7885
const truncatedText =
7986
messageText.length > 100
8087
? messageText.substring(0, 100) + "..."
@@ -90,7 +97,11 @@ export class MessageNotificationService {
9097
let title: string;
9198
let body: string;
9299

93-
if (isDM) {
100+
if (isSystemMessage) {
101+
const groupName = chatData.name || "a chat";
102+
title = `New system message in ${groupName}`;
103+
body = truncatedText || "System update";
104+
} else if (isDM) {
94105
title = `New message from ${senderDisplay}`;
95106
body = truncatedText || "Sent a message";
96107
} else {

infrastructure/evault-core/src/services/NotificationService.ts

Lines changed: 123 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Repository } from "typeorm";
22
import { Verification } from "../entities/Verification";
33
import { Notification } from "../entities/Notification";
4+
import { DeviceToken } from "../entities/DeviceToken";
45

56
export interface DeviceRegistration {
67
eName: string;
@@ -22,10 +23,24 @@ export interface SendNotificationRequest {
2223
sharedSecret: string;
2324
}
2425

26+
const BAD_TOKEN_ERRORS = [
27+
"messaging/registration-token-not-valid",
28+
"messaging/invalid-registration-token",
29+
"BadDeviceToken",
30+
"Unregistered",
31+
"DeviceTokenNotForTopic",
32+
];
33+
34+
function isBadTokenError(error: unknown): boolean {
35+
const msg = error instanceof Error ? error.message : String(error);
36+
return BAD_TOKEN_ERRORS.some((e) => msg.includes(e));
37+
}
38+
2539
export class NotificationService {
2640
constructor(
2741
private verificationRepository: Repository<Verification>,
28-
private notificationRepository: Repository<Notification>
42+
private notificationRepository: Repository<Notification>,
43+
private deviceTokenRepository?: Repository<DeviceToken>,
2944
) {}
3045

3146
async registerDevice(registration: DeviceRegistration): Promise<Verification> {
@@ -56,10 +71,10 @@ export class NotificationService {
5671
if (verification) {
5772
verification.platform = registration.platform;
5873
if (token) {
59-
const existing = verification.pushTokens ?? [];
60-
if (!existing.includes(token)) {
61-
verification.pushTokens = [...existing, token];
62-
}
74+
// Replace all tokens for this device — the latest token from the
75+
// OS is the only valid one. Appending caused stale tokens to
76+
// accumulate and never get cleaned up.
77+
verification.pushTokens = [token];
6378
}
6479
verification.deviceActive = true;
6580
verification.updatedAt = new Date();
@@ -118,6 +133,7 @@ export class NotificationService {
118133

119134
// Send actual push notification via notification-trigger service
120135
const triggerUrl = process.env.NOTIFICATION_TRIGGER_URL || `http://localhost:${process.env.NOTIFICATION_TRIGGER_PORT || 3998}`;
136+
console.log(`[NOTIF] Using trigger URL: ${triggerUrl}`);
121137
const pushPayload = {
122138
title: notification.title,
123139
body: notification.body,
@@ -152,8 +168,13 @@ export class NotificationService {
152168

153169
console.log(`[NOTIF] Sending push to ${allTokens.length} token(s) for eName: ${eName}`);
154170

155-
const pushResults = await Promise.allSettled(
156-
allTokens.map(async ({ token, platform }) => {
171+
// Cycle through tokens sequentially: try each one, remove bad tokens
172+
// inline, and keep going until at least one succeeds.
173+
const badTokens: string[] = [];
174+
let delivered = false;
175+
176+
for (const { token, platform } of allTokens) {
177+
try {
157178
const res = await fetch(`${triggerUrl}/api/send`, {
158179
method: "POST",
159180
headers: { "Content-Type": "application/json" },
@@ -165,27 +186,106 @@ export class NotificationService {
165186
signal: AbortSignal.timeout(10000),
166187
});
167188
const data = await res.json();
168-
if (!data.success) {
169-
throw new Error(data.error || "Push send failed");
189+
190+
if (data.success) {
191+
console.log(`[NOTIF] Push delivered via token ${token.slice(0, 8)}… for ${eName}`);
192+
delivered = true;
193+
// Keep sending to remaining tokens — user may have multiple
194+
// devices (phone + tablet) that should all receive the notif.
195+
continue;
170196
}
171-
return data;
172-
})
173-
);
174-
175-
const pushSucceeded = pushResults.filter(r => r.status === "fulfilled").length;
176-
const pushFailed = pushResults.filter(r => r.status === "rejected").length;
177-
if (pushFailed > 0) {
178-
console.log(`[NOTIF] Push results for ${eName}: ${pushSucceeded} sent, ${pushFailed} failed`);
179-
pushResults.forEach((r, i) => {
180-
if (r.status === "rejected") {
181-
console.error(`[NOTIF] Push failed for token index ${i}:`, r.reason);
197+
198+
// Send returned an explicit failure
199+
const error = data.error || "Push send failed";
200+
console.error(
201+
`[NOTIF] Push rejected for token ${token.slice(0, 8)}…\n` +
202+
` platform : ${platform ?? "auto-detect"}\n` +
203+
` error : ${error}`,
204+
);
205+
206+
if (isBadTokenError(error)) {
207+
badTokens.push(token);
208+
console.log(`[NOTIF] Bad token ${token.slice(0, 8)}… queued for removal, trying next…`);
182209
}
183-
});
210+
} catch (err) {
211+
const msg = err instanceof Error ? err.message : String(err);
212+
const errObj = err as Record<string, unknown>;
213+
const rawCause = errObj?.cause;
214+
const cause = rawCause
215+
? rawCause instanceof Error ? rawCause.message : String(rawCause)
216+
: null;
217+
console.error(
218+
`[NOTIF] Push error for token ${token.slice(0, 8)}…\n` +
219+
` platform : ${platform ?? "auto-detect"}\n` +
220+
` url : ${triggerUrl}/api/send\n` +
221+
` error : ${msg}\n` +
222+
(cause ? ` cause : ${cause}\n` : "") +
223+
` full :`, err,
224+
);
225+
226+
if (msg.includes("fetch failed") || msg.includes("ECONNREFUSED")) {
227+
console.error(`[NOTIF] notification-trigger service appears to be DOWN at ${triggerUrl} — skipping remaining tokens`);
228+
break;
229+
}
230+
231+
if (isBadTokenError(err)) {
232+
badTokens.push(token);
233+
console.log(`[NOTIF] Bad token ${token.slice(0, 8)}… queued for removal, trying next…`);
234+
}
235+
}
236+
}
237+
238+
// Purge bad tokens from both Verification and DeviceToken tables
239+
if (badTokens.length > 0) {
240+
console.log(`[NOTIF] Removing ${badTokens.length} bad token(s) for ${eName}`);
241+
await this.removeBadTokens(eName, badTokens);
242+
}
243+
244+
if (delivered) {
245+
console.log(`[NOTIF] Push delivered for ${eName}`);
184246
} else {
185-
console.log(`[NOTIF] Push sent successfully to ${pushSucceeded} token(s) for ${eName}`);
247+
console.log(`[NOTIF] Push failed for all ${allTokens.length} token(s) for ${eName}`);
186248
}
187249

188-
return pushSucceeded > 0 || pushFailed === 0;
250+
return delivered;
251+
}
252+
253+
private async removeBadTokens(eName: string, badTokens: string[]): Promise<void> {
254+
try {
255+
// Clean Verification table
256+
const verifications = await this.verificationRepository.find({
257+
where: { linkedEName: eName },
258+
});
259+
for (const v of verifications) {
260+
const before = v.pushTokens?.length ?? 0;
261+
v.pushTokens = (v.pushTokens ?? []).filter((t) => !badTokens.includes(t));
262+
if (v.pushTokens.length !== before) {
263+
v.updatedAt = new Date();
264+
await this.verificationRepository.save(v);
265+
}
266+
}
267+
268+
// Clean DeviceToken table
269+
if (this.deviceTokenRepository) {
270+
const normalized = eName.startsWith("@") ? eName : `@${eName}`;
271+
const withoutAt = eName.replace(/^@/, "");
272+
const rows = await this.deviceTokenRepository
273+
.createQueryBuilder("dt")
274+
.where("dt.eName = :e1 OR dt.eName = :e2", { e1: normalized, e2: withoutAt })
275+
.getMany();
276+
277+
for (const row of rows) {
278+
const before = row.tokens.length;
279+
row.tokens = row.tokens.filter((t) => !badTokens.includes(t));
280+
if (row.tokens.length !== before) {
281+
row.updatedAt = new Date();
282+
await this.deviceTokenRepository.save(row);
283+
}
284+
}
285+
}
286+
} catch (err) {
287+
console.error(`[NOTIF] Failed to remove bad tokens for ${eName}:`, err);
288+
}
189289
}
190290

191291
async getUndeliveredNotifications(eName: string): Promise<Notification[]> {

0 commit comments

Comments
 (0)