@rpcbase/server 0.594.0 → 0.596.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/convertHeifToWebp-BT1n72DQ.js +148 -0
- package/dist/convertHeifToWebp-BT1n72DQ.js.map +1 -0
- package/dist/email-Co1GNjlT.js +7204 -0
- package/dist/email-Co1GNjlT.js.map +1 -0
- package/dist/handler-BqlcQ9sE.js +245 -0
- package/dist/handler-BqlcQ9sE.js.map +1 -0
- package/dist/handler-C92_gJkX.js +743 -0
- package/dist/handler-C92_gJkX.js.map +1 -0
- package/dist/handler-CapJTGzc.js +795 -0
- package/dist/handler-CapJTGzc.js.map +1 -0
- package/dist/handler-Fk-5fOSm.js +119 -0
- package/dist/handler-Fk-5fOSm.js.map +1 -0
- package/dist/htmlTemplate.d.ts +16 -0
- package/dist/htmlTemplate.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5140 -5286
- package/dist/index.js.map +1 -1
- package/dist/notifications.js +273 -382
- package/dist/notifications.js.map +1 -1
- package/dist/postProcessors-iffHvje4.js +89 -0
- package/dist/postProcessors-iffHvje4.js.map +1 -0
- package/dist/queryWindow-DP8zD0lb.js +555 -0
- package/dist/queryWindow-DP8zD0lb.js.map +1 -0
- package/dist/render_resend-B1SSQ4f7.js +7 -0
- package/dist/render_resend-B1SSQ4f7.js.map +1 -0
- package/dist/rts/index.js +990 -1207
- package/dist/rts/index.js.map +1 -1
- package/dist/schemas-B2fUvgYo.js +4151 -0
- package/dist/schemas-B2fUvgYo.js.map +1 -0
- package/dist/shared-DfrVDSp0.js +79 -0
- package/dist/shared-DfrVDSp0.js.map +1 -0
- package/dist/ssrMiddleware.d.ts +7 -3
- package/dist/ssrMiddleware.d.ts.map +1 -1
- package/dist/uploads/worker.js +139 -168
- package/dist/uploads/worker.js.map +1 -1
- package/dist/uploads.js +12 -22
- package/dist/uploads.js.map +1 -1
- package/package.json +1 -1
- package/dist/convertHeifToWebp-C-DGXZ2k.js +0 -169
- package/dist/convertHeifToWebp-C-DGXZ2k.js.map +0 -1
- package/dist/email-BCf24GmK.js +0 -8071
- package/dist/email-BCf24GmK.js.map +0 -1
- package/dist/handler-BqoKvylN.js +0 -147
- package/dist/handler-BqoKvylN.js.map +0 -1
- package/dist/handler-CUOJ51-w.js +0 -903
- package/dist/handler-CUOJ51-w.js.map +0 -1
- package/dist/handler-D-XdgeG_.js +0 -986
- package/dist/handler-D-XdgeG_.js.map +0 -1
- package/dist/handler-F0gFTzvh.js +0 -275
- package/dist/handler-F0gFTzvh.js.map +0 -1
- package/dist/postProcessors-D27fGZP0.js +0 -107
- package/dist/postProcessors-D27fGZP0.js.map +0 -1
- package/dist/queryWindow-Cdr7K-S1.js +0 -714
- package/dist/queryWindow-Cdr7K-S1.js.map +0 -1
- package/dist/render_resend_false-MiC__Smr.js +0 -6
- package/dist/render_resend_false-MiC__Smr.js.map +0 -1
- package/dist/schemas-Drf83ni9.js +0 -4517
- package/dist/schemas-Drf83ni9.js.map +0 -1
- package/dist/shared-CJrm9Wjp.js +0 -104
- package/dist/shared-CJrm9Wjp.js.map +0 -1
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
import { createNotification, sendNotificationsDigestForUser } from "./notifications.js";
|
|
2
|
+
import { a as object, c as unknown, i as number, n as array, o as record, r as boolean, s as string, t as _enum } from "./schemas-B2fUvgYo.js";
|
|
3
|
+
import { models } from "@rpcbase/db";
|
|
4
|
+
import { buildAbilityFromSession, getAccessibleByQuery } from "@rpcbase/db/acl";
|
|
5
|
+
//#region src/notifications/api/notifications/shared.ts
|
|
6
|
+
var getSessionUser = (ctx) => {
|
|
7
|
+
const rawSessionUser = ctx.req.session?.user;
|
|
8
|
+
const userId = typeof rawSessionUser?.id === "string" ? rawSessionUser.id.trim() : "";
|
|
9
|
+
const tenantId = typeof rawSessionUser?.currentTenantId === "string" ? rawSessionUser.currentTenantId.trim() : "";
|
|
10
|
+
if (!userId) {
|
|
11
|
+
ctx.res.status(401);
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
if (!tenantId) {
|
|
15
|
+
ctx.res.status(400);
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
userId,
|
|
20
|
+
tenantId
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/notifications/api/notifications/index.ts
|
|
25
|
+
var ListRoute = "/api/rb/notifications";
|
|
26
|
+
var CreateRoute = "/api/rb/notifications/create";
|
|
27
|
+
var MarkReadRoute = "/api/rb/notifications/:notificationId/read";
|
|
28
|
+
var MarkReadByUrlRoute = "/api/rb/notifications/mark-read-by-url";
|
|
29
|
+
var ArchiveRoute = "/api/rb/notifications/:notificationId/archive";
|
|
30
|
+
var MarkAllReadRoute = "/api/rb/notifications/mark-all-read";
|
|
31
|
+
var SettingsRoute = "/api/rb/notifications/settings";
|
|
32
|
+
var DigestRunRoute = "/api/rb/notifications/digest/run";
|
|
33
|
+
var listRequestSchema = object({
|
|
34
|
+
includeArchived: boolean().optional(),
|
|
35
|
+
unreadOnly: boolean().optional(),
|
|
36
|
+
limit: number().int().min(1).max(200).optional(),
|
|
37
|
+
markSeen: boolean().optional()
|
|
38
|
+
});
|
|
39
|
+
var notificationDataSchema = record(string(), string());
|
|
40
|
+
var notificationPlatformPayloadSchema = object({
|
|
41
|
+
webpush: record(string(), unknown()).optional(),
|
|
42
|
+
fcmOptions: record(string(), unknown()).optional(),
|
|
43
|
+
android: record(string(), unknown()).optional(),
|
|
44
|
+
apns: record(string(), unknown()).optional()
|
|
45
|
+
}).passthrough();
|
|
46
|
+
var createRequestStringSchema = string().trim();
|
|
47
|
+
var notificationUrlModeSchema = _enum(["navigate", "merge_current_search"]);
|
|
48
|
+
var notificationContentFieldsSchema = object({
|
|
49
|
+
title: string().min(1),
|
|
50
|
+
body: string().optional(),
|
|
51
|
+
image: string().optional()
|
|
52
|
+
});
|
|
53
|
+
var createRequestSchema = object({
|
|
54
|
+
topic: createRequestStringSchema.min(1).optional(),
|
|
55
|
+
tag: createRequestStringSchema.min(1).optional(),
|
|
56
|
+
title: createRequestStringSchema.min(1),
|
|
57
|
+
body: createRequestStringSchema.optional(),
|
|
58
|
+
image: createRequestStringSchema.optional(),
|
|
59
|
+
url: createRequestStringSchema.min(1).optional(),
|
|
60
|
+
urlMode: notificationUrlModeSchema.optional(),
|
|
61
|
+
metadata: record(string(), unknown()).optional(),
|
|
62
|
+
data: notificationDataSchema.optional(),
|
|
63
|
+
platform: notificationPlatformPayloadSchema.optional()
|
|
64
|
+
});
|
|
65
|
+
var normalizeNotificationActionUrl = (url, origin) => {
|
|
66
|
+
const raw = url.trim();
|
|
67
|
+
if (!raw) return null;
|
|
68
|
+
try {
|
|
69
|
+
const base = new URL(origin);
|
|
70
|
+
const parsed = new URL(raw, base);
|
|
71
|
+
if (parsed.origin !== base.origin) return null;
|
|
72
|
+
return `${parsed.pathname}${parsed.search}`;
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
var normalizeNotificationActionLink = (url, origin, urlMode = "navigate") => {
|
|
78
|
+
const raw = url.trim();
|
|
79
|
+
if (!raw) return null;
|
|
80
|
+
try {
|
|
81
|
+
const base = new URL(origin);
|
|
82
|
+
const parsed = new URL(raw, base);
|
|
83
|
+
if (parsed.origin !== base.origin) return null;
|
|
84
|
+
if (urlMode === "navigate") return `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
85
|
+
return `${parsed.search}${parsed.hash}` || null;
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
var notificationSchema = object({
|
|
91
|
+
id: string(),
|
|
92
|
+
topic: string().optional(),
|
|
93
|
+
tag: string().optional(),
|
|
94
|
+
deliveryId: string().optional(),
|
|
95
|
+
title: notificationContentFieldsSchema.shape.title,
|
|
96
|
+
body: notificationContentFieldsSchema.shape.body,
|
|
97
|
+
image: notificationContentFieldsSchema.shape.image,
|
|
98
|
+
data: notificationDataSchema.optional(),
|
|
99
|
+
platform: notificationPlatformPayloadSchema.optional(),
|
|
100
|
+
createdAt: string(),
|
|
101
|
+
seenAt: string().optional(),
|
|
102
|
+
readAt: string().optional(),
|
|
103
|
+
archivedAt: string().optional()
|
|
104
|
+
});
|
|
105
|
+
var listResponseSchema = object({
|
|
106
|
+
ok: boolean(),
|
|
107
|
+
error: string().optional(),
|
|
108
|
+
notifications: array(notificationSchema).optional(),
|
|
109
|
+
unreadCount: number().int().min(0).optional(),
|
|
110
|
+
unseenCount: number().int().min(0).optional()
|
|
111
|
+
});
|
|
112
|
+
var createResponseSchema = object({
|
|
113
|
+
ok: boolean(),
|
|
114
|
+
error: string().optional(),
|
|
115
|
+
id: string().optional()
|
|
116
|
+
});
|
|
117
|
+
object({
|
|
118
|
+
ok: boolean(),
|
|
119
|
+
error: string().optional()
|
|
120
|
+
});
|
|
121
|
+
var markReadByUrlRequestSchema = object({ url: string().trim().min(1) });
|
|
122
|
+
var markReadByUrlResponseSchema = object({
|
|
123
|
+
ok: boolean(),
|
|
124
|
+
error: string().optional(),
|
|
125
|
+
matchedCount: number().int().min(0).optional(),
|
|
126
|
+
modifiedCount: number().int().min(0).optional()
|
|
127
|
+
});
|
|
128
|
+
object({
|
|
129
|
+
ok: boolean(),
|
|
130
|
+
error: string().optional()
|
|
131
|
+
});
|
|
132
|
+
object({
|
|
133
|
+
ok: boolean(),
|
|
134
|
+
error: string().optional()
|
|
135
|
+
});
|
|
136
|
+
var digestFrequencySchema = _enum([
|
|
137
|
+
"off",
|
|
138
|
+
"daily",
|
|
139
|
+
"weekly"
|
|
140
|
+
]);
|
|
141
|
+
var topicPreferenceSchema = object({
|
|
142
|
+
topic: string(),
|
|
143
|
+
inApp: boolean(),
|
|
144
|
+
emailDigest: boolean(),
|
|
145
|
+
push: boolean()
|
|
146
|
+
});
|
|
147
|
+
var settingsSchema = object({
|
|
148
|
+
digestFrequency: digestFrequencySchema,
|
|
149
|
+
topicPreferences: array(topicPreferenceSchema),
|
|
150
|
+
lastDigestSentAt: string().optional()
|
|
151
|
+
});
|
|
152
|
+
var settingsResponseSchema = object({
|
|
153
|
+
ok: boolean(),
|
|
154
|
+
error: string().optional(),
|
|
155
|
+
settings: settingsSchema.optional()
|
|
156
|
+
});
|
|
157
|
+
var updateSettingsRequestSchema = object({
|
|
158
|
+
digestFrequency: digestFrequencySchema.optional(),
|
|
159
|
+
topicPreferences: array(topicPreferenceSchema).optional()
|
|
160
|
+
});
|
|
161
|
+
var updateSettingsResponseSchema = object({
|
|
162
|
+
ok: boolean(),
|
|
163
|
+
error: string().optional(),
|
|
164
|
+
settings: settingsSchema.optional()
|
|
165
|
+
});
|
|
166
|
+
var digestRunRequestSchema = object({ force: boolean().optional() });
|
|
167
|
+
object({
|
|
168
|
+
ok: boolean(),
|
|
169
|
+
error: string().optional(),
|
|
170
|
+
sent: boolean().optional(),
|
|
171
|
+
skippedReason: string().optional()
|
|
172
|
+
});
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/notifications/api/notifications/handler.ts
|
|
175
|
+
var toIso = (value) => value instanceof Date ? value.toISOString() : void 0;
|
|
176
|
+
var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
177
|
+
var toStringMap = (value) => {
|
|
178
|
+
if (!isRecord(value)) return void 0;
|
|
179
|
+
const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string");
|
|
180
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
181
|
+
};
|
|
182
|
+
var getForwardedHeaderValue = (value) => {
|
|
183
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
184
|
+
if (typeof raw !== "string") return void 0;
|
|
185
|
+
return raw.split(",")[0]?.trim() || void 0;
|
|
186
|
+
};
|
|
187
|
+
var getRequestOrigin = (ctx) => {
|
|
188
|
+
const protocol = getForwardedHeaderValue(ctx.req.headers["x-forwarded-proto"]) ?? ctx.req.protocol;
|
|
189
|
+
const host = getForwardedHeaderValue(ctx.req.headers["x-forwarded-host"]) ?? ctx.req.get("host");
|
|
190
|
+
return protocol && host ? `${protocol}://${host}` : "";
|
|
191
|
+
};
|
|
192
|
+
var getNotificationActionLinks = (notification) => {
|
|
193
|
+
const links = [];
|
|
194
|
+
if (typeof notification.data?.link === "string") links.push({
|
|
195
|
+
link: notification.data.link,
|
|
196
|
+
urlMode: notification.data.linkMode === "merge_current_search" ? "merge_current_search" : "navigate"
|
|
197
|
+
});
|
|
198
|
+
const platformWebpush = notification.platform?.webpush;
|
|
199
|
+
if (platformWebpush && typeof platformWebpush === "object" && !Array.isArray(platformWebpush)) {
|
|
200
|
+
const fcmOptions = platformWebpush.fcmOptions;
|
|
201
|
+
if (fcmOptions && typeof fcmOptions === "object" && !Array.isArray(fcmOptions)) {
|
|
202
|
+
const link = fcmOptions.link;
|
|
203
|
+
if (typeof link === "string") links.push({
|
|
204
|
+
link,
|
|
205
|
+
urlMode: "navigate"
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return links;
|
|
210
|
+
};
|
|
211
|
+
var actionSearchParamsMatchTargetUrl = (actionUrl, targetUrl, origin) => {
|
|
212
|
+
try {
|
|
213
|
+
const base = new URL(origin);
|
|
214
|
+
const action = new URL(actionUrl, base);
|
|
215
|
+
const target = new URL(targetUrl, base);
|
|
216
|
+
for (const [key, value] of action.searchParams) if (target.searchParams.get(key) !== value) return false;
|
|
217
|
+
return Array.from(action.searchParams.keys()).length > 0;
|
|
218
|
+
} catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
var notificationActionLinkMatchesUrl = (action, targetUrl, origin) => {
|
|
223
|
+
if (action.urlMode === "merge_current_search") return actionSearchParamsMatchTargetUrl(action.link, targetUrl, origin);
|
|
224
|
+
return normalizeNotificationActionUrl(action.link, origin) === targetUrl;
|
|
225
|
+
};
|
|
226
|
+
var buildDisabledTopics = (settings, key) => {
|
|
227
|
+
const raw = settings?.topicPreferences;
|
|
228
|
+
if (!Array.isArray(raw) || raw.length === 0) return [];
|
|
229
|
+
return raw.map((pref) => {
|
|
230
|
+
if (!pref || typeof pref !== "object") return null;
|
|
231
|
+
const topic = typeof pref.topic === "string" ? pref.topic.trim() : "";
|
|
232
|
+
if (!topic) return null;
|
|
233
|
+
return pref[key] === true ? null : topic;
|
|
234
|
+
}).filter((topic) => Boolean(topic));
|
|
235
|
+
};
|
|
236
|
+
var listNotifications = async (payload, ctx) => {
|
|
237
|
+
const session = getSessionUser(ctx);
|
|
238
|
+
if (!session) return {
|
|
239
|
+
ok: false,
|
|
240
|
+
error: "unauthorized"
|
|
241
|
+
};
|
|
242
|
+
const ability = buildAbilityFromSession({
|
|
243
|
+
tenantId: session.tenantId,
|
|
244
|
+
session: ctx.req.session
|
|
245
|
+
});
|
|
246
|
+
if (!ability.can("read", "RBNotification")) {
|
|
247
|
+
ctx.res.status(403);
|
|
248
|
+
return {
|
|
249
|
+
ok: false,
|
|
250
|
+
error: "forbidden"
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const parsed = listRequestSchema.safeParse(payload);
|
|
254
|
+
if (!parsed.success) {
|
|
255
|
+
ctx.res.status(400);
|
|
256
|
+
return {
|
|
257
|
+
ok: false,
|
|
258
|
+
error: "invalid_payload"
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const { userId } = session;
|
|
262
|
+
const includeArchived = parsed.data.includeArchived === true;
|
|
263
|
+
const unreadOnly = parsed.data.unreadOnly === true;
|
|
264
|
+
const limit = parsed.data.limit ?? 50;
|
|
265
|
+
const markSeen = parsed.data.markSeen === true;
|
|
266
|
+
const disabledTopics = buildDisabledTopics(await (await models.get("RBNotificationSettings", {
|
|
267
|
+
req: ctx.req,
|
|
268
|
+
ability
|
|
269
|
+
})).findOne({ userId }).lean(), "inApp");
|
|
270
|
+
const NotificationModel = await models.get("RBNotification", {
|
|
271
|
+
req: ctx.req,
|
|
272
|
+
ability
|
|
273
|
+
});
|
|
274
|
+
const queryFilters = [{ userId }, getAccessibleByQuery(ability, "read", "RBNotification")];
|
|
275
|
+
if (!includeArchived) queryFilters.push({ archivedAt: { $exists: false } });
|
|
276
|
+
if (unreadOnly) queryFilters.push({ readAt: { $exists: false } });
|
|
277
|
+
if (disabledTopics.length > 0) queryFilters.push({ topic: { $nin: disabledTopics } });
|
|
278
|
+
const query = { $and: queryFilters };
|
|
279
|
+
const notifications = await NotificationModel.find(query).sort({ createdAt: -1 }).limit(limit).lean();
|
|
280
|
+
const unseenQueryFilters = [
|
|
281
|
+
{ userId },
|
|
282
|
+
{ archivedAt: { $exists: false } },
|
|
283
|
+
{ seenAt: { $exists: false } },
|
|
284
|
+
getAccessibleByQuery(ability, "read", "RBNotification")
|
|
285
|
+
];
|
|
286
|
+
if (disabledTopics.length > 0) unseenQueryFilters.push({ topic: { $nin: disabledTopics } });
|
|
287
|
+
const unseenQuery = { $and: unseenQueryFilters };
|
|
288
|
+
const unreadQueryFilters = [
|
|
289
|
+
{ userId },
|
|
290
|
+
{ archivedAt: { $exists: false } },
|
|
291
|
+
{ readAt: { $exists: false } },
|
|
292
|
+
getAccessibleByQuery(ability, "read", "RBNotification")
|
|
293
|
+
];
|
|
294
|
+
if (disabledTopics.length > 0) unreadQueryFilters.push({ topic: { $nin: disabledTopics } });
|
|
295
|
+
const unreadQuery = { $and: unreadQueryFilters };
|
|
296
|
+
const [unreadCount, unseenCount] = await Promise.all([NotificationModel.countDocuments(unreadQuery), NotificationModel.countDocuments(unseenQuery)]);
|
|
297
|
+
const now = markSeen ? /* @__PURE__ */ new Date() : null;
|
|
298
|
+
if (now && unseenCount > 0) await NotificationModel.updateMany(unseenQuery, { $set: { seenAt: now } });
|
|
299
|
+
return listResponseSchema.parse({
|
|
300
|
+
ok: true,
|
|
301
|
+
notifications: notifications.map((n) => ({
|
|
302
|
+
id: String(n._id),
|
|
303
|
+
topic: typeof n.topic === "string" ? n.topic : void 0,
|
|
304
|
+
tag: typeof n.tag === "string" ? n.tag : void 0,
|
|
305
|
+
deliveryId: typeof n.deliveryId === "string" ? n.deliveryId : void 0,
|
|
306
|
+
title: typeof n.title === "string" ? n.title : "",
|
|
307
|
+
body: typeof n.body === "string" ? n.body : void 0,
|
|
308
|
+
image: typeof n.image === "string" ? n.image : void 0,
|
|
309
|
+
data: toStringMap(n.data),
|
|
310
|
+
platform: isRecord(n.platform) ? n.platform : void 0,
|
|
311
|
+
createdAt: toIso(n.createdAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
312
|
+
seenAt: toIso(n.seenAt) ?? (now && !n.archivedAt && !n.seenAt ? now.toISOString() : void 0),
|
|
313
|
+
readAt: toIso(n.readAt),
|
|
314
|
+
archivedAt: toIso(n.archivedAt)
|
|
315
|
+
})),
|
|
316
|
+
unreadCount,
|
|
317
|
+
unseenCount: now ? 0 : unseenCount
|
|
318
|
+
});
|
|
319
|
+
};
|
|
320
|
+
var createNotificationForCurrentUser = async (payload, ctx) => {
|
|
321
|
+
const session = getSessionUser(ctx);
|
|
322
|
+
if (!session) return {
|
|
323
|
+
ok: false,
|
|
324
|
+
error: "unauthorized"
|
|
325
|
+
};
|
|
326
|
+
const ability = buildAbilityFromSession({
|
|
327
|
+
tenantId: session.tenantId,
|
|
328
|
+
session: ctx.req.session
|
|
329
|
+
});
|
|
330
|
+
if (!ability.can("create", "RBNotification")) {
|
|
331
|
+
ctx.res.status(403);
|
|
332
|
+
return {
|
|
333
|
+
ok: false,
|
|
334
|
+
error: "forbidden"
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const parsed = createRequestSchema.safeParse(payload);
|
|
338
|
+
if (!parsed.success) {
|
|
339
|
+
ctx.res.status(400);
|
|
340
|
+
return {
|
|
341
|
+
ok: false,
|
|
342
|
+
error: "invalid_payload"
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const origin = getRequestOrigin(ctx);
|
|
346
|
+
const actionLink = parsed.data.url ? normalizeNotificationActionLink(parsed.data.url, origin, parsed.data.urlMode) ?? void 0 : void 0;
|
|
347
|
+
if (parsed.data.url && !actionLink) {
|
|
348
|
+
ctx.res.status(400);
|
|
349
|
+
return {
|
|
350
|
+
ok: false,
|
|
351
|
+
error: "invalid_payload"
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
const created = await createNotification(ctx, {
|
|
355
|
+
userId: session.userId,
|
|
356
|
+
topic: parsed.data.topic,
|
|
357
|
+
tag: parsed.data.tag,
|
|
358
|
+
title: parsed.data.title,
|
|
359
|
+
body: parsed.data.body,
|
|
360
|
+
image: parsed.data.image,
|
|
361
|
+
url: actionLink,
|
|
362
|
+
urlMode: parsed.data.urlMode,
|
|
363
|
+
metadata: parsed.data.metadata,
|
|
364
|
+
data: parsed.data.data,
|
|
365
|
+
platform: parsed.data.platform
|
|
366
|
+
}, ability);
|
|
367
|
+
return createResponseSchema.parse({
|
|
368
|
+
ok: true,
|
|
369
|
+
id: created.id
|
|
370
|
+
});
|
|
371
|
+
};
|
|
372
|
+
var markRead = async (_payload, ctx) => {
|
|
373
|
+
const session = getSessionUser(ctx);
|
|
374
|
+
if (!session) return {
|
|
375
|
+
ok: false,
|
|
376
|
+
error: "unauthorized"
|
|
377
|
+
};
|
|
378
|
+
const ability = buildAbilityFromSession({
|
|
379
|
+
tenantId: session.tenantId,
|
|
380
|
+
session: ctx.req.session
|
|
381
|
+
});
|
|
382
|
+
if (!ability.can("update", "RBNotification")) {
|
|
383
|
+
ctx.res.status(403);
|
|
384
|
+
return {
|
|
385
|
+
ok: false,
|
|
386
|
+
error: "forbidden"
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
const notificationId = typeof ctx.req.params.notificationId === "string" ? ctx.req.params.notificationId.trim() : "";
|
|
390
|
+
if (!notificationId) {
|
|
391
|
+
ctx.res.status(400);
|
|
392
|
+
return {
|
|
393
|
+
ok: false,
|
|
394
|
+
error: "missing_notification_id"
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
const NotificationModel = await models.get("RBNotification", {
|
|
398
|
+
req: ctx.req,
|
|
399
|
+
ability
|
|
400
|
+
});
|
|
401
|
+
const now = /* @__PURE__ */ new Date();
|
|
402
|
+
try {
|
|
403
|
+
await NotificationModel.updateOne({ $and: [
|
|
404
|
+
{ _id: notificationId },
|
|
405
|
+
{ archivedAt: { $exists: false } },
|
|
406
|
+
getAccessibleByQuery(ability, "update", "RBNotification")
|
|
407
|
+
] }, { $set: {
|
|
408
|
+
readAt: now,
|
|
409
|
+
seenAt: now
|
|
410
|
+
} });
|
|
411
|
+
} catch {
|
|
412
|
+
ctx.res.status(400);
|
|
413
|
+
return {
|
|
414
|
+
ok: false,
|
|
415
|
+
error: "invalid_notification_id"
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
return { ok: true };
|
|
419
|
+
};
|
|
420
|
+
var markReadByUrl = async (payload, ctx) => {
|
|
421
|
+
const session = getSessionUser(ctx);
|
|
422
|
+
if (!session) return {
|
|
423
|
+
ok: false,
|
|
424
|
+
error: "unauthorized"
|
|
425
|
+
};
|
|
426
|
+
const ability = buildAbilityFromSession({
|
|
427
|
+
tenantId: session.tenantId,
|
|
428
|
+
session: ctx.req.session
|
|
429
|
+
});
|
|
430
|
+
if (!ability.can("update", "RBNotification")) {
|
|
431
|
+
ctx.res.status(403);
|
|
432
|
+
return {
|
|
433
|
+
ok: false,
|
|
434
|
+
error: "forbidden"
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
const parsed = markReadByUrlRequestSchema.safeParse(payload);
|
|
438
|
+
if (!parsed.success) {
|
|
439
|
+
ctx.res.status(400);
|
|
440
|
+
return {
|
|
441
|
+
ok: false,
|
|
442
|
+
error: "invalid_payload"
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
const origin = getRequestOrigin(ctx);
|
|
446
|
+
const targetUrl = normalizeNotificationActionUrl(parsed.data.url, origin);
|
|
447
|
+
if (!targetUrl) {
|
|
448
|
+
ctx.res.status(400);
|
|
449
|
+
return {
|
|
450
|
+
ok: false,
|
|
451
|
+
error: "invalid_payload"
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
const NotificationModel = await models.get("RBNotification", {
|
|
455
|
+
req: ctx.req,
|
|
456
|
+
ability
|
|
457
|
+
});
|
|
458
|
+
const query = { $and: [
|
|
459
|
+
{ userId: session.userId },
|
|
460
|
+
{ archivedAt: { $exists: false } },
|
|
461
|
+
{ readAt: { $exists: false } },
|
|
462
|
+
{ $or: [{ "data.link": { $type: "string" } }, { "platform.webpush.fcmOptions.link": { $type: "string" } }] },
|
|
463
|
+
getAccessibleByQuery(ability, "update", "RBNotification")
|
|
464
|
+
] };
|
|
465
|
+
const matchingIds = (await NotificationModel.find(query).select({
|
|
466
|
+
_id: 1,
|
|
467
|
+
data: 1,
|
|
468
|
+
platform: 1
|
|
469
|
+
}).lean()).filter((notification) => getNotificationActionLinks(notification).some((action) => notificationActionLinkMatchesUrl(action, targetUrl, origin))).map((notification) => notification._id);
|
|
470
|
+
if (matchingIds.length === 0) return markReadByUrlResponseSchema.parse({
|
|
471
|
+
ok: true,
|
|
472
|
+
matchedCount: 0,
|
|
473
|
+
modifiedCount: 0
|
|
474
|
+
});
|
|
475
|
+
const now = /* @__PURE__ */ new Date();
|
|
476
|
+
const result = await NotificationModel.updateMany({ $and: [
|
|
477
|
+
{ _id: { $in: matchingIds } },
|
|
478
|
+
{ userId: session.userId },
|
|
479
|
+
{ archivedAt: { $exists: false } },
|
|
480
|
+
{ readAt: { $exists: false } },
|
|
481
|
+
getAccessibleByQuery(ability, "update", "RBNotification")
|
|
482
|
+
] }, { $set: {
|
|
483
|
+
readAt: now,
|
|
484
|
+
seenAt: now
|
|
485
|
+
} });
|
|
486
|
+
const modifiedCount = typeof result.modifiedCount === "number" ? result.modifiedCount : matchingIds.length;
|
|
487
|
+
return markReadByUrlResponseSchema.parse({
|
|
488
|
+
ok: true,
|
|
489
|
+
matchedCount: matchingIds.length,
|
|
490
|
+
modifiedCount
|
|
491
|
+
});
|
|
492
|
+
};
|
|
493
|
+
var markAllRead = async (_payload, ctx) => {
|
|
494
|
+
const session = getSessionUser(ctx);
|
|
495
|
+
if (!session) return {
|
|
496
|
+
ok: false,
|
|
497
|
+
error: "unauthorized"
|
|
498
|
+
};
|
|
499
|
+
const ability = buildAbilityFromSession({
|
|
500
|
+
tenantId: session.tenantId,
|
|
501
|
+
session: ctx.req.session
|
|
502
|
+
});
|
|
503
|
+
if (!ability.can("update", "RBNotification")) {
|
|
504
|
+
ctx.res.status(403);
|
|
505
|
+
return {
|
|
506
|
+
ok: false,
|
|
507
|
+
error: "forbidden"
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
const disabledTopics = buildDisabledTopics(await (await models.get("RBNotificationSettings", {
|
|
511
|
+
req: ctx.req,
|
|
512
|
+
ability
|
|
513
|
+
})).findOne({ userId: session.userId }).lean(), "inApp");
|
|
514
|
+
const NotificationModel = await models.get("RBNotification", {
|
|
515
|
+
req: ctx.req,
|
|
516
|
+
ability
|
|
517
|
+
});
|
|
518
|
+
const queryFilters = [
|
|
519
|
+
{ userId: session.userId },
|
|
520
|
+
{ archivedAt: { $exists: false } },
|
|
521
|
+
{ readAt: { $exists: false } },
|
|
522
|
+
getAccessibleByQuery(ability, "update", "RBNotification")
|
|
523
|
+
];
|
|
524
|
+
if (disabledTopics.length > 0) queryFilters.push({ topic: { $nin: disabledTopics } });
|
|
525
|
+
const query = { $and: queryFilters };
|
|
526
|
+
const now = /* @__PURE__ */ new Date();
|
|
527
|
+
await NotificationModel.updateMany(query, { $set: {
|
|
528
|
+
readAt: now,
|
|
529
|
+
seenAt: now
|
|
530
|
+
} });
|
|
531
|
+
return { ok: true };
|
|
532
|
+
};
|
|
533
|
+
var archiveNotification = async (_payload, ctx) => {
|
|
534
|
+
const session = getSessionUser(ctx);
|
|
535
|
+
if (!session) return {
|
|
536
|
+
ok: false,
|
|
537
|
+
error: "unauthorized"
|
|
538
|
+
};
|
|
539
|
+
const ability = buildAbilityFromSession({
|
|
540
|
+
tenantId: session.tenantId,
|
|
541
|
+
session: ctx.req.session
|
|
542
|
+
});
|
|
543
|
+
if (!ability.can("update", "RBNotification")) {
|
|
544
|
+
ctx.res.status(403);
|
|
545
|
+
return {
|
|
546
|
+
ok: false,
|
|
547
|
+
error: "forbidden"
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
const notificationId = typeof ctx.req.params.notificationId === "string" ? ctx.req.params.notificationId.trim() : "";
|
|
551
|
+
if (!notificationId) {
|
|
552
|
+
ctx.res.status(400);
|
|
553
|
+
return {
|
|
554
|
+
ok: false,
|
|
555
|
+
error: "missing_notification_id"
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
const NotificationModel = await models.get("RBNotification", {
|
|
559
|
+
req: ctx.req,
|
|
560
|
+
ability
|
|
561
|
+
});
|
|
562
|
+
try {
|
|
563
|
+
await NotificationModel.updateOne({ $and: [
|
|
564
|
+
{ _id: notificationId },
|
|
565
|
+
{ archivedAt: { $exists: false } },
|
|
566
|
+
getAccessibleByQuery(ability, "update", "RBNotification")
|
|
567
|
+
] }, { $set: {
|
|
568
|
+
archivedAt: /* @__PURE__ */ new Date(),
|
|
569
|
+
drawerActive: false
|
|
570
|
+
} });
|
|
571
|
+
} catch {
|
|
572
|
+
ctx.res.status(400);
|
|
573
|
+
return {
|
|
574
|
+
ok: false,
|
|
575
|
+
error: "invalid_notification_id"
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
return { ok: true };
|
|
579
|
+
};
|
|
580
|
+
var getSettings = async (_payload, ctx) => {
|
|
581
|
+
const session = getSessionUser(ctx);
|
|
582
|
+
if (!session) return {
|
|
583
|
+
ok: false,
|
|
584
|
+
error: "unauthorized"
|
|
585
|
+
};
|
|
586
|
+
const ability = buildAbilityFromSession({
|
|
587
|
+
tenantId: session.tenantId,
|
|
588
|
+
session: ctx.req.session
|
|
589
|
+
});
|
|
590
|
+
if (!ability.can("read", "RBNotificationSettings")) {
|
|
591
|
+
ctx.res.status(403);
|
|
592
|
+
return {
|
|
593
|
+
ok: false,
|
|
594
|
+
error: "forbidden"
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
const settings = await (await models.get("RBNotificationSettings", {
|
|
598
|
+
req: ctx.req,
|
|
599
|
+
ability
|
|
600
|
+
})).findOne({ $and: [{ userId: session.userId }, getAccessibleByQuery(ability, "read", "RBNotificationSettings")] }).lean();
|
|
601
|
+
const digestFrequencyRaw = typeof settings?.digestFrequency === "string" ? settings.digestFrequency : "weekly";
|
|
602
|
+
const digestFrequency = digestFrequencyRaw === "off" || digestFrequencyRaw === "daily" || digestFrequencyRaw === "weekly" ? digestFrequencyRaw : "weekly";
|
|
603
|
+
const topicPreferences = Array.isArray(settings?.topicPreferences) ? settings.topicPreferences.map((pref) => ({
|
|
604
|
+
topic: typeof pref.topic === "string" ? pref.topic : "",
|
|
605
|
+
inApp: pref.inApp === true,
|
|
606
|
+
emailDigest: pref.emailDigest === true,
|
|
607
|
+
push: pref.push === true
|
|
608
|
+
})).filter((pref) => pref.topic.length > 0) : [];
|
|
609
|
+
return settingsResponseSchema.parse({
|
|
610
|
+
ok: true,
|
|
611
|
+
settings: {
|
|
612
|
+
digestFrequency,
|
|
613
|
+
topicPreferences,
|
|
614
|
+
lastDigestSentAt: toIso(settings?.lastDigestSentAt)
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
};
|
|
618
|
+
var updateSettings = async (payload, ctx) => {
|
|
619
|
+
const session = getSessionUser(ctx);
|
|
620
|
+
if (!session) return {
|
|
621
|
+
ok: false,
|
|
622
|
+
error: "unauthorized"
|
|
623
|
+
};
|
|
624
|
+
const ability = buildAbilityFromSession({
|
|
625
|
+
tenantId: session.tenantId,
|
|
626
|
+
session: ctx.req.session
|
|
627
|
+
});
|
|
628
|
+
if (!ability.can("update", "RBNotificationSettings")) {
|
|
629
|
+
ctx.res.status(403);
|
|
630
|
+
return {
|
|
631
|
+
ok: false,
|
|
632
|
+
error: "forbidden"
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
const parsed = updateSettingsRequestSchema.safeParse(payload);
|
|
636
|
+
if (!parsed.success) {
|
|
637
|
+
ctx.res.status(400);
|
|
638
|
+
return {
|
|
639
|
+
ok: false,
|
|
640
|
+
error: "invalid_payload"
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
const SettingsModel = await models.get("RBNotificationSettings", {
|
|
644
|
+
req: ctx.req,
|
|
645
|
+
ability
|
|
646
|
+
});
|
|
647
|
+
const nextValues = {};
|
|
648
|
+
if (parsed.data.digestFrequency) nextValues.digestFrequency = parsed.data.digestFrequency;
|
|
649
|
+
if (parsed.data.topicPreferences) {
|
|
650
|
+
const seen = /* @__PURE__ */ new Set();
|
|
651
|
+
nextValues.topicPreferences = parsed.data.topicPreferences.map((pref) => ({
|
|
652
|
+
topic: pref.topic.trim(),
|
|
653
|
+
inApp: pref.inApp,
|
|
654
|
+
emailDigest: pref.emailDigest,
|
|
655
|
+
push: pref.push
|
|
656
|
+
})).filter((pref) => pref.topic.length > 0).filter((pref) => {
|
|
657
|
+
if (seen.has(pref.topic)) return false;
|
|
658
|
+
seen.add(pref.topic);
|
|
659
|
+
return true;
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
const ops = { $setOnInsert: { userId: session.userId } };
|
|
663
|
+
if (Object.keys(nextValues).length > 0) ops.$set = nextValues;
|
|
664
|
+
const settings = await SettingsModel.findOneAndUpdate({ $and: [{ userId: session.userId }, getAccessibleByQuery(ability, "update", "RBNotificationSettings")] }, ops, {
|
|
665
|
+
upsert: true,
|
|
666
|
+
returnDocument: "after",
|
|
667
|
+
setDefaultsOnInsert: true
|
|
668
|
+
}).lean();
|
|
669
|
+
const digestFrequencyRaw = typeof settings?.digestFrequency === "string" ? settings.digestFrequency : "weekly";
|
|
670
|
+
const digestFrequency = digestFrequencyRaw === "off" || digestFrequencyRaw === "daily" || digestFrequencyRaw === "weekly" ? digestFrequencyRaw : "weekly";
|
|
671
|
+
const topicPreferences = Array.isArray(settings?.topicPreferences) ? settings.topicPreferences.map((pref) => ({
|
|
672
|
+
topic: typeof pref.topic === "string" ? pref.topic : "",
|
|
673
|
+
inApp: pref.inApp === true,
|
|
674
|
+
emailDigest: pref.emailDigest === true,
|
|
675
|
+
push: pref.push === true
|
|
676
|
+
})).filter((pref) => pref.topic.length > 0) : [];
|
|
677
|
+
return updateSettingsResponseSchema.parse({
|
|
678
|
+
ok: true,
|
|
679
|
+
settings: {
|
|
680
|
+
digestFrequency,
|
|
681
|
+
topicPreferences,
|
|
682
|
+
lastDigestSentAt: toIso(settings?.lastDigestSentAt)
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
};
|
|
686
|
+
var runDigest = async (payload, ctx) => {
|
|
687
|
+
const session = getSessionUser(ctx);
|
|
688
|
+
if (!session) return {
|
|
689
|
+
ok: false,
|
|
690
|
+
error: "unauthorized"
|
|
691
|
+
};
|
|
692
|
+
const ability = buildAbilityFromSession({
|
|
693
|
+
tenantId: session.tenantId,
|
|
694
|
+
session: ctx.req.session
|
|
695
|
+
});
|
|
696
|
+
if (!ability.can("read", "RBNotification")) {
|
|
697
|
+
ctx.res.status(403);
|
|
698
|
+
return {
|
|
699
|
+
ok: false,
|
|
700
|
+
error: "forbidden"
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
const parsed = digestRunRequestSchema.safeParse(payload);
|
|
704
|
+
if (!parsed.success) {
|
|
705
|
+
ctx.res.status(400);
|
|
706
|
+
return {
|
|
707
|
+
ok: false,
|
|
708
|
+
error: "invalid_payload"
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
const result = await sendNotificationsDigestForUser(ctx, {
|
|
712
|
+
userId: session.userId,
|
|
713
|
+
ability,
|
|
714
|
+
force: parsed.data.force === true
|
|
715
|
+
});
|
|
716
|
+
if (!result.ok) {
|
|
717
|
+
ctx.res.status(500);
|
|
718
|
+
return {
|
|
719
|
+
ok: false,
|
|
720
|
+
error: result.error
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
return {
|
|
724
|
+
ok: true,
|
|
725
|
+
sent: result.sent,
|
|
726
|
+
...result.skippedReason ? { skippedReason: result.skippedReason } : {}
|
|
727
|
+
};
|
|
728
|
+
};
|
|
729
|
+
var handler_default = (api) => {
|
|
730
|
+
api.post(ListRoute, listNotifications);
|
|
731
|
+
api.post(CreateRoute, createNotificationForCurrentUser);
|
|
732
|
+
api.post(MarkReadRoute, markRead);
|
|
733
|
+
api.post(MarkReadByUrlRoute, markReadByUrl);
|
|
734
|
+
api.post(MarkAllReadRoute, markAllRead);
|
|
735
|
+
api.post(ArchiveRoute, archiveNotification);
|
|
736
|
+
api.get(SettingsRoute, getSettings);
|
|
737
|
+
api.put(SettingsRoute, updateSettings);
|
|
738
|
+
api.post(DigestRunRoute, runDigest);
|
|
739
|
+
};
|
|
740
|
+
//#endregion
|
|
741
|
+
export { handler_default as default };
|
|
742
|
+
|
|
743
|
+
//# sourceMappingURL=handler-C92_gJkX.js.map
|