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