Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57a4b1fa84 | ||
|
|
c344351c8b | ||
|
|
5fe5274589 | ||
|
|
55110eebec | ||
|
|
71e100e0a5 | ||
|
|
a8c9ae3d81 | ||
|
|
ee7eb51517 | ||
|
|
ee5c4e35c9 | ||
|
|
c31d88228e | ||
|
|
7c7c077744 | ||
|
|
20e0167543 | ||
|
|
c75884b8eb | ||
|
|
f59fed2f84 | ||
|
|
5ea856bd30 | ||
|
|
1880d5080b | ||
|
|
f9ddd7b88e | ||
|
|
50f1142a53 | ||
|
|
a28fed97ff | ||
|
|
b2d42eae15 | ||
|
|
473654059f | ||
|
|
e4a8bfc510 | ||
|
|
cced5eb226 | ||
|
|
d50166720f | ||
|
|
f11c6ab7bf | ||
|
|
98968add7e | ||
|
|
a42f0cc93d | ||
|
|
cb07b18944 | ||
|
|
e7dfed2627 | ||
|
|
bf5fb39edc | ||
|
|
a1488ba665 | ||
|
|
f5afa3851f | ||
|
|
23bdf3d361 | ||
|
|
b2ca5d270e | ||
|
|
b65e77f9a9 | ||
|
|
5c8c8a6dc7 | ||
|
|
9552f532f1 | ||
|
|
33feb16dc3 | ||
|
|
7b839ad8f3 | ||
|
|
0198845ef0 | ||
|
|
b62deec46d | ||
|
|
a54ae4289b | ||
|
|
fb0aa227b6 | ||
|
|
a3f0962456 | ||
|
|
4d0fcfb3bd | ||
|
|
b0c01cac6a | ||
|
|
51850950ec | ||
|
|
74db58b65c | ||
|
|
4bf58e8c5a | ||
|
|
8926c09b3a | ||
|
|
868d7360ce | ||
|
|
99ffd812da | ||
|
|
0874f473d3 | ||
|
|
109ecac891 | ||
|
|
6cf3d04a9d | ||
|
|
1ed7b330f3 | ||
|
|
cc471966d5 | ||
|
|
d51396a684 | ||
|
|
857301f0f3 | ||
|
|
774164c2a3 | ||
|
|
5d3e678d0b | ||
|
|
b29fe40e6d | ||
|
|
8141608c76 | ||
|
|
bd0410d4d9 | ||
|
|
f7d3fdbfb2 | ||
|
|
0248237cbc | ||
|
|
81a8395efb | ||
|
|
a02972cc66 | ||
|
|
4ed1d2ba18 | ||
|
|
79450920fb | ||
|
|
f2ef215c52 | ||
|
|
93c1500e5b | ||
|
|
3861b7681d | ||
|
|
d5893c79f9 | ||
|
|
2cf5c7e4f2 | ||
|
|
72a93e81ed | ||
|
|
82d1694781 | ||
|
|
745f01e280 | ||
|
|
8b563c50d6 | ||
|
|
4e83d653fa | ||
|
|
923bdb8f9b | ||
|
|
cf5a3497cc | ||
|
|
be6bf380a6 | ||
|
|
8c9361fe8b | ||
|
|
4d07ce6ab9 | ||
|
|
3324bd0eb6 | ||
|
|
24a4e93f09 | ||
|
|
53193938e5 | ||
|
|
6d0616ec5b | ||
|
|
aa7aeb0e8f | ||
|
|
4c7ea54080 | ||
|
|
79f6e6a417 | ||
|
|
395d177fe3 | ||
|
|
0c8ebdd252 | ||
|
|
f4caf20670 | ||
|
|
9d980657df | ||
|
|
dcb4a9d94d | ||
|
|
e505e32aab | ||
|
|
927c09f5de | ||
|
|
4b209475d2 | ||
|
|
e213e772ab | ||
|
|
b77e3bf489 | ||
|
|
e2635ae1e5 | ||
|
|
d76c9ea422 | ||
|
|
cd0657cceb | ||
|
|
9a55aad66a | ||
|
|
efd90cc62e | ||
|
|
bb06ec508f | ||
|
|
0b94723ee0 | ||
|
|
fb5d0c780b | ||
|
|
454218193a | ||
|
|
dd3b85868b | ||
|
|
0ac1aeb4be | ||
|
|
b2349f3b17 | ||
|
|
40d73a27b6 | ||
|
|
13d75c68a1 | ||
|
|
cc00632fd1 | ||
|
|
ba5a1db942 | ||
|
|
c742bf304c | ||
|
|
e9089ac752 | ||
|
|
97f7edbc7a | ||
|
|
4f5ee072ae | ||
|
|
dad7050f5f | ||
|
|
b11fe15378 | ||
|
|
5c1348d7d6 | ||
|
|
360f238687 | ||
|
|
cbd7417ed3 | ||
|
|
5b44e39eec | ||
|
|
0be13e559c | ||
|
|
c6a7eae348 | ||
|
|
66134f5546 | ||
|
|
d28169c6e9 | ||
|
|
db1ad4ea76 | ||
|
|
4e45de4fa2 | ||
|
|
f876825c3b | ||
|
|
0e26618d19 | ||
|
|
57bacc5590 | ||
|
|
1e7d0d5a18 | ||
|
|
9c42813d27 | ||
|
|
3c25e47a31 | ||
|
|
75cad02641 | ||
|
|
b3ba26b6e3 | ||
|
|
cee724d733 | ||
|
|
d4202e9166 | ||
|
|
e7c3e9efb2 | ||
|
|
a3404103ac | ||
|
|
d7c9bf46eb | ||
|
|
40a550a08f | ||
|
|
1d2c213f82 | ||
|
|
0e86c438ea | ||
|
|
0d028a0616 | ||
|
|
3dc51a9648 | ||
|
|
559bdbc2a0 | ||
|
|
309465768f | ||
|
|
e6a836e586 | ||
|
|
ccc9b69f65 | ||
|
|
608051d439 | ||
|
|
c65fd8d846 | ||
|
|
da2282ab63 | ||
|
|
8e49080e10 | ||
|
|
8e03c45af5 | ||
|
|
34caca02c9 | ||
|
|
6fa242be3f | ||
|
|
0c5d2c34ce | ||
|
|
44e479e4cd | ||
|
|
5ca5c14a04 | ||
|
|
1f52ccbe0e | ||
|
|
4e67418dec | ||
|
|
fff98beed3 | ||
|
|
4e7ac21be0 | ||
|
|
22a203658e | ||
|
|
5e666ee1eb | ||
|
|
30983b3f14 | ||
|
|
17a52c1532 | ||
|
|
5300cf6c15 | ||
|
|
5ffbf572ae | ||
|
|
b93dbf519b | ||
|
|
e3574f1ac9 | ||
|
|
2dbbd63abe | ||
|
|
e6400f67fe | ||
|
|
dd9d34d7e7 | ||
|
|
79a0cacc0f | ||
|
|
7084abe9b7 | ||
|
|
c89f335238 | ||
|
|
5637d9ed46 | ||
|
|
d1d926df16 | ||
|
|
02b0e2e901 | ||
|
|
0463d81122 | ||
|
|
7dfc673e58 | ||
|
|
0779b5d8b5 | ||
|
|
6ed6757ecd | ||
|
|
295e6e3ec7 | ||
|
|
d9ee24851f | ||
|
|
818d147b5c | ||
|
|
11f5b96d44 | ||
|
|
ed5a128946 | ||
|
|
32efc25fe1 | ||
|
|
931446d4b1 | ||
|
|
030758afbf | ||
|
|
f69d2e587a | ||
|
|
bbf8bbbfcc | ||
|
|
cd965043ce | ||
|
|
e0ae6fa4fb | ||
|
|
b19e0120ae | ||
|
|
61ef55ddf2 | ||
|
|
cf0406cfda | ||
|
|
bdbc3b8029 | ||
|
|
99eaf29c8c | ||
|
|
a581f22c6b | ||
|
|
8dbd8b4b48 | ||
|
|
ebc6554395 | ||
|
|
2836ed2a12 | ||
|
|
60afce75d1 | ||
|
|
ac7f8d14c0 | ||
|
|
1122cfda2f | ||
|
|
4ae172ba12 | ||
|
|
344604f6cb | ||
|
|
8941e43696 | ||
|
|
deb75fc47e | ||
|
|
64b4b08b86 | ||
|
|
3bc4aaf125 | ||
|
|
ae80791fe9 | ||
|
|
86a30cedc9 | ||
|
|
0772b57156 | ||
|
|
7deab7fd42 | ||
|
|
119c263500 | ||
|
|
de61abaaf4 | ||
|
|
d6e3cb1e5d | ||
|
|
29f3428b41 | ||
|
|
bce6518ca6 | ||
|
|
b58543cfec | ||
|
|
f93ecb1d7b | ||
|
|
f590f23523 | ||
|
|
76bca1946f | ||
|
|
f37c4d57be | ||
|
|
01283a0af0 | ||
|
|
03c5ee027f | ||
|
|
1f8163bb94 | ||
|
|
5245b01271 | ||
|
|
7eadfab7c9 | ||
|
|
d07f4a5382 | ||
|
|
ac2349135f | ||
|
|
cc43c0e0ed | ||
|
|
38f0d75ef6 | ||
|
|
279f005d92 | ||
|
|
1758383222 |
@@ -25,13 +25,9 @@ jobs:
|
||||
KV_NAMESPACE_ID: ${{ secrets.KV_NAMESPACE_ID || vars.KV_NAMESPACE_ID }}
|
||||
R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME || vars.R2_BUCKET_NAME }}
|
||||
PROJECT_LINK: ${{ secrets.PROJECT_LINK || vars.PROJECT_LINK }}
|
||||
AI_MODEL: ${{ secrets.AI_MODEL || vars.AI_MODEL || '@cf/meta/llama-3.1-8b-instruct' }}
|
||||
AI_MODEL: ${{ secrets.AI_MODEL || vars.AI_MODEL || '@cf/meta/llama-3.1-8b-instruct-fast' }}
|
||||
CLOUDFLARE_EMAIL: ${{ secrets.CF_EMAIL || vars.CF_EMAIL || false }}
|
||||
ANALYSIS_CACHE: ${{ secrets.ANALYSIS_CACHE || vars.ANALYSIS_CACHE || false }}
|
||||
LINUXDO_CLIENT_ID: ${{ secrets.LINUXDO_CLIENT_ID || vars.LINUXDO_CLIENT_ID }}
|
||||
LINUXDO_CLIENT_SECRET: ${{ secrets.LINUXDO_CLIENT_SECRET || vars.LINUXDO_CLIENT_SECRET }}
|
||||
LINUXDO_CALLBACK_URL: ${{ secrets.LINUXDO_CALLBACK_URL || vars.LINUXDO_CALLBACK_URL }}
|
||||
LINUXDO_SWITCH: ${{ secrets.LINUXDO_SWITCH || vars.LINUXDO_SWITCH }}
|
||||
|
||||
|
||||
outputs:
|
||||
@@ -45,12 +41,12 @@ jobs:
|
||||
- name: ⚙ 设置 pnpm / Set up pnpm
|
||||
uses: pnpm/[email protected]
|
||||
with:
|
||||
version: 9
|
||||
version: 11
|
||||
|
||||
- name: ⚙ 设置 Node.js / Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "24"
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "./mail-worker/pnpm-lock.yaml"
|
||||
|
||||
@@ -107,10 +103,6 @@ jobs:
|
||||
sed -i '/^project_link = /d' "$CONFIG_FILE"
|
||||
fi
|
||||
|
||||
if [ -z "$LINUXDO_CLIENT_ID" ] || [ -z "$LINUXDO_CLIENT_SECRET" ]; then
|
||||
sed -i '/^linuxdo_client_id = /,/^linuxdo_switch = /d' "$CONFIG_FILE"
|
||||
fi
|
||||
|
||||
if [ -z "$CUSTOM_DOMAIN" ]; then
|
||||
sed -i '/\[\[routes\]\]/,/^$/d' "$CONFIG_FILE"
|
||||
fi
|
||||
@@ -135,7 +127,6 @@ jobs:
|
||||
sed -i "s|\${LINUXDO_CLIENT_ID}|${LINUXDO_CLIENT_ID}|g" "$CONFIG_FILE"
|
||||
sed -i "s|\${LINUXDO_CLIENT_SECRET}|${LINUXDO_CLIENT_SECRET}|g" "$CONFIG_FILE"
|
||||
sed -i "s|\${LINUXDO_CALLBACK_URL}|${LINUXDO_CALLBACK_URL}|g" "$CONFIG_FILE"
|
||||
sed -i "s|\${LINUXDO_SWITCH}|${LINUXDO_SWITCH}|g" "$CONFIG_FILE"
|
||||
|
||||
echo "✅ Environment setup completed."
|
||||
|
||||
|
||||
@@ -25,3 +25,4 @@ mail-vue/node_modules
|
||||
mail-vue/dist
|
||||
.wrangler
|
||||
.venv
|
||||
git-tools.js
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2025 eoao
|
||||
Copyright (c) 2025 aslost
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
## Github Action 部署
|
||||
|
||||
**配置 Github 仓库**
|
||||
|
||||
1. Fork 或克隆仓库 [https://github.com/eoao/cloud-mail](https://github.com/eoao/cloud-mail)
|
||||
2. 进入您的 GitHub 仓库设置
|
||||
3. 转到 Settings → Secrets and variables → Actions → New Repository secrets
|
||||
4. 添加以下 Secrets:
|
||||
|
||||
| Secret 名称 | 必需 | 用途 |
|
||||
| ----------------------- | :--: | ----------------------------------------------------- |
|
||||
| `CLOUDFLARE_API_TOKEN` | ✅ | Cloudflare API 令牌(需要 Workers 和相关资源权限) |
|
||||
| `CLOUDFLARE_ACCOUNT_ID` | ✅ | Cloudflare 账户 ID |
|
||||
| `D1_DATABASE_ID` | ✅ | 您的 D1 数据库的 ID |
|
||||
| `KV_NAMESPACE_ID` | ✅ | 您的 KV 命名空间的 ID |
|
||||
| `R2_BUCKET_NAME` | ✅ | 您的 R2 存储桶的名称 |
|
||||
| `DOMAIN` | ✅ | 您要用于邮件服务的域名(例如 `["xx.xx"],多域名用,分隔`) |
|
||||
| `ADMIN` | ✅ | 您的管理员邮箱地址(例如 `[email protected]`) |
|
||||
| `JWT_SECRET` | ✅ | 用于生成和验证 JWT 的随机长字符串 |
|
||||
| `INIT_URL` | ❌ | (可选)部署后用于初始化数据库的 Worker URL(格式参考下述手动初始化) |
|
||||
|
||||
---
|
||||
|
||||
**获取 Cloudflare API 令牌**
|
||||
|
||||
1. 访问 [Cloudflare Dashboard](https://dash.cloudflare.com/profile/api-tokens)
|
||||
2. 创建新的 API 令牌
|
||||
3. 选择"编辑 Cloudflare Workers"模板,并参照下表添加相应权限
|
||||

|
||||
4. 保存令牌并复制到 GitHub Secrets 中的 `CLOUDFLARE_API_TOKEN`
|
||||
|
||||
**获取 Cloudflare 账户 ID**
|
||||
1. 账户 ID 可以在 Cloudflare 仪表盘的账户设置中找到。
|
||||
2. 复制到 GitHub Secrets 中的 `CLOUDFLARE_ACCOUNT_ID`
|
||||
|
||||
**运行工作流**
|
||||
1. 然后在Action页面手动运行工作流,后续同步上游后会自动部署到 Cloudflare Workers。如未配置 `INIT_URL`,则需要手动访问 `https://你的项目域名/api/init/你的jwt_secret` 进行数据库初始化。
|
||||
2. 自动同步上游可使用bot或者手动点击Sync Upstream按钮。
|
||||
@@ -12,7 +12,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify/vue": "^4.3.0",
|
||||
"@simplewebauthn/browser": "^13.3.0",
|
||||
"@vueuse/components": "^14.1.0",
|
||||
"@vueuse/core": "^12.0.0",
|
||||
"axios": "1.15.2",
|
||||
|
||||
Generated
+4
-15
@@ -11,9 +11,6 @@ importers:
|
||||
'@iconify/vue':
|
||||
specifier: ^4.3.0
|
||||
version: 4.3.0([email protected])
|
||||
'@simplewebauthn/browser':
|
||||
specifier: ^13.3.0
|
||||
version: 13.3.0
|
||||
'@vueuse/components':
|
||||
specifier: ^14.1.0
|
||||
version: 14.1.0([email protected])
|
||||
@@ -22,7 +19,7 @@ importers:
|
||||
version: 12.8.2
|
||||
axios:
|
||||
specifier: 1.15.2
|
||||
version: 1.15.2([email protected])
|
||||
version: 1.15.2
|
||||
compressorjs:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
@@ -1064,9 +1061,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@simplewebauthn/[email protected]':
|
||||
resolution: {integrity: sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==}
|
||||
|
||||
'@surma/[email protected]':
|
||||
resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==}
|
||||
|
||||
@@ -2429,7 +2423,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-LvyteQoXeQiuILbzqv13LbyBna/TEv2Ha+4ZWK2AwGHUzZ8+IBaZS0TJkCgn5izSPLcgZwXy9yyTrewCb2u/MA==}
|
||||
engines: {node: '>= 16'}
|
||||
deprecated: This version is NOT deprecated. Previous deprecation was a mistake.
|
||||
peerDependencies:
|
||||
vue: ^3.0.0
|
||||
|
||||
@@ -3507,8 +3500,6 @@ snapshots:
|
||||
'@rollup/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@simplewebauthn/[email protected]': {}
|
||||
|
||||
'@surma/[email protected]':
|
||||
dependencies:
|
||||
ejs: 3.1.10
|
||||
@@ -3713,9 +3704,9 @@ snapshots:
|
||||
dependencies:
|
||||
possible-typed-array-names: 1.1.0
|
||||
|
||||
[email protected]([email protected]):
|
||||
[email protected]:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.11([email protected])
|
||||
follow-redirects: 1.15.11
|
||||
form-data: 4.0.5
|
||||
proxy-from-env: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
@@ -4087,9 +4078,7 @@ snapshots:
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
|
||||
[email protected]([email protected]):
|
||||
optionalDependencies:
|
||||
debug: 4.4.1
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
packages:
|
||||
- '.'
|
||||
allowBuilds:
|
||||
'@parcel/watcher': true
|
||||
esbuild: true
|
||||
vue-demi: true
|
||||
@@ -38,16 +38,17 @@
|
||||
:key="keyCount"
|
||||
>
|
||||
<template #default="{ data: item, index }" >
|
||||
<div :class="'email-row ' + props.type"
|
||||
<div :class="['email-row', props.type, { 'right-checked': item.rightChecked }]"
|
||||
:data-checked="item.checked"
|
||||
@click="jumpDetails(item)"
|
||||
v-if="!item.expand"
|
||||
:key="item.emailId"
|
||||
@contextmenu="handleContextmenu($event, item)"
|
||||
:style="item.rightChecked ? 'background: #FDF6EC' : ''"
|
||||
>
|
||||
<el-checkbox :class=" props.type === 'all-email' ? 'all-email-checkbox' : 'checkbox'"
|
||||
v-model="item.checked" @click.stop></el-checkbox>
|
||||
v-model="item.checked"
|
||||
:disabled="!item.checked && isSelectMax"
|
||||
@click.stop></el-checkbox>
|
||||
<div @click.stop="starChange(item)" class="pc-star" v-if="showStar">
|
||||
<Icon v-if="item.isStar" icon="fluent-color:star-16" width="20" height="20"/>
|
||||
<Icon v-else icon="solar:star-line-duotone" width="18" height="18"/>
|
||||
@@ -89,7 +90,7 @@
|
||||
</slot>
|
||||
</span>
|
||||
</span>
|
||||
<span class="email-content">{{ item.formatText || '\u200B' }}</span>
|
||||
<span class="email-content">{{ item.text || '\u200B' }}</span>
|
||||
</div>
|
||||
<div class="user-info" v-if="showUserInfo">
|
||||
<div class="user">
|
||||
@@ -324,7 +325,9 @@ const dropdownRef = ref(null);
|
||||
const dropdownCloseLock = ref(false);
|
||||
const dropdownShow = ref(false);
|
||||
const rightClickEmail = ref({});
|
||||
const MAX_SELECT_COUNT = 95;
|
||||
const checkedEmailCount = ref(0);
|
||||
const isSelectMax = computed(() => checkedEmailCount.value >= MAX_SELECT_COUNT);
|
||||
let timer = null
|
||||
const position = ref(
|
||||
DOMRect.fromRect({
|
||||
@@ -551,37 +554,6 @@ const accountShow = computed(() => {
|
||||
return uiStore.accountShow && settingStore.settings.manyEmail === 0
|
||||
})
|
||||
|
||||
function htmlToText(email) {
|
||||
if (email.content) {
|
||||
|
||||
const tempDiv = document.createElement('div');
|
||||
|
||||
tempDiv.innerHTML = email.content.replace(
|
||||
/<(img|iframe|object|embed|video|audio|source|link)[^>]*>/gi, ''
|
||||
);
|
||||
|
||||
const scriptsAndStyles = tempDiv.querySelectorAll('script, style, title');
|
||||
scriptsAndStyles.forEach(el => el.remove());
|
||||
let text = tempDiv.textContent || tempDiv.innerText || '';
|
||||
text = text.replace(/\s+/g, ' ').trim();
|
||||
return cleanSpace(text)
|
||||
}
|
||||
|
||||
if (email.text) {
|
||||
return cleanSpace(email.text)
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function cleanSpace(text) {
|
||||
return text
|
||||
.replace(/[\u200B-\u200F\uFEFF\u034F\u200B-\u200F\u00A0\u3000\u00AD]/g, '')// 移除零宽空格
|
||||
.replace(/\s+/g, ' ') // 多空白合并成一个空格
|
||||
.trim();
|
||||
}
|
||||
|
||||
function starChange(email) {
|
||||
|
||||
if (!email.isStar) {
|
||||
@@ -731,7 +703,6 @@ function addItem(email) {
|
||||
return false;
|
||||
}
|
||||
|
||||
email.formatText = htmlToText(email);
|
||||
email.formatCreateTime = fromNow(email.formatCreateTime);
|
||||
|
||||
if (props.timeSort) {
|
||||
@@ -770,7 +741,19 @@ function addItem(email) {
|
||||
}
|
||||
|
||||
function handleCheckAllChange(val) {
|
||||
emailList.forEach(item => item.checked = val);
|
||||
if (val) {
|
||||
let count = 0;
|
||||
emailList.forEach(item => {
|
||||
if (count < MAX_SELECT_COUNT) {
|
||||
item.checked = true;
|
||||
count++;
|
||||
} else {
|
||||
item.checked = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
emailList.forEach(item => item.checked = false);
|
||||
}
|
||||
isIndeterminate.value = false;
|
||||
}
|
||||
|
||||
@@ -786,8 +769,9 @@ function getSelectedDraftsIds() {
|
||||
function updateCheckStatus() {
|
||||
const checkedCount = emailList.filter(item => item.checked).length;
|
||||
checkedEmailCount.value = checkedCount;
|
||||
checkAll.value = checkedCount === emailList.length;
|
||||
isIndeterminate.value = checkedCount > 0 && checkedCount < emailList.length;
|
||||
const atMax = checkedCount >= MAX_SELECT_COUNT;
|
||||
checkAll.value = emailList.length > 0 && (checkedCount === emailList.length || atMax);
|
||||
isIndeterminate.value = checkedCount > 0 && !checkAll.value;
|
||||
}
|
||||
|
||||
function jumpDetails(email) {
|
||||
@@ -872,7 +856,6 @@ function getEmailList(refresh = false) {
|
||||
|
||||
function handleList(list) {
|
||||
list.forEach(email => {
|
||||
email.formatText = htmlToText(email)
|
||||
email.formatCreateTime = fromNow(email.createTime);
|
||||
email.test = t('received')
|
||||
const statusIconMap = {
|
||||
@@ -1237,6 +1220,11 @@ function loadData() {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
&.right-checked,
|
||||
&.right-checked:hover {
|
||||
background-color: var(--email-right-click-background);
|
||||
}
|
||||
|
||||
/*&[data-checked="true"] {
|
||||
background-color: #c2dbff;
|
||||
}*/
|
||||
|
||||
+29
-21
@@ -30,6 +30,7 @@ const en = {
|
||||
change: 'Change',
|
||||
changePwdBtn: 'Change',
|
||||
username: 'Username',
|
||||
trustLevel: 'Level',
|
||||
password: 'Password',
|
||||
delAccount: 'Delete Account',
|
||||
delAccountMsg: 'This will permanently delete your account and data. It cannot be reactivated',
|
||||
@@ -134,25 +135,13 @@ const en = {
|
||||
hasAccount: 'Already have an account? \u200B',
|
||||
regSwitch: 'Sign up',
|
||||
loginSwitch: 'Sign in',
|
||||
websiteSetting: 'Website',
|
||||
websiteSetting: 'General',
|
||||
websiteReg: 'Sign Up',
|
||||
loginDomain: 'Hide Sign-In Domain',
|
||||
passkeyLogin: 'Passkey',
|
||||
passkeySetting: 'Passkey',
|
||||
passkeyNotConfigured: 'No Passkey configured',
|
||||
passkeyConfigured: 'Passkey configured',
|
||||
passkeyCreatedAt: 'Created: {time}',
|
||||
passkeyCreate: 'Create Passkey',
|
||||
passkeyReplace: 'Replace',
|
||||
passkeyDelete: 'Delete Passkey',
|
||||
passkeyCurrentPassword: 'Enter your current password',
|
||||
passkeyUnsupported: 'Passkeys are not supported by this browser or device',
|
||||
passkeyLoginFailed: 'Passkey verification failed. Please try again',
|
||||
passkeyCreateSuccess: 'Passkey created successfully',
|
||||
passkeyDeleteSuccess: 'Passkey deleted successfully',
|
||||
passkeyDeleteConfirm: 'Delete the current Passkey?',
|
||||
syncDelete: 'Sync Delete',
|
||||
syncDeleteDesc: 'Permanently delete emails and email addresses instead of marking them as deleted.',
|
||||
multipleEmail: 'Multiple Email Address',
|
||||
multipleEmailDesc: 'Enable this feature to allow users to add multiple email',
|
||||
multipleEmailDesc: 'Enable this feature to allow users to add multiple addresses.',
|
||||
customization: 'Customization',
|
||||
websiteTitle: 'Title',
|
||||
loginBoxOpacity: 'Login Box Opacity',
|
||||
@@ -165,6 +154,13 @@ const en = {
|
||||
resendToken: 'Resend Token',
|
||||
cloudflareEmailSending: 'Cloudflare Email Sending',
|
||||
blackList: 'Blocklist',
|
||||
autoClean: 'Auto Cleanup',
|
||||
autoCleanDesc: 'Automatically delete emails older than the configured days. 0 means disabled.',
|
||||
autoCleanDays: 'Retention Days',
|
||||
autoCleanRetain: 'Retain {days} days',
|
||||
autoCleanExclude: 'Excluded Users',
|
||||
autoCleanExcludeDesc: 'Enter user emails that should not be cleaned up',
|
||||
dayUnit: ' days',
|
||||
codeRecognition: 'Verification Code Recognition',
|
||||
codeRecognitionRules: 'Recognition Rules',
|
||||
codeRecognitionRulesDesc: 'Only recognize emails containing the following info. Empty means all.',
|
||||
@@ -176,6 +172,13 @@ const en = {
|
||||
disable: 'Disable',
|
||||
disabled: 'Disabled',
|
||||
otherEmail: 'Forwarding to External Email',
|
||||
webhook: 'Webhook',
|
||||
webhookDesc: 'POST received emails as JSON to this URL',
|
||||
webhookUrl: 'Request URL',
|
||||
webhookIpNotSupported: 'IP addresses are not supported. Use a domain name.',
|
||||
webhookSecret: 'Secret (Optional)',
|
||||
webhookRetry: 'Retries',
|
||||
webhookFormat: 'Payload format',
|
||||
forwardingRules: 'Forwarding Rules',
|
||||
forwardAll: 'All',
|
||||
rules: 'Rules',
|
||||
@@ -189,12 +192,12 @@ const en = {
|
||||
addResendTokenDesc: 'Input to add; leave empty to delete',
|
||||
addOsDomain: 'Add Domain',
|
||||
domainDesc: 'Domain',
|
||||
addTurnstileSecret: 'Add turnstile secret',
|
||||
addTurnstileSecret: 'Add Turnstile secret',
|
||||
backgroundTitle: 'Change Background',
|
||||
tgBotDesc: 'Forward received emails to a Telegram bot',
|
||||
tgBotToken: 'Bot token',
|
||||
toBotTokenDesc: 'Multiple user chat_ids, separated by commas',
|
||||
otherEmailDesc: 'emails can be forwarded to external email, but must be verified via cloudflare',
|
||||
otherEmailDesc: 'Emails can be forwarded to an external address, but must be verified via Cloudflare.',
|
||||
otherEmailInputDesc: 'Separate multiple email addresses with commas',
|
||||
forwardingRulesDesc: 'Rule-based forwarding only forwards emails received by the specified address',
|
||||
ruleEmailsInputDesc: 'Separate multiple email addresses with commas',
|
||||
@@ -265,7 +268,7 @@ const en = {
|
||||
timeoutErrorMsg: 'Timeout. Try again later',
|
||||
serverBusyErrorMsg: 'Server busy. Please try again later',
|
||||
reqFailErrorMsg: 'Request failed. Try again later',
|
||||
message: 'Message',
|
||||
message: 'Mail',
|
||||
language: 'Language',
|
||||
totalUserAccount: '{msg}',
|
||||
sendBanned: 'Banned',
|
||||
@@ -279,13 +282,14 @@ const en = {
|
||||
availableDomains: 'Available Domains',
|
||||
availableDomainsDesc: 'Restrict users to email domains specified. Domains not on the approved list will be blocked from registration, adding email addresses, and sending/receiving emails. If left blank, all domains will be allowed by default',
|
||||
backgroundUrlDesc: 'Image URL',
|
||||
localUpload: ' Local upload',
|
||||
localUpload: 'Local upload',
|
||||
imageLink: 'Image URL',
|
||||
imageLinkErrorMsg: 'Invalid image URL',
|
||||
backgroundWarning: 'Image file size affects website load speed',
|
||||
rulesVerify: 'Rules',
|
||||
rulesVerifyTitle: 'Trigger After {count} Daily Uses per IP',
|
||||
botVerifyMsg: 'Please verify that you are human',
|
||||
oauthLogin: 'OAuth2',
|
||||
noticeTitle: 'Notice',
|
||||
noticePopup: 'Sign-In Popup',
|
||||
icon: 'Icon',
|
||||
@@ -350,7 +354,11 @@ const en = {
|
||||
searchUser: 'Search by user',
|
||||
searchEmail: 'Search by Email',
|
||||
searchSender: 'Search by Sender',
|
||||
userEmail: 'Email Address'
|
||||
userEmail: 'Email Address',
|
||||
oauthSetting: 'OAuth',
|
||||
clientId: 'Client ID',
|
||||
clientSecret: 'Client Secret',
|
||||
notOwner: 'Base email does not belong to you',
|
||||
}
|
||||
|
||||
export default en
|
||||
|
||||
+24
-16
@@ -30,6 +30,7 @@ const zh = {
|
||||
change: '修改',
|
||||
changePwdBtn: '修改密码',
|
||||
username: '用户名',
|
||||
trustLevel: '等级',
|
||||
password: '密码',
|
||||
delAccount: '删除账户',
|
||||
delAccountMsg: '此操作将永久删除您的账户及其所有数据,无法恢复',
|
||||
@@ -134,23 +135,11 @@ const zh = {
|
||||
hasAccount: '已有账号?',
|
||||
regSwitch: '创建账号',
|
||||
loginSwitch: '去登录',
|
||||
websiteSetting: '网站设置',
|
||||
websiteSetting: '基本设置',
|
||||
websiteReg: '用户注册',
|
||||
loginDomain: '隐藏登录域名',
|
||||
passkeyLogin: 'Passkey 登录',
|
||||
passkeySetting: 'Passkey',
|
||||
passkeyNotConfigured: '尚未创建 Passkey',
|
||||
passkeyConfigured: '已创建 Passkey',
|
||||
passkeyCreatedAt: '创建时间:{time}',
|
||||
passkeyCreate: '创建 Passkey',
|
||||
passkeyReplace: '重新创建',
|
||||
passkeyDelete: '删除 Passkey',
|
||||
passkeyCurrentPassword: '请输入当前密码',
|
||||
passkeyUnsupported: '当前浏览器或设备不支持 Passkey',
|
||||
passkeyLoginFailed: 'Passkey 验证失败,请重试',
|
||||
passkeyCreateSuccess: 'Passkey 创建成功',
|
||||
passkeyDeleteSuccess: 'Passkey 删除成功',
|
||||
passkeyDeleteConfirm: '确认删除当前 Passkey?',
|
||||
syncDelete: '同步删除',
|
||||
syncDeleteDesc: '开启后收件箱和邮箱不再假删',
|
||||
multipleEmail: '多号模式',
|
||||
multipleEmailDesc: '开启后账号栏出现一个用户可以添加多个邮箱',
|
||||
customization: '个性化设置',
|
||||
@@ -165,6 +154,13 @@ const zh = {
|
||||
resendToken: 'Resend Token',
|
||||
cloudflareEmailSending: 'Cloudflare 邮件发送',
|
||||
blackList: '邮件黑名单',
|
||||
autoClean: '自动清理',
|
||||
autoCleanDesc: '自动删除超过指定天数的邮件,0 表示关闭',
|
||||
autoCleanDays: '保留天数',
|
||||
autoCleanRetain: '保留{days}天',
|
||||
autoCleanExclude: '排除用户',
|
||||
autoCleanExcludeDesc: '输入用户邮箱,这些用户的邮件不会被清理',
|
||||
dayUnit: '天',
|
||||
codeRecognition: '识别验证码',
|
||||
codeRecognitionRules: '识别规则',
|
||||
codeRecognitionRulesDesc: '只识别包含以下信息的邮件,为空识别全部',
|
||||
@@ -176,6 +172,13 @@ const zh = {
|
||||
disable: '关闭',
|
||||
disabled: '已关闭',
|
||||
otherEmail: '第三方邮箱',
|
||||
webhook: 'Webhook',
|
||||
webhookDesc: '收到邮件后向指定地址 POST JSON',
|
||||
webhookUrl: '请求地址',
|
||||
webhookIpNotSupported: '不能使用 IP 地址,请填写域名',
|
||||
webhookSecret: '密钥(可选)',
|
||||
webhookRetry: '重试次数',
|
||||
webhookFormat: '格式说明',
|
||||
forwardingRules: '转发规则',
|
||||
forwardAll: '全部转发',
|
||||
rules: '规则转发',
|
||||
@@ -286,6 +289,7 @@ const zh = {
|
||||
rulesVerify: '规则',
|
||||
rulesVerifyTitle: 'IP 每天使用 {count} 次后触发',
|
||||
botVerifyMsg: '请完成人机验证',
|
||||
oauthLogin: 'OAuth2',
|
||||
noticeTitle: '网站公告',
|
||||
noticePopup: '登录弹窗',
|
||||
icon: '图标',
|
||||
@@ -350,6 +354,10 @@ const zh = {
|
||||
searchUser: '搜索用户',
|
||||
searchEmail: '搜索邮箱',
|
||||
searchSender: '搜索发件人',
|
||||
userEmail: '用户邮箱'
|
||||
userEmail: '用户邮箱',
|
||||
oauthSetting: 'OAuth',
|
||||
clientId: '客户端 ID',
|
||||
clientSecret: '客户端密钥',
|
||||
notOwner: '基础邮箱不属于您',
|
||||
}
|
||||
export default zh
|
||||
|
||||
@@ -295,6 +295,9 @@ addCollection({
|
||||
},
|
||||
"search": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"m17 17l4 4M3 11a8 8 0 1 0 16 0a8 8 0 0 0-16 0\"/>"
|
||||
},
|
||||
"arrow-up-right": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M6 19L19 6m0 0v12.48M19 6H6.52\"/>"
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -706,4 +709,18 @@ addCollection({
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
addCollection({
|
||||
"prefix": "devicon",
|
||||
"lastModified": 1774935148,
|
||||
"aliases": {},
|
||||
"width": 128,
|
||||
"height": 128,
|
||||
"icons": {
|
||||
"github": {
|
||||
"body": "<g fill=\"#181616\"><path fill-rule=\"evenodd\" d=\"M64 5.103c-33.347 0-60.388 27.035-60.388 60.388c0 26.682 17.303 49.317 41.297 57.303c3.017.56 4.125-1.31 4.125-2.905c0-1.44-.056-6.197-.082-11.243c-16.8 3.653-20.345-7.125-20.345-7.125c-2.747-6.98-6.705-8.836-6.705-8.836c-5.48-3.748.413-3.67.413-3.67c6.063.425 9.257 6.223 9.257 6.223c5.386 9.23 14.127 6.562 17.573 5.02c.542-3.903 2.107-6.568 3.834-8.076c-13.413-1.525-27.514-6.704-27.514-29.843c0-6.593 2.36-11.98 6.223-16.21c-.628-1.52-2.695-7.662.584-15.98c0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033c11.526-7.813 16.59-6.19 16.59-6.19c3.287 8.317 1.22 14.46.593 15.98c3.872 4.23 6.215 9.617 6.215 16.21c0 23.194-14.127 28.3-27.574 29.796c2.167 1.874 4.097 5.55 4.097 11.183c0 8.08-.07 14.583-.07 16.572c0 1.607 1.088 3.49 4.148 2.897c23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z\" clip-rule=\"evenodd\"/><path d=\"M26.484 91.806c-.133.3-.605.39-1.035.185c-.44-.196-.685-.605-.543-.906c.13-.31.603-.395 1.04-.188c.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28c-.396-.42-.47-.983-.177-1.254c.298-.266.844-.14 1.24.28c.394.426.472.984.17 1.255zm2.382 3.477c-.37.258-.976.017-1.35-.52c-.37-.538-.37-1.183.01-1.44c.373-.258.97-.025 1.35.507c.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23c-.527-.487-.674-1.18-.343-1.544c.336-.366 1.045-.264 1.564.23c.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486c-.683-.207-1.13-.76-.99-1.238c.14-.477.823-.7 1.512-.485c.683.206 1.13.756.988 1.237m4.943.361c.017.498-.563.91-1.28.92c-.723.017-1.308-.387-1.315-.877c0-.503.568-.91 1.29-.924c.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117c-.7.13-1.35-.172-1.44-.653c-.086-.498.422-.997 1.122-1.126c.714-.123 1.354.17 1.444.663zm0 0\"/></g>"
|
||||
},
|
||||
"google": {
|
||||
"body": "<path fill=\"#fff\" d=\"M44.59 4.21a63.28 63.28 0 0 0 4.33 120.9a67.6 67.6 0 0 0 32.36.35a57.13 57.13 0 0 0 25.9-13.46a57.44 57.44 0 0 0 16-26.26a74.3 74.3 0 0 0 1.61-33.58H65.27v24.69h34.47a29.72 29.72 0 0 1-12.66 19.52a36.2 36.2 0 0 1-13.93 5.5a41.3 41.3 0 0 1-15.1 0A37.2 37.2 0 0 1 44 95.74a39.3 39.3 0 0 1-14.5-19.42a38.3 38.3 0 0 1 0-24.63a39.25 39.25 0 0 1 9.18-14.91A37.17 37.17 0 0 1 76.13 27a34.3 34.3 0 0 1 13.64 8q5.83-5.8 11.64-11.63c2-2.09 4.18-4.08 6.15-6.22A61.2 61.2 0 0 0 87.2 4.59a64 64 0 0 0-42.61-.38\"/><path fill=\"#e33629\" d=\"M44.59 4.21a64 64 0 0 1 42.61.37a61.2 61.2 0 0 1 20.35 12.62c-2 2.14-4.11 4.14-6.15 6.22Q95.58 29.23 89.77 35a34.3 34.3 0 0 0-13.64-8a37.17 37.17 0 0 0-37.46 9.74a39.25 39.25 0 0 0-9.18 14.91L8.76 35.6A63.53 63.53 0 0 1 44.59 4.21\"/><path fill=\"#f8bd00\" d=\"M3.26 51.5a63 63 0 0 1 5.5-15.9l20.73 16.09a38.3 38.3 0 0 0 0 24.63q-10.36 8-20.73 16.08a63.33 63.33 0 0 1-5.5-40.9\"/><path fill=\"#587dbd\" d=\"M65.27 52.15h59.52a74.3 74.3 0 0 1-1.61 33.58a57.44 57.44 0 0 1-16 26.26c-6.69-5.22-13.41-10.4-20.1-15.62a29.72 29.72 0 0 0 12.66-19.54H65.27c-.01-8.22 0-16.45 0-24.68\"/><path fill=\"#319f43\" d=\"M8.75 92.4q10.37-8 20.73-16.08A39.3 39.3 0 0 0 44 95.74a37.2 37.2 0 0 0 14.08 6.08a41.3 41.3 0 0 0 15.1 0a36.2 36.2 0 0 0 13.93-5.5c6.69 5.22 13.41 10.4 20.1 15.62a57.13 57.13 0 0 1-25.9 13.47a67.6 67.6 0 0 1-32.36-.35a63 63 0 0 1-23-11.59A63.7 63.7 0 0 1 8.75 92.4\"/>"
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -77,7 +77,7 @@
|
||||
</el-scrollbar>
|
||||
<el-dialog v-model="showAdd" :title="$t('addAccount')">
|
||||
<div class="container">
|
||||
<el-input v-model="addForm.email" ref="addRef" type="text" :placeholder="$t('emailAccount')" autocomplete="off">
|
||||
<el-input v-model="addForm.email" ref="addRef" type="text" :placeholder="$t('emailAccount')" autocomplete="off" @keyup.enter="submit">
|
||||
<template #append>
|
||||
<div @click.stop="openSelect">
|
||||
<el-select
|
||||
@@ -116,7 +116,7 @@
|
||||
</el-dialog>
|
||||
<el-dialog v-model="setNameShow" :title="$t('changeUserName')">
|
||||
<div class="container">
|
||||
<el-input v-model="accountName" type="text" :placeholder="$t('username')" autocomplete="off">
|
||||
<el-input v-model="accountName" type="text" :placeholder="$t('username')" autocomplete="off" @keyup.enter="setName">
|
||||
</el-input>
|
||||
<el-button class="btn" type="primary" @click="setName" :loading="setNameLoading"
|
||||
>{{ $t('save') }}
|
||||
@@ -229,6 +229,8 @@ function getSkeletonRows() {
|
||||
|
||||
function setName() {
|
||||
|
||||
if (setNameLoading.value) return
|
||||
|
||||
let name = accountName.value
|
||||
|
||||
if (name === account.name) {
|
||||
@@ -427,6 +429,8 @@ function getAccountList() {
|
||||
|
||||
function submit() {
|
||||
|
||||
if (addLoading.value) return
|
||||
|
||||
if (!addForm.email) {
|
||||
ElMessage({
|
||||
message: t('emptyEmailMsg'),
|
||||
@@ -482,7 +486,6 @@ function submit() {
|
||||
addLoading.value = true
|
||||
accountAdd(addForm.email + addForm.suffix, verifyToken).then(account => {
|
||||
addLoading.value = false
|
||||
showAdd.value = false
|
||||
addForm.email = ''
|
||||
accounts.push(account)
|
||||
verifyToken = ''
|
||||
@@ -493,6 +496,7 @@ function submit() {
|
||||
plain: true
|
||||
})
|
||||
verifyShow.value = false
|
||||
showAdd.value = false
|
||||
userStore.refreshUserInfo()
|
||||
}).catch(res => {
|
||||
if (res.code === 400) {
|
||||
|
||||
@@ -80,7 +80,7 @@ const routers = {
|
||||
}
|
||||
}],
|
||||
'setting:query': [{
|
||||
path: '/system-setting',
|
||||
path: '/system-settings',
|
||||
name: 'sys-setting',
|
||||
component: () => import('@/views/sys-setting/index.vue'),
|
||||
meta: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import http from '@/axios/index.js';
|
||||
|
||||
export function emailList(accountId, allReceive, emailId, timeSort, size, type) {
|
||||
return http.get('/email/list', {params: {accountId, allReceive, emailId, timeSort, size, type}})
|
||||
export function emailList(accountId, allReceive, emailId, timeSort, size, type, full) {
|
||||
return http.get('/email/list', {params: {accountId, allReceive, emailId, timeSort, size, type, full}})
|
||||
}
|
||||
|
||||
export function emailDelete(emailIds) {
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import http from '@/axios/index.js';
|
||||
|
||||
export function oauthLinuxDoLogin(code) {
|
||||
return http.post('/oauth/linuxDo/login',{code})
|
||||
export function oauthLinuxDoLogin(code, redirectUri) {
|
||||
return http.post('/oauth/linuxDo/login',{code, redirectUri})
|
||||
}
|
||||
|
||||
export function oauthGithubLogin(code, redirectUri) {
|
||||
return http.post('/oauth/github/login',{code, redirectUri})
|
||||
}
|
||||
|
||||
export function oauthGoogleLogin(code, redirectUri) {
|
||||
return http.post('/oauth/google/login',{code, redirectUri})
|
||||
}
|
||||
|
||||
export function oauthBindUser(form) {
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import http from '@/axios/index.js';
|
||||
|
||||
export function passkeyAuthOptions() {
|
||||
return http.post('/passkey/auth/options')
|
||||
}
|
||||
|
||||
export function passkeyAuthVerify(transactionId, response) {
|
||||
return http.post('/passkey/auth/verify', {transactionId, response})
|
||||
}
|
||||
|
||||
export function passkeyStatus() {
|
||||
return http.get('/my/passkey')
|
||||
}
|
||||
|
||||
export function passkeyRegOptions(password) {
|
||||
return http.post('/my/passkey/reg/options', {password})
|
||||
}
|
||||
|
||||
export function passkeyRegVerify(transactionId, response) {
|
||||
return http.post('/my/passkey/reg/verify', {transactionId, response})
|
||||
}
|
||||
|
||||
export function passkeyDelete(password) {
|
||||
return http.delete('/my/passkey', {data: {password}})
|
||||
}
|
||||
@@ -8,6 +8,6 @@ export function starCancel(emailId) {
|
||||
return http.delete('/star/cancel', {params: {emailId}})
|
||||
}
|
||||
|
||||
export function starList(emailId,size) {
|
||||
return http.get('/star/list', {params: {emailId,size}})
|
||||
export function starList(emailId,size,full) {
|
||||
return http.get('/star/list', {params: {emailId,size,full}})
|
||||
}
|
||||
@@ -22,7 +22,7 @@ const routes = [
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/message',
|
||||
path: '/mail',
|
||||
name: 'content',
|
||||
component: () => import('@/views/content/index.vue'),
|
||||
meta: {
|
||||
@@ -100,16 +100,16 @@ router.beforeEach((to, from, next) => {
|
||||
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
if (!token && to.name !== 'login') {
|
||||
if (!token && !to.path.startsWith('/login')) {
|
||||
return next({name: 'login'})
|
||||
}
|
||||
|
||||
if (!token && to.name === 'login') {
|
||||
if (!token && to.path.startsWith('/login')) {
|
||||
loadBackground(next)
|
||||
return
|
||||
}
|
||||
|
||||
if (token && to.name === 'login') {
|
||||
if (token && to.path.startsWith('/login')) {
|
||||
return next(from.path)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { EmailUnreadEnum } from '@/enums/email-enum.js'
|
||||
|
||||
export const useEmailStore = defineStore('email', {
|
||||
state: () => ({
|
||||
@@ -15,8 +16,62 @@ export const useEmailStore = defineStore('email', {
|
||||
showUnread: false
|
||||
},
|
||||
sendScroll: null,
|
||||
detailMap: {},
|
||||
}),
|
||||
persist: {
|
||||
pick: ['contentData'],
|
||||
},
|
||||
actions: {
|
||||
fetchList(request) {
|
||||
request(1).then(data => {
|
||||
const list = Array.isArray(data) ? data : data?.list
|
||||
this.applyFullList(list)
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
})
|
||||
return request(0)
|
||||
},
|
||||
applyFullList(list) {
|
||||
if (!list?.length) return
|
||||
const currentId = this.contentData.email?.emailId
|
||||
for (const item of list) {
|
||||
if (!item?.emailId) continue
|
||||
if (!item.attList) item.attList = []
|
||||
// 完整列表可能早于「标已读」返回,避免把本地已读状态盖回未读
|
||||
const prev = this.detailMap[item.emailId]
|
||||
const keepRead = prev?.unread === EmailUnreadEnum.READ
|
||||
|| (currentId === item.emailId && this.contentData.email?.unread === EmailUnreadEnum.READ)
|
||||
if (keepRead) {
|
||||
item.unread = EmailUnreadEnum.READ
|
||||
}
|
||||
this.detailMap[item.emailId] = item
|
||||
if (currentId && item.emailId === currentId) {
|
||||
this.contentData.email = item
|
||||
}
|
||||
}
|
||||
},
|
||||
toContentEmail(email) {
|
||||
const id = email?.emailId
|
||||
if (id && this.detailMap[id]) {
|
||||
return this.detailMap[id]
|
||||
}
|
||||
return {
|
||||
...email,
|
||||
emailId: id || 0,
|
||||
content: '',
|
||||
text: '',
|
||||
attList: [],
|
||||
recipient: email?.recipient || '[]',
|
||||
}
|
||||
},
|
||||
markListRead(emailId) {
|
||||
const scrolls = [this.emailScroll, this.starScroll, this.sendScroll]
|
||||
for (const scroll of scrolls) {
|
||||
const list = scroll?.emailList
|
||||
if (!list?.length) continue
|
||||
const item = list.find(e => e.emailId === emailId)
|
||||
if (item) item.unread = EmailUnreadEnum.READ
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -128,6 +128,7 @@ button, input, select, textarea {
|
||||
--el-text-color-regular: var(--el-text-color-primary);
|
||||
--email-scroll-content-color: rgba(25, 41, 59, 0.4);
|
||||
--email-hover-background: #F2F6FC;
|
||||
--email-right-click-background: #FDF6EC;
|
||||
--login-border: none;
|
||||
--form-desc-color: #71717a;
|
||||
--login-switch-color: #006be6;
|
||||
@@ -157,6 +158,7 @@ button, input, select, textarea {
|
||||
--choose-account-background: #39393A;
|
||||
--email-scroll-content-color: rgba(255, 255, 255, 0.3);
|
||||
--email-hover-background: #1D1E1F;
|
||||
--email-right-click-background: var(--email-hover-background);
|
||||
--login-border: none;
|
||||
--form-desc-color: #8D9095;
|
||||
--login-switch-color: rgb(102, 177, 255);
|
||||
|
||||
@@ -5,4 +5,17 @@ export function isEmail(email) {
|
||||
|
||||
export function isDomain(str) {
|
||||
return /^(?!:\/\/)([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/.test(str);
|
||||
}
|
||||
|
||||
export function isIpUrl(url) {
|
||||
if (!url) return false
|
||||
try {
|
||||
const { hostname } = new URL(url.startsWith('http') ? url : 'https://' + url)
|
||||
if (/^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/.test(hostname)) {
|
||||
return true
|
||||
}
|
||||
return hostname.includes(':')
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -62,10 +62,10 @@
|
||||
<el-dialog v-model="showBathDelete" :title="$t('clearEmail')" width="335"
|
||||
@closed="closedClear">
|
||||
<div class="clear-email">
|
||||
<el-input v-model="clearParams.sendName" :placeholder="$t('sender')"/>
|
||||
<el-input v-model="clearParams.subject" :placeholder="$t('subject')"/>
|
||||
<el-input v-model="clearParams.sendEmail" :placeholder="$t('sendEmailAddress')"/>
|
||||
<el-input v-model="clearParams.toEmail" :placeholder="$t('toEmail')"/>
|
||||
<el-input v-model="clearParams.sendName" :placeholder="$t('sender')" @keyup.enter="batchDelete"/>
|
||||
<el-input v-model="clearParams.subject" :placeholder="$t('subject')" @keyup.enter="batchDelete"/>
|
||||
<el-input v-model="clearParams.sendEmail" :placeholder="$t('sendEmailAddress')" @keyup.enter="batchDelete"/>
|
||||
<el-input v-model="clearParams.toEmail" :placeholder="$t('toEmail')" @keyup.enter="batchDelete"/>
|
||||
<el-date-picker popper-class="my-date-picker"
|
||||
v-model="clearTime"
|
||||
type="daterange"
|
||||
@@ -193,6 +193,8 @@ function openBathDelete() {
|
||||
|
||||
function batchDelete() {
|
||||
|
||||
if (clearLoading.value) return
|
||||
|
||||
if (clearTime.value) {
|
||||
clearParams.startTime = toUtc(clearTime.value[0]).format("YYYY-MM-DD HH:mm:ss")
|
||||
clearParams.endTime = toUtc(clearTime.value[1]).add(1, 'day').format("YYYY-MM-DD HH:mm:ss")
|
||||
@@ -280,7 +282,7 @@ function typeSelectChange() {
|
||||
}
|
||||
|
||||
function jumpContent(email) {
|
||||
emailStore.contentData.email = email
|
||||
emailStore.contentData.email = emailStore.toContentEmail(email)
|
||||
emailStore.contentData.delType = 'physics'
|
||||
emailStore.contentData.showStar = false
|
||||
emailStore.contentData.showReply = false
|
||||
@@ -289,7 +291,7 @@ function jumpContent(email) {
|
||||
|
||||
|
||||
function getEmailList(emailId, size) {
|
||||
return allEmailList({emailId, size, ...params})
|
||||
return emailStore.fetchList(full => allEmailList({emailId, size, full, ...params}))
|
||||
}
|
||||
|
||||
async function latest() {
|
||||
|
||||
@@ -34,11 +34,11 @@
|
||||
<el-alert v-if="email.status === 4" :closable="false" :title="$t('complained')" class="email-msg" type="warning" show-icon />
|
||||
<el-alert v-if="email.status === 5" :closable="false" :title="$t('delayed')" class="email-msg" type="warning" show-icon />
|
||||
</div>
|
||||
<el-scrollbar class="htm-scrollbar" :class="email.attList.length === 0 ? 'bottom-distance' : ''">
|
||||
<el-scrollbar class="htm-scrollbar" :class="!email.attList?.length ? 'bottom-distance' : ''">
|
||||
<ShadowHtml class="shadow-html" :html="formatImage(email.content)" v-if="email.content" />
|
||||
<pre v-else class="email-text" >{{email.text}}</pre>
|
||||
</el-scrollbar>
|
||||
<div class="att" v-if="email.attList.length > 0">
|
||||
<div class="att" v-if="email.attList?.length > 0">
|
||||
<div class="att-title">
|
||||
<span>{{$t('attachments')}}</span>
|
||||
<span>{{$t('attCount',{total: email.attList.length})}}</span>
|
||||
@@ -75,7 +75,7 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import ShadowHtml from '@/components/shadow-html/index.vue'
|
||||
import {reactive, ref, watch, onMounted, onUnmounted} from "vue";
|
||||
import {computed, reactive, ref, watch, onMounted, onUnmounted} from "vue";
|
||||
import {useRouter} from 'vue-router'
|
||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||
import {emailDelete, emailRead} from "@/request/email.js";
|
||||
@@ -98,7 +98,13 @@ const settingStore = useSettingStore();
|
||||
const accountStore = useAccountStore();
|
||||
const emailStore = useEmailStore();
|
||||
const router = useRouter()
|
||||
const email = emailStore.contentData.email
|
||||
const email = computed(() => emailStore.contentData.email || {
|
||||
emailId: 0,
|
||||
attList: [],
|
||||
content: '',
|
||||
text: '',
|
||||
recipient: '[]',
|
||||
})
|
||||
const showPreview = ref(false)
|
||||
const srcList = reactive([])
|
||||
|
||||
@@ -107,23 +113,67 @@ watch(() => accountStore.currentAccountId, () => {
|
||||
handleBack()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (emailStore.contentData.showUnread && email.unread === EmailUnreadEnum.UNREAD) {
|
||||
email.unread = EmailUnreadEnum.READ;
|
||||
emailRead([email.emailId]);
|
||||
let readRequesting = false
|
||||
|
||||
function tryMarkRead() {
|
||||
if (!emailStore.contentData.showUnread || readRequesting) return
|
||||
const current = email.value
|
||||
if (!current?.emailId || current.unread !== EmailUnreadEnum.UNREAD) return
|
||||
|
||||
// 等详情数据就绪(detailMap 已写入,或正文已有内容)再标已读
|
||||
const full = emailStore.detailMap[current.emailId]
|
||||
const detailReady = !!full || !!(current.content || current.text)
|
||||
if (!detailReady) return
|
||||
|
||||
readRequesting = true
|
||||
const emailId = current.emailId
|
||||
current.unread = EmailUnreadEnum.READ
|
||||
if (emailStore.detailMap[emailId]) {
|
||||
emailStore.detailMap[emailId].unread = EmailUnreadEnum.READ
|
||||
}
|
||||
emailStore.markListRead(emailId)
|
||||
emailRead([emailId]).finally(() => {
|
||||
readRequesting = false
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [
|
||||
email.value?.emailId,
|
||||
email.value?.content,
|
||||
email.value?.text,
|
||||
emailStore.detailMap[email.value?.emailId]
|
||||
],
|
||||
() => tryMarkRead(),
|
||||
{ flush: 'post' }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
tryMarkRead()
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
emailStore.contentData.showUnread = false;
|
||||
readRequesting = false
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
})
|
||||
|
||||
function handleKeyDown(event) {
|
||||
if (event.key !== 'Escape') return;
|
||||
if (showPreview.value) return;
|
||||
if (document.querySelector('.el-message-box')) return;
|
||||
const writeBox = document.querySelector('.write-box');
|
||||
if (writeBox && writeBox.offsetParent !== null) return;
|
||||
handleBack();
|
||||
}
|
||||
|
||||
function openReply() {
|
||||
uiStore.writerRef.openReply(email)
|
||||
uiStore.writerRef.openReply(email.value)
|
||||
}
|
||||
|
||||
function openForward() {
|
||||
uiStore.writerRef.openForward(email)
|
||||
uiStore.writerRef.openForward(email.value)
|
||||
}
|
||||
|
||||
function toMessage(message) {
|
||||
@@ -149,32 +199,33 @@ function isImage(filename) {
|
||||
}
|
||||
|
||||
function formateReceive(recipient) {
|
||||
if (!recipient) return ''
|
||||
recipient = JSON.parse(recipient)
|
||||
return recipient.map(item => item.address).join(', ')
|
||||
}
|
||||
|
||||
function changeStar() {
|
||||
if (email.isStar) {
|
||||
email.isStar = 0;
|
||||
starCancel(email.emailId).then(() => {
|
||||
email.isStar = 0;
|
||||
emailStore.cancelStarEmailId = email.emailId
|
||||
if (email.value.isStar) {
|
||||
email.value.isStar = 0;
|
||||
starCancel(email.value.emailId).then(() => {
|
||||
email.value.isStar = 0;
|
||||
emailStore.cancelStarEmailId = email.value.emailId
|
||||
setTimeout(() => emailStore.cancelStarEmailId = 0)
|
||||
emailStore.starScroll?.deleteEmail([email.emailId])
|
||||
emailStore.starScroll?.deleteEmail([email.value.emailId])
|
||||
}).catch((e) => {
|
||||
console.error(e)
|
||||
email.isStar = 1;
|
||||
email.value.isStar = 1;
|
||||
})
|
||||
} else {
|
||||
email.isStar = 1;
|
||||
starAdd(email.emailId).then(() => {
|
||||
email.isStar = 1;
|
||||
emailStore.addStarEmailId = email.emailId
|
||||
email.value.isStar = 1;
|
||||
starAdd(email.value.emailId).then(() => {
|
||||
email.value.isStar = 1;
|
||||
emailStore.addStarEmailId = email.value.emailId
|
||||
setTimeout(() => emailStore.addStarEmailId = 0)
|
||||
emailStore.starScroll?.addItem(email)
|
||||
emailStore.starScroll?.addItem(email.value)
|
||||
}).catch((e) => {
|
||||
console.error(e)
|
||||
email.isStar = 0;
|
||||
email.value.isStar = 0;
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -190,23 +241,23 @@ const handleDelete = () => {
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
if (emailStore.contentData.delType === 'logic') {
|
||||
emailDelete(email.emailId).then(() => {
|
||||
emailDelete(email.value.emailId).then(() => {
|
||||
ElMessage({
|
||||
message: t('delSuccessMsg'),
|
||||
type: 'success',
|
||||
plain: true,
|
||||
})
|
||||
emailStore.deleteIds = [email.emailId]
|
||||
emailStore.deleteIds = [email.value.emailId]
|
||||
})
|
||||
} else {
|
||||
|
||||
allEmailDelete(email.emailId).then(() => {
|
||||
allEmailDelete(email.value.emailId).then(() => {
|
||||
ElMessage({
|
||||
message: t('delSuccessMsg'),
|
||||
type: 'success',
|
||||
plain: true,
|
||||
})
|
||||
emailStore.deleteIds = [email.emailId]
|
||||
emailStore.deleteIds = [email.value.emailId]
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -64,12 +64,12 @@ function changeTimeSort() {
|
||||
}
|
||||
|
||||
function jumpContent(email) {
|
||||
emailStore.contentData.email = email
|
||||
emailStore.contentData.email = emailStore.toContentEmail(email)
|
||||
emailStore.contentData.delType = 'logic'
|
||||
emailStore.contentData.showUnread = true
|
||||
emailStore.contentData.showStar = true
|
||||
emailStore.contentData.showReply = true
|
||||
router.push('/message')
|
||||
router.push('/mail')
|
||||
}
|
||||
|
||||
const existIds = new Set();
|
||||
@@ -141,7 +141,9 @@ function cancelStar(email) {
|
||||
function getEmailList(emailId, size) {
|
||||
const accountId = accountStore.currentAccountId;
|
||||
const allReceive = accountStore.currentAccount.allReceive;
|
||||
return emailList(accountId, allReceive, emailId, params.timeSort, size, 0).then(data => {
|
||||
return emailStore.fetchList(full =>
|
||||
emailList(accountId, allReceive, emailId, params.timeSort, size, 0, full)
|
||||
).then(data => {
|
||||
data.latestEmail.reqAccountId = accountId;
|
||||
data.latestEmail.allReceive = allReceive;
|
||||
return data;
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
<span class="form-desc" v-if="show === 'login'">{{ $t('loginTitle') }}</span>
|
||||
<span class="form-desc" v-else>{{ $t('regTitle') }}</span>
|
||||
<div v-show="show === 'login'">
|
||||
<el-input ref="loginEmailInput" :class="!hideLoginDomain ? 'email-input' : ''" v-model="form.email"
|
||||
type="text" :placeholder="$t('emailAccount')" autocomplete="off" @keydown.tab="handleEmailTab">
|
||||
<el-input :class="!hideLoginDomain ? 'email-input' : ''" v-model="form.email"
|
||||
type="text" :placeholder="$t('emailAccount')" autocomplete="off" @keyup.enter="submit">
|
||||
<template #append v-if="!hideLoginDomain">
|
||||
<div @click.stop="openSelect">
|
||||
<el-select
|
||||
@@ -39,25 +39,20 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input ref="loginPasswordInput" v-model="form.password" :placeholder="$t('password')" type="password"
|
||||
autocomplete="off" @keydown.tab="handlePasswordTab" @keyup.enter="submit">
|
||||
<el-input v-model="form.password" :placeholder="$t('password')" type="password" autocomplete="off" @keyup.enter="submit">
|
||||
</el-input>
|
||||
<div class="login-actions">
|
||||
<el-button class="btn" type="primary" @click="submit" :loading="loginLoading"
|
||||
>{{ $t('loginBtn') }}
|
||||
</el-button>
|
||||
<el-button v-if="settingStore.settings.passkey === 1" class="btn" @click="submitPasskey" :loading="passkeyLoading">
|
||||
<Icon icon="material-symbols:passkey-outline-rounded" width="18" height="18"/>
|
||||
{{ $t('passkeyLogin') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button class="btn" v-if="settingStore.settings.linuxdoSwitch" style="margin-top: 10px" @click="linuxDoLogin">
|
||||
<el-avatar src="/image/linuxdo.webp" :size="18" style="margin-right: 10px" />LinuxDo
|
||||
<el-button class="btn" type="primary" @click="submit" :loading="loginLoading"
|
||||
>{{ $t('loginBtn') }}
|
||||
</el-button>
|
||||
<el-button v-for="p in oauthProviders" :key="p.key" class="btn" style="margin-top: 10px" @click="oauthLogin(p.key)">
|
||||
<el-avatar v-if="p.iconType === 'image'" :src="p.icon" :size="18" style="margin-right: 10px" />
|
||||
<Icon v-else :icon="p.icon" width="18" height="18" style="margin-right: 10px" />
|
||||
{{ p.label }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-show="show !== 'login'">
|
||||
<el-input :class="!hideLoginDomain ? 'email-input' : ''" v-model="registerForm.email" type="text" :placeholder="$t('emailAccount')"
|
||||
autocomplete="off">
|
||||
autocomplete="off" @keyup.enter="submitRegister">
|
||||
<template #append v-if="!hideLoginDomain">
|
||||
<div @click.stop="openSelect">
|
||||
<el-select
|
||||
@@ -81,13 +76,13 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input v-model="registerForm.password" :placeholder="$t('password')" type="password" autocomplete="off"/>
|
||||
<el-input v-model="registerForm.password" :placeholder="$t('password')" type="password" autocomplete="off" @keyup.enter="submitRegister"/>
|
||||
<el-input v-model="registerForm.confirmPassword" :placeholder="$t('confirmPwd')" type="password"
|
||||
autocomplete="off"/>
|
||||
autocomplete="off" @keyup.enter="submitRegister"/>
|
||||
<el-input v-if="settingStore.settings.regKey === 0" v-model="registerForm.code" :placeholder="$t('regKey')"
|
||||
type="text" autocomplete="off"/>
|
||||
type="text" autocomplete="off" @keyup.enter="submitRegister"/>
|
||||
<el-input v-if="settingStore.settings.regKey === 2" v-model="registerForm.code"
|
||||
:placeholder="$t('regKeyOptional')" type="text" autocomplete="off"/>
|
||||
:placeholder="$t('regKeyOptional')" type="text" autocomplete="off" @keyup.enter="submitRegister"/>
|
||||
<div v-show="verifyShow"
|
||||
class="register-turnstile"
|
||||
:data-sitekey="settingStore.settings.siteKey"
|
||||
@@ -101,8 +96,10 @@
|
||||
<el-button class="btn" style="margin: 0" type="primary" @click="submitRegister" :loading="registerLoading"
|
||||
>{{ $t('regBtn') }}
|
||||
</el-button>
|
||||
<el-button v-if="settingStore.settings.linuxdoSwitch" class="btn" style="margin-top: 10px" @click="linuxDoLogin">
|
||||
<el-avatar src="/image/linuxdo.webp" :size="18" style="margin-right: 10px" />LinuxDo
|
||||
<el-button v-for="p in oauthProviders" :key="p.key" class="btn" style="margin-top: 10px" @click="oauthLogin(p.key)">
|
||||
<el-avatar v-if="p.iconType === 'image'" :src="p.icon" :size="18" style="margin-right: 10px" />
|
||||
<Icon v-else :icon="p.icon" width="18" height="18" style="margin-right: 10px" />
|
||||
{{ p.label }}
|
||||
</el-button>
|
||||
</div>
|
||||
<template v-if="settingStore.settings.register === 0">
|
||||
@@ -115,7 +112,7 @@
|
||||
</div>
|
||||
<el-dialog class="bind-dialog" v-model="showBindForm" title="注册邮箱" >
|
||||
<div class="bind-container">
|
||||
<el-input :class="!hideLoginDomain ? 'email-input' : ''" v-model="bindForm.email" type="text" :placeholder="$t('emailAccount')" autocomplete="off">
|
||||
<el-input :class="!hideLoginDomain ? 'email-input' : ''" v-model="bindForm.email" type="text" :placeholder="$t('emailAccount')" autocomplete="off" @keyup.enter="bind">
|
||||
<template #append v-if="!hideLoginDomain">
|
||||
<div @click.stop="openSelect">
|
||||
<el-select
|
||||
@@ -139,9 +136,9 @@
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input v-if="settingStore.settings.regKey === 0" v-model="bindForm.code" :placeholder="$t('regKey')"
|
||||
type="text" autocomplete="off"/>
|
||||
type="text" autocomplete="off" @keyup.enter="bind"/>
|
||||
<el-input v-if="settingStore.settings.regKey === 2" v-model="bindForm.code"
|
||||
:placeholder="$t('regKeyOptional')" type="text" autocomplete="off"/>
|
||||
:placeholder="$t('regKeyOptional')" type="text" autocomplete="off" @keyup.enter="bind"/>
|
||||
<el-button class="btn" type="primary" @click="bind" :loading="bindLoading"
|
||||
>绑定
|
||||
</el-button>
|
||||
@@ -155,6 +152,7 @@
|
||||
|
||||
<script setup>
|
||||
import router from "@/router";
|
||||
import {useRoute} from "vue-router";
|
||||
import {computed, nextTick, reactive, ref} from "vue";
|
||||
import {login} from "@/request/login.js";
|
||||
import {register} from "@/request/login.js";
|
||||
@@ -169,23 +167,37 @@ import {cvtR2Url} from "@/utils/convert.js";
|
||||
import {loginUserInfo} from "@/request/my.js";
|
||||
import {permsToRouter} from "@/perm/perm.js";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {oauthBindUser, oauthLinuxDoLogin} from "@/request/ouath.js";
|
||||
import {browserSupportsWebAuthn, startAuthentication} from "@simplewebauthn/browser";
|
||||
import {passkeyAuthOptions, passkeyAuthVerify} from "@/request/passkey.js";
|
||||
import {oauthBindUser, oauthLinuxDoLogin, oauthGithubLogin, oauthGoogleLogin} from "@/request/ouath.js";
|
||||
|
||||
const {t} = useI18n();
|
||||
const accountStore = useAccountStore();
|
||||
const userStore = useUserStore();
|
||||
const uiStore = useUiStore();
|
||||
const settingStore = useSettingStore();
|
||||
const route = useRoute();
|
||||
const loginLoading = ref(false)
|
||||
const passkeyLoading = ref(false)
|
||||
const bindLoading = ref(false)
|
||||
const oauthLoading = ref(false);
|
||||
const showBindForm = ref(false);
|
||||
const show = ref('login')
|
||||
const loginEmailInput = ref()
|
||||
const loginPasswordInput = ref()
|
||||
|
||||
const oauthKeys = ['linuxdo', 'github', 'google']
|
||||
|
||||
const oauthProvider = computed(() => {
|
||||
const fromState = route.query.state
|
||||
if (oauthKeys.includes(fromState)) return fromState
|
||||
const fromStore = sessionStorage.getItem('oauthProvider')
|
||||
return oauthKeys.includes(fromStore) ? fromStore : null
|
||||
})
|
||||
|
||||
const oauthProviders = computed(() => {
|
||||
const allProviders = [
|
||||
{ key: 'google', label: 'Google', icon: 'devicon:google', iconType: 'iconify' },
|
||||
{ key: 'github', label: 'GitHub', icon: 'codicon:github-inverted', iconType: 'iconify' },
|
||||
{ key: 'linuxdo', label: 'LinuxDo', icon: '/image/linuxdo.webp', iconType: 'image' },
|
||||
]
|
||||
return allProviders.filter(p => settingStore.settings[p.key + 'Switch'] === 0)
|
||||
})
|
||||
|
||||
const bindForm = reactive({
|
||||
email: '',
|
||||
@@ -211,18 +223,6 @@ const registerLoading = ref(false)
|
||||
suffix.value = domainList[0]
|
||||
const verifyShow = ref(false)
|
||||
let verifyToken = ''
|
||||
|
||||
function handleEmailTab(event) {
|
||||
if (event.shiftKey) return
|
||||
event.preventDefault()
|
||||
loginPasswordInput.value?.focus()
|
||||
}
|
||||
|
||||
function handlePasswordTab(event) {
|
||||
if (!event.shiftKey) return
|
||||
event.preventDefault()
|
||||
loginEmailInput.value?.focus()
|
||||
}
|
||||
let turnstileId = null
|
||||
let botJsError = ref(false)
|
||||
let verifyErrorCount = 0
|
||||
@@ -285,51 +285,63 @@ const getEmailName = (email) => {
|
||||
return email.split('@')[0]
|
||||
}
|
||||
|
||||
function linuxDoLogin() {
|
||||
const clientId = settingStore.settings.linuxdoClientId
|
||||
const redirectUri = encodeURIComponent(settingStore.settings.linuxdoCallbackUrl)
|
||||
window.location.href =
|
||||
`https://connect.linux.do/oauth2/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&scope=openid+profile+email`
|
||||
function oauthLogin(provider) {
|
||||
const clientId = settingStore.settings[provider + 'ClientId']
|
||||
const redirectUri = encodeURIComponent(window.location.origin + '/login')
|
||||
sessionStorage.setItem('oauthProvider', provider)
|
||||
const authorizeUrls = {
|
||||
linuxdo: `https://connect.linux.do/oauth2/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&scope=openid+profile+email&state=${provider}`,
|
||||
github: `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=user:email&state=${provider}`,
|
||||
google: `https://accounts.google.com/o/oauth2/v2/auth?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&scope=openid+profile+email&state=${provider}`,
|
||||
}
|
||||
window.location.href = authorizeUrls[provider]
|
||||
}
|
||||
|
||||
linuxDoGetUser();
|
||||
const loginFns = {
|
||||
linuxdo: oauthLinuxDoLogin,
|
||||
github: oauthGithubLogin,
|
||||
google: oauthGoogleLogin,
|
||||
}
|
||||
|
||||
async function linuxDoGetUser() {
|
||||
oauthGetUser();
|
||||
|
||||
async function oauthGetUser() {
|
||||
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const code = params.get('code')
|
||||
if (!code || !oauthProvider.value) return
|
||||
|
||||
if (code) {
|
||||
const provider = oauthProvider.value
|
||||
oauthLoading.value = true
|
||||
sessionStorage.removeItem('oauthProvider')
|
||||
window.history.replaceState({}, '', window.location.origin + window.location.pathname)
|
||||
|
||||
oauthLoading.value = true
|
||||
oauthLinuxDoLogin(code).then(data => {
|
||||
loginFns[provider](code, window.location.origin + '/login').then(data => {
|
||||
|
||||
bindForm.oauthUserId = data.userInfo.oauthUserId;
|
||||
bindForm.oauthUserId = data.userInfo.oauthUserId;
|
||||
|
||||
if (!data.token) {
|
||||
showBindForm.value = true
|
||||
oauthLoading.value = false
|
||||
ElMessage({
|
||||
message: '请注册绑定一个邮箱',
|
||||
type: 'warning',
|
||||
duration: 4000,
|
||||
plain: true,
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
saveToken(data.token);
|
||||
}).catch(() => {
|
||||
if (!data.token) {
|
||||
showBindForm.value = true
|
||||
oauthLoading.value = false
|
||||
})
|
||||
}
|
||||
ElMessage({
|
||||
message: '请注册绑定一个邮箱',
|
||||
type: 'warning',
|
||||
duration: 4000,
|
||||
plain: true,
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanUrl = window.location.origin + window.location.pathname
|
||||
window.history.replaceState({}, '', cleanUrl)
|
||||
saveToken(data.token);
|
||||
}).catch(() => {
|
||||
oauthLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function bind() {
|
||||
|
||||
if (bindLoading.value) return
|
||||
|
||||
if (!bindForm.email) {
|
||||
ElMessage({
|
||||
message: t('emptyEmailMsg'),
|
||||
@@ -387,6 +399,8 @@ function bind() {
|
||||
|
||||
const submit = () => {
|
||||
|
||||
if (loginLoading.value) return
|
||||
|
||||
if (!form.email) {
|
||||
ElMessage({
|
||||
message: t('emptyEmailMsg'),
|
||||
@@ -424,35 +438,6 @@ const submit = () => {
|
||||
})
|
||||
}
|
||||
|
||||
async function submitPasskey() {
|
||||
if (!browserSupportsWebAuthn()) {
|
||||
ElMessage({
|
||||
message: t('passkeyUnsupported'),
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
passkeyLoading.value = true
|
||||
try {
|
||||
const {transactionId, options} = await passkeyAuthOptions()
|
||||
const response = await startAuthentication({optionsJSON: options})
|
||||
const data = await passkeyAuthVerify(transactionId, response)
|
||||
await saveToken(data.token)
|
||||
} catch (error) {
|
||||
if (!error?.code) {
|
||||
ElMessage({
|
||||
message: t('passkeyLoginFailed'),
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
passkeyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveToken(token) {
|
||||
localStorage.setItem('token', token)
|
||||
refreshWebsiteConfig()
|
||||
@@ -486,6 +471,8 @@ function refreshWebsiteConfig() {
|
||||
|
||||
function submitRegister() {
|
||||
|
||||
if (registerLoading.value) return
|
||||
|
||||
if (!registerForm.email) {
|
||||
ElMessage({
|
||||
message: t('emptyEmailMsg'),
|
||||
@@ -690,15 +677,6 @@ function submitRegister() {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.login-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.form-desc {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 18px;
|
||||
@@ -804,6 +782,7 @@ function submitRegister() {
|
||||
width: 100px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.custom-style {
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
</el-scrollbar>
|
||||
<el-dialog v-model="showAdd" :title="$t('addRegKey')">
|
||||
<div class="container">
|
||||
<el-input v-model="addForm.code" :placeholder="$t('regKey')">
|
||||
<el-input v-model="addForm.code" :placeholder="$t('regKey')" @keyup.enter="submit">
|
||||
<template #suffix>
|
||||
<Icon @click.stop="genCode" class="gen-code" icon="bitcoin-icons:refresh-filled" width="24" height="24"/>
|
||||
</template>
|
||||
@@ -75,7 +75,7 @@
|
||||
type="date"
|
||||
:placeholder="$t('validUntil')"
|
||||
/>
|
||||
<el-input-number v-model="addForm.count" :min="1" :max="99999"/>
|
||||
<el-input-number v-model="addForm.count" :min="1" :max="99999" @keyup.enter="submit"/>
|
||||
<el-button class="btn" type="primary" @click="submit" :loading="addLoading"
|
||||
>{{ $t('add') }}
|
||||
</el-button>
|
||||
@@ -308,6 +308,8 @@ function clearNotUse() {
|
||||
|
||||
function submit() {
|
||||
|
||||
if (addLoading.value) return
|
||||
|
||||
if (!addForm.code) {
|
||||
ElMessage({
|
||||
message: $('emptyRegKeyMsg'),
|
||||
|
||||
@@ -68,9 +68,9 @@
|
||||
</template>
|
||||
<div class="dialog-box">
|
||||
<el-input class="dialog-input" v-model="form.name" type="text" :maxlength="12" :placeholder="$t('roleName')"
|
||||
autocomplete="off"/>
|
||||
autocomplete="off" @keyup.enter="roleFormClick"/>
|
||||
<el-input class="dialog-input" v-model="form.description" :maxlength="30" type="text"
|
||||
:placeholder="$t('description')" autocomplete="off"/>
|
||||
:placeholder="$t('description')" autocomplete="off" @keyup.enter="roleFormClick"/>
|
||||
<el-input-tag class="dialog-input" tag-type="warning" v-model="form.banEmail"
|
||||
@add-tag="banEmailAddTag" type="text" :placeholder="$t('emailInterception')" autocomplete="off"/>
|
||||
<el-select
|
||||
@@ -233,6 +233,7 @@ function banEmailAddTag(val) {
|
||||
|
||||
|
||||
function roleFormClick() {
|
||||
if (permLoading.value) return
|
||||
if (dialogType.type === 'add') {
|
||||
addRole()
|
||||
} else {
|
||||
|
||||
@@ -56,11 +56,11 @@ function changeTimeSort() {
|
||||
}
|
||||
|
||||
function jumpContent(email) {
|
||||
emailStore.contentData.email = email
|
||||
emailStore.contentData.email = emailStore.toContentEmail(email)
|
||||
emailStore.contentData.delType = 'logic'
|
||||
emailStore.contentData.showStar = true
|
||||
emailStore.contentData.showReply = true
|
||||
router.push('/message')
|
||||
router.push('/mail')
|
||||
}
|
||||
|
||||
function addStar(email) {
|
||||
@@ -74,7 +74,9 @@ function cancelStar(email) {
|
||||
function getEmailList(emailId, size) {
|
||||
const accountId = accountStore.currentAccountId;
|
||||
const allReceive = accountStore.currentAccount.allReceive;
|
||||
return emailList(accountId, allReceive, emailId, params.timeSort, size, 1).then(data => {
|
||||
return emailStore.fetchList(full =>
|
||||
emailList(accountId, allReceive, emailId, params.timeSort, size, 1, full)
|
||||
).then(data => {
|
||||
data.latestEmail.reqAccountId = accountId;
|
||||
data.latestEmail.allReceive = allReceive;
|
||||
return data;
|
||||
|
||||
@@ -30,25 +30,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="passkey" v-if="settingStore.settings.passkey === 1">
|
||||
<div class="title">{{$t('passkeySetting')}}</div>
|
||||
<div class="passkey-status">
|
||||
<div>
|
||||
<div>{{ passkey.registered ? $t('passkeyConfigured') : $t('passkeyNotConfigured') }}</div>
|
||||
<div class="passkey-time" v-if="passkey.registered && passkey.createTime">
|
||||
{{$t('passkeyCreatedAt', {time: passkey.createTime})}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="passkey-actions">
|
||||
<el-button type="primary" :loading="passkeyLoading" @click="openPasskeyAction('register')">
|
||||
{{ passkey.registered ? $t('passkeyReplace') : $t('passkeyCreate') }}
|
||||
</el-button>
|
||||
<el-button v-if="passkey.registered" :loading="passkeyLoading" @click="openPasskeyAction('delete')">
|
||||
{{$t('passkeyDelete')}}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="language">
|
||||
<div class="title">{{$t('language')}}</div>
|
||||
<el-select
|
||||
@@ -67,25 +48,16 @@
|
||||
{{$t('delAccountMsg')}}
|
||||
</div>
|
||||
<div>
|
||||
<el-button type="danger" @click="deleteConfirm">{{$t('deleteUserBtn')}}</el-button>
|
||||
<el-button type="primary" @click="deleteConfirm">{{$t('deleteUserBtn')}}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="pwdShow" :title="$t('changePassword')" width="340">
|
||||
<div class="update-pwd">
|
||||
<el-input type="password" :placeholder="$t('newPassword')" v-model="form.password" autocomplete="off"/>
|
||||
<el-input type="password" :placeholder="$t('confirmPassword')" v-model="form.newPwd" autocomplete="off"/>
|
||||
<el-input type="password" :placeholder="$t('newPassword')" v-model="form.password" autocomplete="off" @keyup.enter="submitPwd"/>
|
||||
<el-input type="password" :placeholder="$t('confirmPassword')" v-model="form.newPwd" autocomplete="off" @keyup.enter="submitPwd"/>
|
||||
<el-button type="primary" :loading="setPwdLoading" @click="submitPwd">{{$t('save')}}</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="passkeyPwdShow"
|
||||
:title="passkeyAction === 'delete' ? $t('passkeyDelete') : $t('passkeySetting')" width="340">
|
||||
<div class="update-pwd">
|
||||
<div v-if="passkeyAction === 'delete'">{{$t('passkeyDeleteConfirm')}}</div>
|
||||
<el-input type="password" :placeholder="$t('passkeyCurrentPassword')" v-model="passkeyPassword"
|
||||
autocomplete="current-password" @keyup.enter="submitPasskeyAction"/>
|
||||
<el-button type="primary" :loading="passkeyLoading" @click="submitPasskeyAction">{{$t('confirm')}}</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
@@ -97,13 +69,6 @@ import {accountSetName} from "@/request/account.js";
|
||||
import {useAccountStore} from "@/store/account.js";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {useSettingStore} from "@/store/setting.js";
|
||||
import {browserSupportsWebAuthn, startRegistration} from "@simplewebauthn/browser";
|
||||
import {
|
||||
passkeyDelete,
|
||||
passkeyRegOptions,
|
||||
passkeyRegVerify,
|
||||
passkeyStatus
|
||||
} from "@/request/passkey.js";
|
||||
|
||||
const { t } = useI18n()
|
||||
const accountStore = useAccountStore()
|
||||
@@ -113,80 +78,11 @@ const setPwdLoading = ref(false)
|
||||
const setNameShow = ref(false)
|
||||
const accountName = ref(null)
|
||||
const langSelect = ref(settingStore.lang)
|
||||
const passkey = reactive({registered: false, createTime: null})
|
||||
const passkeyLoading = ref(false)
|
||||
const passkeyPwdShow = ref(false)
|
||||
const passkeyPassword = ref('')
|
||||
const passkeyAction = ref('register')
|
||||
|
||||
defineOptions({
|
||||
name: 'setting'
|
||||
})
|
||||
|
||||
if (settingStore.settings.passkey === 1) {
|
||||
loadPasskeyStatus()
|
||||
}
|
||||
|
||||
async function loadPasskeyStatus() {
|
||||
try {
|
||||
const status = await passkeyStatus()
|
||||
passkey.registered = status.registered
|
||||
passkey.createTime = status.createTime
|
||||
} catch (_) {
|
||||
// The shared HTTP interceptor already displays the server error.
|
||||
}
|
||||
}
|
||||
|
||||
function openPasskeyAction(action) {
|
||||
if (action === 'register' && !browserSupportsWebAuthn()) {
|
||||
ElMessage({
|
||||
message: t('passkeyUnsupported'),
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
passkeyAction.value = action
|
||||
passkeyPassword.value = ''
|
||||
passkeyPwdShow.value = true
|
||||
}
|
||||
|
||||
async function submitPasskeyAction() {
|
||||
if (!passkeyPassword.value) {
|
||||
ElMessage({
|
||||
message: t('emptyPwdMsg'),
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
passkeyLoading.value = true
|
||||
try {
|
||||
if (passkeyAction.value === 'delete') {
|
||||
await passkeyDelete(passkeyPassword.value)
|
||||
passkey.registered = false
|
||||
passkey.createTime = null
|
||||
ElMessage({message: t('passkeyDeleteSuccess'), type: 'success', plain: true})
|
||||
} else {
|
||||
const {transactionId, options} = await passkeyRegOptions(passkeyPassword.value)
|
||||
const response = await startRegistration({optionsJSON: options})
|
||||
const status = await passkeyRegVerify(transactionId, response)
|
||||
passkey.registered = status.registered
|
||||
passkey.createTime = status.createTime
|
||||
ElMessage({message: t('passkeyCreateSuccess'), type: 'success', plain: true})
|
||||
}
|
||||
passkeyPwdShow.value = false
|
||||
passkeyPassword.value = ''
|
||||
} catch (error) {
|
||||
if (!error?.code) {
|
||||
ElMessage({message: t('passkeyLoginFailed'), type: 'error', plain: true})
|
||||
}
|
||||
} finally {
|
||||
passkeyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function showSetName() {
|
||||
accountName.value = userStore.user.name
|
||||
setNameShow.value = true
|
||||
@@ -264,6 +160,8 @@ const deleteConfirm = () => {
|
||||
|
||||
function submitPwd() {
|
||||
|
||||
if (setPwdLoading.value) return
|
||||
|
||||
if (!form.password) {
|
||||
ElMessage({
|
||||
message: t('emptyPwdMsg'),
|
||||
@@ -390,34 +288,6 @@ function submitPwd() {
|
||||
}
|
||||
}
|
||||
|
||||
.passkey {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
|
||||
.passkey-status {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.passkey-time {
|
||||
color: var(--regular-text-color);
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.passkey-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
|
||||
.el-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.del-email {
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<emailScroll type="star" ref="scroll"
|
||||
:allow-star="false"
|
||||
:cancel-success="cancelStar"
|
||||
:getEmailList="starList"
|
||||
:getEmailList="getEmailList"
|
||||
:emailDelete="emailDelete"
|
||||
:star-add="starAdd"
|
||||
:star-cancel="starCancel"
|
||||
@@ -28,11 +28,15 @@ const scroll = ref({})
|
||||
const emailStore = useEmailStore();
|
||||
|
||||
function jumpContent(email) {
|
||||
emailStore.contentData.email = email
|
||||
emailStore.contentData.email = emailStore.toContentEmail(email)
|
||||
emailStore.contentData.delType = 'logic'
|
||||
emailStore.contentData.showStar = true
|
||||
emailStore.contentData.showReply = true
|
||||
router.push('/message')
|
||||
router.push('/mail')
|
||||
}
|
||||
|
||||
function getEmailList(emailId, size) {
|
||||
return emailStore.fetchList(full => starList(emailId, size, full))
|
||||
}
|
||||
|
||||
function cancelStar(email) {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<el-scrollbar class="scroll" v-if="!firstLoading">
|
||||
<div class="scroll-body">
|
||||
<div class="card-grid">
|
||||
<!-- Website Settings Card -->
|
||||
<!-- Basic Settings Card -->
|
||||
<div class="settings-card">
|
||||
<div class="card-title">{{ $t('websiteSetting') }}</div>
|
||||
<div class="card-content">
|
||||
@@ -24,13 +24,6 @@
|
||||
v-model="setting.loginDomain"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div><span>{{ $t('passkeyLogin') }}</span></div>
|
||||
<div>
|
||||
<el-switch @change="change" :before-change="beforeChange" :active-value="1" :inactive-value="0"
|
||||
v-model="setting.passkey"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div><span>{{ $t('regKey') }}</span></div>
|
||||
<div>
|
||||
@@ -68,6 +61,18 @@
|
||||
v-model="setting.manyEmail"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div>
|
||||
<span>{{ $t('syncDelete') }}</span>
|
||||
<el-tooltip effect="dark" :content="$t('syncDeleteDesc')">
|
||||
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
|
||||
v-model="setting.syncDelete"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div>
|
||||
<span>{{ $t('emailPrefix') }}</span>
|
||||
@@ -208,6 +213,63 @@
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div>
|
||||
<span>{{ $t('autoClean') }}</span>
|
||||
<el-tooltip effect="dark" :content="$t('autoCleanDesc')">
|
||||
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="forward">
|
||||
<span>{{ setting.autoCleanDays > 0 ? $t('autoCleanRetain', { days: setting.autoCleanDays }) : $t('disabled') }}</span>
|
||||
<el-button class="opt-button" style="margin-top: 0" @click="openAutoClean" size="small"
|
||||
type="primary">
|
||||
<Icon icon="fluent:settings-48-regular" width="16" height="16"/>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="card-title">{{ $t('emailPush') }}</div>
|
||||
<div class="card-content">
|
||||
<div class="setting-item">
|
||||
<div><span>{{ $t('tgBot') }}</span></div>
|
||||
<div class="forward">
|
||||
<span>{{ setting.tgBotStatus === 0 ? $t('enabled') : $t('disabled') }}</span>
|
||||
<el-button class="opt-button" size="small" type="primary" @click="openTgSetting">
|
||||
<Icon icon="fluent:settings-48-regular" width="18" height="18"/>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div><span>{{ $t('otherEmail') }}</span></div>
|
||||
<div class="forward">
|
||||
<span>{{ setting.forwardStatus === 0 ? $t('enabled') : $t('disabled') }}</span>
|
||||
<el-button class="opt-button" size="small" type="primary" @click="openThirdEmailSetting">
|
||||
<Icon icon="fluent:settings-48-regular" width="18" height="18"/>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div><span>{{ $t('webhook') }}</span></div>
|
||||
<div class="forward">
|
||||
<span>{{ setting.webhookStatus === 0 ? $t('enabled') : $t('disabled') }}</span>
|
||||
<el-button class="opt-button" size="small" type="primary" @click="openWebhookSetting">
|
||||
<Icon icon="fluent:settings-48-regular" width="18" height="18"/>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div><span>{{ $t('forwardingRules') }}</span></div>
|
||||
<div class="forward">
|
||||
<span>{{ setting.ruleType === 0 ? $t('forwardAll') : $t('rules') }}</span>
|
||||
<el-button class="opt-button" size="small" type="primary" @click="openForwardRules">
|
||||
<Icon icon="fluent:settings-48-regular" width="18" height="18"/>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -252,39 +314,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="card-title">{{ $t('emailPush') }}</div>
|
||||
<div class="card-content">
|
||||
<div class="setting-item">
|
||||
<div><span>{{ $t('tgBot') }}</span></div>
|
||||
<div class="forward">
|
||||
<span>{{ setting.tgBotStatus === 0 ? $t('enabled') : $t('disabled') }}</span>
|
||||
<el-button class="opt-button" size="small" type="primary" @click="openTgSetting">
|
||||
<Icon icon="fluent:settings-48-regular" width="18" height="18"/>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div><span>{{ $t('otherEmail') }}</span></div>
|
||||
<div class="forward">
|
||||
<span>{{ setting.forwardStatus === 0 ? $t('enabled') : $t('disabled') }}</span>
|
||||
<el-button class="opt-button" size="small" type="primary" @click="openThirdEmailSetting">
|
||||
<Icon icon="fluent:settings-48-regular" width="18" height="18"/>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div><span>{{ $t('forwardingRules') }}</span></div>
|
||||
<div class="forward">
|
||||
<span>{{ setting.ruleType === 0 ? $t('forwardAll') : $t('rules') }}</span>
|
||||
<el-button class="opt-button" size="small" type="primary" @click="openForwardRules">
|
||||
<Icon icon="fluent:settings-48-regular" width="18" height="18"/>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Turnstile Verification Card -->
|
||||
<div class="settings-card">
|
||||
<div class="card-title">{{ $t('turnstileSetting') }}</div>
|
||||
@@ -392,6 +421,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OAuth Login Card -->
|
||||
<div class="settings-card">
|
||||
<div class="card-title">{{ $t('oauthLogin') }}</div>
|
||||
<div class="card-content">
|
||||
<div class="setting-item" v-for="p in oauthPlatforms" :key="p.key">
|
||||
<div>
|
||||
<el-avatar v-if="p.iconType === 'image'" :src="p.icon" :size="22" class="oauth-icon"/>
|
||||
<Icon v-else :icon="p.icon" width="22" height="22" class="oauth-icon"/>
|
||||
<span>{{ p.label }}</span>
|
||||
</div>
|
||||
<div class="forward">
|
||||
<span>{{ setting[p.key + 'Switch'] === 0 ? $t('enabled') : $t('disabled') }}</span>
|
||||
<el-button class="opt-button" size="small" type="primary" @click="openOauthSetting(p)">
|
||||
<Icon icon="fluent:settings-48-regular" width="18" height="18"/>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card about">
|
||||
<div class="card-title">{{ $t('about') }}</div>
|
||||
<div class="card-content">
|
||||
@@ -448,13 +497,13 @@
|
||||
|
||||
<!-- Dialogs remain the same -->
|
||||
<el-dialog v-model="editTitleShow" :title="$t('changeTitle')" width="340" @closed="editTitle = setting.title">
|
||||
<form>
|
||||
<el-input type="text" :placeholder="$t('websiteTitle')" v-model="editTitle"/>
|
||||
<form @submit.prevent>
|
||||
<el-input type="text" :placeholder="$t('websiteTitle')" v-model="editTitle" @keyup.enter="saveTitle"/>
|
||||
<el-button type="primary" :loading="settingLoading" @click="saveTitle">{{ $t('save') }}</el-button>
|
||||
</form>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="resendTokenFormShow" :title="$t('resendToken')" width="340" @closed="cleanResendTokenForm">
|
||||
<form>
|
||||
<form @submit.prevent>
|
||||
<el-select style="margin-bottom: 15px" v-model="resendTokenForm.domain" placeholder="Select">
|
||||
<el-option
|
||||
v-for="item in settingStore.domainList"
|
||||
@@ -463,25 +512,39 @@
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input type="text" :placeholder="$t('addResendTokenDesc')" v-model="resendTokenForm.token"/>
|
||||
<el-input type="text" :placeholder="$t('addResendTokenDesc')" v-model="resendTokenForm.token" @keyup.enter="saveResendToken"/>
|
||||
<el-button type="primary" :loading="settingLoading" @click="saveResendToken">{{ $t('save') }}</el-button>
|
||||
</form>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="r2DomainShow" :title="$t('addOsDomain')" width="340"
|
||||
@closed="r2DomainInput = setting.r2Domain">
|
||||
<form>
|
||||
<el-input type="text" :placeholder="$t('domainDesc')" v-model="r2DomainInput"/>
|
||||
<form @submit.prevent>
|
||||
<el-input type="text" :placeholder="$t('domainDesc')" v-model="r2DomainInput" @keyup.enter="saveR2domain"/>
|
||||
<el-button type="primary" :loading="settingLoading" @click="saveR2domain">{{ $t('save') }}</el-button>
|
||||
</form>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="turnstileShow" :title="$t('addTurnstileSecret')" width="340"
|
||||
@closed="turnstileForm.secretKey = '';turnstileForm.siteKey = ''">
|
||||
<form>
|
||||
<el-input type="text" placeholder="Site Key" v-model="turnstileForm.siteKey"/>
|
||||
<el-input type="text" style="margin-top: 15px" placeholder="Secret Key" v-model="turnstileForm.secretKey"/>
|
||||
<form @submit.prevent>
|
||||
<el-input type="text" placeholder="Site Key" v-model="turnstileForm.siteKey" @keyup.enter="saveTurnstileKey"/>
|
||||
<el-input type="text" style="margin-top: 15px" placeholder="Secret Key" v-model="turnstileForm.secretKey" @keyup.enter="saveTurnstileKey"/>
|
||||
<el-button type="primary" :loading="settingLoading" @click="saveTurnstileKey">{{ $t('save') }}</el-button>
|
||||
</form>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="oauthSettingShow" :title="$t('oauthSetting') + ' - ' + oauthForm.label" width="340"
|
||||
@closed="oauthForm.clientId = ''; oauthForm.clientSecret = ''; oauthForm.switch = 1">
|
||||
<div class="dialog-content">
|
||||
<el-input type="text" :placeholder="$t('clientId')" v-model="oauthForm.clientId"/>
|
||||
<el-input type="text" style="margin-top: 15px" :placeholder="$t('clientSecret')" v-model="oauthForm.clientSecret"/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-switch v-model="oauthForm.switch" :active-value="0" :inactive-value="1" :active-text="$t('enable')"
|
||||
:inactive-text="$t('disable')"/>
|
||||
<el-button type="primary" :loading="settingLoading" @click="saveOauth">{{ $t('save') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
v-model="showSetBackground"
|
||||
class="cut-dialog"
|
||||
@@ -499,7 +562,7 @@
|
||||
</span>
|
||||
</template>
|
||||
<el-input :placeholder="$t('backgroundUrlDesc')" v-model="backgroundUrl" v-if="!localUpShow"
|
||||
class="background-url"/>
|
||||
class="background-url" @keyup.enter="saveBackground"/>
|
||||
<el-image
|
||||
v-if="localUpShow"
|
||||
:preview-src-list="[backgroundImage]"
|
||||
@@ -531,10 +594,10 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="forward-set-body">
|
||||
<el-input :placeholder="setting.tgBotToken || $t('tgBotToken')" v-model="tgBotToken"></el-input>
|
||||
<el-input :placeholder="setting.tgBotToken || $t('tgBotToken')" v-model="tgBotToken" @keyup.enter="tgBotSave"></el-input>
|
||||
<el-input-tag tag-type="warning" :placeholder="$t('toBotTokenDesc')" v-model="tgChatId"
|
||||
@add-tag="addChatTag"></el-input-tag>
|
||||
<el-input tag-type="warning" :placeholder="$t('customDomainDesc')" v-model="customDomain" ></el-input>
|
||||
<el-input tag-type="warning" :placeholder="$t('customDomainDesc')" v-model="customDomain" @keyup.enter="tgBotSave"></el-input>
|
||||
<div class="tg-msg-label">
|
||||
<span>{{t('from')}}</span>
|
||||
<el-select v-model="tgMsgFrom" >
|
||||
@@ -605,6 +668,48 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
v-model="webhookShow"
|
||||
class="forward-dialog"
|
||||
@closed="webhookFormatShow = false"
|
||||
>
|
||||
<template #header>
|
||||
<div class="forward-head">
|
||||
<span class="forward-set-title">{{ $t('webhook') }}</span>
|
||||
<el-tooltip effect="dark" :content="$t('webhookDesc')">
|
||||
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<div class="forward-set-body">
|
||||
<el-input :placeholder="$t('webhookUrl')" v-model="webhookUrl" @keyup.enter="webhookSave"></el-input>
|
||||
<el-input :placeholder="$t('webhookSecret')" v-model="webhookSecret" @keyup.enter="webhookSave"></el-input>
|
||||
<div class="tg-msg-label">
|
||||
<span>{{ t('webhookRetry') }}</span>
|
||||
<el-input-number v-model="webhookRetry" :min="0" :max="5" controls-position="right"/>
|
||||
</div>
|
||||
<div v-show="webhookFormatShow" class="webhook-format">
|
||||
<pre>Content-Type: application/json
|
||||
Authorization: <secret></pre>
|
||||
<pre>{{ webhookPayloadExample }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-switch v-model="webhookStatus" :active-value="0" :inactive-value="1" :active-text="$t('enable')"
|
||||
:inactive-text="$t('disable')"/>
|
||||
<div class="webhook-footer-right">
|
||||
<div class="webhook-format-title" @click="webhookFormatShow = !webhookFormatShow">
|
||||
<span>{{ $t('webhookFormat') }}</span>
|
||||
<Icon class="webhook-format-icon" :class="{ open: webhookFormatShow }" icon="mingcute:down-small-fill" width="18" height="18"/>
|
||||
</div>
|
||||
<el-button :loading="settingLoading" type="primary" @click="webhookSave">
|
||||
{{ $t('save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
v-model="forwardRulesShow"
|
||||
class="forward-dialog"
|
||||
@@ -643,23 +748,23 @@
|
||||
</el-dialog>
|
||||
<el-dialog v-model="regVerifyCountShow" :title="$t('rulesVerifyTitle',{count: regVerifyCount})"
|
||||
@closed="regVerifyCount = setting.regVerifyCount">
|
||||
<form>
|
||||
<el-input-number type="text" v-model="regVerifyCount" :min="1">
|
||||
<form @submit.prevent>
|
||||
<el-input-number type="text" v-model="regVerifyCount" :min="1" @keyup.enter="saveRegVerifyCount">
|
||||
</el-input-number>
|
||||
<el-button type="primary" :loading="settingLoading" @click="saveRegVerifyCount">{{ $t('save') }}</el-button>
|
||||
</form>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="addVerifyCountShow" :title="$t('rulesVerifyTitle',{count: addVerifyCount})"
|
||||
@closed="addVerifyCount = setting.addVerifyCount">
|
||||
<form>
|
||||
<el-input-number type="text" v-model="addVerifyCount" :min="1"/>
|
||||
<form @submit.prevent>
|
||||
<el-input-number type="text" v-model="addVerifyCount" :min="1" @keyup.enter="saveAddVerifyCount"/>
|
||||
<el-button type="primary" :loading="settingLoading" @click="saveAddVerifyCount">{{ $t('save') }}</el-button>
|
||||
</form>
|
||||
</el-dialog>
|
||||
<el-dialog top="5vh" v-model="noticePopupShow" :title="$t('noticePopup')" class="notice-popup"
|
||||
@closed="resetNoticeForm">
|
||||
<form>
|
||||
<el-input v-model="noticeForm.noticeTitle" :placeholder="t('titleDesc')"/>
|
||||
<form @submit.prevent>
|
||||
<el-input v-model="noticeForm.noticeTitle" :placeholder="t('titleDesc')" @keyup.enter="saveNoticePopup"/>
|
||||
<div class="notice-line-item">
|
||||
<el-select v-model="noticeForm.noticeType">
|
||||
<template #prefix>
|
||||
@@ -730,13 +835,13 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="addS3Show" :title="t('s3Configuration')" width="340" @closed="resetAddS3Form">
|
||||
<form>
|
||||
<el-input class="dialog-input" type="text" placeholder="Bucket" v-model="s3.bucket"/>
|
||||
<el-input class="dialog-input" type="text" placeholder="Endpoint" v-model="s3.endpoint"/>
|
||||
<el-input class="dialog-input" type="text" placeholder="Region" v-model="s3.region"/>
|
||||
<form @submit.prevent>
|
||||
<el-input class="dialog-input" type="text" placeholder="Bucket" v-model="s3.bucket" @keyup.enter="saveS3"/>
|
||||
<el-input class="dialog-input" type="text" placeholder="Endpoint" v-model="s3.endpoint" @keyup.enter="saveS3"/>
|
||||
<el-input class="dialog-input" type="text" placeholder="Region" v-model="s3.region" @keyup.enter="saveS3"/>
|
||||
<el-input class="dialog-input" type="text" :placeholder="setting.s3AccessKey || 'Access Key'"
|
||||
v-model="s3.s3AccessKey"/>
|
||||
<el-input style="margin-bottom: 10px" type="text" :placeholder="setting.s3SecretKey || 'Secret Key'" v-model="s3.s3SecretKey"/>
|
||||
v-model="s3.s3AccessKey" @keyup.enter="saveS3"/>
|
||||
<el-input style="margin-bottom: 10px" type="text" :placeholder="setting.s3SecretKey || 'Secret Key'" v-model="s3.s3SecretKey" @keyup.enter="saveS3"/>
|
||||
<div class="force-path-style">
|
||||
<div class="force-path-style-left">
|
||||
<span>ForcePathStyle</span>
|
||||
@@ -756,7 +861,7 @@
|
||||
<el-dialog v-model="emailPrefixShow" :title="t('emailPrefix')" @closed="resetEmailPrefix" >
|
||||
<div class="email-prefix">
|
||||
<div>{{ t('atLeast') }}</div>
|
||||
<el-input-number v-model="minEmailPrefix" :min="1" :max="20" style="width: 150px" >
|
||||
<el-input-number v-model="minEmailPrefix" :min="1" :max="20" style="width: 150px" @keyup.enter="saveEmailPrefix" >
|
||||
<template #suffix>
|
||||
<span>{{ t('character') }}</span>
|
||||
</template>
|
||||
@@ -790,6 +895,22 @@
|
||||
</el-form>
|
||||
<el-button type="primary" style="width: 100%;" :loading="settingLoading" @click="saveBlackList">{{ $t('save') }}</el-button>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="autoCleanShow" :title="t('autoClean')" class="forward-dialog" @closed="resetAutoClean">
|
||||
<el-form>
|
||||
<el-form-item :label="t('autoCleanDays')" label-position="top">
|
||||
<el-input-number v-model="autoCleanDays" :min="0" :max="3650" style="width: 100%"/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('autoCleanExclude')" label-position="top">
|
||||
<el-input-tag
|
||||
tag-type="warning"
|
||||
:placeholder="$t('autoCleanExcludeDesc')"
|
||||
v-model="autoCleanExclude"
|
||||
@add-tag="autoCleanExcludeAddTag"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button type="primary" style="width: 100%;" :loading="settingLoading" @click="saveAutoClean">{{ $t('save') }}</el-button>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="aiCodeFilterShow" class="forward-dialog" @closed="resetAiCodeFilter">
|
||||
<template #header>
|
||||
<div class="forward-head">
|
||||
@@ -818,10 +939,10 @@ import {useUiStore} from "@/store/ui.js";
|
||||
import {useUserStore} from "@/store/user.js";
|
||||
import {useAccountStore} from "@/store/account.js";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import {cvtR2Url} from "@/utils/convert.js";
|
||||
import {cvtR2Url, toOssDomain} from "@/utils/convert.js";
|
||||
import {storeToRefs} from "pinia";
|
||||
import {debounce} from 'lodash-es'
|
||||
import {isDomain, isEmail} from "@/utils/verify-utils.js";
|
||||
import {isDomain, isEmail, isIpUrl} from "@/utils/verify-utils.js";
|
||||
import loading from "@/components/loading/index.vue";
|
||||
import {getTextWidth} from "@/utils/text.js";
|
||||
import {fileToBase64} from "@/utils/file-utils.js"
|
||||
@@ -832,7 +953,7 @@ defineOptions({
|
||||
name: 'sys-setting'
|
||||
})
|
||||
|
||||
const currentVersion = 'v3.0.0'
|
||||
const currentVersion = 'v3.2.0'
|
||||
const hasUpdate = ref(false)
|
||||
let getUpdateErrorCount = 1;
|
||||
const {t, locale} = useI18n();
|
||||
@@ -845,12 +966,14 @@ const userStore = useUserStore();
|
||||
const editTitleShow = ref(false)
|
||||
const resendTokenFormShow = ref(false)
|
||||
const blackFormShow = ref(false)
|
||||
const autoCleanShow = ref(false)
|
||||
const aiCodeFilterShow = ref(false)
|
||||
const r2DomainShow = ref(false)
|
||||
const turnstileShow = ref(false)
|
||||
const tgSettingShow = ref(false)
|
||||
const noticePopupShow = ref(false)
|
||||
const thirdEmailShow = ref(false)
|
||||
const webhookShow = ref(false)
|
||||
const forwardRulesShow = ref(false)
|
||||
const emailPrefixShow = ref(false)
|
||||
const showResendList = ref(false)
|
||||
@@ -864,6 +987,8 @@ const r2DomainInput = ref('')
|
||||
const loginOpacity = ref(0)
|
||||
const minEmailPrefix = ref(0)
|
||||
const emailPrefixFilter = ref([])
|
||||
const autoCleanDays = ref(0)
|
||||
const autoCleanExclude = ref([])
|
||||
const backgroundUrl = ref('')
|
||||
let backgroundFile = {}
|
||||
const showSetBackground = ref(false)
|
||||
@@ -882,6 +1007,20 @@ const turnstileForm = reactive({
|
||||
secretKey: ''
|
||||
})
|
||||
|
||||
const oauthPlatforms = [
|
||||
{ key: 'google', label: 'Google', icon: 'devicon:google', iconType: 'iconify' },
|
||||
{ key: 'github', label: 'GitHub', icon: 'codicon:github-inverted', iconType: 'iconify' },
|
||||
{ key: 'linuxdo', label: 'LinuxDo', icon: '/image/linuxdo.webp', iconType: 'image' },
|
||||
]
|
||||
const oauthSettingShow = ref(false)
|
||||
const oauthForm = reactive({
|
||||
key: '',
|
||||
label: '',
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
switch: 1,
|
||||
})
|
||||
|
||||
const s3 = reactive({
|
||||
bucket: '',
|
||||
endpoint: '',
|
||||
@@ -930,6 +1069,23 @@ const tgBotStatus = ref(0)
|
||||
const tgBotToken = ref('')
|
||||
const forwardEmail = ref([])
|
||||
const forwardStatus = ref(0)
|
||||
const webhookUrl = ref('')
|
||||
const webhookStatus = ref(1)
|
||||
const webhookRetry = ref(0)
|
||||
const webhookSecret = ref('')
|
||||
const webhookFormatShow = ref(false)
|
||||
const webhookPayloadExample = `{
|
||||
"emailId": 1,
|
||||
"sendEmail": "[email protected]",
|
||||
"sendName": "hello",
|
||||
"toEmail": "[email protected]",
|
||||
"toName": "admin",
|
||||
"subject": "Hello",
|
||||
"text": "Hello",
|
||||
"content": "<div>Hello</div>",
|
||||
"code": "123456",
|
||||
"createTime": "2099-12-30 23:59:59"
|
||||
}`
|
||||
const emailColumnWidth = ref(0)
|
||||
const tokenColumnWidth = ref(0)
|
||||
const ruleType = ref(0)
|
||||
@@ -950,7 +1106,6 @@ function getSettings() {
|
||||
settingReady.value = false
|
||||
settingQuery().then(settingData => {
|
||||
setting.value = settingData
|
||||
settingStore.settings.passkey = settingData.passkey
|
||||
settingStore.domainList = settingData.domainList;
|
||||
resendTokenForm.domain = setting.value.domainList[0]
|
||||
loginOpacity.value = setting.value.loginOpacity
|
||||
@@ -1116,10 +1271,26 @@ function openThirdEmailSetting() {
|
||||
thirdEmailShow.value = true
|
||||
}
|
||||
|
||||
function openWebhookSetting() {
|
||||
webhookStatus.value = setting.value.webhookStatus
|
||||
webhookUrl.value = setting.value.webhookUrl || ''
|
||||
webhookRetry.value = setting.value.webhookRetry ?? 0
|
||||
webhookSecret.value = setting.value.webhookSecret || ''
|
||||
webhookShow.value = true
|
||||
}
|
||||
|
||||
function openEmailPrefix() {
|
||||
emailPrefixShow.value = true
|
||||
}
|
||||
|
||||
function openAutoClean() {
|
||||
autoCleanDays.value = setting.value.autoCleanDays ?? 0
|
||||
autoCleanExclude.value = setting.value.autoCleanExclude
|
||||
? setting.value.autoCleanExclude.split(',').filter(Boolean)
|
||||
: []
|
||||
autoCleanShow.value = true
|
||||
}
|
||||
|
||||
function openForwardRules() {
|
||||
ruleType.value = setting.value.ruleType
|
||||
ruleEmail.value = []
|
||||
@@ -1223,6 +1394,29 @@ function forwardEmailSave() {
|
||||
editSetting(form)
|
||||
}
|
||||
|
||||
function webhookSave() {
|
||||
let retry = Number(webhookRetry.value)
|
||||
if (isNaN(retry) || retry < 0) {
|
||||
retry = 0
|
||||
}
|
||||
const url = toOssDomain(webhookUrl.value.trim())
|
||||
if (isIpUrl(url)) {
|
||||
ElMessage({
|
||||
message: t('webhookIpNotSupported'),
|
||||
type: 'warning',
|
||||
plain: true
|
||||
})
|
||||
return
|
||||
}
|
||||
const form = {
|
||||
webhookStatus: webhookStatus.value,
|
||||
webhookUrl: url,
|
||||
webhookRetry: retry,
|
||||
webhookSecret: webhookSecret.value.trim()
|
||||
}
|
||||
editSetting(form)
|
||||
}
|
||||
|
||||
|
||||
function ruleEmailSave() {
|
||||
const form = {
|
||||
@@ -1250,6 +1444,13 @@ function resetBlackList() {
|
||||
blackListForm.value.blackSubject = setting.value.blackSubject ? setting.value.blackSubject.split(',') : []
|
||||
}
|
||||
|
||||
function resetAutoClean() {
|
||||
autoCleanDays.value = setting.value.autoCleanDays ?? 0
|
||||
autoCleanExclude.value = setting.value.autoCleanExclude
|
||||
? setting.value.autoCleanExclude.split(',').filter(Boolean)
|
||||
: []
|
||||
}
|
||||
|
||||
function resetAiCodeFilter() {
|
||||
aiCodeFilter.value = setting.value.aiCodeFilter ? setting.value.aiCodeFilter.split(',') : []
|
||||
}
|
||||
@@ -1261,6 +1462,13 @@ function saveEmailPrefix() {
|
||||
editSetting(form, true)
|
||||
}
|
||||
|
||||
function saveAutoClean() {
|
||||
editSetting({
|
||||
autoCleanDays: autoCleanDays.value,
|
||||
autoCleanExclude: autoCleanExclude.value.join(',')
|
||||
}, true)
|
||||
}
|
||||
|
||||
function saveAiCodeFilter() {
|
||||
editSetting({aiCodeFilter: aiCodeFilter.value + ''})
|
||||
}
|
||||
@@ -1307,6 +1515,20 @@ function banEmailAddTag(val) {
|
||||
})
|
||||
}
|
||||
|
||||
function autoCleanExcludeAddTag(val) {
|
||||
const emails = Array.from(new Set(
|
||||
val.split(/[,,]/).map(item => item.trim()).filter(item => item)
|
||||
));
|
||||
|
||||
autoCleanExclude.value.splice(autoCleanExclude.value.length - 1, 1)
|
||||
|
||||
emails.forEach(email => {
|
||||
if (isEmail(email) && !autoCleanExclude.value.includes(email)) {
|
||||
autoCleanExclude.value.push(email)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function aiCodeFilterAddTag(val) {
|
||||
const emails = Array.from(new Set(
|
||||
val.split(/[,,]/).map(item => item.trim()).filter(item => item)
|
||||
@@ -1340,6 +1562,23 @@ function delBackground() {
|
||||
})
|
||||
}
|
||||
|
||||
function openOauthSetting(p) {
|
||||
oauthForm.key = p.key
|
||||
oauthForm.label = p.label
|
||||
oauthForm.clientId = setting.value[p.key + 'ClientId'] || ''
|
||||
oauthForm.clientSecret = setting.value[p.key + 'ClientSecret'] || ''
|
||||
oauthForm.switch = setting.value[p.key + 'Switch'] ?? 1
|
||||
oauthSettingShow.value = true
|
||||
}
|
||||
|
||||
function saveOauth() {
|
||||
const form = {}
|
||||
form[oauthForm.key + 'ClientId'] = oauthForm.clientId
|
||||
form[oauthForm.key + 'ClientSecret'] = oauthForm.clientSecret
|
||||
form[oauthForm.key + 'Switch'] = oauthForm.switch
|
||||
editSetting(form)
|
||||
}
|
||||
|
||||
function saveTurnstileKey() {
|
||||
const settingForm = {}
|
||||
settingForm.siteKey = turnstileForm.siteKey
|
||||
@@ -1349,6 +1588,8 @@ function saveTurnstileKey() {
|
||||
|
||||
async function saveBackground() {
|
||||
|
||||
if (settingLoading.value) return
|
||||
|
||||
let image = ''
|
||||
|
||||
if (localUpShow.value) {
|
||||
@@ -1494,6 +1735,7 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
turnstileShow.value = false
|
||||
tgSettingShow.value = false
|
||||
thirdEmailShow.value = false
|
||||
webhookShow.value = false
|
||||
forwardRulesShow.value = false
|
||||
addVerifyCountShow.value = false
|
||||
regVerifyCountShow.value = false
|
||||
@@ -1501,6 +1743,8 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
addS3Show.value = false
|
||||
emailPrefixShow.value = false
|
||||
aiCodeFilterShow.value = false
|
||||
autoCleanShow.value = false
|
||||
oauthSettingShow.value = false
|
||||
}).catch((e) => {
|
||||
loginOpacity.value = setting.value.loginOpacity
|
||||
setting.value = {...setting.value, ...JSON.parse(backup)}
|
||||
@@ -1559,14 +1803,11 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(440px, 1fr));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
@media (max-width: 500px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
}
|
||||
@media (max-width: 1023px) {
|
||||
gap: 15px;
|
||||
padding: 15px;
|
||||
@@ -1596,17 +1837,19 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
|
||||
.settings-card {
|
||||
background-color: var(--el-bg-color);
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
transition: all 300ms;
|
||||
overflow: hidden;
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.card-title {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
padding: 10px 20px;
|
||||
padding: 15px 20px 12px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
@@ -1637,6 +1880,14 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
}
|
||||
}
|
||||
|
||||
.oauth-icon {
|
||||
width: 22px !important;
|
||||
height: 22px !important;
|
||||
min-width: 22px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.r2domain-item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
@@ -1679,6 +1930,29 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.webhook-footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.webhook-format-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: var(--el-color-primary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.webhook-format-icon {
|
||||
transition: transform 0.2s;
|
||||
&.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.notice-popup-item {
|
||||
@@ -1837,6 +2111,19 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
.el-select {
|
||||
width: v-bind(tgMsgLabelWidth);
|
||||
}
|
||||
.el-input-number {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.webhook-format pre {
|
||||
margin: 8px 0 0;
|
||||
padding: 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
border-radius: 4px;
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,9 +45,19 @@
|
||||
<el-table-column show-overflow-tooltip :tooltip-formatter="tableRowFormatter" :label="$t('tabEmailAddress')"
|
||||
:min-width="emailWidth">
|
||||
<template #default="props">
|
||||
<div style="display: flex;gap: 5px">
|
||||
<div style="display: flex;gap: 5px;align-items: center">
|
||||
<div class="email-row">{{ props.row.email }}</div>
|
||||
<el-tag type="warning" v-if="props.row.username">L</el-tag>
|
||||
<template v-if="oauthPlatform(props.row)">
|
||||
<el-avatar v-if="oauthPlatform(props.row).iconType === 'image'"
|
||||
:src="oauthPlatform(props.row).icon"
|
||||
:size="16"
|
||||
class="oauth-platform-icon"/>
|
||||
<Icon v-else
|
||||
:icon="oauthPlatform(props.row).icon"
|
||||
width="16"
|
||||
height="16"
|
||||
class="oauth-platform-icon"/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -135,7 +145,7 @@
|
||||
</el-scrollbar>
|
||||
<el-dialog class="dialog" v-model="setPwdShow" :title="$t('changePassword')" @closed="resetUserForm">
|
||||
<div class="dialog-box">
|
||||
<el-input v-model="userForm.password" type="password" :placeholder="$t('newPassword')" autocomplete="off">
|
||||
<el-input v-model="userForm.password" type="password" :placeholder="$t('newPassword')" autocomplete="off" @keyup.enter="updatePwd">
|
||||
</el-input>
|
||||
<el-button class="btn" type="primary" :loading="settingLoading" @click="updatePwd"
|
||||
>{{ $t('save') }}
|
||||
@@ -155,22 +165,24 @@
|
||||
</el-dialog>
|
||||
<el-dialog v-model="showAdd" :title="$t('addUser')" @closed="resetAddForm">
|
||||
<div class="container">
|
||||
<el-input v-model="addForm.email" type="text" :placeholder="$t('emailAccount')" autocomplete="off">
|
||||
<el-input v-model="addForm.email" type="text" :placeholder="$t('emailAccount')" autocomplete="off" @keyup.enter="submit">
|
||||
<template #append>
|
||||
<div @click.stop="openSelect">
|
||||
<el-select
|
||||
ref="mySelect"
|
||||
v-model="addForm.suffix"
|
||||
:placeholder="$t('select')"
|
||||
class="select"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in domainList"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
<div inert>
|
||||
<el-select
|
||||
ref="mySelect"
|
||||
v-model="addForm.suffix"
|
||||
:placeholder="$t('select')"
|
||||
class="select"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in domainList"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ addForm.suffix }}</span>
|
||||
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20"/>
|
||||
@@ -178,7 +190,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input type="password" v-model="addForm.password" :placeholder="$t('password')"/>
|
||||
<el-input type="password" v-model="addForm.password" :placeholder="$t('password')" @keyup.enter="submit"/>
|
||||
<el-select v-model="addForm.type" :placeholder="$t('perm')">
|
||||
<el-option v-for="item in roleList" :label="item.name" :value="item.roleId" :key="item.roleId"/>
|
||||
</el-select>
|
||||
@@ -227,12 +239,12 @@
|
||||
</el-dialog>
|
||||
<el-dialog class="account-dialog" v-model="detailsShow" :title="t('userDetails')" >
|
||||
<div class="details">
|
||||
<div v-if="userDetails.username"><span class="details-item-title">LinuxDo:</span>
|
||||
<el-avatar :src="userDetails.avatar" :size="30" class="linuxdo-avatar" />
|
||||
<span style="margin: 0 10px">用户名:{{userDetails.username}}</span>
|
||||
<span>
|
||||
等级:<el-tag type="success">{{userDetails.trustLevel}}</el-tag>
|
||||
</span>
|
||||
<div v-if="userDetails.platform || userDetails.username" class="oauth-details">
|
||||
<el-avatar :src="userDetails.avatar" :size="30" class="oauth-avatar"/>
|
||||
<span>{{ $t('username') }}:{{ userDetails.username }}</span>
|
||||
<span v-if="userDetails.trustLevel != null">
|
||||
{{ $t('trustLevel') }}:<el-tag type="success">{{ userDetails.trustLevel }}</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="!sendNumShow"><span
|
||||
class="details-item-title">{{ $t('tabSent') }}:</span>{{ userDetails.sendEmailCount }}
|
||||
@@ -396,6 +408,19 @@ const {t, locale} = useI18n();
|
||||
const roleStore = useRoleStore()
|
||||
const userStore = useUserStore()
|
||||
const settingStore = useSettingStore()
|
||||
const oauthPlatformMap = {
|
||||
google: { key: 'google', label: 'Google', icon: 'devicon:google', iconType: 'iconify' },
|
||||
github: { key: 'github', label: 'GitHub', icon: 'codicon:github-inverted', iconType: 'iconify' },
|
||||
linuxdo: { key: 'linuxdo', label: 'LinuxDo', icon: '/image/linuxdo.webp', iconType: 'image' },
|
||||
}
|
||||
function oauthPlatform(row) {
|
||||
if (row?.platform && oauthPlatformMap[row.platform]) {
|
||||
return oauthPlatformMap[row.platform]
|
||||
}
|
||||
// 旧数据可能只有 username、无 platform
|
||||
if (row?.username) return oauthPlatformMap.linuxdo
|
||||
return null
|
||||
}
|
||||
const filteredValue = ['normal', 'del']
|
||||
const filters = [{text: t('active'), value: 'normal'}, {text: t('deleted'), value: 'del'}]
|
||||
const preserveExpanded = ref(false)
|
||||
@@ -682,8 +707,6 @@ const openSelect = () => {
|
||||
|
||||
function resetAddForm() {
|
||||
addForm.email = ''
|
||||
addForm.suffix = settingStore.domainList[0]
|
||||
addForm.type = null
|
||||
addForm.password = ''
|
||||
}
|
||||
|
||||
@@ -693,6 +716,8 @@ function openAdd() {
|
||||
|
||||
function submit() {
|
||||
|
||||
if (addLoading.value) return
|
||||
|
||||
if (!addForm.email) {
|
||||
ElMessage({
|
||||
message: t('emptyEmailMsg'),
|
||||
@@ -743,7 +768,7 @@ function submit() {
|
||||
form.email = form.email + form.suffix
|
||||
userAdd(form).then(() => {
|
||||
addLoading.value = false
|
||||
addForm.email = ''
|
||||
showAdd.value = false
|
||||
ElMessage({
|
||||
message: t('addSuccessMsg'),
|
||||
type: "success",
|
||||
@@ -895,6 +920,7 @@ function httpSetStatus(user) {
|
||||
}
|
||||
|
||||
function setType() {
|
||||
if (settingLoading.value) return
|
||||
settingLoading.value = true
|
||||
userSetType({type: userForm.type, userId: userForm.userId}).then(() => {
|
||||
chooseUser.type = userForm.type
|
||||
@@ -923,6 +949,8 @@ function search() {
|
||||
|
||||
function updatePwd() {
|
||||
|
||||
if (settingLoading.value) return
|
||||
|
||||
if (!userForm.password) {
|
||||
ElMessage({
|
||||
message: t('emptyPwdMsg'),
|
||||
@@ -1147,9 +1175,19 @@ function adjustWidth() {
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.linuxdo-avatar) {
|
||||
position: relative !important;
|
||||
top: 10px;
|
||||
.oauth-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
:deep(.oauth-avatar) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:deep(.oauth-platform-icon) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.account-pagination {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.882.0",
|
||||
"@cloudflare/vite-plugin": "1.6.0",
|
||||
"@simplewebauthn/server": "^13.3.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"drizzle-orm": "^0.42.0",
|
||||
"hono": "4.12.16",
|
||||
|
||||
Generated
+10
-223
@@ -14,9 +14,6 @@ importers:
|
||||
'@cloudflare/vite-plugin':
|
||||
specifier: 1.6.0
|
||||
version: 1.6.0([email protected])([email protected](@types/[email protected]))([email protected])([email protected])
|
||||
'@simplewebauthn/server':
|
||||
specifier: ^13.3.2
|
||||
version: 13.3.2
|
||||
dayjs:
|
||||
specifier: ^1.11.13
|
||||
version: 1.11.18
|
||||
@@ -47,10 +44,10 @@ importers:
|
||||
devDependencies:
|
||||
'@cloudflare/vitest-pool-workers':
|
||||
specifier: ^0.7.5
|
||||
version: 0.7.8(@vitest/[email protected])(@vitest/[email protected])([email protected](@types/[email protected])([email protected]))
|
||||
version: 0.7.8(@vitest/[email protected])(@vitest/[email protected])([email protected](@types/[email protected]))
|
||||
vitest:
|
||||
specifier: ~3.0.7
|
||||
version: 3.0.9(@types/[email protected])([email protected])
|
||||
version: 3.0.9(@types/[email protected])
|
||||
wrangler:
|
||||
specifier: ^4.90.0
|
||||
version: 4.90.0
|
||||
@@ -818,9 +815,6 @@ packages:
|
||||
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@hexagon/[email protected]':
|
||||
resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==}
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1101,52 +1095,9 @@ packages:
|
||||
'@jridgewell/[email protected]':
|
||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||
|
||||
'@levischuck/[email protected]':
|
||||
resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==}
|
||||
|
||||
'@mjackson/[email protected]':
|
||||
resolution: {integrity: sha512-9ZJnk/DJjt805uv5PPv11haJIW+HHf3YEEyVXv+8iLQxLD/iXA68FH220XoiTPBC4gCg5q+IMadDw8qPqlA5wg==}
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
resolution: {integrity: sha512-skLbS+IOGv1lUgDqtChr8xvtvEr3HMse/JGBaL2r1J1o/n7a8wqOrovMtlRq/UXLhxvmLaONP67hwtshgzwfzA==}
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
resolution: {integrity: sha512-NgekZOrSJFSBFLFoLfwePguAWAx7z1+f2TEsWFUMyiqqfntZ4+S/S5hzqME3q4pCA0iOsFKdwiQ35dwY24eVqA==}
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
resolution: {integrity: sha512-akbF8+uvleHs8sejNPQxwmVFuInAg6FMNHOwMILXfP518YfFJwdR3jr6oNUPOaEJfuEhn/vkNOCIT6ASUd4mbg==}
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
resolution: {integrity: sha512-ohwlk+u9Rv2NOAY1c6MfHj45ATVF8R1DUN/WCgABiRtLi2ZftlZWZX7KvpAbU8v9xPcmoILfELeEABj/rn18AQ==}
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
resolution: {integrity: sha512-5yof1ytoB++RQtaFbqSUJ8pxDJtZT6vbVqZ8XoJ61ph7UjNVvfFwAilnCodqkNsAodpy13gDhoxZXw00pghnyg==}
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
resolution: {integrity: sha512-qAKXtLpBEw9LqhKpjw3ajZSXlBur+ipW+y2ivVBQAG6F6qRx94yO+1ZR4mvw+YaCfKSaOzLeYEzsPaBp4SJELA==}
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
resolution: {integrity: sha512-b5nDWCnkV60+cQ141D6sVVwK9nz64R5n3zSVnklGd+ECdkW2Ol3U1a6yYFlalpSOaD557yuJB64A+q42jG7lUQ==}
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
resolution: {integrity: sha512-zHEUlCqB2mk7x2lxDwHHJy7hWZOPdGHVlsmITWKB5/PbQo61atbu9PJ/0r9dQNMwFzbKPXZ8uK8/91eUhRznSg==}
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
resolution: {integrity: sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==}
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
resolution: {integrity: sha512-tHjkfS/qhMnmrlB2J9NhflQlQ7In3khO3CfmVrriOlpTeErY9ZIKOso1hQ5JQiyrJ7ShvqVPk7E5fQmbclkSKA==}
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
resolution: {integrity: sha512-N0CMuhWUzsWEVq6F1q9X6+VKUnWzSW+cSVg+aPaGGwDdbFoFWTYgin5MHwXgpWd6y9COMBxnfy/Qc+Xc7F0Zwg==}
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
resolution: {integrity: sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==}
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
resolution: {integrity: sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@poppinss/[email protected]':
|
||||
resolution: {integrity: sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw==}
|
||||
|
||||
@@ -1300,10 +1251,6 @@ packages:
|
||||
'@selderee/[email protected]':
|
||||
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||
|
||||
'@simplewebauthn/[email protected]':
|
||||
resolution: {integrity: sha512-KEDhfcGP1PAKRVSDjA3npTQFqS2b/srm+ipoNBNHdkzrHAlaRQUTE+a5f4ywsx6thxAw1NU2rYcLEY1949RGbQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@sindresorhus/[email protected]':
|
||||
resolution: {integrity: sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1693,10 +1640,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -2215,13 +2158,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
|
||||
|
||||
@@ -2237,9 +2173,6 @@ packages:
|
||||
resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
||||
|
||||
@@ -2357,16 +2290,9 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==}
|
||||
|
||||
@@ -2408,7 +2334,6 @@ packages:
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
@@ -2417,7 +2342,6 @@ packages:
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
@@ -3082,7 +3006,7 @@ snapshots:
|
||||
- utf-8-validate
|
||||
- workerd
|
||||
|
||||
'@cloudflare/[email protected](@vitest/[email protected])(@vitest/[email protected])([email protected](@types/[email protected])([email protected]))':
|
||||
'@cloudflare/[email protected](@vitest/[email protected])(@vitest/[email protected])([email protected](@types/[email protected]))':
|
||||
dependencies:
|
||||
'@vitest/runner': 3.0.9
|
||||
'@vitest/snapshot': 3.0.9
|
||||
@@ -3092,7 +3016,7 @@ snapshots:
|
||||
esbuild: 0.17.19
|
||||
miniflare: 3.20250310.0
|
||||
semver: 7.7.2
|
||||
vitest: 3.0.9(@types/[email protected])([email protected])
|
||||
vitest: 3.0.9(@types/[email protected])
|
||||
wrangler: 3.114.1
|
||||
zod: 3.25.76
|
||||
transitivePeerDependencies:
|
||||
@@ -3393,8 +3317,6 @@ snapshots:
|
||||
|
||||
'@fastify/[email protected]': {}
|
||||
|
||||
'@hexagon/[email protected]': {}
|
||||
|
||||
'@img/[email protected]': {}
|
||||
|
||||
'@img/[email protected]':
|
||||
@@ -3575,110 +3497,8 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@levischuck/[email protected]': {}
|
||||
|
||||
'@mjackson/[email protected]': {}
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.8.0
|
||||
asn1js: 3.0.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.8.0
|
||||
'@peculiar/asn1-x509': 2.8.0
|
||||
'@peculiar/asn1-x509-attr': 2.8.0
|
||||
asn1js: 3.0.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.8.0
|
||||
'@peculiar/asn1-x509': 2.8.0
|
||||
asn1js: 3.0.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.8.0
|
||||
'@peculiar/asn1-x509': 2.8.0
|
||||
asn1js: 3.0.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
dependencies:
|
||||
'@peculiar/asn1-cms': 2.8.0
|
||||
'@peculiar/asn1-pkcs8': 2.8.0
|
||||
'@peculiar/asn1-rsa': 2.8.0
|
||||
'@peculiar/asn1-schema': 2.8.0
|
||||
asn1js: 3.0.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.8.0
|
||||
'@peculiar/asn1-x509': 2.8.0
|
||||
asn1js: 3.0.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
dependencies:
|
||||
'@peculiar/asn1-cms': 2.8.0
|
||||
'@peculiar/asn1-pfx': 2.8.0
|
||||
'@peculiar/asn1-pkcs8': 2.8.0
|
||||
'@peculiar/asn1-schema': 2.8.0
|
||||
'@peculiar/asn1-x509': 2.8.0
|
||||
'@peculiar/asn1-x509-attr': 2.8.0
|
||||
asn1js: 3.0.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.8.0
|
||||
'@peculiar/asn1-x509': 2.8.0
|
||||
asn1js: 3.0.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
dependencies:
|
||||
'@peculiar/utils': 2.0.3
|
||||
asn1js: 3.0.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.8.0
|
||||
'@peculiar/asn1-x509': 2.8.0
|
||||
asn1js: 3.0.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.8.0
|
||||
'@peculiar/utils': 2.0.3
|
||||
asn1js: 3.0.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/[email protected]':
|
||||
dependencies:
|
||||
'@peculiar/asn1-cms': 2.8.0
|
||||
'@peculiar/asn1-csr': 2.8.0
|
||||
'@peculiar/asn1-ecc': 2.8.0
|
||||
'@peculiar/asn1-pkcs9': 2.8.0
|
||||
'@peculiar/asn1-rsa': 2.8.0
|
||||
'@peculiar/asn1-schema': 2.8.0
|
||||
'@peculiar/asn1-x509': 2.8.0
|
||||
pvtsutils: 1.3.6
|
||||
reflect-metadata: 0.2.2
|
||||
tslib: 2.8.1
|
||||
tsyringe: 4.10.0
|
||||
|
||||
'@poppinss/[email protected]':
|
||||
dependencies:
|
||||
kleur: 4.1.5
|
||||
@@ -3784,17 +3604,6 @@ snapshots:
|
||||
selderee: 0.11.0
|
||||
optional: true
|
||||
|
||||
'@simplewebauthn/[email protected]':
|
||||
dependencies:
|
||||
'@hexagon/base64': 1.1.28
|
||||
'@levischuck/tiny-cbor': 0.2.11
|
||||
'@peculiar/asn1-android': 2.8.0
|
||||
'@peculiar/asn1-ecc': 2.8.0
|
||||
'@peculiar/asn1-rsa': 2.8.0
|
||||
'@peculiar/asn1-schema': 2.8.0
|
||||
'@peculiar/asn1-x509': 2.8.0
|
||||
'@peculiar/x509': 1.14.3
|
||||
|
||||
'@sindresorhus/[email protected]': {}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
@@ -4359,12 +4168,6 @@ snapshots:
|
||||
dependencies:
|
||||
printable-characters: 1.0.42
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
pvtsutils: 1.3.6
|
||||
pvutils: 1.1.5
|
||||
tslib: 2.8.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -4438,11 +4241,9 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]([email protected]):
|
||||
[email protected]:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
optionalDependencies:
|
||||
supports-color: 10.2.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
@@ -4840,12 +4641,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]([email protected]):
|
||||
@@ -4862,8 +4657,6 @@ snapshots:
|
||||
[email protected]:
|
||||
optional: true
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected](@react-email/[email protected]([email protected]([email protected]))([email protected])):
|
||||
@@ -5035,14 +4828,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
tslib: 1.14.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -5100,10 +4887,10 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected](@types/[email protected])([email protected]):
|
||||
[email protected](@types/[email protected]):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.1([email protected])
|
||||
debug: 4.4.1
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 2.0.3
|
||||
vite: 6.3.5(@types/[email protected])
|
||||
@@ -5133,7 +4920,7 @@ snapshots:
|
||||
'@types/node': 24.3.0
|
||||
fsevents: 2.3.3
|
||||
|
||||
[email protected](@types/[email protected])([email protected]):
|
||||
[email protected](@types/[email protected]):
|
||||
dependencies:
|
||||
'@vitest/expect': 3.0.9
|
||||
'@vitest/mocker': 3.0.9([email protected](@types/[email protected]))
|
||||
@@ -5143,7 +4930,7 @@ snapshots:
|
||||
'@vitest/spy': 3.0.9
|
||||
'@vitest/utils': 3.0.9
|
||||
chai: 5.3.3
|
||||
debug: 4.4.1([email protected])
|
||||
debug: 4.4.1
|
||||
expect-type: 1.2.2
|
||||
magic-string: 0.30.18
|
||||
pathe: 2.0.3
|
||||
@@ -5153,7 +4940,7 @@ snapshots:
|
||||
tinypool: 1.1.1
|
||||
tinyrainbow: 2.0.0
|
||||
vite: 6.3.5(@types/[email protected])
|
||||
vite-node: 3.0.9(@types/[email protected])([email protected])
|
||||
vite-node: 3.0.9(@types/[email protected])
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 24.3.0
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
packages:
|
||||
- '.'
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
sharp: true
|
||||
workerd: true
|
||||
@@ -7,6 +7,16 @@ app.post('/oauth/linuxDo/login', async (c) => {
|
||||
return c.json(result.ok(loginInfo))
|
||||
});
|
||||
|
||||
app.post('/oauth/github/login', async (c) => {
|
||||
const loginInfo = await oauthService.githubLogin(c, await c.req.json());
|
||||
return c.json(result.ok(loginInfo))
|
||||
});
|
||||
|
||||
app.post('/oauth/google/login', async (c) => {
|
||||
const loginInfo = await oauthService.googleLogin(c, await c.req.json());
|
||||
return c.json(result.ok(loginInfo))
|
||||
});
|
||||
|
||||
app.put('/oauth/bindUser', async (c) => {
|
||||
const loginInfo = await oauthService.bindUser(c, await c.req.json());
|
||||
return c.json(result.ok(loginInfo))
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import app from '../hono/hono';
|
||||
import result from '../model/result';
|
||||
import userContext from '../security/user-context';
|
||||
import passkeyService from '../service/passkey-service';
|
||||
|
||||
app.post('/passkey/auth/options', async (c) => {
|
||||
return c.json(result.ok(await passkeyService.authenticationOptions(c)));
|
||||
});
|
||||
|
||||
app.post('/passkey/auth/verify', async (c) => {
|
||||
const token = await passkeyService.verifyAuthentication(c, await c.req.json());
|
||||
return c.json(result.ok({ token }));
|
||||
});
|
||||
|
||||
app.get('/my/passkey', async (c) => {
|
||||
return c.json(result.ok(await passkeyService.status(c, userContext.getUserId(c))));
|
||||
});
|
||||
|
||||
app.post('/my/passkey/reg/options', async (c) => {
|
||||
const data = await passkeyService.registrationOptions(c, userContext.getUserId(c), await c.req.json());
|
||||
return c.json(result.ok(data));
|
||||
});
|
||||
|
||||
app.post('/my/passkey/reg/verify', async (c) => {
|
||||
const data = await passkeyService.verifyRegistration(c, userContext.getUserId(c), await c.req.json());
|
||||
return c.json(result.ok(data));
|
||||
});
|
||||
|
||||
app.delete('/my/passkey', async (c) => {
|
||||
await passkeyService.delete(c, userContext.getUserId(c), await c.req.json());
|
||||
return c.json(result.ok());
|
||||
});
|
||||
@@ -112,6 +112,10 @@ export const settingConst = {
|
||||
OPEN: 0,
|
||||
CLOSE: 1,
|
||||
},
|
||||
webhookStatus: {
|
||||
OPEN: 0,
|
||||
CLOSE: 1,
|
||||
},
|
||||
ruleType: {
|
||||
ALL: 0,
|
||||
RULE: 1
|
||||
@@ -132,6 +136,10 @@ export const settingConst = {
|
||||
OPEN: 0,
|
||||
CLOSE: 1
|
||||
},
|
||||
syncDelete: {
|
||||
OPEN: 0,
|
||||
CLOSE: 1
|
||||
},
|
||||
authRefresh: {
|
||||
OPEN: 1,
|
||||
CLOSE: 0
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { emailConst } from '../const/entity-const';
|
||||
|
||||
const analysisDao = {
|
||||
async numberCount(c) {
|
||||
const { results } = await c.env.db.prepare(`
|
||||
@@ -26,7 +24,7 @@ const analysisDao = {
|
||||
SUM(CASE WHEN type = 0 AND is_del = 0 THEN 1 ELSE 0 END) AS normalReceiveTotal,
|
||||
SUM(CASE WHEN type = 1 AND is_del = 0 THEN 1 ELSE 0 END) AS normalSendTotal
|
||||
FROM
|
||||
email where status != ${emailConst.status.SAVING}
|
||||
email
|
||||
) e
|
||||
CROSS JOIN (
|
||||
SELECT
|
||||
@@ -49,16 +47,18 @@ const analysisDao = {
|
||||
},
|
||||
|
||||
async userDayCount(c, diffHours) {
|
||||
const { tzMod, tzBack } = this.tzModifiers(diffHours);
|
||||
const { results } = await c.env.db.prepare(`
|
||||
SELECT
|
||||
DATE(create_time,'+${diffHours} hours') AS date,
|
||||
DATE(create_time, '${tzMod}') AS date,
|
||||
COUNT(*) AS total
|
||||
FROM
|
||||
user
|
||||
WHERE
|
||||
DATE(create_time,'+${diffHours} hours') BETWEEN DATE('now', '-15 days', '+${diffHours} hours') AND DATE('now','-1 day','+${diffHours} hours')
|
||||
create_time >= datetime('now', '${tzMod}', 'start of day', '-15 days', '${tzBack}')
|
||||
AND create_time < datetime('now', '${tzMod}', 'start of day', '${tzBack}')
|
||||
GROUP BY
|
||||
DATE(create_time,'+${diffHours} hours')
|
||||
DATE(create_time, '${tzMod}')
|
||||
ORDER BY
|
||||
date ASC
|
||||
`).all();
|
||||
@@ -66,17 +66,19 @@ const analysisDao = {
|
||||
},
|
||||
|
||||
async receiveDayCount(c, diffHours) {
|
||||
const { tzMod, tzBack } = this.tzModifiers(diffHours);
|
||||
const { results } = await c.env.db.prepare(`
|
||||
SELECT
|
||||
DATE(create_time,'+${diffHours} hours') AS date,
|
||||
DATE(create_time, '${tzMod}') AS date,
|
||||
COUNT(*) AS total
|
||||
FROM
|
||||
email
|
||||
WHERE
|
||||
DATE(create_time,'+${diffHours} hours') BETWEEN DATE('now', '-15 days', '+${diffHours} hours') AND DATE('now','-1 day','+${diffHours} hours')
|
||||
AND type = 0
|
||||
type = 0
|
||||
AND create_time >= datetime('now', '${tzMod}', 'start of day', '-15 days', '${tzBack}')
|
||||
AND create_time < datetime('now', '${tzMod}', 'start of day', '${tzBack}')
|
||||
GROUP BY
|
||||
DATE(create_time,'+${diffHours} hours')
|
||||
DATE(create_time, '${tzMod}')
|
||||
ORDER BY
|
||||
date ASC
|
||||
`).all();
|
||||
@@ -84,21 +86,29 @@ const analysisDao = {
|
||||
},
|
||||
|
||||
async sendDayCount(c, diffHours) {
|
||||
const { tzMod, tzBack } = this.tzModifiers(diffHours);
|
||||
const { results } = await c.env.db.prepare(`
|
||||
SELECT
|
||||
DATE(create_time,'+${diffHours} hours') AS date,
|
||||
DATE(create_time, '${tzMod}') AS date,
|
||||
COUNT(*) AS total
|
||||
FROM
|
||||
email
|
||||
WHERE
|
||||
DATE(create_time,'+${diffHours} hours') BETWEEN DATE('now', '-15 days', '+${diffHours} hours') AND DATE('now','-1 day','+${diffHours} hours')
|
||||
AND type = 1
|
||||
type = 1
|
||||
AND create_time >= datetime('now', '${tzMod}', 'start of day', '-15 days', '${tzBack}')
|
||||
AND create_time < datetime('now', '${tzMod}', 'start of day', '${tzBack}')
|
||||
GROUP BY
|
||||
DATE(create_time,'+${diffHours} hours')
|
||||
DATE(create_time, '${tzMod}')
|
||||
ORDER BY
|
||||
date ASC
|
||||
`).all();
|
||||
return results;
|
||||
},
|
||||
|
||||
tzModifiers(diffHours) {
|
||||
const tzMod = diffHours >= 0 ? `+${diffHours} hours` : `${diffHours} hours`;
|
||||
const tzBack = (-diffHours) >= 0 ? `+${-diffHours} hours` : `${-diffHours} hours`;
|
||||
return { tzMod, tzBack };
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import roleService from '../service/role-service';
|
||||
import userService from '../service/user-service';
|
||||
import telegramService from '../service/telegram-service';
|
||||
import aiService from '../service/ai-service';
|
||||
import webhookService from '../service/webhook-service';
|
||||
|
||||
export async function email(message, env, ctx) {
|
||||
|
||||
@@ -22,6 +23,10 @@ export async function email(message, env, ctx) {
|
||||
tgBotStatus,
|
||||
forwardStatus,
|
||||
forwardEmail,
|
||||
webhookStatus,
|
||||
webhookUrl,
|
||||
webhookRetry,
|
||||
webhookSecret,
|
||||
ruleEmail,
|
||||
ruleType,
|
||||
r2Domain,
|
||||
@@ -57,7 +62,14 @@ export async function email(message, env, ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
const account = await accountService.selectByEmailIncludeDel({ env: env }, message.to);
|
||||
let account = await accountService.selectByEmailIncludeDel({ env: env }, message.to);
|
||||
|
||||
if (!account) {
|
||||
const baseEmail = emailUtils.getBaseEmail(message.to);
|
||||
if (baseEmail && baseEmail !== message.to) {
|
||||
account = await accountService.selectByEmailIncludeDel({ env: env }, baseEmail);
|
||||
}
|
||||
}
|
||||
|
||||
if (!account && noRecipient === settingConst.noRecipient.CLOSE) {
|
||||
message.setReject('Recipient not found');
|
||||
@@ -179,6 +191,11 @@ export async function email(message, env, ctx) {
|
||||
|
||||
}
|
||||
|
||||
//转发到 Webhook
|
||||
if (webhookStatus === settingConst.webhookStatus.OPEN && webhookUrl) {
|
||||
await webhookService.sendEmail({ env }, emailRow, webhookUrl, webhookRetry, webhookSecret);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error('邮件接收异常: ', e);
|
||||
throw e
|
||||
|
||||
@@ -11,6 +11,7 @@ export const oauth = sqliteTable('oauth', {
|
||||
trustLevel: integer('trust_level'),
|
||||
silenced: integer('silenced'),
|
||||
createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
platform: text('platform'),
|
||||
userId: integer('user_id').default(0).notNull()
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const passkeyCredential = sqliteTable('passkey_credential', {
|
||||
passkeyId: integer('passkey_id').primaryKey({ autoIncrement: true }),
|
||||
userId: integer('user_id').notNull().unique(),
|
||||
userHandle: text('user_handle').notNull().unique(),
|
||||
credentialId: text('credential_id').notNull().unique(),
|
||||
publicKey: text('public_key').notNull(),
|
||||
counter: integer('counter').default(0).notNull(),
|
||||
transports: text('transports').default('[]').notNull(),
|
||||
createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
});
|
||||
|
||||
export default passkeyCredential;
|
||||
@@ -35,7 +35,6 @@ export const setting = sqliteTable('setting', {
|
||||
notice: integer('notice').default(0).notNull(),
|
||||
noRecipient: integer('no_recipient').default(1).notNull(),
|
||||
loginDomain: integer('login_domain').default(0).notNull(),
|
||||
passkey: integer('passkey').default(0).notNull(),
|
||||
bucket: text('bucket').default('').notNull(),
|
||||
region: text('region').default('').notNull(),
|
||||
endpoint: text('endpoint').default('').notNull(),
|
||||
@@ -52,6 +51,22 @@ export const setting = sqliteTable('setting', {
|
||||
blackContent: text('black_content').default('').notNull(),
|
||||
blackFrom: text('black_from').default('').notNull(),
|
||||
aiCode: integer('ai_code').default(1).notNull(),
|
||||
aiCodeFilter: text('ai_code_filter').default('').notNull()
|
||||
syncDelete: integer('sync_delete').default(1).notNull(),
|
||||
aiCodeFilter: text('ai_code_filter').default('').notNull(),
|
||||
linuxdoClientId: text('linuxdo_client_id').default('').notNull(),
|
||||
linuxdoClientSecret: text('linuxdo_client_secret').default('').notNull(),
|
||||
linuxdoSwitch: integer('linuxdo_switch').default(1).notNull(),
|
||||
githubClientId: text('github_client_id').default('').notNull(),
|
||||
githubClientSecret: text('github_client_secret').default('').notNull(),
|
||||
githubSwitch: integer('github_switch').default(1).notNull(),
|
||||
googleClientId: text('google_client_id').default('').notNull(),
|
||||
googleClientSecret: text('google_client_secret').default('').notNull(),
|
||||
googleSwitch: integer('google_switch').default(1).notNull(),
|
||||
autoCleanDays: integer('auto_clean_days').default(0).notNull(),
|
||||
autoCleanExclude: text('auto_clean_exclude').default('').notNull(),
|
||||
webhookUrl: text('webhook_url').default('').notNull(),
|
||||
webhookStatus: integer('webhook_status').default(1).notNull(),
|
||||
webhookRetry: integer('webhook_retry').default(0).notNull(),
|
||||
webhookSecret: text('webhook_secret').default('').notNull()
|
||||
});
|
||||
export default setting
|
||||
|
||||
@@ -14,15 +14,19 @@ app.onError((err, c) => {
|
||||
}
|
||||
|
||||
if (err.message === `Cannot read properties of undefined (reading 'get')`) {
|
||||
return c.json(result.fail('KV数据库未绑定 KV database not bound',502));
|
||||
return c.json(result.fail('KV数据库未绑定<br/>KV database not bound',502));
|
||||
}
|
||||
|
||||
if (err.message === `Cannot read properties of undefined (reading 'put')`) {
|
||||
return c.json(result.fail('KV数据库未绑定 KV database not bound',502));
|
||||
return c.json(result.fail('KV数据库未绑定<br/>KV database not bound',502));
|
||||
}
|
||||
|
||||
if (err.message === `Cannot read properties of undefined (reading 'prepare')`) {
|
||||
return c.json(result.fail('D1数据库未绑定 D1 database not bound',502));
|
||||
return c.json(result.fail('D1数据库未绑定<br/>D1 database not bound',502));
|
||||
}
|
||||
|
||||
if (err.message?.includes('D1_ERROR: no such column')) {
|
||||
return c.json(result.fail('请按照文档更新数据库<br/>Please update the database as documented',502));
|
||||
}
|
||||
|
||||
return c.json(result.fail(err.message, err.code));
|
||||
|
||||
@@ -20,5 +20,4 @@ import '../api/reg-key-api'
|
||||
import '../api/public-api'
|
||||
import '../api/telegram-api'
|
||||
import '../api/oauth-api'
|
||||
import '../api/passkey-api'
|
||||
export default app;
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
const en = {
|
||||
passkeyDisabled: 'Passkey sign-in is disabled',
|
||||
passkeyChallengeInvalid: 'The Passkey request expired. Please try again',
|
||||
passkeyVerifyFailed: 'Passkey verification failed',
|
||||
passkeyNotFound: 'No matching Passkey was found',
|
||||
passkeyOriginInvalid: 'Passkeys are unavailable on this site',
|
||||
IncorrectPwd: 'Incorrect password',
|
||||
addAccountDisabled: 'Add Email Address feature is disabled',
|
||||
regDisabled: 'Sign up is disabled',
|
||||
emptyEmail: 'Email cannot be empty',
|
||||
emptyAccountId: 'accountId cannot be empty',
|
||||
notEmail: 'Invalid email',
|
||||
notExistDomain: 'Email domain does not exist',
|
||||
isDelAccount: 'This Email has been deleted',
|
||||
@@ -20,6 +16,7 @@ const en = {
|
||||
noOsDomainSendAtt: 'Cannot send attachments: object storage domain not configured',
|
||||
noOsSendAtt: 'Cannot send attachments: object storage not configured',
|
||||
disabledSend: 'Email sending feature is disabled',
|
||||
oauthDisabled: 'This OAuth login is not enabled',
|
||||
daySendLimit: 'Daily send limit reached',
|
||||
totalSendLimit: 'Total send limit reached',
|
||||
daySendLack: 'Not enough remaining sends today',
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
const zh = {
|
||||
passkeyDisabled: 'Passkey 登录未启用',
|
||||
passkeyChallengeInvalid: 'Passkey 请求已过期,请重试',
|
||||
passkeyVerifyFailed: 'Passkey 验证失败',
|
||||
passkeyNotFound: '未找到可用的 Passkey',
|
||||
passkeyOriginInvalid: '当前站点无法使用 Passkey',
|
||||
IncorrectPwd: '密码输入错误',
|
||||
addAccountDisabled: '添加邮箱功能已关闭',
|
||||
regDisabled: '注册功能已关闭',
|
||||
emptyEmail: '邮箱不能为空',
|
||||
emptyAccountId: 'accountId不能为空',
|
||||
notEmail: '非法邮箱',
|
||||
notExistDomain: '不存在的邮箱域名',
|
||||
isDelAccount: '该邮箱已被注销',
|
||||
@@ -20,6 +16,7 @@ const zh = {
|
||||
noOsDomainSendAtt: '对象存储域名未配置不能发送附件',
|
||||
noOsSendAtt: '对象存储未配置不能发送附件',
|
||||
disabledSend: '邮件发送功能已停用',
|
||||
oauthDisabled: '该 OAuth 登录未启用',
|
||||
daySendLimit: '发送次数已到达每日限制',
|
||||
totalSendLimit: '发送次数已到达限制',
|
||||
daySendLack: '当日剩余发送次数不足',
|
||||
|
||||
@@ -4,7 +4,7 @@ import userService from './service/user-service';
|
||||
import verifyRecordService from './service/verify-record-service';
|
||||
import emailService from './service/email-service';
|
||||
import kvObjService from './service/kv-obj-service';
|
||||
import oauthService from "./service/oauth-service";
|
||||
import oauthService from './service/oauth-service';
|
||||
import analysisService from './service/analysis-service';
|
||||
export default {
|
||||
async fetch(req, env, ctx) {
|
||||
@@ -33,7 +33,8 @@ export default {
|
||||
await verifyRecordService.clearRecord({ env })
|
||||
await userService.resetDaySendCount({ env })
|
||||
await emailService.completeReceiveAll({ env })
|
||||
await oauthService.clearNoBindOathUser({ env })
|
||||
await emailService.autoClean({ env })
|
||||
await analysisService.refreshEchartsCache({ env })
|
||||
await oauthService.clearNoBindOathUser({ env })
|
||||
},
|
||||
};
|
||||
|
||||
@@ -30,46 +30,90 @@ const dbInit = {
|
||||
await this.v2_9DB(c);
|
||||
await this.v3_0DB(c);
|
||||
await this.v3_1DB(c);
|
||||
await this.v3_2DB(c);
|
||||
await this.v3_3DB(c);
|
||||
await settingService.refresh(c);
|
||||
return c.text('success');
|
||||
},
|
||||
|
||||
async v3_1DB(c) {
|
||||
async v3_3DB(c) {
|
||||
try {
|
||||
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN passkey INTEGER NOT NULL DEFAULT 0;`).run();
|
||||
await c.env.db.batch([
|
||||
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN auto_clean_days INTEGER NOT NULL DEFAULT 0;`),
|
||||
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN auto_clean_exclude TEXT NOT NULL DEFAULT '';`),
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_email_create_time ON email(create_time)`)
|
||||
]);
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段:${e.message}`);
|
||||
}
|
||||
|
||||
await c.env.db.batch([
|
||||
c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS passkey_credential (
|
||||
passkey_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL UNIQUE,
|
||||
user_handle TEXT NOT NULL UNIQUE,
|
||||
credential_id TEXT NOT NULL UNIQUE,
|
||||
public_key TEXT NOT NULL,
|
||||
counter INTEGER NOT NULL DEFAULT 0,
|
||||
transports TEXT NOT NULL DEFAULT '[]',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`),
|
||||
c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS passkey_challenge (
|
||||
challenge_id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
challenge TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL DEFAULT 0,
|
||||
user_handle TEXT NOT NULL DEFAULT '',
|
||||
origin TEXT NOT NULL,
|
||||
rp_id TEXT NOT NULL,
|
||||
expires_at INTEGER NOT NULL
|
||||
)
|
||||
`),
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_passkey_challenge_expires ON passkey_challenge(expires_at)`),
|
||||
]);
|
||||
try {
|
||||
await c.env.db.batch([
|
||||
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN webhook_url TEXT NOT NULL DEFAULT '';`),
|
||||
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN webhook_status INTEGER NOT NULL DEFAULT 1;`),
|
||||
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN webhook_retry INTEGER NOT NULL DEFAULT 0;`),
|
||||
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN webhook_secret TEXT NOT NULL DEFAULT '';`)
|
||||
]);
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段:${e.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async v3_2DB(c) {
|
||||
try {
|
||||
await c.env.db.batch([
|
||||
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN linuxdo_client_id TEXT NOT NULL DEFAULT '';`),
|
||||
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN linuxdo_client_secret TEXT NOT NULL DEFAULT '';`),
|
||||
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN github_client_id TEXT NOT NULL DEFAULT '';`),
|
||||
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN github_client_secret TEXT NOT NULL DEFAULT '';`),
|
||||
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN google_client_id TEXT NOT NULL DEFAULT '';`),
|
||||
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN google_client_secret TEXT NOT NULL DEFAULT '';`),
|
||||
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN linuxdo_switch INTEGER NOT NULL DEFAULT 1;`),
|
||||
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN github_switch INTEGER NOT NULL DEFAULT 1;`),
|
||||
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN google_switch INTEGER NOT NULL DEFAULT 1;`)
|
||||
]);
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段:${e.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
await c.env.db.batch([
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_email_list_user ON email(user_id, type, is_del, email_id)`),
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_email_list_account ON email(user_id, account_id, type, is_del, email_id)`),
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_star_user_email ON star(user_id, email_id)`),
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_star_email_user ON star(email_id, user_id)`),
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_email_name_nocase ON email(name COLLATE NOCASE)`),
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_email_subject_nocase ON email(subject COLLATE NOCASE)`),
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_user_email_nocase ON user(email COLLATE NOCASE)`),
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_email_to_email_nocase ON email(to_email COLLATE NOCASE)`),
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_email_send_email_nocase ON email(send_email COLLATE NOCASE)`),
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_email_noone_id ON email(email_id) WHERE status = 7`),
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_email_type_id ON email(type, email_id)`),
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_account_user_del_sort ON account(user_id, is_del, sort, account_id)`),
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_email_saving_account ON email(account_id) WHERE status = 6`),
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_email_type_name ON email(type, name)`),
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_email_type_create_time ON email(type, create_time)`),
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_user_create_time ON user(create_time)`),
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_user_type ON user(type)`),
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_attachments_email_type ON attachments(email_id, type)`),
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_role_perm_role ON role_perm(role_id)`),
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_oauth_oauth_user_id ON oauth(oauth_user_id)`),
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_oauth_user_id ON oauth(user_id)`)
|
||||
]);
|
||||
} catch (e) {
|
||||
console.warn(`跳过索引:${e.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async v3_1DB(c) {
|
||||
try {
|
||||
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN sync_delete INTEGER NOT NULL DEFAULT 0;`).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段:${e.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
async v3_0DB(c) {
|
||||
try {
|
||||
await c.env.db.batch([
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { getTableColumns, sql } from 'drizzle-orm';
|
||||
import email from '../entity/email';
|
||||
|
||||
export const EMAIL_LIST_TEXT_LEN = 300;
|
||||
|
||||
/** 去掉换行/回车/制表符,并压缩连续空格、标签间空白 */
|
||||
function sqlStripWhitespace(column) {
|
||||
return sql`trim(replace(replace(replace(replace(replace(replace(
|
||||
coalesce(${column}, ''),
|
||||
char(13), ''),
|
||||
char(10), ''),
|
||||
char(9), ' '),
|
||||
' ', ' '),
|
||||
' ', ' '),
|
||||
'> <', '><'))`;
|
||||
}
|
||||
|
||||
/** 完整查询:全部字段 */
|
||||
export const emailListColumns = getTableColumns(email);
|
||||
|
||||
/** 摘要查询:列表 + 详情头部;有 text 则不读 content,没有才查 content(去空白),响应里不返回 content */
|
||||
export const emailBriefColumns = {
|
||||
emailId: email.emailId,
|
||||
sendEmail: email.sendEmail,
|
||||
name: email.name,
|
||||
subject: email.subject,
|
||||
code: email.code,
|
||||
recipient: email.recipient,
|
||||
toEmail: email.toEmail,
|
||||
type: email.type,
|
||||
status: email.status,
|
||||
message: email.message,
|
||||
unread: email.unread,
|
||||
createTime: email.createTime,
|
||||
isDel: email.isDel,
|
||||
content: sql`CASE WHEN trim(coalesce(${email.text}, '')) != '' THEN NULL ELSE ${sqlStripWhitespace(email.content)} END`.as('content'),
|
||||
text: sql`substr(coalesce(${email.text}, ''), 1, ${EMAIL_LIST_TEXT_LEN})`.as('text'),
|
||||
};
|
||||
@@ -18,8 +18,7 @@ const exclude = [
|
||||
'/public/genToken',
|
||||
'/telegram',
|
||||
'/test',
|
||||
'/oauth',
|
||||
'/passkey/auth/'
|
||||
'/oauth'
|
||||
];
|
||||
|
||||
const requirePerms = [
|
||||
|
||||
@@ -57,6 +57,14 @@ const accountService = {
|
||||
throw new BizError(t('isRegAccount'));
|
||||
}
|
||||
|
||||
if (email.includes('+')) {
|
||||
const baseEmail = emailUtils.getBaseEmail(email);
|
||||
const baseAccount = await this.selectByEmailIncludeDel(c, baseEmail);
|
||||
if (!baseAccount || baseAccount.userId !== userId) {
|
||||
throw new BizError(t('notOwner'));
|
||||
}
|
||||
}
|
||||
|
||||
const userRow = await userService.selectById(c, userId);
|
||||
const roleRow = await roleService.selectById(c, userRow.type);
|
||||
|
||||
@@ -155,6 +163,12 @@ const accountService = {
|
||||
throw new BizError(t('noUserAccount'));
|
||||
}
|
||||
|
||||
const { syncDelete } = await settingService.query(c);
|
||||
if (syncDelete === settingConst.syncDelete.OPEN) {
|
||||
await this.physicsDelete(c, { accountId });
|
||||
return;
|
||||
}
|
||||
|
||||
await orm(c).update(account).set({ isDel: isDel.DELETE }).where(
|
||||
and(eq(account.userId, userId),
|
||||
eq(account.accountId, accountId)))
|
||||
|
||||
@@ -19,7 +19,7 @@ const aiService = {
|
||||
return '';
|
||||
}
|
||||
|
||||
const result = await ai.run(c.env.ai_model || '@cf/meta/llama-3.1-8b-instruct', {
|
||||
const result = await ai.run(c.env.ai_model || '@cf/meta/llama-3.1-8b-instruct-fast', {
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
@@ -35,7 +35,7 @@ const aiService = {
|
||||
});
|
||||
|
||||
const content = typeof result === 'string' ? result : result?.response || '';
|
||||
const json = JSON.parse(content);
|
||||
const json = typeof content === 'string' ? JSON.parse(content) : content;
|
||||
if (typeof json.code !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -241,7 +241,11 @@ const attService = {
|
||||
const delKeyList = attListResult.flatMap(r => r.results ? r.results.map(row => row.key) : []);
|
||||
|
||||
if (delKeyList.length > 0) {
|
||||
await this.batchDelete(c, delKeyList);
|
||||
try {
|
||||
await this.batchDelete(c, delKeyList);
|
||||
} catch (e) {
|
||||
console.error('删除附件文件失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import orm from '../entity/orm';
|
||||
import email from '../entity/email';
|
||||
import { emailListColumns, emailBriefColumns, EMAIL_LIST_TEXT_LEN } from '../lib/email-list-columns';
|
||||
import { attConst, emailConst, isDel, settingConst } from '../const/entity-const';
|
||||
import { and, desc, eq, gt, inArray, lt, count, asc, sql, ne, or, like, lte, gte } from 'drizzle-orm';
|
||||
import { and, desc, eq, gt, inArray, notInArray, lt, count, asc, sql, ne, or, like, lte, gte } from 'drizzle-orm';
|
||||
import { star } from '../entity/star';
|
||||
import settingService from './setting-service';
|
||||
import accountService from './account-service';
|
||||
@@ -27,36 +28,50 @@ const emailService = {
|
||||
|
||||
async list(c, params, userId) {
|
||||
|
||||
let { emailId, type, accountId, size, timeSort, allReceive } = params;
|
||||
let { emailId, type, accountId, size, timeSort, allReceive, full } = params;
|
||||
|
||||
size = Number(size);
|
||||
emailId = Number(emailId);
|
||||
type = Number(type);
|
||||
emailId = Number(emailId) || 0;
|
||||
timeSort = Number(timeSort);
|
||||
accountId = Number(accountId);
|
||||
allReceive = Number(allReceive);
|
||||
full = Number(full);
|
||||
|
||||
if (isNaN(type)) {
|
||||
type = 0;
|
||||
}
|
||||
|
||||
if (isNaN(accountId)) {
|
||||
throw new BizError(t('emptyAccountId'));
|
||||
}
|
||||
|
||||
if (isNaN(size)) {
|
||||
size = 10;
|
||||
}
|
||||
|
||||
if (isNaN(full)) {
|
||||
full = 1;
|
||||
}
|
||||
|
||||
full = full === 1;
|
||||
|
||||
if (size > 50) {
|
||||
size = 50;
|
||||
}
|
||||
|
||||
if (!emailId) {
|
||||
|
||||
if (timeSort) {
|
||||
emailId = 0;
|
||||
} else {
|
||||
emailId = 9999999999;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (isNaN(allReceive)) {
|
||||
let accountRow = await accountService.selectById(c, accountId);
|
||||
allReceive = accountRow.allReceive;
|
||||
}
|
||||
|
||||
const filters = this.emailListFilters({ userId, accountId, type, allReceive, emailId, timeSort });
|
||||
const countFilters = this.emailListFilters({ userId, accountId, type, allReceive, withCursor: false });
|
||||
const columns = full ? emailListColumns : emailBriefColumns;
|
||||
|
||||
const query = orm(c)
|
||||
.select({
|
||||
...email,
|
||||
...columns,
|
||||
starId: star.starId
|
||||
})
|
||||
.from(email)
|
||||
@@ -66,20 +81,12 @@ const emailService = {
|
||||
eq(star.emailId, email.emailId),
|
||||
eq(star.userId, userId)
|
||||
)
|
||||
).leftJoin(
|
||||
)
|
||||
.innerJoin(
|
||||
account,
|
||||
eq(account.accountId, email.accountId)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
allReceive ? eq(1,1) : eq(email.accountId, accountId),
|
||||
eq(email.userId, userId),
|
||||
timeSort ? gt(email.emailId, emailId) : lt(email.emailId, emailId),
|
||||
eq(email.type, type),
|
||||
eq(email.isDel, isDel.NORMAL),
|
||||
eq(account.isDel, isDel.NORMAL)
|
||||
)
|
||||
);
|
||||
.where(and(...filters));
|
||||
|
||||
if (timeSort) {
|
||||
query.orderBy(asc(email.emailId));
|
||||
@@ -90,26 +97,23 @@ const emailService = {
|
||||
const listQuery = query.limit(size).all();
|
||||
|
||||
const totalQuery = orm(c).select({ total: count() }).from(email)
|
||||
.leftJoin(
|
||||
.innerJoin(
|
||||
account,
|
||||
eq(account.accountId, email.accountId)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
allReceive ? eq(1,1) : eq(email.accountId, accountId),
|
||||
eq(email.userId, userId),
|
||||
eq(email.type, type),
|
||||
eq(email.isDel, isDel.NORMAL),
|
||||
eq(account.isDel, isDel.NORMAL)
|
||||
)
|
||||
).get();
|
||||
.where(and(...countFilters))
|
||||
.get();
|
||||
|
||||
const latestEmailQuery = orm(c).select().from(email).where(
|
||||
const latestEmailQuery = orm(c).select({
|
||||
emailId: email.emailId,
|
||||
accountId: email.accountId,
|
||||
userId: email.userId,
|
||||
}).from(email).where(
|
||||
and(
|
||||
allReceive ? eq(1,1) : eq(email.accountId, accountId),
|
||||
eq(email.userId, userId),
|
||||
eq(email.type, type),
|
||||
eq(email.isDel, isDel.NORMAL)
|
||||
eq(email.isDel, isDel.NORMAL),
|
||||
allReceive ? undefined : eq(email.accountId, accountId)
|
||||
))
|
||||
.orderBy(desc(email.emailId)).limit(1).get();
|
||||
|
||||
@@ -120,8 +124,11 @@ const emailService = {
|
||||
isStar: item.starId != null ? 1 : 0
|
||||
}));
|
||||
|
||||
|
||||
await this.emailAddAtt(c, list);
|
||||
if (full) {
|
||||
await this.emailAddAtt(c, list);
|
||||
} else {
|
||||
this.applyListText(list);
|
||||
}
|
||||
|
||||
if (!latestEmail) {
|
||||
latestEmail = {
|
||||
@@ -134,9 +141,98 @@ const emailService = {
|
||||
return { list, total: totalRow.total, latestEmail };
|
||||
},
|
||||
|
||||
toListText(item) {
|
||||
const raw = emailUtils.formatText(item.text) || emailUtils.htmlToText(item.content);
|
||||
return raw.replace(/\s+/g, ' ').trim().slice(0, EMAIL_LIST_TEXT_LEN);
|
||||
},
|
||||
|
||||
applyListText(list) {
|
||||
for (const item of list) {
|
||||
item.text = this.toListText(item);
|
||||
delete item.content;
|
||||
}
|
||||
return list;
|
||||
},
|
||||
|
||||
emailListFilters({ userId, accountId, type, allReceive, emailId, timeSort, withCursor = true }) {
|
||||
const conditions = [
|
||||
eq(email.userId, userId),
|
||||
eq(email.type, type),
|
||||
eq(email.isDel, isDel.NORMAL),
|
||||
eq(account.isDel, isDel.NORMAL),
|
||||
];
|
||||
if (!allReceive) {
|
||||
conditions.push(eq(email.accountId, accountId));
|
||||
}
|
||||
if (withCursor && emailId) {
|
||||
conditions.push(timeSort ? gt(email.emailId, emailId) : lt(email.emailId, emailId));
|
||||
}
|
||||
return conditions;
|
||||
},
|
||||
|
||||
allEmailListFilters({ emailId, name, subject, accountEmail, userEmail, type, timeSort, withCursor = true }) {
|
||||
const conditions = [];
|
||||
|
||||
if (type === 'send') {
|
||||
conditions.push(eq(email.type, emailConst.type.SEND));
|
||||
}
|
||||
|
||||
if (type === 'receive') {
|
||||
conditions.push(eq(email.type, emailConst.type.RECEIVE));
|
||||
}
|
||||
|
||||
if (type === 'delete') {
|
||||
conditions.push(eq(email.isDel, isDel.DELETE));
|
||||
}
|
||||
|
||||
if (type === 'noone') {
|
||||
conditions.push(eq(email.status, emailConst.status.NOONE));
|
||||
}
|
||||
|
||||
if (userEmail) {
|
||||
conditions.push(sql`${user.email} COLLATE NOCASE LIKE ${userEmail + '%'}`);
|
||||
}
|
||||
|
||||
if (accountEmail) {
|
||||
conditions.push(
|
||||
or(
|
||||
sql`${email.toEmail} COLLATE NOCASE LIKE ${accountEmail + '%'}`,
|
||||
sql`${email.sendEmail} COLLATE NOCASE LIKE ${accountEmail + '%'}`,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (name) {
|
||||
conditions.push(sql`${email.name} COLLATE NOCASE LIKE ${name + '%'}`);
|
||||
}
|
||||
|
||||
if (subject) {
|
||||
conditions.push(sql`${email.subject} COLLATE NOCASE LIKE ${subject + '%'}`);
|
||||
}
|
||||
|
||||
if (withCursor && emailId) {
|
||||
conditions.push(timeSort ? gt(email.emailId, emailId) : lt(email.emailId, emailId));
|
||||
}
|
||||
|
||||
return conditions;
|
||||
},
|
||||
|
||||
async delete(c, params, userId) {
|
||||
const { emailIds } = params;
|
||||
const emailIdList = emailIds.split(',').map(Number);
|
||||
const { syncDelete } = await settingService.query(c);
|
||||
|
||||
if (syncDelete === settingConst.syncDelete.OPEN) {
|
||||
const owned = await orm(c).select({ emailId: email.emailId }).from(email)
|
||||
.where(and(eq(email.userId, userId), inArray(email.emailId, emailIdList)))
|
||||
.all();
|
||||
const ownedIds = owned.map(row => row.emailId);
|
||||
if (ownedIds.length) {
|
||||
await this.physicsDelete(c, { emailIds: ownedIds.join(',') });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await orm(c).update(email).set({ isDel: isDel.DELETE }).where(
|
||||
and(
|
||||
eq(email.userId, userId),
|
||||
@@ -548,8 +644,29 @@ const emailService = {
|
||||
//查询所有收件人账号信息
|
||||
let accountList = await orm(c).select().from(account).where(inArray(account.email, receiveEmail)).all();
|
||||
|
||||
// 对于含+未精确匹配的收件人,获取基础地址账号
|
||||
const plusEmails = receiveEmail.filter(
|
||||
e => e.includes('+') && !accountList.some(a => a.email === e)
|
||||
);
|
||||
const baseAccounts = [];
|
||||
if (plusEmails.length > 0) {
|
||||
const baseEmails = [...new Set(
|
||||
plusEmails.map(e => emailUtils.getBaseEmail(e)).filter(Boolean)
|
||||
)];
|
||||
const existing = new Set(accountList.map(a => a.email));
|
||||
const needed = baseEmails.filter(e => !existing.has(e));
|
||||
if (needed.length > 0) {
|
||||
const rows = await orm(c).select().from(account)
|
||||
.where(inArray(account.email, needed)).all();
|
||||
baseAccounts.push(...rows);
|
||||
}
|
||||
}
|
||||
|
||||
// 合并精确匹配和基础地址匹配的账号用于权限查询
|
||||
const allAccounts = [...accountList, ...baseAccounts];
|
||||
|
||||
//查询所有收件人权限身份
|
||||
const userIds = accountList.map(accountRow => accountRow.userId);
|
||||
const userIds = allAccounts.map(accountRow => accountRow.userId);
|
||||
let roleList = await roleService.selectByUserIds(c, userIds);
|
||||
|
||||
//封装数据库准备保存到数据库
|
||||
@@ -565,7 +682,13 @@ const emailService = {
|
||||
emailValues.toName = emailUtils.getName(email);
|
||||
emailValues.emailId = null;
|
||||
|
||||
const accountRow = accountList.find(accountRow => accountRow.email === email);
|
||||
let accountRow = allAccounts.find(accountRow => accountRow.email === email);
|
||||
|
||||
// 精确匹配不到时回退到主地址(去掉 +tag)
|
||||
if (!accountRow && email.includes('+')) {
|
||||
const baseEmail = emailUtils.getBaseEmail(email);
|
||||
accountRow = allAccounts.find(accountRow => accountRow.email === baseEmail);
|
||||
}
|
||||
|
||||
//如果收件人存在就把邮件信息改成收件人的
|
||||
if (accountRow) {
|
||||
@@ -709,8 +832,8 @@ const emailService = {
|
||||
allReceive = accountRow.allReceive;
|
||||
}
|
||||
|
||||
let list = await orm(c).select({...email}).from(email)
|
||||
.leftJoin(
|
||||
let list = await orm(c).select({ ...emailBriefColumns }).from(email)
|
||||
.innerJoin(
|
||||
account,
|
||||
eq(account.accountId, email.accountId)
|
||||
)
|
||||
@@ -720,15 +843,13 @@ const emailService = {
|
||||
eq(email.userId, userId),
|
||||
eq(email.isDel, isDel.NORMAL),
|
||||
eq(account.isDel, isDel.NORMAL),
|
||||
allReceive ? eq(1,1) : eq(email.accountId, accountId),
|
||||
allReceive ? undefined : eq(email.accountId, accountId),
|
||||
eq(email.type, emailConst.type.RECEIVE)
|
||||
))
|
||||
.orderBy(desc(email.emailId))
|
||||
.limit(20);
|
||||
|
||||
await this.emailAddAtt(c, list);
|
||||
|
||||
return list;
|
||||
return this.applyListText(list);
|
||||
},
|
||||
|
||||
async physicsDelete(c, params) {
|
||||
@@ -771,85 +892,49 @@ const emailService = {
|
||||
|
||||
async allList(c, params) {
|
||||
|
||||
let { emailId, size, name, subject, accountEmail, userEmail, type, timeSort } = params;
|
||||
let { emailId, size, name, subject, accountEmail, userEmail, type, timeSort, full } = params;
|
||||
|
||||
size = Number(size);
|
||||
|
||||
emailId = Number(emailId);
|
||||
emailId = Number(emailId) || 0;
|
||||
timeSort = Number(timeSort);
|
||||
full = Number(full);
|
||||
|
||||
if (type === undefined) {
|
||||
type = 'receive';
|
||||
}
|
||||
|
||||
if (isNaN(size)) {
|
||||
size = 10;
|
||||
}
|
||||
|
||||
if (size > 50) {
|
||||
size = 50;
|
||||
}
|
||||
|
||||
if (!emailId) {
|
||||
|
||||
if (timeSort) {
|
||||
emailId = 0;
|
||||
} else {
|
||||
emailId = 9999999999;
|
||||
}
|
||||
|
||||
if (isNaN(full)) {
|
||||
full = 1;
|
||||
}
|
||||
|
||||
const conditions = [];
|
||||
full = full === 1;
|
||||
|
||||
if (type === 'send') {
|
||||
conditions.push(eq(email.type, emailConst.type.SEND));
|
||||
}
|
||||
const filters = this.allEmailListFilters({ emailId, name, subject, accountEmail, userEmail, type, timeSort });
|
||||
const countFilters = this.allEmailListFilters({ emailId, name, subject, accountEmail, userEmail, type, timeSort, withCursor: false });
|
||||
const columns = full ? emailListColumns : emailBriefColumns;
|
||||
|
||||
if (type === 'receive') {
|
||||
conditions.push(eq(email.type, emailConst.type.RECEIVE));
|
||||
}
|
||||
|
||||
if (type === 'delete') {
|
||||
conditions.push(eq(email.isDel, isDel.DELETE));
|
||||
}
|
||||
|
||||
if (type === 'noone') {
|
||||
conditions.push(eq(email.status, emailConst.status.NOONE));
|
||||
}
|
||||
|
||||
if (userEmail) {
|
||||
conditions.push(sql`${user.email} COLLATE NOCASE LIKE ${'%'+ userEmail + '%'}`);
|
||||
}
|
||||
|
||||
if (accountEmail) {
|
||||
conditions.push(
|
||||
or(
|
||||
sql`${email.toEmail} COLLATE NOCASE LIKE ${'%'+ accountEmail + '%'}`,
|
||||
sql`${email.sendEmail} COLLATE NOCASE LIKE ${'%'+ accountEmail + '%'}`,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (name) {
|
||||
conditions.push(sql`${email.name} COLLATE NOCASE LIKE ${'%'+ name + '%'}`);
|
||||
}
|
||||
|
||||
if (subject) {
|
||||
conditions.push(sql`${email.subject} COLLATE NOCASE LIKE ${'%'+ subject + '%'}`);
|
||||
}
|
||||
|
||||
conditions.push(ne(email.status, emailConst.status.SAVING));
|
||||
|
||||
const countConditions = [...conditions];
|
||||
|
||||
if (timeSort) {
|
||||
conditions.unshift(gt(email.emailId, emailId));
|
||||
} else {
|
||||
conditions.unshift(lt(email.emailId, emailId));
|
||||
}
|
||||
|
||||
const query = orm(c).select({ ...email, userEmail: user.email })
|
||||
const query = orm(c).select({ ...columns, userEmail: user.email })
|
||||
.from(email)
|
||||
.leftJoin(user, eq(email.userId, user.userId))
|
||||
.where(and(...conditions));
|
||||
.where(and(...filters));
|
||||
|
||||
const queryCount = orm(c).select({ total: count() })
|
||||
.from(email)
|
||||
.leftJoin(user, eq(email.userId, user.userId))
|
||||
.where(and(...countConditions));
|
||||
// count 不搜用户时无需 join user
|
||||
const queryCount = userEmail
|
||||
? orm(c).select({ total: count() })
|
||||
.from(email)
|
||||
.leftJoin(user, eq(email.userId, user.userId))
|
||||
.where(and(...countFilters))
|
||||
: orm(c).select({ total: count() })
|
||||
.from(email)
|
||||
.where(and(...countFilters));
|
||||
|
||||
if (timeSort) {
|
||||
query.orderBy(asc(email.emailId));
|
||||
@@ -859,16 +944,21 @@ const emailService = {
|
||||
|
||||
const listQuery = query.limit(size).all();
|
||||
const totalQuery = queryCount.get();
|
||||
const latestEmailQuery = orm(c).select().from(email)
|
||||
.where(and(
|
||||
eq(email.type, emailConst.type.RECEIVE),
|
||||
ne(email.status, emailConst.status.SAVING)
|
||||
))
|
||||
const latestEmailQuery = orm(c).select({
|
||||
emailId: email.emailId,
|
||||
accountId: email.accountId,
|
||||
userId: email.userId,
|
||||
}).from(email)
|
||||
.where(eq(email.type, emailConst.type.RECEIVE))
|
||||
.orderBy(desc(email.emailId)).limit(1).get();
|
||||
|
||||
let [list, totalRow, latestEmail] = await Promise.all([listQuery, totalQuery, latestEmailQuery]);
|
||||
|
||||
await this.emailAddAtt(c, list);
|
||||
if (full) {
|
||||
await this.emailAddAtt(c, list);
|
||||
} else {
|
||||
this.applyListText(list);
|
||||
}
|
||||
|
||||
if (!latestEmail) {
|
||||
latestEmail = {
|
||||
@@ -885,20 +975,17 @@ const emailService = {
|
||||
|
||||
const { emailId } = params;
|
||||
|
||||
let list = await orm(c).select({...email, userEmail: user.email}).from(email)
|
||||
let list = await orm(c).select({ ...emailBriefColumns, userEmail: user.email }).from(email)
|
||||
.leftJoin(user, eq(email.userId, user.userId))
|
||||
.where(
|
||||
and(
|
||||
gt(email.emailId, emailId),
|
||||
eq(email.type, emailConst.type.RECEIVE),
|
||||
ne(email.status, emailConst.status.SAVING)
|
||||
eq(email.type, emailConst.type.RECEIVE)
|
||||
))
|
||||
.orderBy(desc(email.emailId))
|
||||
.limit(20);
|
||||
|
||||
await this.emailAddAtt(c, list);
|
||||
|
||||
return list;
|
||||
return this.applyListText(list);
|
||||
},
|
||||
|
||||
async emailAddAtt(c, list) {
|
||||
@@ -928,8 +1015,70 @@ const emailService = {
|
||||
},
|
||||
|
||||
async completeReceiveAll(c) {
|
||||
await c.env.db.prepare(`UPDATE email as e SET status = ${emailConst.status.RECEIVE} WHERE status = ${emailConst.status.SAVING} AND EXISTS (SELECT 1 FROM account WHERE account_id = e.account_id)`).run();
|
||||
await c.env.db.prepare(`UPDATE email as e SET status = ${emailConst.status.NOONE} WHERE status = ${emailConst.status.SAVING} AND NOT EXISTS (SELECT 1 FROM account WHERE account_id = e.account_id)`).run();
|
||||
// 用 EXISTS 走 status=6 部分索引 + account 主键;避免 IN (SELECT account_id FROM account) 触发全盘扫描
|
||||
await c.env.db.prepare(
|
||||
`UPDATE email
|
||||
SET status = ${emailConst.status.RECEIVE}
|
||||
WHERE status = ${emailConst.status.SAVING}
|
||||
AND EXISTS (SELECT 1 FROM account WHERE account.account_id = email.account_id)`
|
||||
).run();
|
||||
await c.env.db.prepare(
|
||||
`UPDATE email
|
||||
SET status = ${emailConst.status.NOONE}
|
||||
WHERE status = ${emailConst.status.SAVING}`
|
||||
).run();
|
||||
},
|
||||
|
||||
async autoClean(c) {
|
||||
const { autoCleanDays, autoCleanExclude } = await settingService.query(c);
|
||||
const days = Number(autoCleanDays);
|
||||
|
||||
if (!days || days <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cutoff = dayjs().subtract(days, 'day').format('YYYY-MM-DD HH:mm:ss');
|
||||
const excludeEmails = String(autoCleanExclude || '')
|
||||
.split(/[,,]/)
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
let excludeUserIds = [];
|
||||
if (excludeEmails.length) {
|
||||
const rows = await orm(c)
|
||||
.select({ userId: user.userId })
|
||||
.from(user)
|
||||
.where(sql`lower(${user.email}) IN (${sql.join(excludeEmails.map(email => sql`${email.toLowerCase()}`), sql`, `)})`)
|
||||
.all();
|
||||
excludeUserIds = rows.map(row => row.userId);
|
||||
}
|
||||
|
||||
const batchSize = 95;
|
||||
|
||||
while (true) {
|
||||
const conditions = [lt(email.createTime, cutoff)];
|
||||
if (excludeUserIds.length) {
|
||||
conditions.push(notInArray(email.userId, excludeUserIds));
|
||||
}
|
||||
|
||||
const rows = await orm(c)
|
||||
.select({ emailId: email.emailId })
|
||||
.from(email)
|
||||
.where(and(...conditions))
|
||||
.limit(batchSize)
|
||||
.all();
|
||||
|
||||
if (!rows.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
const emailIds = rows.map(row => row.emailId);
|
||||
await this.physicsDelete(c, { emailIds: emailIds.join(',') });
|
||||
|
||||
if (rows.length < batchSize) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async batchDelete(c, params) {
|
||||
|
||||
@@ -213,18 +213,6 @@ const loginService = {
|
||||
throw new BizError(t('notExistUser'));
|
||||
}
|
||||
|
||||
if (!await cryptoUtils.verifyPassword(password, userRow.salt, userRow.password) && !noVerifyPwd) {
|
||||
throw new BizError(t('IncorrectPwd'));
|
||||
}
|
||||
|
||||
return this.loginUser(c, userRow);
|
||||
},
|
||||
|
||||
async loginUser(c, userRow) {
|
||||
if (!userRow) {
|
||||
throw new BizError(t('notExistUser'));
|
||||
}
|
||||
|
||||
if(userRow.isDel === isDel.DELETE) {
|
||||
throw new BizError(t('isDelUser'));
|
||||
}
|
||||
@@ -233,6 +221,10 @@ const loginService = {
|
||||
throw new BizError(t('isBanUser'));
|
||||
}
|
||||
|
||||
if (!await cryptoUtils.verifyPassword(password, userRow.salt, userRow.password) && !noVerifyPwd) {
|
||||
throw new BizError(t('IncorrectPwd'));
|
||||
}
|
||||
|
||||
const uuid = uuidv4();
|
||||
const jwt = await JwtUtils.generateToken(c,{ userId: userRow.userId, token: uuid });
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import { eq, inArray } from 'drizzle-orm';
|
||||
import userService from "./user-service";
|
||||
import loginService from "./login-service";
|
||||
import cryptoUtils from "../utils/crypto-utils";
|
||||
import settingService from "./setting-service";
|
||||
import {t} from '../i18n/i18n';
|
||||
|
||||
const oauthService = {
|
||||
|
||||
@@ -32,16 +34,16 @@ const oauthService = {
|
||||
|
||||
async linuxDoLogin(c, params) {
|
||||
|
||||
const { code } = params;
|
||||
const { code, redirectUri } = params;
|
||||
|
||||
let token = '';
|
||||
let userInfo = {}
|
||||
const setting = await settingService.query(c);
|
||||
this.assertEnabled(setting, 'linuxdoSwitch');
|
||||
|
||||
const reqParams = new URLSearchParams()
|
||||
reqParams.append('client_id', c.env.linuxdo_client_id)
|
||||
reqParams.append('client_secret', c.env.linuxdo_client_secret)
|
||||
reqParams.append('client_id', setting.linuxdoClientId)
|
||||
reqParams.append('client_secret', setting.linuxdoClientSecret)
|
||||
reqParams.append('code', code)
|
||||
reqParams.append('redirect_uri', c.env.linuxdo_callback_url)
|
||||
reqParams.append('redirect_uri', redirectUri)
|
||||
reqParams.append('grant_type', 'authorization_code')
|
||||
|
||||
const tokenRes = await fetch("https://connect.linux.do/oauth2/token", {
|
||||
@@ -54,7 +56,7 @@ const oauthService = {
|
||||
throw new BizError(tokenRes.statusText)
|
||||
}
|
||||
|
||||
token = await tokenRes.json()
|
||||
const token = await tokenRes.json()
|
||||
|
||||
const userRes = await fetch('https://connect.linux.do/api/user', {
|
||||
headers: {
|
||||
@@ -66,23 +68,128 @@ const oauthService = {
|
||||
throw new BizError(userRes.statusText)
|
||||
}
|
||||
|
||||
userInfo = await userRes.json();
|
||||
const userInfo = await userRes.json();
|
||||
|
||||
userInfo.oauthUserId = String(userInfo.id);
|
||||
userInfo.active = userInfo.active ? 0 : 1;
|
||||
userInfo.silenced = userInfo.silenced ? 0 : 1;
|
||||
userInfo.trustLevel = userInfo.trust_level;
|
||||
userInfo.avatar = userInfo.avatar_url;
|
||||
userInfo.platform = 'linuxdo';
|
||||
|
||||
const oauthRow = await this.saveUser(c, userInfo);
|
||||
return await this.saveAndLogin(c, userInfo)
|
||||
},
|
||||
|
||||
async githubLogin(c, params) {
|
||||
|
||||
const { code, redirectUri } = params;
|
||||
|
||||
const setting = await settingService.query(c);
|
||||
this.assertEnabled(setting, 'githubSwitch');
|
||||
|
||||
const tokenRes = await fetch("https://github.com/login/oauth/access_token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
client_id: setting.githubClientId,
|
||||
client_secret: setting.githubClientSecret,
|
||||
code: code,
|
||||
redirect_uri: redirectUri
|
||||
})
|
||||
});
|
||||
|
||||
if (!tokenRes.ok) {
|
||||
throw new BizError(tokenRes.statusText);
|
||||
}
|
||||
|
||||
const token = await tokenRes.json();
|
||||
|
||||
if (token.error) {
|
||||
throw new BizError(token.error_description || token.error);
|
||||
}
|
||||
|
||||
const userRes = await fetch('https://api.github.com/user', {
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + token.access_token,
|
||||
'User-Agent': 'cloud-mail'
|
||||
}
|
||||
});
|
||||
|
||||
if (!userRes.ok) {
|
||||
throw new BizError(userRes.statusText);
|
||||
}
|
||||
|
||||
const userInfo = await userRes.json();
|
||||
|
||||
userInfo.oauthUserId = String(userInfo.id);
|
||||
userInfo.username = userInfo.login;
|
||||
userInfo.avatar = userInfo.avatar_url;
|
||||
userInfo.platform = 'github';
|
||||
|
||||
return await this.saveAndLogin(c, userInfo);
|
||||
},
|
||||
|
||||
async googleLogin(c, params) {
|
||||
|
||||
const { code, redirectUri } = params;
|
||||
|
||||
const setting = await settingService.query(c);
|
||||
this.assertEnabled(setting, 'googleSwitch');
|
||||
|
||||
const reqParams = new URLSearchParams()
|
||||
reqParams.append('client_id', setting.googleClientId)
|
||||
reqParams.append('client_secret', setting.googleClientSecret)
|
||||
reqParams.append('code', code)
|
||||
reqParams.append('redirect_uri', redirectUri)
|
||||
reqParams.append('grant_type', 'authorization_code')
|
||||
|
||||
const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: reqParams.toString()
|
||||
});
|
||||
|
||||
if (!tokenRes.ok) {
|
||||
throw new BizError(tokenRes.statusText);
|
||||
}
|
||||
|
||||
const token = await tokenRes.json();
|
||||
|
||||
const userRes = await fetch('https://openidconnect.googleapis.com/v1/userinfo', {
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + token.access_token
|
||||
}
|
||||
});
|
||||
|
||||
if (!userRes.ok) {
|
||||
throw new BizError(userRes.statusText);
|
||||
}
|
||||
|
||||
const userInfo = await userRes.json();
|
||||
|
||||
userInfo.oauthUserId = String(userInfo.sub);
|
||||
userInfo.username = userInfo.email;
|
||||
userInfo.name = userInfo.name;
|
||||
userInfo.avatar = userInfo.picture;
|
||||
userInfo.platform = 'google';
|
||||
|
||||
return await this.saveAndLogin(c, userInfo);
|
||||
},
|
||||
|
||||
async saveAndLogin(c, userInfo) {
|
||||
|
||||
const oauthRow = await this.saveUser(c, userInfo);
|
||||
const userRow = await userService.selectByIdIncludeDel(c, oauthRow.userId);
|
||||
|
||||
if (!userRow) {
|
||||
return { userInfo: oauthRow, token: null }
|
||||
return { userInfo: oauthRow, token: null };
|
||||
}
|
||||
|
||||
const JwtToken = await loginService.login(c, { email: userRow.email, password: null }, true);
|
||||
return { userInfo: oauthRow, token: JwtToken }
|
||||
return { userInfo: oauthRow, token: JwtToken };
|
||||
},
|
||||
|
||||
async saveUser(c, userInfo) {
|
||||
@@ -97,6 +204,12 @@ const oauthService = {
|
||||
|
||||
},
|
||||
|
||||
assertEnabled(setting, switchKey) {
|
||||
if (setting[switchKey] !== 0) {
|
||||
throw new BizError(t('oauthDisabled'));
|
||||
}
|
||||
},
|
||||
|
||||
async getById(c, oauthUserId) {
|
||||
return await orm(c).select().from(oauth).where(eq(oauth.oauthUserId, oauthUserId)).get();
|
||||
},
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
import {
|
||||
generateAuthenticationOptions,
|
||||
generateRegistrationOptions,
|
||||
verifyAuthenticationResponse,
|
||||
verifyRegistrationResponse,
|
||||
} from '@simplewebauthn/server';
|
||||
import { isoBase64URL } from '@simplewebauthn/server/helpers';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import BizError from '../error/biz-error';
|
||||
import orm from '../entity/orm';
|
||||
import passkeyCredential from '../entity/passkey-credential';
|
||||
import cryptoUtils from '../utils/crypto-utils';
|
||||
import settingService from './setting-service';
|
||||
import userService from './user-service';
|
||||
import loginService from './login-service';
|
||||
import { t } from '../i18n/i18n.js';
|
||||
|
||||
const CHALLENGE_TTL = 5 * 60 * 1000;
|
||||
const PASSKEY_ENABLED = 1;
|
||||
|
||||
const passkeyService = {
|
||||
async status(c, userId) {
|
||||
const credential = await this.findByUserId(c, userId);
|
||||
return {
|
||||
registered: !!credential,
|
||||
createTime: credential?.createTime || null,
|
||||
};
|
||||
},
|
||||
|
||||
async registrationOptions(c, userId, params) {
|
||||
await this.ensureEnabled(c);
|
||||
const user = await this.verifyCurrentPassword(c, userId, params?.password);
|
||||
const { origin, rpID } = this.getRelyingParty(c);
|
||||
const userHandleBytes = new Uint8Array(32);
|
||||
crypto.getRandomValues(userHandleBytes);
|
||||
const userHandle = isoBase64URL.fromBuffer(userHandleBytes);
|
||||
const options = await generateRegistrationOptions({
|
||||
rpName: (await settingService.query(c)).title || 'Cloud Mail',
|
||||
rpID,
|
||||
userID: userHandleBytes,
|
||||
userName: user.email,
|
||||
userDisplayName: user.email,
|
||||
attestationType: 'none',
|
||||
authenticatorSelection: {
|
||||
residentKey: 'required',
|
||||
userVerification: 'required',
|
||||
},
|
||||
});
|
||||
const transactionId = await this.saveChallenge(c, {
|
||||
type: 'registration',
|
||||
challenge: options.challenge,
|
||||
userId,
|
||||
userHandle,
|
||||
origin,
|
||||
rpID,
|
||||
});
|
||||
return { transactionId, options };
|
||||
},
|
||||
|
||||
async verifyRegistration(c, userId, params) {
|
||||
await this.ensureEnabled(c);
|
||||
const challenge = await this.consumeChallenge(c, params?.transactionId, 'registration');
|
||||
if (!challenge || challenge.user_id !== userId) {
|
||||
throw new BizError(t('passkeyChallengeInvalid'));
|
||||
}
|
||||
|
||||
let verification;
|
||||
try {
|
||||
verification = await verifyRegistrationResponse({
|
||||
response: params?.response,
|
||||
expectedChallenge: challenge.challenge,
|
||||
expectedOrigin: challenge.origin,
|
||||
expectedRPID: challenge.rp_id,
|
||||
requireUserVerification: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(JSON.stringify({
|
||||
message: 'Passkey registration verification failed',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}));
|
||||
throw new BizError(t('passkeyVerifyFailed'));
|
||||
}
|
||||
if (!verification.verified || !verification.registrationInfo) {
|
||||
throw new BizError(t('passkeyVerifyFailed'));
|
||||
}
|
||||
|
||||
const credential = verification.registrationInfo.credential;
|
||||
const values = {
|
||||
userId,
|
||||
userHandle: challenge.user_handle,
|
||||
credentialId: credential.id,
|
||||
publicKey: isoBase64URL.fromBuffer(credential.publicKey),
|
||||
counter: credential.counter,
|
||||
transports: JSON.stringify(credential.transports || params?.response?.response?.transports || []),
|
||||
createTime: new Date().toISOString(),
|
||||
};
|
||||
await orm(c).insert(passkeyCredential).values(values).onConflictDoUpdate({
|
||||
target: passkeyCredential.userId,
|
||||
set: values,
|
||||
}).run();
|
||||
return this.status(c, userId);
|
||||
},
|
||||
|
||||
async authenticationOptions(c) {
|
||||
await this.ensureEnabled(c);
|
||||
const { origin, rpID } = this.getRelyingParty(c);
|
||||
const options = await generateAuthenticationOptions({
|
||||
rpID,
|
||||
userVerification: 'required',
|
||||
});
|
||||
const transactionId = await this.saveChallenge(c, {
|
||||
type: 'authentication',
|
||||
challenge: options.challenge,
|
||||
origin,
|
||||
rpID,
|
||||
});
|
||||
return { transactionId, options };
|
||||
},
|
||||
|
||||
async verifyAuthentication(c, params) {
|
||||
await this.ensureEnabled(c);
|
||||
const challenge = await this.consumeChallenge(c, params?.transactionId, 'authentication');
|
||||
if (!challenge) {
|
||||
throw new BizError(t('passkeyChallengeInvalid'));
|
||||
}
|
||||
const credential = await this.findByCredentialId(c, params?.response?.id);
|
||||
if (!credential || params?.response?.response?.userHandle !== credential.userHandle) {
|
||||
throw new BizError(t('passkeyNotFound'));
|
||||
}
|
||||
|
||||
let verification;
|
||||
try {
|
||||
verification = await verifyAuthenticationResponse({
|
||||
response: params.response,
|
||||
expectedChallenge: challenge.challenge,
|
||||
expectedOrigin: challenge.origin,
|
||||
expectedRPID: challenge.rp_id,
|
||||
requireUserVerification: true,
|
||||
credential: {
|
||||
id: credential.credentialId,
|
||||
publicKey: isoBase64URL.toBuffer(credential.publicKey),
|
||||
counter: credential.counter,
|
||||
transports: this.parseTransports(credential.transports),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(JSON.stringify({
|
||||
message: 'Passkey authentication verification failed',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}));
|
||||
throw new BizError(t('passkeyVerifyFailed'));
|
||||
}
|
||||
if (!verification.verified) {
|
||||
throw new BizError(t('passkeyVerifyFailed'));
|
||||
}
|
||||
|
||||
await orm(c).update(passkeyCredential).set({
|
||||
counter: verification.authenticationInfo.newCounter,
|
||||
}).where(eq(passkeyCredential.passkeyId, credential.passkeyId)).run();
|
||||
const user = await userService.selectByIdIncludeDel(c, credential.userId);
|
||||
return loginService.loginUser(c, user);
|
||||
},
|
||||
|
||||
async delete(c, userId, params) {
|
||||
await this.verifyCurrentPassword(c, userId, params?.password);
|
||||
await orm(c).delete(passkeyCredential).where(eq(passkeyCredential.userId, userId)).run();
|
||||
},
|
||||
|
||||
async ensureEnabled(c) {
|
||||
const { passkey } = await settingService.query(c);
|
||||
if (passkey !== PASSKEY_ENABLED) {
|
||||
throw new BizError(t('passkeyDisabled'));
|
||||
}
|
||||
},
|
||||
|
||||
async verifyCurrentPassword(c, userId, password) {
|
||||
if (!password) {
|
||||
throw new BizError(t('emailAndPwdEmpty'));
|
||||
}
|
||||
const user = await userService.selectByIdIncludeDel(c, userId);
|
||||
if (!user || !await cryptoUtils.verifyPassword(password, user.salt, user.password)) {
|
||||
throw new BizError(t('IncorrectPwd'));
|
||||
}
|
||||
return user;
|
||||
},
|
||||
|
||||
async findByUserId(c, userId) {
|
||||
return orm(c).select().from(passkeyCredential).where(eq(passkeyCredential.userId, userId)).get();
|
||||
},
|
||||
|
||||
async findByCredentialId(c, credentialId) {
|
||||
if (!credentialId) return null;
|
||||
return orm(c).select().from(passkeyCredential).where(eq(passkeyCredential.credentialId, credentialId)).get();
|
||||
},
|
||||
|
||||
parseTransports(transports) {
|
||||
try {
|
||||
return JSON.parse(transports || '[]');
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
getRelyingParty(c) {
|
||||
try {
|
||||
const requestUrl = new URL(c.req.url);
|
||||
const origin = new URL(c.req.header('Origin') || requestUrl.origin);
|
||||
const loopbackHosts = ['localhost', '127.0.0.1', '[::1]'];
|
||||
const originIsLoopback = loopbackHosts.includes(origin.hostname);
|
||||
const requestIsLoopback = loopbackHosts.includes(requestUrl.hostname);
|
||||
const secureOrigin = origin.protocol === 'https:'
|
||||
|| (origin.protocol === 'http:' && originIsLoopback);
|
||||
const trustedOrigin = origin.origin === requestUrl.origin
|
||||
|| (originIsLoopback && requestIsLoopback);
|
||||
if (secureOrigin && trustedOrigin) {
|
||||
return { origin: origin.origin, rpID: origin.hostname };
|
||||
}
|
||||
} catch (_) {
|
||||
throw new BizError(t('passkeyOriginInvalid'));
|
||||
}
|
||||
throw new BizError(t('passkeyOriginInvalid'));
|
||||
},
|
||||
|
||||
async saveChallenge(c, challenge) {
|
||||
const transactionId = crypto.randomUUID();
|
||||
const now = Date.now();
|
||||
await c.env.db.batch([
|
||||
c.env.db.prepare('DELETE FROM passkey_challenge WHERE expires_at <= ?').bind(now),
|
||||
c.env.db.prepare(`
|
||||
INSERT INTO passkey_challenge
|
||||
(challenge_id, type, challenge, user_id, user_handle, origin, rp_id, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).bind(
|
||||
transactionId,
|
||||
challenge.type,
|
||||
challenge.challenge,
|
||||
challenge.userId || 0,
|
||||
challenge.userHandle || '',
|
||||
challenge.origin,
|
||||
challenge.rpID,
|
||||
now + CHALLENGE_TTL,
|
||||
),
|
||||
]);
|
||||
return transactionId;
|
||||
},
|
||||
|
||||
async consumeChallenge(c, transactionId, type) {
|
||||
if (!transactionId) return null;
|
||||
return c.env.db.prepare(`
|
||||
DELETE FROM passkey_challenge
|
||||
WHERE challenge_id = ? AND type = ? AND expires_at > ?
|
||||
RETURNING *
|
||||
`).bind(transactionId, type, Date.now()).first();
|
||||
},
|
||||
};
|
||||
|
||||
export default passkeyService;
|
||||
@@ -9,6 +9,7 @@ import BizError from '../error/biz-error';
|
||||
import {t} from '../i18n/i18n'
|
||||
import verifyRecordService from './verify-record-service';
|
||||
import userContext from '../security/user-context';
|
||||
import domainUtils from '../utils/domain-uitls';
|
||||
|
||||
const settingService = {
|
||||
|
||||
@@ -48,18 +49,7 @@ const settingService = {
|
||||
domainList = domainList.map(item => '@' + item);
|
||||
setting.domainList = domainList;
|
||||
|
||||
|
||||
let linuxdoSwitch = c.env.linuxdo_switch;
|
||||
let projectLink = c.env.project_link;
|
||||
|
||||
if (typeof linuxdoSwitch === 'string' && linuxdoSwitch === 'true') {
|
||||
linuxdoSwitch = true
|
||||
} else if (linuxdoSwitch === true) {
|
||||
linuxdoSwitch = true
|
||||
} else {
|
||||
linuxdoSwitch = false
|
||||
}
|
||||
|
||||
if (typeof projectLink === 'string' && projectLink === 'false') {
|
||||
projectLink = false
|
||||
} else if (projectLink === false) {
|
||||
@@ -70,10 +60,6 @@ const settingService = {
|
||||
|
||||
setting.projectLink = projectLink;
|
||||
|
||||
setting.linuxdoClientId = c.env.linuxdo_client_id;
|
||||
setting.linuxdoCallbackUrl = c.env.linuxdo_callback_url;
|
||||
setting.linuxdoSwitch = linuxdoSwitch;
|
||||
|
||||
setting.emailPrefixFilter = setting.emailPrefixFilter.split(",").filter(Boolean);
|
||||
|
||||
c.set?.('setting', setting);
|
||||
@@ -139,7 +125,12 @@ const settingService = {
|
||||
params.aiCodeFilter = params.aiCodeFilter + '';
|
||||
}
|
||||
|
||||
if (params.webhookUrl !== undefined) {
|
||||
params.webhookUrl = domainUtils.toOssDomain(params.webhookUrl) || '';
|
||||
}
|
||||
|
||||
params.resendTokens = JSON.stringify(resendTokens);
|
||||
|
||||
await orm(c).update(setting).set({ ...params }).returning().get();
|
||||
await this.refresh(c);
|
||||
},
|
||||
@@ -228,10 +219,12 @@ const settingService = {
|
||||
noticeOffset: settingRow.noticeOffset,
|
||||
notice: settingRow.notice,
|
||||
loginDomain: settingRow.loginDomain,
|
||||
passkey: settingRow.passkey,
|
||||
linuxdoClientId: settingRow.linuxdoClientId,
|
||||
linuxdoCallbackUrl: settingRow.linuxdoCallbackUrl,
|
||||
linuxdoSwitch: settingRow.linuxdoSwitch,
|
||||
githubClientId: settingRow.githubClientId,
|
||||
githubSwitch: settingRow.githubSwitch,
|
||||
googleClientId: settingRow.googleClientId,
|
||||
googleSwitch: settingRow.googleSwitch,
|
||||
minEmailPrefix: settingRow.minEmailPrefix,
|
||||
projectLink: settingRow.projectLink
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import emailService from './email-service';
|
||||
import BizError from '../error/biz-error';
|
||||
import { and, desc, eq, lt, sql, inArray } from 'drizzle-orm';
|
||||
import email from '../entity/email';
|
||||
import { emailListColumns, emailBriefColumns } from '../lib/email-list-columns';
|
||||
import { isDel } from '../const/entity-const';
|
||||
import attService from "./att-service";
|
||||
import { t } from '../i18n/i18n'
|
||||
@@ -41,42 +42,48 @@ const starService = {
|
||||
},
|
||||
|
||||
async list(c, params, userId) {
|
||||
let { emailId, size } = params;
|
||||
emailId = Number(emailId);
|
||||
let { emailId, size, full } = params;
|
||||
emailId = Number(emailId) || 0;
|
||||
size = Number(size);
|
||||
|
||||
if (!emailId) {
|
||||
emailId = 9999999999;
|
||||
}
|
||||
full = Number(full) === 1;
|
||||
const columns = full ? emailListColumns : emailBriefColumns;
|
||||
|
||||
const list = await orm(c).select({
|
||||
isStar: sql`1`.as('isStar'),
|
||||
starId: star.starId
|
||||
, ...email
|
||||
starId: star.starId,
|
||||
...columns
|
||||
}).from(star)
|
||||
.leftJoin(email, eq(email.emailId, star.emailId))
|
||||
.where(
|
||||
and(
|
||||
eq(star.userId, userId),
|
||||
eq(email.isDel, isDel.NORMAL),
|
||||
lt(star.emailId, emailId)))
|
||||
emailId ? lt(star.emailId, emailId) : undefined))
|
||||
.orderBy(desc(star.emailId))
|
||||
.limit(size)
|
||||
.all();
|
||||
|
||||
const emailIds = list.map(item => item.emailId);
|
||||
|
||||
const attsList = await attService.selectByEmailIds(c, emailIds);
|
||||
|
||||
list.forEach(emailRow => {
|
||||
const atts = attsList.filter(attsRow => attsRow.emailId === emailRow.emailId);
|
||||
emailRow.attList = atts;
|
||||
});
|
||||
if (full) {
|
||||
const emailIds = list.map(item => item.emailId);
|
||||
const attsList = await attService.selectByEmailIds(c, emailIds);
|
||||
list.forEach(emailRow => {
|
||||
emailRow.attList = attsList.filter(attsRow => attsRow.emailId === emailRow.emailId);
|
||||
});
|
||||
} else {
|
||||
list.forEach(emailRow => {
|
||||
emailRow.text = emailService.toListText(emailRow);
|
||||
delete emailRow.content;
|
||||
});
|
||||
}
|
||||
|
||||
return { list };
|
||||
},
|
||||
async removeByEmailIds(c, emailIds) {
|
||||
await orm(c).delete(star).where(inArray(star.emailId, emailIds)).run();
|
||||
},
|
||||
|
||||
async removeByUserIds(c, userIds) {
|
||||
await orm(c).delete(star).where(inArray(star.userId, userIds)).run();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import accountService from './account-service';
|
||||
import orm from '../entity/orm';
|
||||
import user from '../entity/user';
|
||||
import { and, asc, count, desc, eq, inArray, sql } from 'drizzle-orm';
|
||||
import { emailConst, isDel, roleConst, userConst } from '../const/entity-const';
|
||||
import { emailConst, isDel, roleConst, settingConst, userConst } from '../const/entity-const';
|
||||
import kvConst from '../const/kv-const';
|
||||
import KvConst from '../const/kv-const';
|
||||
import cryptoUtils from '../utils/crypto-utils';
|
||||
@@ -18,6 +18,8 @@ import { t } from '../i18n/i18n'
|
||||
import reqUtils from '../utils/req-utils';
|
||||
import {oauth} from "../entity/oauth";
|
||||
import oauthService from "./oauth-service";
|
||||
import settingService from './setting-service';
|
||||
import starService from './star-service';
|
||||
|
||||
const userService = {
|
||||
|
||||
@@ -68,7 +70,7 @@ const userService = {
|
||||
selectByEmail(c, email) {
|
||||
return orm(c).select().from(user).where(
|
||||
and(
|
||||
eq(user.email, email),
|
||||
sql`${user.email} COLLATE NOCASE = ${email}`,
|
||||
eq(user.isDel, isDel.NORMAL)))
|
||||
.get();
|
||||
},
|
||||
@@ -95,6 +97,12 @@ const userService = {
|
||||
},
|
||||
|
||||
async delete(c, userId) {
|
||||
const { syncDelete } = await settingService.query(c);
|
||||
if (syncDelete === settingConst.syncDelete.OPEN) {
|
||||
await this.physicsDelete(c, { userIds: String(userId) });
|
||||
await c.env.kv.delete(kvConst.AUTH_INFO + userId)
|
||||
return;
|
||||
}
|
||||
await orm(c).update(user).set({ isDel: isDel.DELETE }).where(eq(user.userId, userId)).run();
|
||||
await c.env.kv.delete(kvConst.AUTH_INFO + userId)
|
||||
},
|
||||
@@ -102,6 +110,7 @@ const userService = {
|
||||
async physicsDelete(c, params) {
|
||||
let { userIds } = params;
|
||||
userIds = userIds.split(',').map(Number);
|
||||
await starService.removeByUserIds(c, userIds);
|
||||
await accountService.physicsDeleteByUserIds(c, userIds);
|
||||
await oauthService.deleteByUserIds(c, userIds);
|
||||
await orm(c).delete(user).where(inArray(user.userId, userIds)).run();
|
||||
@@ -115,6 +124,15 @@ const userService = {
|
||||
num = Number(num);
|
||||
timeSort = Number(timeSort);
|
||||
params.isDel = Number(params.isDel);
|
||||
|
||||
if (isNaN(size)) {
|
||||
size = 50;
|
||||
}
|
||||
|
||||
if (isNaN(num)) {
|
||||
num = 1;
|
||||
}
|
||||
|
||||
if (size > 50) {
|
||||
size = 50;
|
||||
}
|
||||
@@ -130,7 +148,7 @@ const userService = {
|
||||
|
||||
|
||||
if (email) {
|
||||
conditions.push(sql`${user.email} COLLATE NOCASE LIKE ${'%'+ email + '%'}`);
|
||||
conditions.push(sql`${user.email} COLLATE NOCASE LIKE ${email + '%'}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -144,7 +162,8 @@ const userService = {
|
||||
username: oauth.username,
|
||||
trustLevel: oauth.trustLevel,
|
||||
avatar: oauth.avatar,
|
||||
name: oauth.name
|
||||
name: oauth.name,
|
||||
platform: oauth.platform
|
||||
}).from(user).leftJoin(oauth, eq(oauth.userId, user.userId))
|
||||
.where(and(...conditions));
|
||||
|
||||
@@ -304,7 +323,7 @@ const userService = {
|
||||
|
||||
async add(c, params) {
|
||||
|
||||
const { email, type, password } = params;
|
||||
let { email, type, password } = params;
|
||||
|
||||
if (!c.env.domain.includes(emailUtils.getDomain(email))) {
|
||||
throw new BizError(t('notEmailDomain'));
|
||||
@@ -324,7 +343,13 @@ const userService = {
|
||||
throw new BizError(t('isRegAccount'));
|
||||
}
|
||||
|
||||
const role = roleService.selectById(c, type);
|
||||
let role;
|
||||
if (type === undefined) {
|
||||
role = await roleService.selectDefaultRole(c);
|
||||
type = role?.roleId;
|
||||
} else {
|
||||
role = await roleService.selectById(c, type);
|
||||
}
|
||||
|
||||
if (!role) {
|
||||
throw new BizError(t('roleNotExist'));
|
||||
@@ -340,6 +365,10 @@ const userService = {
|
||||
},
|
||||
|
||||
async resetDaySendCount(c) {
|
||||
// 仅 UTC 0 点执行,便于配合每小时 cron
|
||||
if (new Date().getUTCHours() !== 0) {
|
||||
return;
|
||||
}
|
||||
const roleList = await roleService.selectByIdsAndSendType(c, 'email:send', roleConst.sendType.DAY);
|
||||
const roleIds = roleList.map(action => action.roleId);
|
||||
await orm(c).update(user).set({ sendCount: 0 }).where(inArray(user.type, roleIds)).run();
|
||||
|
||||
@@ -13,6 +13,10 @@ const verifyRecordService = {
|
||||
},
|
||||
|
||||
async clearRecord(c) {
|
||||
// 仅 UTC 0 点执行,便于配合每小时 cron
|
||||
if (new Date().getUTCHours() !== 0) {
|
||||
return;
|
||||
}
|
||||
await orm(c).delete(verifyRecord).run();
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import domainUtils from '../utils/domain-uitls';
|
||||
|
||||
const webhookService = {
|
||||
|
||||
async sendEmail(c, emailRow, webhookUrl, retry = 0, webhookSecret) {
|
||||
|
||||
webhookUrl = domainUtils.toOssDomain(webhookUrl);
|
||||
|
||||
if (!webhookUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
retry = Number(retry);
|
||||
if (isNaN(retry) || retry < 0) {
|
||||
retry = 0;
|
||||
}
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
if (webhookSecret) {
|
||||
headers['Authorization'] = webhookSecret;
|
||||
}
|
||||
|
||||
const body = JSON.stringify({
|
||||
emailId: emailRow.emailId,
|
||||
sendEmail: emailRow.sendEmail,
|
||||
sendName: emailRow.name,
|
||||
toEmail: emailRow.toEmail,
|
||||
toName: emailRow.toName,
|
||||
subject: emailRow.subject,
|
||||
text: emailRow.text,
|
||||
content: emailRow.content,
|
||||
code: emailRow.code,
|
||||
createTime: emailRow.createTime
|
||||
});
|
||||
|
||||
let lastError = '';
|
||||
|
||||
for (let i = 0; i <= retry; i++) {
|
||||
try {
|
||||
const res = await fetch(webhookUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastError = `status: ${res.status} response: ${await res.text()}`;
|
||||
} catch (e) {
|
||||
lastError = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`Webhook 推送失败: ${lastError}`);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default webhookService;
|
||||
@@ -14,6 +14,13 @@ const emailUtils = {
|
||||
return parts.length === 2 ? parts[0] : '';
|
||||
},
|
||||
|
||||
getBaseEmail(email) {
|
||||
const parts = email.split('@');
|
||||
if (parts.length !== 2) return '';
|
||||
const localPart = parts[0].split('+')[0];
|
||||
return localPart + '@' + parts[1];
|
||||
},
|
||||
|
||||
formatText(text) {
|
||||
if (!text) return ''
|
||||
return text
|
||||
|
||||
@@ -47,7 +47,6 @@ const jwtUtils = {
|
||||
},
|
||||
|
||||
async verifyToken(c, token) {
|
||||
if (!token) return null;
|
||||
try {
|
||||
const [headerB64, payloadB64, signatureB64] = token.split('.');
|
||||
|
||||
|
||||
@@ -1,103 +1,20 @@
|
||||
import { env, SELF } from 'cloudflare:test';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import passkeyService from '../src/service/passkey-service';
|
||||
import settingService from '../src/service/setting-service';
|
||||
import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import worker from '../src';
|
||||
|
||||
describe('Passkey support', () => {
|
||||
beforeAll(async () => {
|
||||
const response = await SELF.fetch(`http://127.0.0.1/api/init/${env.jwt_secret}`);
|
||||
expect(await response.text()).toBe('success');
|
||||
describe('Hello World worker', () => {
|
||||
it('responds with Hello World! (unit style)', async () => {
|
||||
const request = new Request('http://example.com');
|
||||
// Create an empty context to pass to `worker.fetch()`.
|
||||
const ctx = createExecutionContext();
|
||||
const response = await worker.fetch(request, env, ctx);
|
||||
// Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
|
||||
await waitOnExecutionContext(ctx);
|
||||
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
|
||||
});
|
||||
|
||||
it('creates disabled-by-default Passkey schema', async () => {
|
||||
const settingColumn = await env.db.prepare(`SELECT name FROM pragma_table_info('setting') WHERE name = 'passkey'`).first();
|
||||
const credentialTable = await env.db
|
||||
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'passkey_credential'`).first();
|
||||
const challengeTable = await env.db.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'passkey_challenge'`).first();
|
||||
const setting = await env.db.prepare('SELECT passkey FROM setting').first();
|
||||
|
||||
expect(settingColumn?.name).toBe('passkey');
|
||||
expect(credentialTable?.name).toBe('passkey_credential');
|
||||
expect(challengeTable?.name).toBe('passkey_challenge');
|
||||
expect(setting?.passkey).toBe(0);
|
||||
});
|
||||
|
||||
it('exposes the disabled flag and rejects authentication options', async () => {
|
||||
const configResponse = await SELF.fetch('http://127.0.0.1/api/setting/websiteConfig');
|
||||
const config = await configResponse.json();
|
||||
expect(config.data.passkey).toBe(0);
|
||||
|
||||
const optionsResponse = await SELF.fetch('http://127.0.0.1/api/passkey/auth/options', {
|
||||
method: 'POST',
|
||||
headers: { Origin: 'http://127.0.0.1' },
|
||||
});
|
||||
const options = await optionsResponse.json();
|
||||
expect(options.code).toBe(501);
|
||||
});
|
||||
|
||||
it('keeps password login working', async () => {
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
const registerResponse = await SELF.fetch('http://127.0.0.1/api/register', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ email: '[email protected]', password: 'test-password' }),
|
||||
});
|
||||
expect((await registerResponse.json()).code).toBe(200);
|
||||
|
||||
const loginResponse = await SELF.fetch('http://127.0.0.1/api/login', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ email: '[email protected]', password: 'test-password' }),
|
||||
});
|
||||
const login = await loginResponse.json();
|
||||
expect(login.code).toBe(200);
|
||||
expect(login.data.token).toBeTruthy();
|
||||
});
|
||||
|
||||
it('accepts only exact production origins or loopback development origins', () => {
|
||||
const context = (url, origin) => ({ req: { url, header: () => origin } });
|
||||
expect(passkeyService.getRelyingParty(context('https://mail.example.com/api', 'https://mail.example.com'))).toEqual({
|
||||
origin: 'https://mail.example.com',
|
||||
rpID: 'mail.example.com',
|
||||
});
|
||||
expect(passkeyService.getRelyingParty(context('http://127.0.0.1:8787/api', 'http://localhost:3001'))).toEqual({
|
||||
origin: 'http://localhost:3001',
|
||||
rpID: 'localhost',
|
||||
});
|
||||
expect(() => passkeyService.getRelyingParty(context('https://mail.example.com/api', 'https://evil.example.com'))).toThrow();
|
||||
expect(() => passkeyService.getRelyingParty(context('https://mail.example.com/api', 'http://mail.example.com'))).toThrow();
|
||||
expect(() => passkeyService.getRelyingParty(context('https://mail.example.com/api', 'https://mail.example.com:8443'))).toThrow();
|
||||
});
|
||||
|
||||
it('consumes each challenge only once', async () => {
|
||||
const context = { env };
|
||||
const transactionId = await passkeyService.saveChallenge(context, {
|
||||
type: 'authentication',
|
||||
challenge: 'test-challenge',
|
||||
origin: 'http://127.0.0.1',
|
||||
rpID: '127.0.0.1',
|
||||
});
|
||||
|
||||
const first = await passkeyService.consumeChallenge(context, transactionId, 'authentication');
|
||||
const second = await passkeyService.consumeChallenge(context, transactionId, 'authentication');
|
||||
expect(first?.challenge).toBe('test-challenge');
|
||||
expect(second).toBeNull();
|
||||
});
|
||||
|
||||
it('generates discoverable authentication options when enabled', async () => {
|
||||
await env.db.prepare('UPDATE setting SET passkey = 1').run();
|
||||
await settingService.refresh({ env, set: () => {} });
|
||||
const response = await SELF.fetch('http://127.0.0.1/api/passkey/auth/options', {
|
||||
method: 'POST',
|
||||
headers: { Origin: 'http://127.0.0.1' },
|
||||
});
|
||||
const body = await response.json();
|
||||
const challenge = await env.db.prepare('SELECT challenge_id FROM passkey_challenge WHERE challenge_id = ?')
|
||||
.bind(body.data.transactionId).first();
|
||||
|
||||
expect(body.code).toBe(200);
|
||||
expect(body.data.options.rpId).toBe('127.0.0.1');
|
||||
expect(body.data.options.userVerification).toBe('required');
|
||||
expect(challenge?.challenge_id).toBe(body.data.transactionId);
|
||||
it('responds with Hello World! (integration style)', async () => {
|
||||
const response = await SELF.fetch('http://example.com');
|
||||
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ export default defineWorkersConfig({
|
||||
test: {
|
||||
poolOptions: {
|
||||
workers: {
|
||||
wrangler: { configPath: './wrangler-dev.toml' },
|
||||
wrangler: { configPath: './wrangler.jsonc' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
name = "${NAME}"
|
||||
main = "src/index.js"
|
||||
compatibility_date = "2025-06-04"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
|
||||
[observability]
|
||||
enabled = true
|
||||
@@ -33,7 +32,7 @@ not_found_handling = "single-page-application"
|
||||
run_worker_first = true
|
||||
|
||||
[triggers]
|
||||
crons = ["0 16 * * *"]
|
||||
crons = ["0 * * * *"] #每小时执行;resetDaySendCount 仅在 UTC 0 点生效
|
||||
|
||||
|
||||
[vars]
|
||||
@@ -45,10 +44,5 @@ admin = "${ADMIN}"
|
||||
jwt_secret = "${JWT_SECRET}"
|
||||
project_link = "${PROJECT_LINK}"
|
||||
|
||||
linuxdo_client_id = "${LINUXDO_CLIENT_ID}"
|
||||
linuxdo_client_secret = "${LINUXDO_CLIENT_SECRET}"
|
||||
linuxdo_callback_url = "${LINUXDO_CALLBACK_URL}"
|
||||
linuxdo_switch = "${LINUXDO_SWITCH}"
|
||||
|
||||
[build]
|
||||
command = "pnpm --prefix ../mail-vue install && pnpm --prefix ../mail-vue run build"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
name = "cloud-mail-dev"
|
||||
main = "src/index.js"
|
||||
compatibility_date = "2025-06-04"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
keep_vars = true
|
||||
|
||||
[observability]
|
||||
@@ -39,7 +38,7 @@ run_worker_first = true
|
||||
[vars]
|
||||
ai_model = ""
|
||||
analysis_cache = false
|
||||
orm_log = false
|
||||
orm_log = true
|
||||
domain = ["example.com", "example2.com", "example3.com", "example4.com"]
|
||||
admin = "[email protected]"
|
||||
jwt_secret = "b7f29a1d-18e2-4d3b-941f-f6b2c97c02fd"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
name = "test-mail"
|
||||
main = "src/index.js"
|
||||
compatibility_date = "2025-06-04"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
keep_vars = true
|
||||
|
||||
[observability]
|
||||
@@ -31,11 +30,11 @@ not_found_handling = "single-page-application"
|
||||
run_worker_first = true
|
||||
|
||||
[triggers]
|
||||
crons = ["0 16 * * *"] #每天晚上12点执行每日任务,刷新分析缓存
|
||||
crons = ["0 * * * *"] #每小时执行;resetDaySendCount 仅在 UTC 0 点生效
|
||||
|
||||
|
||||
[vars]
|
||||
ai_model = "" #ai模型,不填默认使用@cf/meta/llama-3.1-8b-instruct
|
||||
ai_model = "" #ai模型,不填默认使用@cf/meta/llama-3.1-8b-instruct-fast
|
||||
analysis_cache = false #是否开启分析数据缓存
|
||||
orm_log = false #是否sql日志
|
||||
domain = ["example.com"] #邮件域名可可配置多个 示例: ["example1.com","example2.com"]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
name = "cloud-mail"
|
||||
main = "src/index.js"
|
||||
compatibility_date = "2025-06-04"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
keep_vars = true
|
||||
|
||||
[observability]
|
||||
@@ -33,15 +32,15 @@ not_found_handling = "single-page-application"
|
||||
run_worker_first = true
|
||||
|
||||
[triggers]
|
||||
crons = ["0 16 * * *"] #每天晚上12点执行每日任务,刷新分析缓存
|
||||
crons = ["0 * * * *"] #每小时执行;resetDaySendCount 仅在 UTC 0 点生效
|
||||
|
||||
|
||||
[vars]
|
||||
#ai_model = "" #ai模型,不填默认使用@cf/meta/llama-3.1-8b-instruct
|
||||
#ai_model = "" #ai模型,不填默认使用@cf/meta/llama-3.1-8b-instruct-fast
|
||||
#analysis_cache = false #是否开启分析数据缓存
|
||||
#orm_log = false #是否sql日志
|
||||
#domain = [] #邮件域名可可配置多个 示例: ["example1.com","example2.com"]
|
||||
#admin = "" #管理员的邮箱 示例: [email protected]
|
||||
#admin = "" #管理员的邮箱 示例: [email protected]
|
||||
#jwt_secret = "" #jwt令牌的密钥,随便填一串字符串
|
||||
|
||||
[build]
|
||||
|
||||
Reference in New Issue
Block a user