@valentinkolb/cloud 0.5.2 → 0.5.3
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/package.json
CHANGED
|
@@ -11,10 +11,14 @@ import { z } from "zod";
|
|
|
11
11
|
import { listApps } from "../_internal/registry";
|
|
12
12
|
import { auth, v, type AuthContext } from "../server";
|
|
13
13
|
import { settingsDeleteLegacyKeys, settingsListLegacyKeys } from "../services";
|
|
14
|
+
import { sendEmail } from "../services/notifications/email";
|
|
14
15
|
import * as settings from "../services/settings";
|
|
15
16
|
import { SETTINGS_MAP } from "../services/settings/defaults";
|
|
16
17
|
|
|
17
18
|
const BulkUpdateSchema = z.record(z.string(), z.unknown());
|
|
19
|
+
const TestEmailSchema = z.object({
|
|
20
|
+
recipient: z.email(),
|
|
21
|
+
});
|
|
18
22
|
|
|
19
23
|
type FieldErrors = Record<string, string>;
|
|
20
24
|
|
|
@@ -28,6 +32,24 @@ const app = new Hono<AuthContext>()
|
|
|
28
32
|
.delete("/legacy", auth.requireRole("admin"), async (c) => {
|
|
29
33
|
return c.json(await settingsDeleteLegacyKeys(await liveSettingKeys()));
|
|
30
34
|
})
|
|
35
|
+
.post("/test-email", auth.requireRole("admin"), v("json", TestEmailSchema), async (c) => {
|
|
36
|
+
const { recipient } = c.req.valid("json");
|
|
37
|
+
const sentAt = new Date().toISOString();
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await sendEmail(recipient, "Cloud test email", {
|
|
41
|
+
rawHtml: `
|
|
42
|
+
<p>This is a test email from Cloud.</p>
|
|
43
|
+
<p>If you received this message, SMTP delivery is configured correctly.</p>
|
|
44
|
+
<p style="margin-top:24px;color:#71717a;font-size:12px;">Sent at ${sentAt}</p>
|
|
45
|
+
`,
|
|
46
|
+
});
|
|
47
|
+
return c.json({ ok: true });
|
|
48
|
+
} catch (error) {
|
|
49
|
+
const message = error instanceof Error ? error.message : "Failed to send test email";
|
|
50
|
+
return c.json({ message }, 500);
|
|
51
|
+
}
|
|
52
|
+
})
|
|
31
53
|
.put(
|
|
32
54
|
"/",
|
|
33
55
|
auth.requireRole("admin"),
|
|
@@ -9,6 +9,11 @@ const log = logger("notifications");
|
|
|
9
9
|
export type NotificationType = "email";
|
|
10
10
|
export type NotificationStatus = "sent" | "pending" | "error";
|
|
11
11
|
export type NotificationStatusSummary = Record<NotificationStatus, number>;
|
|
12
|
+
export type NotificationSearchSummary = NotificationStatusSummary & {
|
|
13
|
+
total: number;
|
|
14
|
+
system: number;
|
|
15
|
+
latestCreatedAt: Date | null;
|
|
16
|
+
};
|
|
12
17
|
|
|
13
18
|
/**
|
|
14
19
|
* Computes notification delivery status from sent/error timestamps.
|
|
@@ -63,6 +68,15 @@ const emptyStatusSummary = (): NotificationStatusSummary => ({
|
|
|
63
68
|
error: 0,
|
|
64
69
|
});
|
|
65
70
|
|
|
71
|
+
const emptySearchSummary = (): NotificationSearchSummary => ({
|
|
72
|
+
total: 0,
|
|
73
|
+
sent: 0,
|
|
74
|
+
pending: 0,
|
|
75
|
+
error: 0,
|
|
76
|
+
system: 0,
|
|
77
|
+
latestCreatedAt: null,
|
|
78
|
+
});
|
|
79
|
+
|
|
66
80
|
type DbNotificationRow = {
|
|
67
81
|
id: string;
|
|
68
82
|
type: NotificationType;
|
|
@@ -316,6 +330,79 @@ export const getStatusSummary = async (options?: {
|
|
|
316
330
|
return summary;
|
|
317
331
|
};
|
|
318
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Count statuses across all entries matching a search term in the current access scope.
|
|
335
|
+
*/
|
|
336
|
+
export const getSearchSummary = async (options: {
|
|
337
|
+
search: string;
|
|
338
|
+
sentBy?: string;
|
|
339
|
+
isAdmin?: boolean;
|
|
340
|
+
}): Promise<NotificationSearchSummary> => {
|
|
341
|
+
const { sentBy, isAdmin } = options;
|
|
342
|
+
const search = options.search.trim();
|
|
343
|
+
if (!search) return emptySearchSummary();
|
|
344
|
+
|
|
345
|
+
const searchPattern = `%${escapeLikePattern(search)}%`;
|
|
346
|
+
let rows: Array<{
|
|
347
|
+
total: number | string;
|
|
348
|
+
sent: number | string;
|
|
349
|
+
pending: number | string;
|
|
350
|
+
error: number | string;
|
|
351
|
+
system: number | string;
|
|
352
|
+
latest_created_at: Date | null;
|
|
353
|
+
}> = [];
|
|
354
|
+
|
|
355
|
+
if (isAdmin) {
|
|
356
|
+
rows = await sql`
|
|
357
|
+
SELECT
|
|
358
|
+
COUNT(*)::int AS total,
|
|
359
|
+
COUNT(*) FILTER (WHERE sent_at IS NOT NULL)::int AS sent,
|
|
360
|
+
COUNT(*) FILTER (WHERE sent_at IS NULL AND error IS NULL)::int AS pending,
|
|
361
|
+
COUNT(*) FILTER (WHERE sent_at IS NULL AND error IS NOT NULL)::int AS error,
|
|
362
|
+
COUNT(*) FILTER (WHERE sent_by IS NULL)::int AS system,
|
|
363
|
+
MAX(created_at) AS latest_created_at
|
|
364
|
+
FROM notifications.messages
|
|
365
|
+
WHERE subject ILIKE ${searchPattern} ESCAPE '\'
|
|
366
|
+
OR content ILIKE ${searchPattern} ESCAPE '\'
|
|
367
|
+
OR recipient ILIKE ${searchPattern} ESCAPE '\'
|
|
368
|
+
`;
|
|
369
|
+
} else if (sentBy) {
|
|
370
|
+
rows = await sql`
|
|
371
|
+
SELECT
|
|
372
|
+
COUNT(*)::int AS total,
|
|
373
|
+
COUNT(*) FILTER (WHERE sent_at IS NOT NULL)::int AS sent,
|
|
374
|
+
COUNT(*) FILTER (WHERE sent_at IS NULL AND error IS NULL)::int AS pending,
|
|
375
|
+
COUNT(*) FILTER (WHERE sent_at IS NULL AND error IS NOT NULL)::int AS error,
|
|
376
|
+
COUNT(*) FILTER (WHERE sent_by IS NULL)::int AS system,
|
|
377
|
+
MAX(created_at) AS latest_created_at
|
|
378
|
+
FROM notifications.messages
|
|
379
|
+
WHERE sent_by = ${sentBy}
|
|
380
|
+
AND (
|
|
381
|
+
subject ILIKE ${searchPattern} ESCAPE '\'
|
|
382
|
+
OR content ILIKE ${searchPattern} ESCAPE '\'
|
|
383
|
+
OR recipient ILIKE ${searchPattern} ESCAPE '\'
|
|
384
|
+
)
|
|
385
|
+
`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const row = rows[0];
|
|
389
|
+
if (!row) return emptySearchSummary();
|
|
390
|
+
|
|
391
|
+
const toNumber = (value: number | string) => {
|
|
392
|
+
const parsed = typeof value === "string" ? Number.parseInt(value, 10) : value;
|
|
393
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
total: toNumber(row.total),
|
|
398
|
+
sent: toNumber(row.sent),
|
|
399
|
+
pending: toNumber(row.pending),
|
|
400
|
+
error: toNumber(row.error),
|
|
401
|
+
system: toNumber(row.system),
|
|
402
|
+
latestCreatedAt: row.latest_created_at,
|
|
403
|
+
};
|
|
404
|
+
};
|
|
405
|
+
|
|
319
406
|
/**
|
|
320
407
|
* Get a single notification by ID.
|
|
321
408
|
*/
|
|
@@ -481,4 +568,5 @@ export const notifications = {
|
|
|
481
568
|
getPendingSystemCount,
|
|
482
569
|
sendAllPendingSystem,
|
|
483
570
|
getStatusSummary,
|
|
571
|
+
getSearchSummary,
|
|
484
572
|
};
|