feat: implement email notification system for comments

- Added MailModule to handle email notifications for comment events.
- Introduced CommentCreatedEvent to encapsulate comment creation details.
- Implemented CommentNotificationListener to send notifications to relevant users when a comment is created.
- Integrated Resend service for sending emails, with templates for comment notifications.
- Updated various modules to support the new email notification feature.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2025-12-03 20:57:26 +08:00
parent 1aac293020
commit ced9e747aa
26 changed files with 455 additions and 36 deletions

106
pnpm-lock.yaml generated
View File

@@ -819,7 +819,7 @@ importers:
version: 0.5.19(tailwindcss@4.1.17)
'@tailwindcss/vite':
specifier: 4.1.17
version: 4.1.17(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
version: 4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
'@types/node':
specifier: 24.10.1
version: 24.10.1
@@ -831,7 +831,7 @@ importers:
version: 19.2.3(@types/react@19.2.7)
'@vitejs/plugin-react':
specifier: ^5.1.1
version: 5.1.1(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
version: 5.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
ast-kit:
specifier: 2.2.0
version: 2.2.0
@@ -888,10 +888,10 @@ importers:
version: 0.15.4
vite-plugin-html:
specifier: 3.2.2
version: 3.2.2(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
version: 3.2.2(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
vite-plugin-pwa:
specifier: 1.1.0
version: 1.1.0(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0)
version: 1.1.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0)
be:
dependencies:
@@ -977,6 +977,9 @@ importers:
drizzle-orm:
specifier: ^0.44.7
version: 0.44.7(@types/pg@8.15.6)(@vercel/postgres@0.10.0)(kysely@0.28.8)(pg@8.16.3)(postgres@3.4.7)
ejs:
specifier: 3.1.10
version: 3.1.10
hono:
specifier: 4.10.7
version: 4.10.7
@@ -995,6 +998,9 @@ importers:
reflect-metadata:
specifier: 0.2.2
version: 0.2.2
resend:
specifier: 6.5.2
version: 6.5.2
satori:
specifier: 0.18.3
version: 0.18.3
@@ -1005,6 +1011,9 @@ importers:
specifier: ^4.1.13
version: 4.1.13
devDependencies:
'@types/ejs':
specifier: 3.1.5
version: 3.1.5
'@types/mime-types':
specifier: 3.0.1
version: 3.0.1
@@ -1608,7 +1617,7 @@ importers:
version: 0.16.7(synckit@0.11.11)(typescript@5.9.3)
unplugin-dts:
specifier: 1.0.0-beta.6
version: 1.0.0-beta.6(@microsoft/api-extractor@7.52.13(@types/node@24.10.1))(esbuild@0.27.0)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
version: 1.0.0-beta.6(@microsoft/api-extractor@7.52.13(@types/node@24.10.1))(esbuild@0.27.0)(rolldown@1.0.0-beta.51)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
vite:
specifier: 7.2.4
version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)
@@ -5659,6 +5668,9 @@ packages:
'@splinetool/runtime@0.9.526':
resolution: {integrity: sha512-qznHbXA5aKwDbCgESAothCNm1IeEZcmNWG145p5aXj4w5uoqR1TZ9qkTHTKLTsUbHeitCwdhzmRqan1kxboLgQ==}
'@stablelib/base64@1.0.1':
resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==}
'@standard-schema/spec@1.0.0':
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
@@ -6091,6 +6103,9 @@ packages:
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
'@types/ejs@3.1.5':
resolution: {integrity: sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==}
'@types/estree-jsx@1.0.5':
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
@@ -6149,6 +6164,9 @@ packages:
'@types/node@20.19.25':
resolution: {integrity: sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==}
'@types/node@22.19.1':
resolution: {integrity: sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==}
'@types/node@24.10.1':
resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
@@ -7877,6 +7895,9 @@ packages:
es-toolkit@1.42.0:
resolution: {integrity: sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA==}
es6-promise@4.2.8:
resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==}
esast-util-from-estree@2.0.0:
resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==}
@@ -8276,6 +8297,9 @@ packages:
fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
fast-sha256@1.3.0:
resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==}
fast-shallow-equal@1.0.0:
resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==}
@@ -10410,6 +10434,9 @@ packages:
resolution: {integrity: sha512-3jTGGLRzlhu/1ws2zlr4Q+GVMLCQTLFOj8CMX5x44cdZG9FQE07x2mQhaNxaKVPNmIDu0mvJ/cEwtY7Pim7hqA==}
engines: {node: '>=18'}
querystringify@2.2.0:
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@@ -11094,9 +11121,21 @@ packages:
resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==}
engines: {node: '>=0.10.5'}
requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
reselect@5.1.1:
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
resend@6.5.2:
resolution: {integrity: sha512-Yl83UvS8sYsjgmF8dVbNPzlfpmb3DkLUk3VwsAbkaEFo9UMswpNuPGryHBXGk+Ta4uYMv5HmjVk3j9jmNkcEDg==}
engines: {node: '>=20'}
peerDependencies:
'@react-email/render': '*'
peerDependenciesMeta:
'@react-email/render':
optional: true
resize-observer-polyfill@1.5.1:
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
@@ -11599,6 +11638,9 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
svix@1.76.1:
resolution: {integrity: sha512-CRuDWBTgYfDnBLRaZdKp9VuoPcNUq9An14c/k+4YJ15Qc5Grvf66vp0jvTltd4t7OIRj+8lM1DAgvSgvf7hdLw==}
swiper@12.0.3:
resolution: {integrity: sha512-BHd6U1VPEIksrXlyXjMmRWO0onmdNPaTAFduzqR3pgjvi7KfmUCAm/0cj49u2D7B0zNjMw02TSeXfinC1hDCXg==}
engines: {node: '>= 4.7.0'}
@@ -12096,6 +12138,9 @@ packages:
resolution: {integrity: sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
url-parse@1.5.10:
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
use-callback-ref@1.3.3:
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
engines: {node: '>=10'}
@@ -12154,6 +12199,10 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
uuid@10.0.0:
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
hasBin: true
uuid@11.1.0:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
hasBin: true
@@ -17356,6 +17405,8 @@ snapshots:
on-change: 4.0.2
semver-compare: 1.0.0
'@stablelib/base64@1.0.1': {}
'@standard-schema/spec@1.0.0': {}
'@stitches/react@1.2.8(react@19.2.0)':
@@ -17782,6 +17833,8 @@ snapshots:
'@types/deep-eql@4.0.2': {}
'@types/ejs@3.1.5': {}
'@types/estree-jsx@1.0.5':
dependencies:
'@types/estree': 1.0.8
@@ -17838,6 +17891,10 @@ snapshots:
dependencies:
undici-types: 6.21.0
'@types/node@22.19.1':
dependencies:
undici-types: 6.21.0
'@types/node@24.10.1':
dependencies:
undici-types: 7.16.0
@@ -19750,6 +19807,8 @@ snapshots:
es-toolkit@1.42.0: {}
es6-promise@4.2.8: {}
esast-util-from-estree@2.0.0:
dependencies:
'@types/estree-jsx': 1.0.5
@@ -20493,6 +20552,8 @@ snapshots:
fast-levenshtein@2.0.6: {}
fast-sha256@1.3.0: {}
fast-shallow-equal@1.0.0: {}
fast-uri@3.1.0: {}
@@ -22965,6 +23026,8 @@ snapshots:
filter-obj: 5.1.0
split-on-first: 3.0.0
querystringify@2.2.0: {}
queue-microtask@1.2.3: {}
quickselect@3.0.0: {}
@@ -23933,8 +23996,14 @@ snapshots:
requireindex@1.2.0: {}
requires-port@1.0.0: {}
reselect@5.1.1: {}
resend@6.5.2:
dependencies:
svix: 1.76.1
resize-observer-polyfill@1.5.1: {}
resolve-from@4.0.0: {}
@@ -24529,6 +24598,15 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
svix@1.76.1:
dependencies:
'@stablelib/base64': 1.0.1
'@types/node': 22.19.1
es6-promise: 4.2.8
fast-sha256: 1.3.0
url-parse: 1.5.10
uuid: 10.0.0
swiper@12.0.3: {}
swr@2.3.6(react@19.2.0):
@@ -24945,7 +25023,7 @@ snapshots:
magic-string-ast: 1.0.3
unplugin: 2.3.10
unplugin-dts@1.0.0-beta.6(@microsoft/api-extractor@7.52.13(@types/node@24.10.1))(esbuild@0.27.0)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)):
unplugin-dts@1.0.0-beta.6(@microsoft/api-extractor@7.52.13(@types/node@24.10.1))(esbuild@0.27.0)(rolldown@1.0.0-beta.51)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)):
dependencies:
'@rollup/pluginutils': 5.3.0(rollup@4.53.3)
'@volar/typescript': 2.4.23
@@ -24959,6 +25037,7 @@ snapshots:
optionalDependencies:
'@microsoft/api-extractor': 7.52.13(@types/node@24.10.1)
esbuild: 0.27.0
rolldown: 1.0.0-beta.51
rollup: 4.53.3
vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)
transitivePeerDependencies:
@@ -25038,6 +25117,11 @@ snapshots:
url-join@5.0.0: {}
url-parse@1.5.10:
dependencies:
querystringify: 2.2.0
requires-port: 1.0.0
use-callback-ref@1.3.3(@types/react@19.2.7)(react@19.2.0):
dependencies:
react: 19.2.0
@@ -25085,6 +25169,8 @@ snapshots:
util-deprecate@1.0.2: {}
uuid@10.0.0: {}
uuid@11.1.0: {}
v8n@1.5.1: {}
@@ -25181,7 +25267,7 @@ snapshots:
optionator: 0.9.4
typescript: 5.9.3
vite-plugin-html@3.2.2(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)):
vite-plugin-html@3.2.2(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)):
dependencies:
'@rollup/pluginutils': 4.2.1
colorette: 2.0.20
@@ -25195,14 +25281,14 @@ snapshots:
html-minifier-terser: 6.1.0
node-html-parser: 5.4.2
pathe: 0.2.0
vite: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)
vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)
vite-plugin-pwa@1.1.0(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0):
vite-plugin-pwa@1.1.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0):
dependencies:
debug: 4.4.3(supports-color@5.5.0)
pretty-bytes: 6.1.1
tinyglobby: 0.2.15
vite: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)
vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)
workbox-build: 7.3.0(@types/babel__core@7.20.5)
workbox-window: 7.3.0
transitivePeerDependencies: