PDS Admin tool make it easier to moderate your PDS with labels

resend api so it works on railway

+93 -17
+2
.env.example
··· 1 1 DATABASE_URL=file:./label-watcher.db 2 2 MIGRATIONS_FOLDER=drizzle 3 + # Email sending: set RESEND_API_KEY to use Resend, or NOTIFY_SMTP_URL to use SMTP (one is required) 4 + RESEND_API_KEYb=123 3 5 NOTIFY_SMTP_URL=smtp://localhost:1025 4 6 NOTIFY_SENDER_EMAIL=yougotmail@pdsmoover.com 5 7 LOG_LEVEL=info
+2
Dockerfile
··· 20 20 21 21 COPY --from=builder /app/dist /app/dist 22 22 COPY ./drizzle /app/drizzle 23 + # A very bad hack. need to see how to get a toml file to the volume of railway without this 24 + # COPY settings.toml /app/settings.toml 23 25 COPY --from=builder /app/package.json /app/pnpm-lock.yaml /app/pnpm-workspace.yaml ./ 24 26 # RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile 25 27 RUN pnpm install --prod --frozen-lockfile
+1
package.json
··· 23 23 "nodemailer": "^8.0.1", 24 24 "p-queue": "^9.1.0", 25 25 "pino": "^10.3.1", 26 + "resend": "^6.9.2", 26 27 "smol-toml": "^1.6.0" 27 28 }, 28 29 "devDependencies": {
+54
pnpm-lock.yaml
··· 32 32 pino: 33 33 specifier: ^10.3.1 34 34 version: 10.3.1 35 + resend: 36 + specifier: ^6.9.2 37 + version: 6.9.2 35 38 smol-toml: 36 39 specifier: ^1.6.0 37 40 version: 1.6.0 ··· 456 459 '@pinojs/redact@0.4.0': 457 460 resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} 458 461 462 + '@stablelib/base64@1.0.1': 463 + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} 464 + 459 465 '@standard-schema/spec@1.1.0': 460 466 resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} 461 467 ··· 630 636 fast-safe-stringify@2.1.1: 631 637 resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} 632 638 639 + fast-sha256@1.3.0: 640 + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} 641 + 633 642 fetch-blob@3.2.0: 634 643 resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} 635 644 engines: {node: ^12.20 || >= 14.13} ··· 754 763 resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} 755 764 hasBin: true 756 765 766 + postal-mime@2.7.3: 767 + resolution: {integrity: sha512-MjhXadAJaWgYzevi46+3kLak8y6gbg0ku14O1gO/LNOuay8dO+1PtcSGvAdgDR0DoIsSaiIA8y/Ddw6MnrO0Tw==} 768 + 757 769 postgres-array@2.0.0: 758 770 resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} 759 771 engines: {node: '>=4'} ··· 786 798 resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} 787 799 engines: {node: '>= 12.13.0'} 788 800 801 + resend@6.9.2: 802 + resolution: {integrity: sha512-uIM6CQ08tS+hTCRuKBFbOBvHIGaEhqZe8s4FOgqsVXSbQLAhmNWpmUhG3UAtRnmcwTWFUqnHa/+Vux8YGPyDBA==} 803 + engines: {node: '>=20'} 804 + peerDependencies: 805 + '@react-email/render': '*' 806 + peerDependenciesMeta: 807 + '@react-email/render': 808 + optional: true 809 + 789 810 resolve-pkg-maps@1.0.0: 790 811 resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} 791 812 ··· 814 835 resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} 815 836 engines: {node: '>= 10.x'} 816 837 838 + standardwebhooks@1.0.0: 839 + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} 840 + 817 841 strip-json-comments@5.0.3: 818 842 resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} 819 843 engines: {node: '>=14.16'} 844 + 845 + svix@1.84.1: 846 + resolution: {integrity: sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ==} 820 847 821 848 thread-stream@4.0.0: 822 849 resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} ··· 840 867 unicode-segmenter@0.14.5: 841 868 resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==} 842 869 870 + uuid@10.0.0: 871 + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} 872 + hasBin: true 873 + 843 874 web-streams-polyfill@3.3.3: 844 875 resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} 845 876 engines: {node: '>= 8'} ··· 1157 1188 1158 1189 '@pinojs/redact@0.4.0': {} 1159 1190 1191 + '@stablelib/base64@1.0.1': {} 1192 + 1160 1193 '@standard-schema/spec@1.1.0': {} 1161 1194 1162 1195 '@types/node@25.3.0': ··· 1282 1315 fast-copy@4.0.2: {} 1283 1316 1284 1317 fast-safe-stringify@2.1.1: {} 1318 + 1319 + fast-sha256@1.3.0: {} 1285 1320 1286 1321 fetch-blob@3.2.0: 1287 1322 dependencies: ··· 1433 1468 sonic-boom: 4.2.1 1434 1469 thread-stream: 4.0.0 1435 1470 1471 + postal-mime@2.7.3: {} 1472 + 1436 1473 postgres-array@2.0.0: 1437 1474 optional: true 1438 1475 ··· 1460 1497 1461 1498 real-require@0.2.0: {} 1462 1499 1500 + resend@6.9.2: 1501 + dependencies: 1502 + postal-mime: 2.7.3 1503 + svix: 1.84.1 1504 + 1463 1505 resolve-pkg-maps@1.0.0: {} 1464 1506 1465 1507 safe-stable-stringify@2.5.0: {} ··· 1481 1523 1482 1524 split2@4.2.0: {} 1483 1525 1526 + standardwebhooks@1.0.0: 1527 + dependencies: 1528 + '@stablelib/base64': 1.0.1 1529 + fast-sha256: 1.3.0 1530 + 1484 1531 strip-json-comments@5.0.3: {} 1485 1532 1533 + svix@1.84.1: 1534 + dependencies: 1535 + standardwebhooks: 1.0.0 1536 + uuid: 10.0.0 1537 + 1486 1538 thread-stream@4.0.0: 1487 1539 dependencies: 1488 1540 real-require: 0.2.0 ··· 1496 1548 undici-types@7.18.2: {} 1497 1549 1498 1550 unicode-segmenter@0.14.5: {} 1551 + 1552 + uuid@10.0.0: {} 1499 1553 1500 1554 web-streams-polyfill@3.3.3: {} 1501 1555
+34 -17
src/mailer.ts
··· 1 1 import nodemailer from "nodemailer"; 2 + import { Resend } from "resend"; 2 3 4 + const resendApiKey = process.env.RESEND_API_KEY; 3 5 const smtpUrl = process.env.NOTIFY_SMTP_URL; 4 6 const senderEmail = process.env.NOTIFY_SENDER_EMAIL; 5 7 6 - if (!smtpUrl) throw new Error("NOTIFY_SMTP_URL is not set"); 8 + if (!resendApiKey && !smtpUrl) { 9 + throw new Error("Either RESEND_API_KEY or NOTIFY_SMTP_URL must be set"); 10 + } 7 11 if (!senderEmail) throw new Error("NOTIFY_SENDER_EMAIL is not set"); 8 12 9 - const transporter = nodemailer.createTransport(smtpUrl); 13 + const resend = resendApiKey ? new Resend(resendApiKey) : null; 14 + const transporter = !resendApiKey && smtpUrl ? nodemailer.createTransport(smtpUrl) : null; 10 15 11 16 export const sendLabelNotification = async ( 12 17 emails: string[], ··· 21 26 ) => { 22 27 const { did, pds, label, labeler, negated, dateApplied } = params; 23 28 24 - await transporter.sendMail({ 25 - from: senderEmail, 26 - to: emails.join(", "), 27 - subject: `Label "${label}" ${negated ? "negated" : "applied"} — ${did} - ${pds}`, 28 - text: [ 29 - `A label event was detected.`, 30 - ``, 31 - `DID: ${did}`, 32 - `PDS: ${pds}`, 33 - `Label: ${label}`, 34 - `Labeler: ${labeler}`, 35 - `Negated: ${negated}`, 36 - `Date: ${dateApplied.toISOString()}`, 37 - ].join("\n"), 38 - }); 29 + const subject = `Label "${label}" ${negated ? "negated" : "applied"} — ${did} - ${pds}`; 30 + const text = [ 31 + `A label event was detected.`, 32 + ``, 33 + `DID: ${did}`, 34 + `PDS: ${pds}`, 35 + `Label: ${label}`, 36 + `Labeler: ${labeler}`, 37 + `Negated: ${negated}`, 38 + `Date: ${dateApplied.toISOString()}`, 39 + ].join("\n"); 40 + 41 + if (resend) { 42 + await resend.emails.send({ 43 + from: senderEmail, 44 + to: emails, 45 + subject, 46 + text, 47 + }); 48 + } else { 49 + await transporter!.sendMail({ 50 + from: senderEmail, 51 + to: emails.join(", "), 52 + subject, 53 + text, 54 + }); 55 + } 39 56 };