增加收藏更新邮件发送
This commit is contained in:
@@ -49,6 +49,7 @@
|
||||
"next-pwa": "^5.6.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"node-fetch": "^2.7.0",
|
||||
"nodemailer": "^7.0.12",
|
||||
"nprogress": "^0.2.0",
|
||||
"opencc-js": "^1.0.5",
|
||||
"parse-torrent-name": "^0.5.4",
|
||||
@@ -79,6 +80,7 @@
|
||||
"@types/he": "^1.2.3",
|
||||
"@types/node": "24.0.3",
|
||||
"@types/node-fetch": "^2.6.13",
|
||||
"@types/nodemailer": "^7.0.5",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/react-window": "^2.0.0",
|
||||
|
||||
Generated
+1013
-12
@@ -95,6 +95,9 @@ importers:
|
||||
node-fetch:
|
||||
specifier: ^2.7.0
|
||||
version: 2.7.0
|
||||
nodemailer:
|
||||
specifier: ^7.0.12
|
||||
version: 7.0.12
|
||||
nprogress:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
@@ -180,6 +183,9 @@ importers:
|
||||
'@types/node-fetch':
|
||||
specifier: ^2.6.13
|
||||
version: 2.6.13
|
||||
'@types/nodemailer':
|
||||
specifier: ^7.0.5
|
||||
version: 7.0.5
|
||||
'@types/react':
|
||||
specifier: ^18.3.18
|
||||
version: 18.3.27
|
||||
@@ -265,6 +271,135 @@ packages:
|
||||
peerDependencies:
|
||||
ajv: '>=8'
|
||||
|
||||
'@aws-crypto/[email protected]':
|
||||
resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==}
|
||||
|
||||
'@aws-crypto/[email protected]':
|
||||
resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
|
||||
'@aws-crypto/[email protected]':
|
||||
resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==}
|
||||
|
||||
'@aws-crypto/[email protected]':
|
||||
resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-QLqB8yluIhgXCr/0fY2f0gI3nQl0TInI+4/SFt9i1gGuvIVp7Nc6mFxjFe+CilfIadMqwjH28KvunirmDpWGPQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-ArmgnOsSCXN5VyIvZb4kSP5hpqlRRHolrMtKQ/0N8Hw4MTb7/IeYHSZzVPNzzkuX6gn5Aj8txoUnDPM8O7pc9g==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-klpzObldOq8HXzDjDlY6K8rMhYZU6mXRz6P9F9N+tWnjoYFfeBMra8wYApydElTUYQKP1O7RLHwH1OKFfKcqIA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-rtVzXzEtAfZBfh+lq3DAvRar4c3jyptweOAJR2DweyXx71QSMY+O879hjpMwES7jl07a3O1zlnFIDo4KP/96kQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-CjDbWL7JxjLc9ZxQilMusWSw05yRvUJKRpz59IxDpWUnSMHC9JMMUUkOy5Izk8UAtzi6gupRWArp4NG4labt9Q==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-L5R1hN1FY/xCmH65DOYMXl8zqCFiAq0bAq8tJZU32mGjIl1GzGeOkeDa9c461d81o7gsQeYzXyqFD3vXEbJ+kQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-C+1dcLr+p2E+9hbHyvrQTZ46Kj4vC2RoP6N935GEukHQa637ZjXs8VlyHJ2xTvbvwwLZQNiu56Cx7o/OFOqw1A==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-nMM0eeVuiLtw1taLRQ+H/H5Qp11rva8ILrzAQXSvlbDeVmbc7d8EeW5Q2xnCJu+3U+2JNZ1uxqIL22pB2sLEMA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-0XeT8OaT9iMA62DFV9+m6mZfJhrD0WNKf4IvsIpj2Z7XbaYfz3CoDDvNoALf3rPY9NzyMHgDxOspmqdvXP00mw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-ROb+Aijw8nzkB14Nh2XRH861++SeTZykUzk427y8YtgTLxjAOjgDTchDUFW2Fx6GFWkSjqJ3sY7SZyb33IqyFw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-r7tnYJJg+B6QvnsRHSW5vDol+ks6n+5jBZdCFdGyK63hjcMRMqHx59zEH8O47UR1PFv5hS2Q3uGz6HXvVtP40Q==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-AWa4rVsAfBR4xqm7pybQ8sUNJYnjyP/bJjfAw34qPuh3M9XrfGbAHG0aiAfQGrBnmS28jlO6Kz69o+c6PRw1dw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-xwrxfip7Y2iTtCMJ+iifN1E1XMOuhxIHY9DreMCvgdl4r7+48x2S1bCYPWH3eNY85/7CapBWdJ8cerpEl12sQQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-2r3PuNquU3CcS1Am4vn/KHFwLi8QFjMdA/R+CRDXT4AFO/0qxevF/YStW3gAKntQIgWgQV8ZdEtKAoJvLI4UWg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-v/Y5F1lbFFY7vMeG5yYxuhnn0CAshz6KMxkz1pDyPxejNE9HtA0w8R6OTBh/bVdIm44QpjhbI7qeLdOE/PLzXQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-dnSJGGUGSFGEX2NzvjwSefH+hmZQ347AwbLhAsi0cdnISSge+pcGfOFrJt2XfBIypwFe27chQhlfuf/gWdzpZg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-RIl8s4DCa31MXtRFw23iU90OqEoWuwQxiZOZshzsPtjyrunhHFjyZJEqb+vuQcYd1o22SMaYa3lPJRp64OH35Q==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-scj9OXqKpcjJ4jsFLtqYWz3IaNvNOQTFFvEY8XMJXTv+3qF5I7/x9SJtKzTRJEBF3spjzBUYPtGFbs9sj4fisQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-z3syXfuK/x/IsKf/AeYmgc2NT7fcJ+3fHaGO+fkghkV9WEba3fPyOwtTBX4KpFMNb2t50zDGZwbzW1/5ighcUQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-YO8KgJecxHIFMhfoP880q51VXFL9V1ELywK5yzVEqzyrwqoG93IUmnTygBUylQrfkbH+QqS0FxEdgwpP3fcwoQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-7IIzM5TdiXn+VtgPdVLjmE6uUBUtnga0f4RiSEI1WW10RPuNvZ9U+pL3SwDiRDAdoGrOF9tSLJOFZmfuwYuVYQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-gqqvYcitIIM2K4lrDX9de9YvOfXBcVdxfT/iLnvHJd4YHvSXlt+gs+AsL4FfPCxG4IG9A+FyulP9Sb1MEA75vw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-TZNZqFcMUtjvhZoZRtpEGQAdULYiy6rcGiXAbLU7e9LSpIYlRqpLa207oMNfgbzlL2PnHko+eVg8rajDiSOYCg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-qKgO7wAYsXzhwCHhdbaKFyxd83Fgs8/1Ka+jjSPrv2Ll7mB55Wbwlo0kkfMLh993/yEc8aoDIAc1Fz9h4Spi4Q==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-bpJGjuKmFr0rA6UKUCmN8D19HQFMLXMx5hKBXqBlPFdalMhxJSjcxzX9DbQh0Fn6bJtxCguFmRGOBdQqNOt49g==}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-TNQpwIVD6SxMwkD+QKnaujKVyXy5ljN3O3jrI7nCHJ3GlJu5xJrd8yuBnanYCcrn3e2zwdfOh4d4zJAZvvIvVw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
peerDependencies:
|
||||
aws-crt: '>=1.0.0'
|
||||
peerDependenciesMeta:
|
||||
aws-crt:
|
||||
optional: true
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-BSe4Lx/qdRQQdX8cSSI7Et20vqBspzAjBy8ZmXVoyLkol3y4sXBXzn+BiLtR+oh60ExQn6o2DU4QjdOZbXaKIQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws/[email protected]':
|
||||
resolution: {integrity: sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@babel/[email protected]':
|
||||
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -1238,28 +1373,24 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/[email protected]':
|
||||
resolution: {integrity: sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/[email protected]':
|
||||
resolution: {integrity: sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/[email protected]':
|
||||
resolution: {integrity: sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/[email protected]':
|
||||
resolution: {integrity: sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==}
|
||||
@@ -1408,6 +1539,178 @@ packages:
|
||||
'@sinonjs/[email protected]':
|
||||
resolution: {integrity: sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-BpAffW1mIyRZongoKBbh3RgHG+JDHJek/8hjA/9LnPunM+ejorO6axkxCgwxCe4K//g/JdPeR9vROHDYr/hfnQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-SCmhUG1UwtnEhF5Sxd8qk7bJwkj1BpFzFlHkXqKCEmDPLrRjJyTGM0EhqT7XBtDaDJjCfjRJQodgZcKDR843qg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-lLEmkQj7I7oKfvZ1wsnToGJouLOtfkMXDKRA1Hi6F+mMp5O1N8GcVWmVeNgTtgZtd0OTXDTI2vpVQmeutydGew==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-wcr3UEL26k7lLoyf9eVDZoD1nNY3Fa1gbNuOXvfxvVWLGkOVW+RYZgUUp/bXHryJfycIOQnBq9o1JAE00ax8HQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-O2WXr6ZRqPnbyoepb7pKcLt1QL6uRfFzGYJ9sGb5hMJQi7v/4RjRmCQa9mNjA0YiXqsc5lBmLXqJPhjM1Vjv5A==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-7uMhppVNRbgNIpyUBVRfjGHxygP85wpXalRvn9DvUlCx4qgy1AB/uxOPSiDx/jFyrwD3/BypQhx1JK7f3yxrAw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@socket.io/[email protected]':
|
||||
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
|
||||
|
||||
@@ -1660,6 +1963,9 @@ packages:
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-7WtR4MFJUNN2UFy0NIowBRJswj5KXjXDhlZY43Hmots5eGu5q/dTeFd/I6GgJA/qj3RqO6dDy4SvfcV3fOVeIA==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
|
||||
|
||||
@@ -1828,49 +2134,41 @@ packages:
|
||||
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/[email protected]':
|
||||
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/[email protected]':
|
||||
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/[email protected]':
|
||||
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/[email protected]':
|
||||
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/[email protected]':
|
||||
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/[email protected]':
|
||||
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/[email protected]':
|
||||
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/[email protected]':
|
||||
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
|
||||
@@ -2288,6 +2586,9 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
|
||||
|
||||
@@ -3105,6 +3406,10 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
|
||||
|
||||
@@ -4502,6 +4807,10 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
|
||||
|
||||
@@ -5367,6 +5676,9 @@ packages:
|
||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==}
|
||||
|
||||
@@ -6072,6 +6384,404 @@ snapshots:
|
||||
jsonpointer: 5.0.1
|
||||
leven: 3.1.0
|
||||
|
||||
'@aws-crypto/[email protected]':
|
||||
dependencies:
|
||||
'@aws-crypto/sha256-js': 5.2.0
|
||||
'@aws-crypto/supports-web-crypto': 5.2.0
|
||||
'@aws-crypto/util': 5.2.0
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@aws-sdk/util-locate-window': 3.965.2
|
||||
'@smithy/util-utf8': 2.3.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-crypto/[email protected]':
|
||||
dependencies:
|
||||
'@aws-crypto/util': 5.2.0
|
||||
'@aws-sdk/types': 3.969.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-crypto/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-crypto/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@smithy/util-utf8': 2.3.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-crypto/sha256-browser': 5.2.0
|
||||
'@aws-crypto/sha256-js': 5.2.0
|
||||
'@aws-sdk/core': 3.970.0
|
||||
'@aws-sdk/credential-provider-node': 3.970.0
|
||||
'@aws-sdk/middleware-host-header': 3.969.0
|
||||
'@aws-sdk/middleware-logger': 3.969.0
|
||||
'@aws-sdk/middleware-recursion-detection': 3.969.0
|
||||
'@aws-sdk/middleware-user-agent': 3.970.0
|
||||
'@aws-sdk/region-config-resolver': 3.969.0
|
||||
'@aws-sdk/signature-v4-multi-region': 3.970.0
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@aws-sdk/util-endpoints': 3.970.0
|
||||
'@aws-sdk/util-user-agent-browser': 3.969.0
|
||||
'@aws-sdk/util-user-agent-node': 3.970.0
|
||||
'@smithy/config-resolver': 4.4.6
|
||||
'@smithy/core': 3.20.6
|
||||
'@smithy/fetch-http-handler': 5.3.9
|
||||
'@smithy/hash-node': 4.2.8
|
||||
'@smithy/invalid-dependency': 4.2.8
|
||||
'@smithy/middleware-content-length': 4.2.8
|
||||
'@smithy/middleware-endpoint': 4.4.7
|
||||
'@smithy/middleware-retry': 4.4.23
|
||||
'@smithy/middleware-serde': 4.2.9
|
||||
'@smithy/middleware-stack': 4.2.8
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
'@smithy/node-http-handler': 4.4.8
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/smithy-client': 4.10.8
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/url-parser': 4.2.8
|
||||
'@smithy/util-base64': 4.3.0
|
||||
'@smithy/util-body-length-browser': 4.2.0
|
||||
'@smithy/util-body-length-node': 4.2.1
|
||||
'@smithy/util-defaults-mode-browser': 4.3.22
|
||||
'@smithy/util-defaults-mode-node': 4.2.25
|
||||
'@smithy/util-endpoints': 3.2.8
|
||||
'@smithy/util-middleware': 4.2.8
|
||||
'@smithy/util-retry': 4.2.8
|
||||
'@smithy/util-utf8': 4.2.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-crypto/sha256-browser': 5.2.0
|
||||
'@aws-crypto/sha256-js': 5.2.0
|
||||
'@aws-sdk/core': 3.970.0
|
||||
'@aws-sdk/middleware-host-header': 3.969.0
|
||||
'@aws-sdk/middleware-logger': 3.969.0
|
||||
'@aws-sdk/middleware-recursion-detection': 3.969.0
|
||||
'@aws-sdk/middleware-user-agent': 3.970.0
|
||||
'@aws-sdk/region-config-resolver': 3.969.0
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@aws-sdk/util-endpoints': 3.970.0
|
||||
'@aws-sdk/util-user-agent-browser': 3.969.0
|
||||
'@aws-sdk/util-user-agent-node': 3.970.0
|
||||
'@smithy/config-resolver': 4.4.6
|
||||
'@smithy/core': 3.20.6
|
||||
'@smithy/fetch-http-handler': 5.3.9
|
||||
'@smithy/hash-node': 4.2.8
|
||||
'@smithy/invalid-dependency': 4.2.8
|
||||
'@smithy/middleware-content-length': 4.2.8
|
||||
'@smithy/middleware-endpoint': 4.4.7
|
||||
'@smithy/middleware-retry': 4.4.23
|
||||
'@smithy/middleware-serde': 4.2.9
|
||||
'@smithy/middleware-stack': 4.2.8
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
'@smithy/node-http-handler': 4.4.8
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/smithy-client': 4.10.8
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/url-parser': 4.2.8
|
||||
'@smithy/util-base64': 4.3.0
|
||||
'@smithy/util-body-length-browser': 4.2.0
|
||||
'@smithy/util-body-length-node': 4.2.1
|
||||
'@smithy/util-defaults-mode-browser': 4.3.22
|
||||
'@smithy/util-defaults-mode-node': 4.2.25
|
||||
'@smithy/util-endpoints': 3.2.8
|
||||
'@smithy/util-middleware': 4.2.8
|
||||
'@smithy/util-retry': 4.2.8
|
||||
'@smithy/util-utf8': 4.2.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@aws-sdk/xml-builder': 3.969.0
|
||||
'@smithy/core': 3.20.6
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/signature-v4': 5.3.8
|
||||
'@smithy/smithy-client': 4.10.8
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/util-base64': 4.3.0
|
||||
'@smithy/util-middleware': 4.2.8
|
||||
'@smithy/util-utf8': 4.2.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.970.0
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.970.0
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@smithy/fetch-http-handler': 5.3.9
|
||||
'@smithy/node-http-handler': 4.4.8
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/smithy-client': 4.10.8
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/util-stream': 4.5.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.970.0
|
||||
'@aws-sdk/credential-provider-env': 3.970.0
|
||||
'@aws-sdk/credential-provider-http': 3.970.0
|
||||
'@aws-sdk/credential-provider-login': 3.970.0
|
||||
'@aws-sdk/credential-provider-process': 3.970.0
|
||||
'@aws-sdk/credential-provider-sso': 3.970.0
|
||||
'@aws-sdk/credential-provider-web-identity': 3.970.0
|
||||
'@aws-sdk/nested-clients': 3.970.0
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@smithy/credential-provider-imds': 4.2.8
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/shared-ini-file-loader': 4.4.3
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.970.0
|
||||
'@aws-sdk/nested-clients': 3.970.0
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/shared-ini-file-loader': 4.4.3
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/credential-provider-env': 3.970.0
|
||||
'@aws-sdk/credential-provider-http': 3.970.0
|
||||
'@aws-sdk/credential-provider-ini': 3.970.0
|
||||
'@aws-sdk/credential-provider-process': 3.970.0
|
||||
'@aws-sdk/credential-provider-sso': 3.970.0
|
||||
'@aws-sdk/credential-provider-web-identity': 3.970.0
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@smithy/credential-provider-imds': 4.2.8
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/shared-ini-file-loader': 4.4.3
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.970.0
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/shared-ini-file-loader': 4.4.3
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/client-sso': 3.970.0
|
||||
'@aws-sdk/core': 3.970.0
|
||||
'@aws-sdk/token-providers': 3.970.0
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/shared-ini-file-loader': 4.4.3
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.970.0
|
||||
'@aws-sdk/nested-clients': 3.970.0
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/shared-ini-file-loader': 4.4.3
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@aws/lambda-invoke-store': 0.2.3
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.970.0
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@aws-sdk/util-arn-parser': 3.968.0
|
||||
'@smithy/core': 3.20.6
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/signature-v4': 5.3.8
|
||||
'@smithy/smithy-client': 4.10.8
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/util-config-provider': 4.2.0
|
||||
'@smithy/util-middleware': 4.2.8
|
||||
'@smithy/util-stream': 4.5.10
|
||||
'@smithy/util-utf8': 4.2.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.970.0
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@aws-sdk/util-endpoints': 3.970.0
|
||||
'@smithy/core': 3.20.6
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-crypto/sha256-browser': 5.2.0
|
||||
'@aws-crypto/sha256-js': 5.2.0
|
||||
'@aws-sdk/core': 3.970.0
|
||||
'@aws-sdk/middleware-host-header': 3.969.0
|
||||
'@aws-sdk/middleware-logger': 3.969.0
|
||||
'@aws-sdk/middleware-recursion-detection': 3.969.0
|
||||
'@aws-sdk/middleware-user-agent': 3.970.0
|
||||
'@aws-sdk/region-config-resolver': 3.969.0
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@aws-sdk/util-endpoints': 3.970.0
|
||||
'@aws-sdk/util-user-agent-browser': 3.969.0
|
||||
'@aws-sdk/util-user-agent-node': 3.970.0
|
||||
'@smithy/config-resolver': 4.4.6
|
||||
'@smithy/core': 3.20.6
|
||||
'@smithy/fetch-http-handler': 5.3.9
|
||||
'@smithy/hash-node': 4.2.8
|
||||
'@smithy/invalid-dependency': 4.2.8
|
||||
'@smithy/middleware-content-length': 4.2.8
|
||||
'@smithy/middleware-endpoint': 4.4.7
|
||||
'@smithy/middleware-retry': 4.4.23
|
||||
'@smithy/middleware-serde': 4.2.9
|
||||
'@smithy/middleware-stack': 4.2.8
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
'@smithy/node-http-handler': 4.4.8
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/smithy-client': 4.10.8
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/url-parser': 4.2.8
|
||||
'@smithy/util-base64': 4.3.0
|
||||
'@smithy/util-body-length-browser': 4.2.0
|
||||
'@smithy/util-body-length-node': 4.2.1
|
||||
'@smithy/util-defaults-mode-browser': 4.3.22
|
||||
'@smithy/util-defaults-mode-node': 4.2.25
|
||||
'@smithy/util-endpoints': 3.2.8
|
||||
'@smithy/util-middleware': 4.2.8
|
||||
'@smithy/util-retry': 4.2.8
|
||||
'@smithy/util-utf8': 4.2.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@smithy/config-resolver': 4.4.6
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/middleware-sdk-s3': 3.970.0
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/signature-v4': 5.3.8
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.970.0
|
||||
'@aws-sdk/nested-clients': 3.970.0
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/shared-ini-file-loader': 4.4.3
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/url-parser': 4.2.8
|
||||
'@smithy/util-endpoints': 3.2.8
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@smithy/types': 4.12.0
|
||||
bowser: 2.13.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/middleware-user-agent': 3.970.0
|
||||
'@aws-sdk/types': 3.969.0
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/types': 4.12.0
|
||||
fast-xml-parser: 5.2.5
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws/[email protected]': {}
|
||||
|
||||
'@babel/[email protected]':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
@@ -7544,6 +8254,280 @@ snapshots:
|
||||
dependencies:
|
||||
'@sinonjs/commons': 1.8.6
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/util-config-provider': 4.2.0
|
||||
'@smithy/util-endpoints': 3.2.8
|
||||
'@smithy/util-middleware': 4.2.8
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/middleware-serde': 4.2.9
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/util-base64': 4.3.0
|
||||
'@smithy/util-body-length-browser': 4.2.0
|
||||
'@smithy/util-middleware': 4.2.8
|
||||
'@smithy/util-stream': 4.5.10
|
||||
'@smithy/util-utf8': 4.2.0
|
||||
'@smithy/uuid': 1.1.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/url-parser': 4.2.8
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/querystring-builder': 4.2.8
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/util-base64': 4.3.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/util-buffer-from': 4.2.0
|
||||
'@smithy/util-utf8': 4.2.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/core': 3.20.6
|
||||
'@smithy/middleware-serde': 4.2.9
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
'@smithy/shared-ini-file-loader': 4.4.3
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/url-parser': 4.2.8
|
||||
'@smithy/util-middleware': 4.2.8
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/service-error-classification': 4.2.8
|
||||
'@smithy/smithy-client': 4.10.8
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/util-middleware': 4.2.8
|
||||
'@smithy/util-retry': 4.2.8
|
||||
'@smithy/uuid': 1.1.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/shared-ini-file-loader': 4.4.3
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/abort-controller': 4.2.8
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/querystring-builder': 4.2.8
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/util-uri-escape': 4.2.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/types': 4.12.0
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/is-array-buffer': 4.2.0
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/util-hex-encoding': 4.2.0
|
||||
'@smithy/util-middleware': 4.2.8
|
||||
'@smithy/util-uri-escape': 4.2.0
|
||||
'@smithy/util-utf8': 4.2.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/core': 3.20.6
|
||||
'@smithy/middleware-endpoint': 4.4.7
|
||||
'@smithy/middleware-stack': 4.2.8
|
||||
'@smithy/protocol-http': 5.3.8
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/util-stream': 4.5.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/querystring-parser': 4.2.8
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/util-buffer-from': 4.2.0
|
||||
'@smithy/util-utf8': 4.2.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/is-array-buffer': 2.2.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/is-array-buffer': 4.2.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/smithy-client': 4.10.8
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/config-resolver': 4.4.6
|
||||
'@smithy/credential-provider-imds': 4.2.8
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
'@smithy/property-provider': 4.2.8
|
||||
'@smithy/smithy-client': 4.10.8
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/node-config-provider': 4.3.8
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/service-error-classification': 4.2.8
|
||||
'@smithy/types': 4.12.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/fetch-http-handler': 5.3.9
|
||||
'@smithy/node-http-handler': 4.4.8
|
||||
'@smithy/types': 4.12.0
|
||||
'@smithy/util-base64': 4.3.0
|
||||
'@smithy/util-buffer-from': 4.2.0
|
||||
'@smithy/util-hex-encoding': 4.2.0
|
||||
'@smithy/util-utf8': 4.2.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/util-buffer-from': 2.2.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/util-buffer-from': 4.2.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@socket.io/[email protected]': {}
|
||||
|
||||
'@surma/[email protected]':
|
||||
@@ -7841,6 +8825,13 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 7.8.0
|
||||
|
||||
'@types/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/client-sesv2': 3.970.0
|
||||
'@types/node': 24.0.3
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@types/[email protected]': {}
|
||||
|
||||
'@types/[email protected]': {}
|
||||
@@ -8503,6 +9494,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
@@ -9482,6 +10475,10 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
strnum: 2.1.2
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
reusify: 1.1.0
|
||||
@@ -11478,6 +12475,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
hosted-git-info: 2.8.9
|
||||
@@ -12392,6 +13391,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
stubborn-utils: 1.0.2
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
ExternalLink,
|
||||
FileText,
|
||||
FolderOpen,
|
||||
Mail,
|
||||
Palette,
|
||||
Settings,
|
||||
Tv,
|
||||
@@ -8852,6 +8853,386 @@ const XiaoyaConfigComponent = ({
|
||||
);
|
||||
};
|
||||
|
||||
// 邮件配置组件
|
||||
const EmailConfigComponent = ({
|
||||
config,
|
||||
refreshConfig,
|
||||
}: {
|
||||
config: AdminConfig | null;
|
||||
refreshConfig: () => Promise<void>;
|
||||
}) => {
|
||||
const { alertModal, showAlert, hideAlert } = useAlertModal();
|
||||
const { isLoading, withLoading } = useLoadingState();
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [provider, setProvider] = useState<'smtp' | 'resend'>('smtp');
|
||||
|
||||
// SMTP配置
|
||||
const [smtpHost, setSmtpHost] = useState('');
|
||||
const [smtpPort, setSmtpPort] = useState(587);
|
||||
const [smtpSecure, setSmtpSecure] = useState(false);
|
||||
const [smtpUser, setSmtpUser] = useState('');
|
||||
const [smtpPassword, setSmtpPassword] = useState('');
|
||||
const [smtpFrom, setSmtpFrom] = useState('');
|
||||
|
||||
// Resend配置
|
||||
const [resendApiKey, setResendApiKey] = useState('');
|
||||
const [resendFrom, setResendFrom] = useState('');
|
||||
|
||||
// 测试邮件
|
||||
const [testEmail, setTestEmail] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (config?.EmailConfig) {
|
||||
setEnabled(config.EmailConfig.enabled || false);
|
||||
setProvider(config.EmailConfig.provider || 'smtp');
|
||||
|
||||
if (config.EmailConfig.smtp) {
|
||||
setSmtpHost(config.EmailConfig.smtp.host || '');
|
||||
setSmtpPort(config.EmailConfig.smtp.port || 587);
|
||||
setSmtpSecure(config.EmailConfig.smtp.secure || false);
|
||||
setSmtpUser(config.EmailConfig.smtp.user || '');
|
||||
setSmtpPassword(config.EmailConfig.smtp.password || '');
|
||||
setSmtpFrom(config.EmailConfig.smtp.from || '');
|
||||
}
|
||||
|
||||
if (config.EmailConfig.resend) {
|
||||
setResendApiKey(config.EmailConfig.resend.apiKey || '');
|
||||
setResendFrom(config.EmailConfig.resend.from || '');
|
||||
}
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
const handleSave = async () => {
|
||||
await withLoading('saveEmail', async () => {
|
||||
try {
|
||||
const emailConfig: AdminConfig['EmailConfig'] = {
|
||||
enabled,
|
||||
provider,
|
||||
smtp: provider === 'smtp' ? {
|
||||
host: smtpHost,
|
||||
port: smtpPort,
|
||||
secure: smtpSecure,
|
||||
user: smtpUser,
|
||||
password: smtpPassword,
|
||||
from: smtpFrom,
|
||||
} : undefined,
|
||||
resend: provider === 'resend' ? {
|
||||
apiKey: resendApiKey,
|
||||
from: resendFrom,
|
||||
} : undefined,
|
||||
};
|
||||
|
||||
const response = await fetch('/api/admin/email', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'save',
|
||||
config: emailConfig,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.error || '保存失败');
|
||||
}
|
||||
|
||||
showSuccess('保存成功', showAlert);
|
||||
await refreshConfig();
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '保存失败', showAlert);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleTest = async () => {
|
||||
if (!testEmail) {
|
||||
showError('请输入测试邮箱地址', showAlert);
|
||||
return;
|
||||
}
|
||||
|
||||
await withLoading('testEmail', async () => {
|
||||
try {
|
||||
const emailConfig: AdminConfig['EmailConfig'] = {
|
||||
enabled: true,
|
||||
provider,
|
||||
smtp: provider === 'smtp' ? {
|
||||
host: smtpHost,
|
||||
port: smtpPort,
|
||||
secure: smtpSecure,
|
||||
user: smtpUser,
|
||||
password: smtpPassword,
|
||||
from: smtpFrom,
|
||||
} : undefined,
|
||||
resend: provider === 'resend' ? {
|
||||
apiKey: resendApiKey,
|
||||
from: resendFrom,
|
||||
} : undefined,
|
||||
};
|
||||
|
||||
const response = await fetch('/api/admin/email', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'test',
|
||||
config: emailConfig,
|
||||
testEmail,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showSuccess('测试邮件发送成功,请检查收件箱', showAlert);
|
||||
} else {
|
||||
showError(data.error || '发送失败', showAlert);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '发送失败', showAlert);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='space-y-6'>
|
||||
<div className='bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4'>
|
||||
<h3 className='text-sm font-medium text-blue-900 dark:text-blue-100 mb-2'>
|
||||
关于邮件通知
|
||||
</h3>
|
||||
<div className='text-sm text-blue-800 dark:text-blue-200 space-y-1'>
|
||||
<p>• 当用户收藏的影片有更新时,自动发送邮件通知</p>
|
||||
<p>• 支持 SMTP 和 Resend 两种发送方式</p>
|
||||
<p>• 用户可在个人设置中配置邮箱和通知偏好</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='space-y-4'>
|
||||
{/* 启用开关 */}
|
||||
<div className='flex items-center justify-between py-3 border-b border-gray-200 dark:border-gray-700'>
|
||||
<div>
|
||||
<h3 className='text-sm font-medium text-gray-900 dark:text-white'>
|
||||
启用邮件通知
|
||||
</h3>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
||||
开启后用户可以接收收藏更新的邮件通知
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setEnabled(!enabled)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
enabled ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
enabled ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 发送方式选择 */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
发送方式
|
||||
</label>
|
||||
<div className='flex gap-4'>
|
||||
<label className='flex items-center'>
|
||||
<input
|
||||
type='radio'
|
||||
value='smtp'
|
||||
checked={provider === 'smtp'}
|
||||
onChange={(e) => setProvider(e.target.value as 'smtp')}
|
||||
className='mr-2'
|
||||
/>
|
||||
<span className='text-sm text-gray-700 dark:text-gray-300'>SMTP</span>
|
||||
</label>
|
||||
<label className='flex items-center'>
|
||||
<input
|
||||
type='radio'
|
||||
value='resend'
|
||||
checked={provider === 'resend'}
|
||||
onChange={(e) => setProvider(e.target.value as 'resend')}
|
||||
className='mr-2'
|
||||
/>
|
||||
<span className='text-sm text-gray-700 dark:text-gray-300'>Resend</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SMTP配置 */}
|
||||
{provider === 'smtp' && (
|
||||
<div className='space-y-4 p-4 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700'>
|
||||
<h4 className='text-sm font-medium text-gray-900 dark:text-white'>SMTP 配置</h4>
|
||||
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>
|
||||
SMTP 主机 *
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
value={smtpHost}
|
||||
onChange={(e) => setSmtpHost(e.target.value)}
|
||||
placeholder='smtp.gmail.com'
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>
|
||||
SMTP 端口 *
|
||||
</label>
|
||||
<input
|
||||
type='number'
|
||||
value={smtpPort}
|
||||
onChange={(e) => setSmtpPort(parseInt(e.target.value))}
|
||||
placeholder='587'
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center'>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={smtpSecure}
|
||||
onChange={(e) => setSmtpSecure(e.target.checked)}
|
||||
className='mr-2'
|
||||
/>
|
||||
<label className='text-sm text-gray-700 dark:text-gray-300'>
|
||||
使用 SSL/TLS(端口 465 时启用)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>
|
||||
SMTP 用户名 *
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
value={smtpUser}
|
||||
onChange={(e) => setSmtpUser(e.target.value)}
|
||||
placeholder='[email protected]'
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>
|
||||
SMTP 密码 *
|
||||
</label>
|
||||
<input
|
||||
type='password'
|
||||
value={smtpPassword}
|
||||
onChange={(e) => setSmtpPassword(e.target.value)}
|
||||
placeholder='应用专用密码'
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>
|
||||
发件人邮箱 *
|
||||
</label>
|
||||
<input
|
||||
type='email'
|
||||
value={smtpFrom}
|
||||
onChange={(e) => setSmtpFrom(e.target.value)}
|
||||
placeholder='[email protected]'
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Resend配置 */}
|
||||
{provider === 'resend' && (
|
||||
<div className='space-y-4 p-4 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700'>
|
||||
<h4 className='text-sm font-medium text-gray-900 dark:text-white'>Resend 配置</h4>
|
||||
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>
|
||||
Resend API Key *
|
||||
</label>
|
||||
<input
|
||||
type='password'
|
||||
value={resendApiKey}
|
||||
onChange={(e) => setResendApiKey(e.target.value)}
|
||||
placeholder='re_xxxxx'
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white'
|
||||
/>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
||||
在 <a href='https://resend.com/api-keys' target='_blank' rel='noopener noreferrer' className='text-blue-600 hover:underline'>Resend 控制台</a> 获取
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>
|
||||
发件人邮箱 *
|
||||
</label>
|
||||
<input
|
||||
type='email'
|
||||
value={resendFrom}
|
||||
onChange={(e) => setResendFrom(e.target.value)}
|
||||
placeholder='[email protected]'
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white'
|
||||
/>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
||||
需要先在 Resend 中验证域名
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 测试邮件 */}
|
||||
<div className='p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg'>
|
||||
<h4 className='text-sm font-medium text-blue-900 dark:text-blue-100 mb-2'>
|
||||
发送测试邮件
|
||||
</h4>
|
||||
<div className='flex flex-col sm:flex-row gap-2'>
|
||||
<input
|
||||
type='email'
|
||||
value={testEmail}
|
||||
onChange={(e) => setTestEmail(e.target.value)}
|
||||
placeholder='输入测试邮箱地址'
|
||||
className='flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm'
|
||||
/>
|
||||
<button
|
||||
onClick={handleTest}
|
||||
disabled={isLoading('testEmail') || !testEmail}
|
||||
className={`${buttonStyles.primary} whitespace-nowrap`}
|
||||
>
|
||||
{isLoading('testEmail') ? '发送中...' : '发送测试'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 保存按钮 */}
|
||||
<div className='flex gap-3'>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isLoading('saveEmail')}
|
||||
className={buttonStyles.success}
|
||||
>
|
||||
{isLoading('saveEmail') ? '保存中...' : '保存配置'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AlertModal
|
||||
isOpen={alertModal.isOpen}
|
||||
onClose={hideAlert}
|
||||
type={alertModal.type}
|
||||
title={alertModal.title}
|
||||
message={alertModal.message}
|
||||
timer={alertModal.timer}
|
||||
showConfirm={alertModal.showConfirm}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 求片列表组件
|
||||
const MovieRequestsComponent = ({ config, refreshConfig }: { config: AdminConfig | null; refreshConfig: () => Promise<void> }) => {
|
||||
const { alertModal, showAlert, hideAlert } = useAlertModal();
|
||||
@@ -10322,6 +10703,7 @@ function AdminPageClient() {
|
||||
dataMigration: false,
|
||||
customAdFilter: false,
|
||||
themeConfig: false,
|
||||
emailConfig: false,
|
||||
});
|
||||
|
||||
// 获取管理员配置
|
||||
@@ -10712,6 +11094,18 @@ function AdminPageClient() {
|
||||
<AIConfigComponent config={config} refreshConfig={fetchConfig} />
|
||||
</CollapsibleTab>
|
||||
|
||||
{/* 邮件配置标签 */}
|
||||
<CollapsibleTab
|
||||
title='邮件配置'
|
||||
icon={
|
||||
<Mail size={20} className='text-gray-600 dark:text-gray-400' />
|
||||
}
|
||||
isExpanded={expandedTabs.emailConfig}
|
||||
onToggle={() => toggleTab('emailConfig')}
|
||||
>
|
||||
<EmailConfigComponent config={config} refreshConfig={fetchConfig} />
|
||||
</CollapsibleTab>
|
||||
|
||||
{/* 分类配置标签 */}
|
||||
<CollapsibleTab
|
||||
title='分类配置'
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getStorage } from '@/lib/db';
|
||||
import { getConfig } from '@/lib/config';
|
||||
import { EmailService } from '@/lib/email.service';
|
||||
import type { AdminConfig } from '@/lib/admin.types';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
/**
|
||||
* GET - 获取邮件配置
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
if (!authInfo || !authInfo.username) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const storage = getStorage();
|
||||
const userInfo = await storage.getUserInfoV2?.(authInfo.username);
|
||||
|
||||
// 只有管理员和站长可以访问
|
||||
if (!userInfo || (userInfo.role !== 'admin' && userInfo.role !== 'owner')) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const adminConfig = await getConfig();
|
||||
const emailConfig = adminConfig?.EmailConfig || {
|
||||
enabled: false,
|
||||
provider: 'smtp' as const,
|
||||
};
|
||||
|
||||
// 不返回敏感信息(密码、API Key)
|
||||
const safeConfig = {
|
||||
enabled: emailConfig.enabled,
|
||||
provider: emailConfig.provider,
|
||||
smtp: emailConfig.smtp
|
||||
? {
|
||||
host: emailConfig.smtp.host,
|
||||
port: emailConfig.smtp.port,
|
||||
secure: emailConfig.smtp.secure,
|
||||
user: emailConfig.smtp.user,
|
||||
from: emailConfig.smtp.from,
|
||||
password: emailConfig.smtp.password ? '******' : '',
|
||||
}
|
||||
: undefined,
|
||||
resend: emailConfig.resend
|
||||
? {
|
||||
from: emailConfig.resend.from,
|
||||
apiKey: emailConfig.resend.apiKey ? '******' : '',
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
return NextResponse.json(safeConfig);
|
||||
} catch (error) {
|
||||
console.error('获取邮件配置失败:', error);
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST - 保存邮件配置或发送测试邮件
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
if (!authInfo || !authInfo.username) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const storage = getStorage();
|
||||
const userInfo = await storage.getUserInfoV2?.(authInfo.username);
|
||||
|
||||
// 只有管理员和站长可以访问
|
||||
if (!userInfo || (userInfo.role !== 'admin' && userInfo.role !== 'owner')) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { action, config, testEmail } = body;
|
||||
|
||||
// 发送测试邮件
|
||||
if (action === 'test') {
|
||||
if (!testEmail) {
|
||||
return NextResponse.json(
|
||||
{ error: '请提供测试邮箱地址' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const emailConfig = config as AdminConfig['EmailConfig'];
|
||||
if (!emailConfig || !emailConfig.enabled) {
|
||||
return NextResponse.json(
|
||||
{ error: '邮件配置未启用' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const adminConfig = await getConfig();
|
||||
const siteName = adminConfig?.SiteConfig?.SiteName || 'MoonTVPlus';
|
||||
await EmailService.sendTestEmail(emailConfig, testEmail, siteName);
|
||||
return NextResponse.json({ success: true, message: '测试邮件发送成功' });
|
||||
} catch (error) {
|
||||
console.error('发送测试邮件失败:', error);
|
||||
return NextResponse.json(
|
||||
{ error: `发送失败: ${(error as Error).message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存邮件配置
|
||||
if (action === 'save') {
|
||||
const emailConfig = config as AdminConfig['EmailConfig'];
|
||||
if (!emailConfig) {
|
||||
return NextResponse.json(
|
||||
{ error: '邮件配置不能为空' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 验证配置
|
||||
if (emailConfig.enabled) {
|
||||
if (emailConfig.provider === 'smtp') {
|
||||
if (!emailConfig.smtp?.host || !emailConfig.smtp?.port || !emailConfig.smtp?.user || !emailConfig.smtp?.from) {
|
||||
return NextResponse.json(
|
||||
{ error: 'SMTP配置不完整' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
} else if (emailConfig.provider === 'resend') {
|
||||
if (!emailConfig.resend?.apiKey || !emailConfig.resend?.from) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Resend配置不完整' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取现有配置
|
||||
const adminConfig = await getConfig();
|
||||
if (!adminConfig) {
|
||||
return NextResponse.json(
|
||||
{ error: '管理员配置不存在' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// 如果密码或API Key是占位符,保留原有值
|
||||
if (emailConfig.smtp?.password === '******') {
|
||||
const oldConfig = adminConfig.EmailConfig;
|
||||
if (oldConfig?.smtp?.password) {
|
||||
emailConfig.smtp.password = oldConfig.smtp.password;
|
||||
}
|
||||
}
|
||||
|
||||
if (emailConfig.resend?.apiKey === '******') {
|
||||
const oldConfig = adminConfig.EmailConfig;
|
||||
if (oldConfig?.resend?.apiKey) {
|
||||
emailConfig.resend.apiKey = oldConfig.resend.apiKey;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新配置
|
||||
adminConfig.EmailConfig = emailConfig;
|
||||
await storage.setAdminConfig(adminConfig);
|
||||
|
||||
return NextResponse.json({ success: true, message: '邮件配置保存成功' });
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: '无效的操作' },
|
||||
{ status: 400 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('处理邮件配置失败:', error);
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import { fetchVideoDetail } from '@/lib/fetchVideoDetail';
|
||||
import { refreshLiveChannels } from '@/lib/live';
|
||||
import { startOpenListRefresh } from '@/lib/openlist-refresh';
|
||||
import { SearchResult } from '@/lib/types';
|
||||
import { EmailService } from '@/lib/email.service';
|
||||
import { getBatchFavoriteUpdateEmailTemplate, FavoriteUpdate } from '@/lib/email.templates';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -230,6 +232,7 @@ async function refreshRecordAndFavorites() {
|
||||
const totalFavorites = Object.keys(favorites).length;
|
||||
let processedFavorites = 0;
|
||||
const now = Date.now();
|
||||
const userUpdates: FavoriteUpdate[] = []; // 收集该用户的所有更新
|
||||
|
||||
for (const [key, fav] of Object.entries(favorites)) {
|
||||
try {
|
||||
@@ -279,6 +282,17 @@ async function refreshRecordAndFavorites() {
|
||||
|
||||
await storage.addNotification(user, notification);
|
||||
console.log(`已为用户 ${user} 创建收藏更新通知: ${fav.title}`);
|
||||
|
||||
// 收集更新信息用于邮件
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
|
||||
const playUrl = `${siteUrl}/play?source=${source}&id=${id}`;
|
||||
userUpdates.push({
|
||||
title: fav.title,
|
||||
oldEpisodes: fav.total_episodes,
|
||||
newEpisodes: favEpisodeCount,
|
||||
url: playUrl,
|
||||
cover: favDetail.poster || fav.cover,
|
||||
});
|
||||
}
|
||||
|
||||
processedFavorites++;
|
||||
@@ -289,6 +303,42 @@ async function refreshRecordAndFavorites() {
|
||||
}
|
||||
|
||||
console.log(`收藏处理完成: ${processedFavorites}/${totalFavorites}`);
|
||||
|
||||
// 如果有更新,发送汇总邮件
|
||||
if (userUpdates.length > 0) {
|
||||
try {
|
||||
const userEmail = storage.getUserEmail ? await storage.getUserEmail(user) : null;
|
||||
const emailNotifications = storage.getEmailNotificationPreference
|
||||
? await storage.getEmailNotificationPreference(user)
|
||||
: false;
|
||||
|
||||
if (userEmail && emailNotifications) {
|
||||
const config = await getConfig();
|
||||
const emailConfig = config?.EmailConfig;
|
||||
|
||||
if (emailConfig?.enabled) {
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
|
||||
const siteName = config?.SiteConfig?.SiteName || 'MoonTVPlus';
|
||||
|
||||
await EmailService.send(emailConfig, {
|
||||
to: userEmail,
|
||||
subject: `📺 收藏更新汇总 - ${userUpdates.length} 部影片有更新`,
|
||||
html: getBatchFavoriteUpdateEmailTemplate(
|
||||
user,
|
||||
userUpdates,
|
||||
siteUrl,
|
||||
siteName
|
||||
),
|
||||
});
|
||||
|
||||
console.log(`邮件汇总已发送至: ${userEmail} (${userUpdates.length} 个更新)`);
|
||||
}
|
||||
}
|
||||
} catch (emailError) {
|
||||
console.error(`发送邮件汇总失败 (${user}):`, emailError);
|
||||
// 邮件发送失败不影响主流程
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`获取用户收藏失败 (${user}):`, err);
|
||||
}
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getStorage } from '@/lib/db';
|
||||
import { getAvailableApiSites } from '@/lib/config';
|
||||
import { getDetailFromApi } from '@/lib/downstream';
|
||||
import { Notification } from '@/lib/types';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
if (!authInfo || !authInfo.username) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const storage = getStorage();
|
||||
const username = authInfo.username;
|
||||
const now = Date.now();
|
||||
|
||||
console.log(`用户 ${username} 请求检查收藏更新`);
|
||||
console.log(`当前时间: ${new Date(now).toLocaleString('zh-CN')}`);
|
||||
console.log(`开始检查收藏更新...`);
|
||||
|
||||
// 获取所有收藏
|
||||
const favorites = await storage.getAllFavorites(username);
|
||||
const favoriteKeys = Object.keys(favorites);
|
||||
|
||||
if (favoriteKeys.length === 0) {
|
||||
return NextResponse.json({
|
||||
message: '没有收藏',
|
||||
updates: [],
|
||||
});
|
||||
}
|
||||
|
||||
// 获取可用的 API 站点
|
||||
const apiSites = await getAvailableApiSites(username);
|
||||
|
||||
// 检查每个收藏的更新
|
||||
const updates: Array<{
|
||||
source: string;
|
||||
id: string;
|
||||
title: string;
|
||||
old_episodes: number;
|
||||
new_episodes: number;
|
||||
}> = [];
|
||||
|
||||
// 限制并发请求数量,避免过载
|
||||
const BATCH_SIZE = 5;
|
||||
for (let i = 0; i < favoriteKeys.length; i += BATCH_SIZE) {
|
||||
const batch = favoriteKeys.slice(i, i + BATCH_SIZE);
|
||||
|
||||
await Promise.all(
|
||||
batch.map(async (key) => {
|
||||
try {
|
||||
const favorite = favorites[key];
|
||||
|
||||
// 跳过 live 类型的收藏
|
||||
if (favorite.origin === 'live') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 跳过已完结的收藏
|
||||
if (favorite.is_completed) {
|
||||
console.log(`跳过已完结的收藏: ${favorite.title}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析 source 和 id
|
||||
const [source, id] = key.split('+');
|
||||
if (!source || !id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找对应的 API 站点
|
||||
const apiSite = apiSites.find((site) => site.key === source);
|
||||
if (!apiSite) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取最新详情
|
||||
const detail = await getDetailFromApi(apiSite, id);
|
||||
|
||||
// 比较集数
|
||||
const oldEpisodes = favorite.total_episodes;
|
||||
const newEpisodes = detail.episodes.length;
|
||||
|
||||
console.log(`检查收藏: ${favorite.title} (${source}+${id})`);
|
||||
console.log(` 旧集数: ${oldEpisodes}, 新集数: ${newEpisodes}`);
|
||||
console.log(` 是否完结: ${favorite.is_completed}, 备注: ${favorite.vod_remarks}`);
|
||||
|
||||
if (newEpisodes > oldEpisodes) {
|
||||
updates.push({
|
||||
source,
|
||||
id,
|
||||
title: favorite.title,
|
||||
old_episodes: oldEpisodes,
|
||||
new_episodes: newEpisodes,
|
||||
});
|
||||
|
||||
// 更新收藏的集数和完结状态
|
||||
await storage.setFavorite(username, key, {
|
||||
...favorite,
|
||||
total_episodes: newEpisodes,
|
||||
is_completed: detail.vod_remarks
|
||||
? ['全', '完结', '大结局', 'end', '完'].some((keyword) =>
|
||||
detail.vod_remarks!.toLowerCase().includes(keyword)
|
||||
)
|
||||
: false,
|
||||
vod_remarks: detail.vod_remarks,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`检查收藏更新失败 (${key}):`, error);
|
||||
// 继续处理其他收藏
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`检查完成,发现 ${updates.length} 个更新`);
|
||||
|
||||
// 如果有更新,创建通知
|
||||
if (updates.length > 0) {
|
||||
for (const update of updates) {
|
||||
const notification: Notification = {
|
||||
id: `fav_update_${update.source}_${update.id}_${now}`,
|
||||
type: 'favorite_update',
|
||||
title: '收藏更新',
|
||||
message: `《${update.title}》有新集数更新!从 ${update.old_episodes} 集更新到 ${update.new_episodes} 集`,
|
||||
timestamp: now,
|
||||
read: false,
|
||||
metadata: {
|
||||
source: update.source,
|
||||
id: update.id,
|
||||
title: update.title,
|
||||
old_episodes: update.old_episodes,
|
||||
new_episodes: update.new_episodes,
|
||||
},
|
||||
};
|
||||
|
||||
await storage.addNotification(username, notification);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
message: updates.length > 0 ? `发现 ${updates.length} 个更新` : '没有更新',
|
||||
updates,
|
||||
checked: favoriteKeys.length,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('检查收藏更新失败:', error);
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getStorage } from '@/lib/db';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
/**
|
||||
* GET - 获取用户邮箱设置
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
if (!authInfo || !authInfo.username) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const storage = getStorage();
|
||||
const username = authInfo.username;
|
||||
|
||||
const email = storage.getUserEmail
|
||||
? await storage.getUserEmail(username)
|
||||
: null;
|
||||
|
||||
const emailNotifications = storage.getEmailNotificationPreference
|
||||
? await storage.getEmailNotificationPreference(username)
|
||||
: false;
|
||||
|
||||
return NextResponse.json({
|
||||
email: email || '',
|
||||
emailNotifications,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取用户邮箱设置失败:', error);
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST - 保存用户邮箱设置
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
if (!authInfo || !authInfo.username) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const storage = getStorage();
|
||||
const username = authInfo.username;
|
||||
const body = await request.json();
|
||||
const { email, emailNotifications } = body;
|
||||
|
||||
// 验证邮箱格式
|
||||
if (email && typeof email === 'string') {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return NextResponse.json(
|
||||
{ error: '邮箱格式不正确' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (storage.setUserEmail) {
|
||||
await storage.setUserEmail(username, email);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存邮件通知偏好
|
||||
if (typeof emailNotifications === 'boolean') {
|
||||
if (storage.setEmailNotificationPreference) {
|
||||
await storage.setEmailNotificationPreference(username, emailNotifications);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '邮箱设置保存成功',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('保存用户邮箱设置失败:', error);
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
+193
-5
@@ -15,6 +15,7 @@ import {
|
||||
Home,
|
||||
KeyRound,
|
||||
LogOut,
|
||||
Mail,
|
||||
MoveDown,
|
||||
MoveUp,
|
||||
Rss,
|
||||
@@ -55,6 +56,7 @@ export const UserMenu: React.FC = () => {
|
||||
const [isOfflineDownloadPanelOpen, setIsOfflineDownloadPanelOpen] = useState(false);
|
||||
const [isNotificationPanelOpen, setIsNotificationPanelOpen] = useState(false);
|
||||
const [isFavoritesPanelOpen, setIsFavoritesPanelOpen] = useState(false);
|
||||
const [isEmailSettingsOpen, setIsEmailSettingsOpen] = useState(false);
|
||||
const [authInfo, setAuthInfo] = useState<AuthInfo | null>(null);
|
||||
const [storageType, setStorageType] = useState<string>('localstorage');
|
||||
const [mounted, setMounted] = useState(false);
|
||||
@@ -68,7 +70,7 @@ export const UserMenu: React.FC = () => {
|
||||
|
||||
// Body 滚动锁定 - 使用 overflow 方式避免布局问题
|
||||
useEffect(() => {
|
||||
if (isSettingsOpen || isChangePasswordOpen || isSubscribeOpen || isOfflineDownloadPanelOpen) {
|
||||
if (isSettingsOpen || isChangePasswordOpen || isSubscribeOpen || isOfflineDownloadPanelOpen || isEmailSettingsOpen) {
|
||||
const body = document.body;
|
||||
const html = document.documentElement;
|
||||
|
||||
@@ -87,7 +89,7 @@ export const UserMenu: React.FC = () => {
|
||||
html.style.overflow = originalHtmlOverflow;
|
||||
};
|
||||
}
|
||||
}, [isSettingsOpen, isChangePasswordOpen, isSubscribeOpen, isOfflineDownloadPanelOpen]);
|
||||
}, [isSettingsOpen, isChangePasswordOpen, isSubscribeOpen, isOfflineDownloadPanelOpen, isEmailSettingsOpen]);
|
||||
|
||||
// 设置相关状态
|
||||
const [defaultAggregateSearch, setDefaultAggregateSearch] = useState(true);
|
||||
@@ -108,6 +110,10 @@ export const UserMenu: React.FC = () => {
|
||||
const [nextEpisodeDanmakuPreload, setNextEpisodeDanmakuPreload] = useState(true);
|
||||
const [searchTraditionalToSimplified, setSearchTraditionalToSimplified] = useState(false);
|
||||
|
||||
// 邮件通知设置
|
||||
const [userEmail, setUserEmail] = useState('');
|
||||
const [emailNotifications, setEmailNotifications] = useState(false);
|
||||
|
||||
// 折叠面板状态
|
||||
const [isDoubanSectionOpen, setIsDoubanSectionOpen] = useState(true);
|
||||
const [isUsageSectionOpen, setIsUsageSectionOpen] = useState(false);
|
||||
@@ -390,9 +396,67 @@ export const UserMenu: React.FC = () => {
|
||||
if (savedSearchTraditionalToSimplified !== null) {
|
||||
setSearchTraditionalToSimplified(savedSearchTraditionalToSimplified === 'true');
|
||||
}
|
||||
|
||||
// 加载邮件通知设置
|
||||
loadEmailSettings();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 加载邮件通知设置
|
||||
const loadEmailSettings = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/user/email-settings');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setUserEmail(data.email || '');
|
||||
setEmailNotifications(data.emailNotifications || false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载邮件设置失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 保存邮件通知设置
|
||||
const handleSaveEmailSettings = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/user/email-settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
email: userEmail,
|
||||
emailNotifications,
|
||||
}),
|
||||
});
|
||||
|
||||
const messageEl = document.getElementById('email-settings-message');
|
||||
if (response.ok) {
|
||||
if (messageEl) {
|
||||
messageEl.textContent = '保存成功!';
|
||||
messageEl.className = 'text-xs text-center text-green-600 dark:text-green-400';
|
||||
messageEl.classList.remove('hidden');
|
||||
setTimeout(() => {
|
||||
messageEl.classList.add('hidden');
|
||||
}, 3000);
|
||||
}
|
||||
} else {
|
||||
const data = await response.json();
|
||||
if (messageEl) {
|
||||
messageEl.textContent = data.error || '保存失败';
|
||||
messageEl.className = 'text-xs text-center text-red-600 dark:text-red-400';
|
||||
messageEl.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存邮件设置失败:', error);
|
||||
const messageEl = document.getElementById('email-settings-message');
|
||||
if (messageEl) {
|
||||
messageEl.textContent = '保存失败,请重试';
|
||||
messageEl.className = 'text-xs text-center text-red-600 dark:text-red-400';
|
||||
messageEl.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 点击外部区域关闭下拉框
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
@@ -847,9 +911,23 @@ export const UserMenu: React.FC = () => {
|
||||
<div className='px-3 py-2.5 border-b border-gray-200 dark:border-gray-700 bg-gradient-to-r from-gray-50 to-gray-100/50 dark:from-gray-800 dark:to-gray-800/50'>
|
||||
<div className='space-y-1'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
|
||||
当前用户
|
||||
</span>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<span className='text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
|
||||
当前用户
|
||||
</span>
|
||||
{/* 邮件设置图标按钮 */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(false);
|
||||
setIsEmailSettingsOpen(true);
|
||||
}}
|
||||
className='p-0.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors'
|
||||
title='邮件通知设置'
|
||||
>
|
||||
<Mail className='w-3 h-3 text-gray-500 dark:text-gray-400' />
|
||||
</button>
|
||||
</div>
|
||||
<span
|
||||
className={`inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium ${(authInfo?.role || 'user') === 'owner'
|
||||
? 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300'
|
||||
@@ -2007,6 +2085,111 @@ export const UserMenu: React.FC = () => {
|
||||
</>
|
||||
);
|
||||
|
||||
// 邮件设置面板内容
|
||||
const emailSettingsPanel = (
|
||||
<>
|
||||
{/* 背景遮罩 */}
|
||||
<div
|
||||
className='fixed inset-0 bg-black/50 backdrop-blur-sm z-[1000]'
|
||||
onClick={() => setIsEmailSettingsOpen(false)}
|
||||
onTouchMove={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
onWheel={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
style={{
|
||||
touchAction: 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 邮件设置面板 */}
|
||||
<div
|
||||
className='fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md bg-white dark:bg-gray-900 rounded-xl shadow-xl z-[1001] overflow-hidden'
|
||||
>
|
||||
<div
|
||||
className='h-full p-6'
|
||||
data-panel-content
|
||||
onTouchMove={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
style={{
|
||||
touchAction: 'auto',
|
||||
}}
|
||||
>
|
||||
{/* 标题栏 */}
|
||||
<div className='flex items-center justify-between mb-6'>
|
||||
<h3 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
|
||||
邮件通知设置
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setIsEmailSettingsOpen(false)}
|
||||
className='w-8 h-8 p-1 rounded-full flex items-center justify-center text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors'
|
||||
aria-label='Close'
|
||||
>
|
||||
<X className='w-full h-full' />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 表单 */}
|
||||
<div className='space-y-4'>
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
邮箱地址
|
||||
</label>
|
||||
<input
|
||||
type='email'
|
||||
value={userEmail}
|
||||
onChange={(e) => setUserEmail(e.target.value)}
|
||||
placeholder='输入您的邮箱地址'
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg'>
|
||||
<div>
|
||||
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
|
||||
接收收藏更新通知
|
||||
</h4>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
||||
当收藏的影片有更新时发送邮件通知
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setEmailNotifications(!emailNotifications)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
emailNotifications ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
emailNotifications ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleSaveEmailSettings}
|
||||
className='w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors'
|
||||
>
|
||||
保存设置
|
||||
</button>
|
||||
|
||||
<p id='email-settings-message' className='text-xs text-center hidden'></p>
|
||||
</div>
|
||||
|
||||
{/* 提示信息 */}
|
||||
<div className='mt-6 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg'>
|
||||
<p className='text-xs text-blue-800 dark:text-blue-200'>
|
||||
💡 提示:需要管理员先在管理面板中配置邮件服务
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='relative'>
|
||||
@@ -2079,6 +2262,11 @@ export const UserMenu: React.FC = () => {
|
||||
/>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
{/* 使用 Portal 将邮件设置面板渲染到 document.body */}
|
||||
{isEmailSettingsOpen &&
|
||||
mounted &&
|
||||
createPortal(emailSettingsPanel, document.body)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -206,6 +206,24 @@ export interface AdminConfig {
|
||||
Password?: string; // 密码认证(备选)
|
||||
DisableVideoPreview?: boolean; // 禁用预览视频,直接返回直连链接
|
||||
};
|
||||
EmailConfig?: {
|
||||
enabled: boolean; // 是否启用邮件通知
|
||||
provider: 'smtp' | 'resend'; // 邮件发送方式
|
||||
// SMTP配置
|
||||
smtp?: {
|
||||
host: string; // SMTP服务器地址
|
||||
port: number; // SMTP端口(25/465/587)
|
||||
secure: boolean; // 是否使用SSL/TLS
|
||||
user: string; // SMTP用户名
|
||||
password: string; // SMTP密码
|
||||
from: string; // 发件人邮箱
|
||||
};
|
||||
// Resend配置
|
||||
resend?: {
|
||||
apiKey: string; // Resend API Key
|
||||
from: string; // 发件人邮箱
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface AdminConfigResult {
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
import type { AdminConfig } from './admin.types';
|
||||
|
||||
export interface EmailOptions {
|
||||
to: string;
|
||||
subject: string;
|
||||
html: string;
|
||||
}
|
||||
|
||||
export class EmailService {
|
||||
/**
|
||||
* 通过SMTP发送邮件
|
||||
*/
|
||||
static async sendViaSMTP(
|
||||
config: NonNullable<AdminConfig['EmailConfig']>['smtp'],
|
||||
options: EmailOptions
|
||||
): Promise<void> {
|
||||
if (!config) {
|
||||
throw new Error('SMTP配置不存在');
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
secure: config.secure,
|
||||
auth: {
|
||||
user: config.user,
|
||||
pass: config.password,
|
||||
},
|
||||
});
|
||||
|
||||
await transporter.sendMail({
|
||||
from: config.from,
|
||||
to: options.to,
|
||||
subject: options.subject,
|
||||
html: options.html,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过Resend API发送邮件
|
||||
*/
|
||||
static async sendViaResend(
|
||||
config: NonNullable<AdminConfig['EmailConfig']>['resend'],
|
||||
options: EmailOptions
|
||||
): Promise<void> {
|
||||
if (!config) {
|
||||
throw new Error('Resend配置不存在');
|
||||
}
|
||||
|
||||
const response = await fetch('https://api.resend.com/emails', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from: config.from,
|
||||
to: options.to,
|
||||
subject: options.subject,
|
||||
html: options.html,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Resend API错误: ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一发送接口
|
||||
*/
|
||||
static async send(
|
||||
emailConfig: AdminConfig['EmailConfig'],
|
||||
options: EmailOptions
|
||||
): Promise<void> {
|
||||
if (!emailConfig || !emailConfig.enabled) {
|
||||
console.log('邮件通知未启用,跳过发送');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (emailConfig.provider === 'smtp' && emailConfig.smtp) {
|
||||
await this.sendViaSMTP(emailConfig.smtp, options);
|
||||
console.log(`邮件已通过SMTP发送至: ${options.to}`);
|
||||
} else if (emailConfig.provider === 'resend' && emailConfig.resend) {
|
||||
await this.sendViaResend(emailConfig.resend, options);
|
||||
console.log(`邮件已通过Resend发送至: ${options.to}`);
|
||||
} else {
|
||||
throw new Error('邮件配置不完整');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('邮件发送失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送测试邮件
|
||||
*/
|
||||
static async sendTestEmail(
|
||||
emailConfig: AdminConfig['EmailConfig'],
|
||||
toEmail: string,
|
||||
siteName?: string
|
||||
): Promise<void> {
|
||||
const displayName = siteName || 'MoonTVPlus';
|
||||
await this.send(emailConfig, {
|
||||
to: toEmail,
|
||||
subject: `测试邮件 - ${displayName}`,
|
||||
html: `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 20px auto;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.header {
|
||||
background: white;
|
||||
color: #333;
|
||||
padding: 30px 20px;
|
||||
text-align: center;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.content {
|
||||
padding: 30px 20px;
|
||||
background: white;
|
||||
}
|
||||
.content p {
|
||||
color: #333;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.footer {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
background: white;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>📧 测试邮件</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>这是一封来自 ${displayName} 的测试邮件。</p>
|
||||
<p>如果您收到这封邮件,说明邮件配置正确!</p>
|
||||
<p style="color: #666;">发送时间: ${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>此邮件由 ${displayName} 自动发送</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* 邮件模板
|
||||
*/
|
||||
|
||||
export interface FavoriteUpdate {
|
||||
title: string;
|
||||
oldEpisodes: number;
|
||||
newEpisodes: number;
|
||||
url: string;
|
||||
cover?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收藏更新邮件模板
|
||||
*/
|
||||
export function getFavoriteUpdateEmailTemplate(
|
||||
userName: string,
|
||||
updates: FavoriteUpdate[],
|
||||
siteUrl: string,
|
||||
siteName?: string
|
||||
): string {
|
||||
const updatesList = updates
|
||||
.map(
|
||||
(u) => `
|
||||
<div style="margin: 15px 0; padding: 15px; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
|
||||
${
|
||||
u.cover
|
||||
? `<img src="${u.cover}" alt="${u.title}" style="width: 100%; max-width: 200px; border-radius: 5px; margin-bottom: 10px;" />`
|
||||
: ''
|
||||
}
|
||||
<div style="font-size: 16px; font-weight: bold; margin-bottom: 8px;">${u.title}</div>
|
||||
<div style="color: #666; margin-bottom: 10px;">
|
||||
更新:第 ${u.oldEpisodes} 集 → <span style="color: #4F46E5; font-weight: bold;">第 ${u.newEpisodes} 集</span>
|
||||
</div>
|
||||
<a href="${u.url}" style="display: inline-block; padding: 8px 16px; background: #4F46E5; color: white; text-decoration: none; border-radius: 5px; font-size: 14px;">立即观看</a>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join('');
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 20px auto;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.header {
|
||||
background: white;
|
||||
color: #333;
|
||||
padding: 30px 20px;
|
||||
text-align: center;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.content {
|
||||
padding: 30px 20px;
|
||||
background: white;
|
||||
}
|
||||
.greeting {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.footer {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
background: white;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
.footer a {
|
||||
color: #4F46E5;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>📺 收藏更新通知</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="greeting">
|
||||
Hi <strong>${userName}</strong>,
|
||||
</div>
|
||||
<p style="color: #666; margin-bottom: 20px;">您收藏的以下影片有更新:</p>
|
||||
${updatesList}
|
||||
<p style="color: #666; margin-top: 20px;">快去观看吧!</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>此邮件由 <a href="${siteUrl}">${siteName || 'MoonTVPlus'}</a> 自动发送</p>
|
||||
<p>如不想接收此类邮件,请在用户设置中关闭邮件通知</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个收藏更新邮件模板(简化版)
|
||||
*/
|
||||
export function getSingleFavoriteUpdateEmailTemplate(
|
||||
userName: string,
|
||||
title: string,
|
||||
oldEpisodes: number,
|
||||
newEpisodes: number,
|
||||
url: string,
|
||||
cover?: string,
|
||||
siteName?: string
|
||||
): string {
|
||||
return getFavoriteUpdateEmailTemplate(
|
||||
userName,
|
||||
[{ title, oldEpisodes, newEpisodes, url, cover }],
|
||||
url.split('/play')[0] || 'http://localhost:3000',
|
||||
siteName
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量收藏更新邮件模板(每日汇总)
|
||||
*/
|
||||
export function getBatchFavoriteUpdateEmailTemplate(
|
||||
userName: string,
|
||||
updates: FavoriteUpdate[],
|
||||
siteUrl: string,
|
||||
siteName?: string
|
||||
): string {
|
||||
const totalUpdates = updates.length;
|
||||
const totalNewEpisodes = updates.reduce(
|
||||
(sum, u) => sum + (u.newEpisodes - u.oldEpisodes),
|
||||
0
|
||||
);
|
||||
|
||||
const updatesList = updates
|
||||
.map(
|
||||
(u) => `
|
||||
<div style="margin: 15px 0; padding: 15px; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
|
||||
<div style="display: flex; align-items: center; gap: 15px;">
|
||||
${
|
||||
u.cover
|
||||
? `<img src="${u.cover}" alt="${u.title}" style="width: 80px; height: 120px; object-fit: cover; border-radius: 5px;" />`
|
||||
: ''
|
||||
}
|
||||
<div style="flex: 1;">
|
||||
<div style="font-size: 16px; font-weight: bold; margin-bottom: 8px;">${u.title}</div>
|
||||
<div style="color: #666; margin-bottom: 10px;">
|
||||
第 ${u.oldEpisodes} 集 → <span style="color: #4F46E5; font-weight: bold;">第 ${u.newEpisodes} 集</span>
|
||||
<span style="color: #10b981; font-weight: bold;">(+${u.newEpisodes - u.oldEpisodes})</span>
|
||||
</div>
|
||||
<a href="${u.url}" style="display: inline-block; padding: 6px 12px; background: #4F46E5; color: white; text-decoration: none; border-radius: 5px; font-size: 13px;">立即观看</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join('');
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 20px auto;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.header {
|
||||
background: white;
|
||||
color: #333;
|
||||
padding: 30px 20px;
|
||||
text-align: center;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.header .stats {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
.content {
|
||||
padding: 30px 20px;
|
||||
background: white;
|
||||
}
|
||||
.greeting {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.footer {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
background: white;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
.footer a {
|
||||
color: #4F46E5;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>📺 收藏更新汇总</h1>
|
||||
<div class="stats">
|
||||
${totalUpdates} 部影片更新 · 共 ${totalNewEpisodes} 集新内容
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="greeting">
|
||||
Hi <strong>${userName}</strong>,
|
||||
</div>
|
||||
<p style="color: #666; margin-bottom: 20px;">您收藏的影片有以下更新:</p>
|
||||
${updatesList}
|
||||
<p style="color: #666; margin-top: 20px;">快去观看吧!</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>此邮件由 <a href="${siteUrl}">${siteName || 'MoonTVPlus'}</a> 自动发送</p>
|
||||
<p>如不想接收此类邮件,请在用户设置中关闭邮件通知</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { createClient, RedisClientType } from 'redis';
|
||||
|
||||
import { AdminConfig } from './admin.types';
|
||||
import { Favorite, IStorage, PlayRecord, SkipConfig } from './types';
|
||||
import { userInfoCache } from './user-cache';
|
||||
|
||||
// 搜索历史最大条数
|
||||
const SEARCH_HISTORY_LIMIT = 20;
|
||||
@@ -660,6 +661,8 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
favorite_migrated?: boolean;
|
||||
skip_migrated?: boolean;
|
||||
last_movie_request_time?: number;
|
||||
email?: string;
|
||||
emailNotifications?: boolean;
|
||||
} | null> {
|
||||
const userInfo = await this.withRetry(() =>
|
||||
this.client.hGetAll(this.userInfoKey(userName))
|
||||
@@ -680,6 +683,8 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
favorite_migrated: userInfo.favorite_migrated === 'true',
|
||||
skip_migrated: userInfo.skip_migrated === 'true',
|
||||
last_movie_request_time: userInfo.last_movie_request_time ? parseInt(userInfo.last_movie_request_time, 10) : undefined,
|
||||
email: userInfo.email,
|
||||
emailNotifications: userInfo.emailNotifications === 'true',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1290,4 +1295,31 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
async removeUserMovieRequest(userName: string, requestId: string): Promise<void> {
|
||||
await this.withRetry(() => this.client.sRem(this.userMovieRequestsKey(userName), requestId));
|
||||
}
|
||||
|
||||
// ---------- 用户邮箱相关 ----------
|
||||
async getUserEmail(userName: string): Promise<string | null> {
|
||||
const userInfo = await this.getUserInfoV2(userName);
|
||||
return userInfo?.email || null;
|
||||
}
|
||||
|
||||
async setUserEmail(userName: string, email: string): Promise<void> {
|
||||
await this.withRetry(() =>
|
||||
this.client.hSet(this.userInfoKey(userName), 'email', email)
|
||||
);
|
||||
// 清除缓存
|
||||
userInfoCache?.delete(userName);
|
||||
}
|
||||
|
||||
async getEmailNotificationPreference(userName: string): Promise<boolean> {
|
||||
const userInfo = await this.getUserInfoV2(userName);
|
||||
return userInfo?.emailNotifications || false;
|
||||
}
|
||||
|
||||
async setEmailNotificationPreference(userName: string, enabled: boolean): Promise<void> {
|
||||
await this.withRetry(() =>
|
||||
this.client.hSet(this.userInfoKey(userName), 'emailNotifications', enabled.toString())
|
||||
);
|
||||
// 清除缓存
|
||||
userInfoCache?.delete(userName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,15 @@ export interface IStorage {
|
||||
favorite_migrated?: boolean;
|
||||
skip_migrated?: boolean;
|
||||
last_movie_request_time?: number;
|
||||
email?: string; // 用户邮箱
|
||||
emailNotifications?: boolean; // 是否接收邮件通知
|
||||
} | null>;
|
||||
|
||||
// 用户邮箱相关
|
||||
getUserEmail?(userName: string): Promise<string | null>;
|
||||
setUserEmail?(userName: string, email: string): Promise<void>;
|
||||
getEmailNotificationPreference?(userName: string): Promise<boolean>;
|
||||
setEmailNotificationPreference?(userName: string, enabled: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
// 搜索结果数据结构
|
||||
|
||||
@@ -590,6 +590,8 @@ export class UpstashRedisStorage implements IStorage {
|
||||
favorite_migrated?: boolean;
|
||||
skip_migrated?: boolean;
|
||||
last_movie_request_time?: number;
|
||||
email?: string;
|
||||
emailNotifications?: boolean;
|
||||
} | null> {
|
||||
// 先从缓存获取
|
||||
const cached = userInfoCache?.get(userName);
|
||||
@@ -688,6 +690,8 @@ export class UpstashRedisStorage implements IStorage {
|
||||
? userInfo.last_movie_request_time
|
||||
: parseInt(userInfo.last_movie_request_time as string, 10))
|
||||
: undefined,
|
||||
email: userInfo.email as string | undefined,
|
||||
emailNotifications: userInfo.emailNotifications === 'true' || userInfo.emailNotifications === true,
|
||||
};
|
||||
|
||||
// 存入缓存
|
||||
@@ -1335,6 +1339,33 @@ export class UpstashRedisStorage implements IStorage {
|
||||
async removeUserMovieRequest(userName: string, requestId: string): Promise<void> {
|
||||
await withRetry(() => this._client.srem(this.userMovieRequestsKey(userName), requestId));
|
||||
}
|
||||
|
||||
// ---------- 用户邮箱相关 ----------
|
||||
async getUserEmail(userName: string): Promise<string | null> {
|
||||
const userInfo = await this.getUserInfoV2(userName);
|
||||
return userInfo?.email || null;
|
||||
}
|
||||
|
||||
async setUserEmail(userName: string, email: string): Promise<void> {
|
||||
await withRetry(() =>
|
||||
this._client.hset(this.userInfoKey(userName), { email })
|
||||
);
|
||||
// 清除缓存
|
||||
userInfoCache?.delete(userName);
|
||||
}
|
||||
|
||||
async getEmailNotificationPreference(userName: string): Promise<boolean> {
|
||||
const userInfo = await this.getUserInfoV2(userName);
|
||||
return userInfo?.emailNotifications || false;
|
||||
}
|
||||
|
||||
async setEmailNotificationPreference(userName: string, enabled: boolean): Promise<void> {
|
||||
await withRetry(() =>
|
||||
this._client.hset(this.userInfoKey(userName), { emailNotifications: enabled.toString() })
|
||||
);
|
||||
// 清除缓存
|
||||
userInfoCache?.delete(userName);
|
||||
}
|
||||
}
|
||||
|
||||
// 单例 Upstash Redis 客户端
|
||||
|
||||
Reference in New Issue
Block a user