mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-10 18:07:00 +08:00
Compare commits
2
Commits
v1.0.6
..
8e5293f010
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e5293f010 | ||
|
|
711378cb11 |
@@ -33,17 +33,10 @@ jobs:
|
|||||||
- name: Deploy Backend for ${{ github.ref_name }}
|
- name: Deploy Backend for ${{ github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
export use_worker_assets=${{ secrets.USE_WORKER_ASSETS }}
|
export use_worker_assets=${{ secrets.USE_WORKER_ASSETS }}
|
||||||
export use_worker_assets_with_telegram=${{ secrets.USE_WORKER_ASSETS_WITH_TELEGRAM }}
|
|
||||||
if [ -n "$use_worker_assets" ]; then
|
if [ -n "$use_worker_assets" ]; then
|
||||||
cd frontend/
|
cd frontend/
|
||||||
pnpm install --no-frozen-lockfile
|
pnpm install --no-frozen-lockfile
|
||||||
if [ -n "$use_worker_assets_with_telegram" ]; then
|
|
||||||
echo "Building with telegram pages"
|
|
||||||
pnpm build:telegram:pages
|
|
||||||
else
|
|
||||||
echo "Building with normal pages"
|
|
||||||
pnpm build:pages
|
pnpm build:pages
|
||||||
fi
|
|
||||||
cd ..
|
cd ..
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -60,7 +53,7 @@ jobs:
|
|||||||
echo "Applied mail-parser-wasm-worker patch"
|
echo "Applied mail-parser-wasm-worker patch"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$debug_mode" = "true" ]; then
|
if [ -n "$debug_mode" ]; then
|
||||||
pnpm run deploy
|
pnpm run deploy
|
||||||
else
|
else
|
||||||
output=$(pnpm run deploy 2>&1)
|
output=$(pnpm run deploy 2>&1)
|
||||||
|
|||||||
+1
-11
@@ -1,21 +1,11 @@
|
|||||||
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
|
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
|
||||||
# CHANGE LOG
|
# CHANGE LOG
|
||||||
|
|
||||||
## v1.0.6
|
## main(v1.0.5)
|
||||||
|
|
||||||
- feat: |DB| update db schema add index
|
|
||||||
- feat: |地址密码| 增加地址密码登录功能, 通过 `ENABLE_ADDRESS_PASSWORD` 配置启用, 需要执行 `db/2025-09-23-patch.sql` 文件中的 SQL 更新 `D1` 数据库
|
|
||||||
- fix: |GitHub Actions| 修复 debug 模式配置,仅当 DEBUG_MODE 为 'true' 时才启用调试模式
|
|
||||||
- feat: |Admin| 账户管理页面新增多选批量操作功能(批量删除、批量清空收件箱、批量清空发件箱)
|
|
||||||
- feat: |Admin| 维护页面增加清理未绑定用户地址的功能
|
|
||||||
- feat: 支持针对角色配置不同的绑定地址数量上限, 可在 admin 页面配置
|
|
||||||
|
|
||||||
## v1.0.5
|
|
||||||
|
|
||||||
- feat: 新增 `DISABLE_CUSTOM_ADDRESS_NAME` 配置: 禁用自定义邮箱地址名称功能
|
- feat: 新增 `DISABLE_CUSTOM_ADDRESS_NAME` 配置: 禁用自定义邮箱地址名称功能
|
||||||
- feat: 新增 `CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST` 配置: 创建地址时优先使用第一个域名
|
- feat: 新增 `CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST` 配置: 创建地址时优先使用第一个域名
|
||||||
- feat: |UI| 主页增加进入极简模式按钮
|
- feat: |UI| 主页增加进入极简模式按钮
|
||||||
- feat: |Webhook| 增加白名单开关功能,支持灵活控制访问权限
|
|
||||||
|
|
||||||
## v1.0.4
|
## v1.0.4
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,6 @@
|
|||||||
- 🆓 **完全免费** - 基于 Cloudflare 免费服务构建,零成本运行
|
- 🆓 **完全免费** - 基于 Cloudflare 免费服务构建,零成本运行
|
||||||
- ⚡ **高性能** - Rust WASM 邮件解析,响应速度极快
|
- ⚡ **高性能** - Rust WASM 邮件解析,响应速度极快
|
||||||
- 🎨 **现代化界面** - 响应式设计,支持多语言,操作简便
|
- 🎨 **现代化界面** - 响应式设计,支持多语言,操作简便
|
||||||
- 🔐 **地址密码** - 支持为邮箱地址设置独立密码,增强安全性 (通过 `ENABLE_ADDRESS_PASSWORD` 启用)
|
|
||||||
|
|
||||||
## 📚 部署文档 - 快速开始
|
## 📚 部署文档 - 快速开始
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
ALTER TABLE
|
|
||||||
address
|
|
||||||
ADD
|
|
||||||
password TEXT;
|
|
||||||
@@ -9,22 +9,15 @@ CREATE TABLE IF NOT EXISTS raw_mails (
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_raw_mails_address ON raw_mails(address);
|
CREATE INDEX IF NOT EXISTS idx_raw_mails_address ON raw_mails(address);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_raw_mails_created_at ON raw_mails(created_at);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS address (
|
CREATE TABLE IF NOT EXISTS address (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT UNIQUE,
|
name TEXT UNIQUE,
|
||||||
password TEXT,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_address_name ON address(name);
|
CREATE INDEX IF NOT EXISTS idx_address_name ON address(name);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_address_created_at ON address(created_at);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_address_updated_at ON address(updated_at);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS auto_reply_mails (
|
CREATE TABLE IF NOT EXISTS auto_reply_mails (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
source_prefix TEXT,
|
source_prefix TEXT,
|
||||||
@@ -57,8 +50,6 @@ CREATE TABLE IF NOT EXISTS sendbox (
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sendbox_address ON sendbox(address);
|
CREATE INDEX IF NOT EXISTS idx_sendbox_address ON sendbox(address);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sendbox_created_at ON sendbox(created_at);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
value TEXT,
|
value TEXT,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cloudflare_temp_email",
|
"name": "cloudflare_temp_email",
|
||||||
"version": "1.0.6",
|
"version": "1.0.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -10,7 +10,6 @@
|
|||||||
"build:pages": "vite build -m pages --emptyOutDir",
|
"build:pages": "vite build -m pages --emptyOutDir",
|
||||||
"build:pages:nopwa": "VITE_PWA_DISABLED=true vite build -m pages --emptyOutDir",
|
"build:pages:nopwa": "VITE_PWA_DISABLED=true vite build -m pages --emptyOutDir",
|
||||||
"build:telegram": "VITE_IS_TELEGRAM=true vite build -m prod --emptyOutDir",
|
"build:telegram": "VITE_IS_TELEGRAM=true vite build -m prod --emptyOutDir",
|
||||||
"build:telegram:pages": "VITE_IS_TELEGRAM=true vite build -m pages --emptyOutDir",
|
|
||||||
"build:telegram:release": "VITE_IS_TELEGRAM=true vite build -m example --emptyOutDir",
|
"build:telegram:release": "VITE_IS_TELEGRAM=true vite build -m example --emptyOutDir",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"deploy:telegram": "npm run build:telegram && wrangler pages deploy ./dist --branch production",
|
"deploy:telegram": "npm run build:telegram && wrangler pages deploy ./dist --branch production",
|
||||||
@@ -25,15 +24,15 @@
|
|||||||
"@vueuse/core": "^12.8.2",
|
"@vueuse/core": "^12.8.2",
|
||||||
"@wangeditor/editor": "^5.1.23",
|
"@wangeditor/editor": "^5.1.23",
|
||||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.11.0",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"mail-parser-wasm": "^0.2.1",
|
"mail-parser-wasm": "^0.2.1",
|
||||||
"naive-ui": "^2.43.1",
|
"naive-ui": "^2.42.0",
|
||||||
"postal-mime": "^2.5.0",
|
"postal-mime": "^2.4.4",
|
||||||
"vooks": "^0.2.12",
|
"vooks": "^0.2.12",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.20",
|
||||||
"vue-clipboard3": "^2.0.0",
|
"vue-clipboard3": "^2.0.0",
|
||||||
"vue-i18n": "^11.1.12",
|
"vue-i18n": "^11.1.11",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -42,13 +41,13 @@
|
|||||||
"@vitejs/plugin-vue": "^5.2.4",
|
"@vitejs/plugin-vue": "^5.2.4",
|
||||||
"unplugin-auto-import": "^19.3.0",
|
"unplugin-auto-import": "^19.3.0",
|
||||||
"unplugin-vue-components": "^28.8.0",
|
"unplugin-vue-components": "^28.8.0",
|
||||||
"vite": "^6.3.6",
|
"vite": "^6.3.5",
|
||||||
"vite-plugin-pwa": "^1.0.3",
|
"vite-plugin-pwa": "^1.0.3",
|
||||||
"vite-plugin-top-level-await": "^1.6.0",
|
"vite-plugin-top-level-await": "^1.6.0",
|
||||||
"vite-plugin-wasm": "^3.5.0",
|
"vite-plugin-wasm": "^3.5.0",
|
||||||
"workbox-build": "^7.3.0",
|
"workbox-build": "^7.3.0",
|
||||||
"workbox-window": "^7.3.0",
|
"workbox-window": "^7.3.0",
|
||||||
"wrangler": "^4.42.2"
|
"wrangler": "^4.33.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+928
-944
File diff suppressed because it is too large
Load Diff
@@ -86,7 +86,6 @@ const getOpenSettings = async (message, notification) => {
|
|||||||
cfTurnstileSiteKey: res["cfTurnstileSiteKey"] || "",
|
cfTurnstileSiteKey: res["cfTurnstileSiteKey"] || "",
|
||||||
enableWebhook: res["enableWebhook"] || false,
|
enableWebhook: res["enableWebhook"] || false,
|
||||||
isS3Enabled: res["isS3Enabled"] || false,
|
isS3Enabled: res["isS3Enabled"] || false,
|
||||||
enableAddressPassword: res["enableAddressPassword"] || false,
|
|
||||||
});
|
});
|
||||||
if (openSettings.value.needAuth) {
|
if (openSettings.value.needAuth) {
|
||||||
showAuth.value = true;
|
showAuth.value = true;
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ export const useGlobalState = createGlobalState(
|
|||||||
isS3Enabled: false,
|
isS3Enabled: false,
|
||||||
showGithub: true,
|
showGithub: true,
|
||||||
disableAdminPasswordCheck: false,
|
disableAdminPasswordCheck: false,
|
||||||
enableAddressPassword: false,
|
|
||||||
})
|
})
|
||||||
const settings = ref({
|
const settings = ref({
|
||||||
fetched: false,
|
fetched: false,
|
||||||
@@ -64,7 +63,6 @@ export const useGlobalState = createGlobalState(
|
|||||||
const auth = useStorage('auth', '');
|
const auth = useStorage('auth', '');
|
||||||
const adminAuth = useStorage('adminAuth', '');
|
const adminAuth = useStorage('adminAuth', '');
|
||||||
const jwt = useStorage('jwt', '');
|
const jwt = useStorage('jwt', '');
|
||||||
const addressPassword = useSessionStorage('addressPassword', '');
|
|
||||||
const adminTab = useSessionStorage('adminTab', "account");
|
const adminTab = useSessionStorage('adminTab', "account");
|
||||||
const adminMailTabAddress = ref("");
|
const adminMailTabAddress = ref("");
|
||||||
const adminSendBoxTabAddress = ref("");
|
const adminSendBoxTabAddress = ref("");
|
||||||
@@ -147,7 +145,6 @@ export const useGlobalState = createGlobalState(
|
|||||||
userOauth2SessionState,
|
userOauth2SessionState,
|
||||||
userOauth2SessionClientID,
|
userOauth2SessionClientID,
|
||||||
useSimpleIndex,
|
useSimpleIndex,
|
||||||
addressPassword,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import AccountSettings from './admin/AccountSettings.vue';
|
|||||||
import UserManagement from './admin/UserManagement.vue';
|
import UserManagement from './admin/UserManagement.vue';
|
||||||
import UserSettings from './admin/UserSettings.vue';
|
import UserSettings from './admin/UserSettings.vue';
|
||||||
import UserOauth2Settings from './admin/UserOauth2Settings.vue';
|
import UserOauth2Settings from './admin/UserOauth2Settings.vue';
|
||||||
import RoleAddressConfig from './admin/RoleAddressConfig.vue';
|
|
||||||
import Mails from './admin/Mails.vue';
|
import Mails from './admin/Mails.vue';
|
||||||
import MailsUnknow from './admin/MailsUnknow.vue';
|
import MailsUnknow from './admin/MailsUnknow.vue';
|
||||||
import About from './common/About.vue';
|
import About from './common/About.vue';
|
||||||
@@ -62,7 +61,6 @@ const { t } = useI18n({
|
|||||||
user_management: 'User Management',
|
user_management: 'User Management',
|
||||||
user_settings: 'User Settings',
|
user_settings: 'User Settings',
|
||||||
userOauth2Settings: 'Oauth2 Settings',
|
userOauth2Settings: 'Oauth2 Settings',
|
||||||
roleAddressConfig: 'Role Address Config',
|
|
||||||
unknow: 'Mails with unknow receiver',
|
unknow: 'Mails with unknow receiver',
|
||||||
senderAccess: 'Sender Access Control',
|
senderAccess: 'Sender Access Control',
|
||||||
sendBox: 'Send Box',
|
sendBox: 'Send Box',
|
||||||
@@ -90,7 +88,6 @@ const { t } = useI18n({
|
|||||||
user_management: '用户管理',
|
user_management: '用户管理',
|
||||||
user_settings: '用户设置',
|
user_settings: '用户设置',
|
||||||
userOauth2Settings: 'Oauth2 设置',
|
userOauth2Settings: 'Oauth2 设置',
|
||||||
roleAddressConfig: '角色地址配置',
|
|
||||||
unknow: '无收件人邮件',
|
unknow: '无收件人邮件',
|
||||||
senderAccess: '发件权限控制',
|
senderAccess: '发件权限控制',
|
||||||
sendBox: '发件箱',
|
sendBox: '发件箱',
|
||||||
@@ -176,9 +173,6 @@ onMounted(async () => {
|
|||||||
<n-tab-pane name="userOauth2Settings" :tab="t('userOauth2Settings')">
|
<n-tab-pane name="userOauth2Settings" :tab="t('userOauth2Settings')">
|
||||||
<UserOauth2Settings />
|
<UserOauth2Settings />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="roleAddressConfig" :tab="t('roleAddressConfig')">
|
|
||||||
<RoleAddressConfig />
|
|
||||||
</n-tab-pane>
|
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="mails" :tab="t('mails')">
|
<n-tab-pane name="mails" :tab="t('mails')">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, h, onMounted, watch, computed } from 'vue';
|
import { ref, h, onMounted, watch } from 'vue';
|
||||||
import { NBadge, useMessage } from 'naive-ui'
|
import { NBadge } from 'naive-ui'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
import { useGlobalState } from '../../store'
|
||||||
@@ -9,7 +9,7 @@ import { NButton, NMenu } from 'naive-ui';
|
|||||||
import { MenuFilled } from '@vicons/material'
|
import { MenuFilled } from '@vicons/material'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
loading, adminTab, openSettings,
|
loading, adminTab,
|
||||||
adminMailTabAddress, adminSendBoxTabAddress
|
adminMailTabAddress, adminSendBoxTabAddress
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
@@ -33,25 +33,7 @@ const { t } = useI18n({
|
|||||||
itemCount: 'itemCount',
|
itemCount: 'itemCount',
|
||||||
query: 'Query',
|
query: 'Query',
|
||||||
addressQueryTip: 'Leave blank to query all addresses',
|
addressQueryTip: 'Leave blank to query all addresses',
|
||||||
clearInbox: 'Clear Inbox',
|
actions: 'Actions'
|
||||||
clearSentItems: 'Clear Sent Items',
|
|
||||||
clearInboxTip: 'Are you sure to clear inbox for this email?',
|
|
||||||
clearSentItemsTip: 'Are you sure to clear sent items for this email?',
|
|
||||||
actions: 'Actions',
|
|
||||||
success: 'Success',
|
|
||||||
resetPassword: 'Reset Password',
|
|
||||||
newPassword: 'New Password',
|
|
||||||
passwordResetSuccess: 'Password reset successfully',
|
|
||||||
selectAll: 'Select All of This Page',
|
|
||||||
unselectAll: 'Unselect All',
|
|
||||||
pleaseSelectAddress: 'Please select address',
|
|
||||||
selectedItems: 'Selected',
|
|
||||||
multiDelete: 'Multi Delete',
|
|
||||||
multiDeleteTip: 'Are you sure to delete selected addresses?',
|
|
||||||
multiClearInbox: 'Multi Clear Inbox',
|
|
||||||
multiClearInboxTip: 'Are you sure to clear inbox for selected addresses?',
|
|
||||||
multiClearSentItems: 'Multi Clear Sent Items',
|
|
||||||
multiClearSentItemsTip: 'Are you sure to clear sent items for selected addresses?',
|
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
name: '名称',
|
name: '名称',
|
||||||
@@ -70,25 +52,7 @@ const { t } = useI18n({
|
|||||||
itemCount: '总数',
|
itemCount: '总数',
|
||||||
query: '查询',
|
query: '查询',
|
||||||
addressQueryTip: '留空查询所有地址',
|
addressQueryTip: '留空查询所有地址',
|
||||||
clearInbox: '清空收件箱',
|
|
||||||
clearSentItems: '清空发件箱',
|
|
||||||
clearInboxTip: '确定要清空这个邮箱的收件箱吗?',
|
|
||||||
clearSentItemsTip: '确定要清空这个邮箱的发件箱吗?',
|
|
||||||
actions: '操作',
|
actions: '操作',
|
||||||
success: '成功',
|
|
||||||
resetPassword: '重置密码',
|
|
||||||
newPassword: '新密码',
|
|
||||||
passwordResetSuccess: '密码重置成功',
|
|
||||||
selectAll: '全选本页',
|
|
||||||
unselectAll: '取消全选',
|
|
||||||
pleaseSelectAddress: '请选择地址',
|
|
||||||
selectedItems: '已选择',
|
|
||||||
multiDelete: '批量删除',
|
|
||||||
multiDeleteTip: '确定要删除选中的邮箱吗?',
|
|
||||||
multiClearInbox: '批量清空收件箱',
|
|
||||||
multiClearInboxTip: '确定要清空选中邮箱的收件箱吗?',
|
|
||||||
multiClearSentItems: '批量清空发件箱',
|
|
||||||
multiClearSentItemsTip: '确定要清空选中邮箱的发件箱吗?',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -96,20 +60,6 @@ const { t } = useI18n({
|
|||||||
const showEmailCredential = ref(false)
|
const showEmailCredential = ref(false)
|
||||||
const curEmailCredential = ref("")
|
const curEmailCredential = ref("")
|
||||||
const curDeleteAddressId = ref(0);
|
const curDeleteAddressId = ref(0);
|
||||||
const curClearInboxAddressId = ref(0);
|
|
||||||
const curClearSentItemsAddressId = ref(0);
|
|
||||||
const showResetPassword = ref(false);
|
|
||||||
const curResetPasswordAddressId = ref(0);
|
|
||||||
const newPassword = ref('');
|
|
||||||
|
|
||||||
// Multi-action mode state
|
|
||||||
const checkedRowKeys = ref([]);
|
|
||||||
const showMultiActionModal = ref(false);
|
|
||||||
const multiActionProgress = ref({ percentage: 0, tip: '0/0' });
|
|
||||||
const multiActionTitle = ref('');
|
|
||||||
|
|
||||||
const selectedCount = computed(() => checkedRowKeys.value.length);
|
|
||||||
const showMultiActionBar = computed(() => checkedRowKeys.value.length > 0);
|
|
||||||
|
|
||||||
const addressQuery = ref("")
|
const addressQuery = ref("")
|
||||||
|
|
||||||
@@ -118,8 +68,6 @@ const count = ref(0)
|
|||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const pageSize = ref(20)
|
const pageSize = ref(20)
|
||||||
const showDeleteAccount = ref(false)
|
const showDeleteAccount = ref(false)
|
||||||
const showClearInbox = ref(false)
|
|
||||||
const showClearSentItems = ref(false)
|
|
||||||
|
|
||||||
const showCredential = async (id) => {
|
const showCredential = async (id) => {
|
||||||
try {
|
try {
|
||||||
@@ -135,7 +83,7 @@ const showCredential = async (id) => {
|
|||||||
const deleteEmail = async () => {
|
const deleteEmail = async () => {
|
||||||
try {
|
try {
|
||||||
await api.adminDeleteAddress(curDeleteAddressId.value)
|
await api.adminDeleteAddress(curDeleteAddressId.value)
|
||||||
message.success(t("success"));
|
message.success("success");
|
||||||
await fetchData()
|
await fetchData()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error.message || "error");
|
message.error(error.message || "error");
|
||||||
@@ -144,142 +92,6 @@ const deleteEmail = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearInbox = async () => {
|
|
||||||
try {
|
|
||||||
await api.fetch(`/admin/clear_inbox/${curClearInboxAddressId.value}`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
message.success(t("success"));
|
|
||||||
await fetchData()
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
} finally {
|
|
||||||
showClearInbox.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearSentItems = async () => {
|
|
||||||
try {
|
|
||||||
await api.fetch(`/admin/clear_sent_items/${curClearSentItemsAddressId.value}`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
message.success(t("success"));
|
|
||||||
await fetchData()
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
} finally {
|
|
||||||
showClearSentItems.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetPassword = async () => {
|
|
||||||
try {
|
|
||||||
await api.fetch(`/admin/address/${curResetPasswordAddressId.value}/reset_password`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
password: newPassword.value
|
|
||||||
})
|
|
||||||
});
|
|
||||||
message.success(t("passwordResetSuccess"));
|
|
||||||
newPassword.value = '';
|
|
||||||
showResetPassword.value = false;
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multi-action mode functions
|
|
||||||
const multiActionSelectAll = () => {
|
|
||||||
checkedRowKeys.value = data.value.map(item => item.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const multiActionUnselectAll = () => {
|
|
||||||
checkedRowKeys.value = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通用批量操作函数
|
|
||||||
const executeBatchOperation = async ({
|
|
||||||
shouldSkip = () => false,
|
|
||||||
apiCall,
|
|
||||||
title,
|
|
||||||
operationName = 'operation'
|
|
||||||
}) => {
|
|
||||||
try {
|
|
||||||
loading.value = true;
|
|
||||||
const selectedAddresses = data.value.filter((item) =>
|
|
||||||
checkedRowKeys.value.includes(item.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selectedAddresses.length === 0) {
|
|
||||||
message.error(t('pleaseSelectAddress'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const failedIds = [];
|
|
||||||
const totalCount = selectedAddresses.length;
|
|
||||||
|
|
||||||
multiActionProgress.value = {
|
|
||||||
percentage: 0,
|
|
||||||
tip: `0/${totalCount}`
|
|
||||||
};
|
|
||||||
multiActionTitle.value = title;
|
|
||||||
showMultiActionModal.value = true;
|
|
||||||
|
|
||||||
for (const [index, address] of selectedAddresses.entries()) {
|
|
||||||
try {
|
|
||||||
if (!shouldSkip(address)) {
|
|
||||||
await apiCall(address.id);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`${operationName} failed for address ${address.id}:`, error);
|
|
||||||
failedIds.push(address.id);
|
|
||||||
}
|
|
||||||
multiActionProgress.value = {
|
|
||||||
percentage: Math.floor((index + 1) / totalCount * 100),
|
|
||||||
tip: `${index + 1}/${totalCount}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
await fetchData();
|
|
||||||
checkedRowKeys.value = failedIds;
|
|
||||||
message.success(t("success"));
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const multiActionDeleteAccounts = async () => {
|
|
||||||
await executeBatchOperation({
|
|
||||||
apiCall: (id) => api.adminDeleteAddress(id),
|
|
||||||
title: t('multiDelete') + ' ' + t('success'),
|
|
||||||
operationName: 'Delete'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const multiActionClearInbox = async () => {
|
|
||||||
await executeBatchOperation({
|
|
||||||
shouldSkip: (address) => address.mail_count <= 0,
|
|
||||||
apiCall: (id) => api.fetch(`/admin/clear_inbox/${id}`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
}),
|
|
||||||
title: t('multiClearInbox') + ' ' + t('success'),
|
|
||||||
operationName: 'ClearInbox'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const multiActionClearSentItems = async () => {
|
|
||||||
await executeBatchOperation({
|
|
||||||
shouldSkip: (address) => address.send_count <= 0,
|
|
||||||
apiCall: (id) => api.fetch(`/admin/clear_sent_items/${id}`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
}),
|
|
||||||
title: t('multiClearSentItems') + ' ' + t('success'),
|
|
||||||
operationName: 'ClearSentItems'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
addressQuery.value = addressQuery.value.trim()
|
addressQuery.value = addressQuery.value.trim()
|
||||||
@@ -294,15 +106,12 @@ const fetchData = async () => {
|
|||||||
count.value = addressCount;
|
count.value = addressCount;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.log(error)
|
||||||
message.error(error.message || "error");
|
message.error(error.message || "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
|
||||||
type: 'selection'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "ID",
|
title: "ID",
|
||||||
key: "id"
|
key: "id"
|
||||||
@@ -419,45 +228,6 @@ const columns = [
|
|||||||
),
|
),
|
||||||
show: row.send_count > 0
|
show: row.send_count > 0
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: () => h(NButton,
|
|
||||||
{
|
|
||||||
text: true,
|
|
||||||
onClick: () => {
|
|
||||||
curClearInboxAddressId.value = row.id;
|
|
||||||
showClearInbox.value = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ default: () => t('clearInbox') }
|
|
||||||
),
|
|
||||||
show: row.mail_count > 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: () => h(NButton,
|
|
||||||
{
|
|
||||||
text: true,
|
|
||||||
onClick: () => {
|
|
||||||
curClearSentItemsAddressId.value = row.id;
|
|
||||||
showClearSentItems.value = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ default: () => t('clearSentItems') }
|
|
||||||
),
|
|
||||||
show: row.send_count > 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: () => h(NButton,
|
|
||||||
{
|
|
||||||
text: true,
|
|
||||||
onClick: () => {
|
|
||||||
curResetPasswordAddressId.value = row.id;
|
|
||||||
showResetPassword.value = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ default: () => t('resetPassword') }
|
|
||||||
),
|
|
||||||
show: openSettings.value?.enableAddressPassword
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: () => h(NButton,
|
label: () => h(NButton,
|
||||||
{
|
{
|
||||||
@@ -511,70 +281,13 @@ onMounted(async () => {
|
|||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
<n-modal v-model:show="showClearInbox" preset="dialog" :title="t('clearInbox')">
|
<n-input-group>
|
||||||
<p>{{ t('clearInboxTip') }}</p>
|
|
||||||
<template #action>
|
|
||||||
<n-button :loading="loading" @click="clearInbox" size="small" tertiary type="error">
|
|
||||||
{{ t('clearInbox') }}
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
<n-modal v-model:show="showClearSentItems" preset="dialog" :title="t('clearSentItems')">
|
|
||||||
<p>{{ t('clearSentItemsTip') }}</p>
|
|
||||||
<template #action>
|
|
||||||
<n-button :loading="loading" @click="clearSentItems" size="small" tertiary type="error">
|
|
||||||
{{ t('clearSentItems') }}
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
|
|
||||||
<n-modal v-model:show="showResetPassword" preset="dialog" :title="t('resetPassword')">
|
|
||||||
<n-form-item :label="t('newPassword')">
|
|
||||||
<n-input v-model:value="newPassword" type="password" placeholder="" show-password-on="click" />
|
|
||||||
</n-form-item>
|
|
||||||
<template #action>
|
|
||||||
<n-button :loading="loading" @click="resetPassword" size="small" tertiary type="info">
|
|
||||||
{{ t('resetPassword') }}
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
<n-input-group style="margin-bottom: 10px;">
|
|
||||||
<n-input v-model:value="addressQuery" clearable :placeholder="t('addressQueryTip')"
|
<n-input v-model:value="addressQuery" clearable :placeholder="t('addressQueryTip')"
|
||||||
@keydown.enter="fetchData" />
|
@keydown.enter="fetchData" />
|
||||||
<n-button @click="fetchData" type="primary" tertiary>
|
<n-button @click="fetchData" type="primary" tertiary>
|
||||||
{{ t('query') }}
|
{{ t('query') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-input-group>
|
</n-input-group>
|
||||||
|
|
||||||
<n-space v-if="showMultiActionBar" style="margin-bottom: 10px;">
|
|
||||||
<n-button @click="multiActionSelectAll" tertiary>
|
|
||||||
{{ t('selectAll') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button @click="multiActionUnselectAll" tertiary>
|
|
||||||
{{ t('unselectAll') }}
|
|
||||||
</n-button>
|
|
||||||
<n-popconfirm @positive-click="multiActionDeleteAccounts">
|
|
||||||
<template #trigger>
|
|
||||||
<n-button tertiary type="error">{{ t('multiDelete') }}</n-button>
|
|
||||||
</template>
|
|
||||||
{{ t('multiDeleteTip') }}
|
|
||||||
</n-popconfirm>
|
|
||||||
<n-popconfirm @positive-click="multiActionClearInbox">
|
|
||||||
<template #trigger>
|
|
||||||
<n-button tertiary type="warning">{{ t('multiClearInbox') }}</n-button>
|
|
||||||
</template>
|
|
||||||
{{ t('multiClearInboxTip') }}
|
|
||||||
</n-popconfirm>
|
|
||||||
<n-popconfirm @positive-click="multiActionClearSentItems">
|
|
||||||
<template #trigger>
|
|
||||||
<n-button tertiary type="warning">{{ t('multiClearSentItems') }}</n-button>
|
|
||||||
</template>
|
|
||||||
{{ t('multiClearSentItemsTip') }}
|
|
||||||
</n-popconfirm>
|
|
||||||
<n-tag type="info">
|
|
||||||
{{ t('selectedItems') }}: {{ selectedCount }}
|
|
||||||
</n-tag>
|
|
||||||
</n-space>
|
|
||||||
<div style="overflow: auto;">
|
<div style="overflow: auto;">
|
||||||
<div style="display: inline-block;">
|
<div style="display: inline-block;">
|
||||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count"
|
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count"
|
||||||
@@ -584,21 +297,8 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
</n-pagination>
|
</n-pagination>
|
||||||
</div>
|
</div>
|
||||||
<n-data-table v-model:checked-row-keys="checkedRowKeys" :columns="columns" :data="data" :bordered="false"
|
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||||
:row-key="row => row.id" embedded />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Multi-action progress modal -->
|
|
||||||
<n-modal v-model:show="showMultiActionModal" preset="dialog" :title="multiActionTitle" negative-text="OK">
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-progress type="circle" status="info" :percentage="multiActionProgress.percentage">
|
|
||||||
<span style="text-align: center">
|
|
||||||
{{ multiActionProgress.tip }}
|
|
||||||
</span>
|
|
||||||
</n-progress>
|
|
||||||
</n-space>
|
|
||||||
</n-modal>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ const { t } = useI18n({
|
|||||||
messages: {
|
messages: {
|
||||||
en: {
|
en: {
|
||||||
tip: 'You can manually input the following multiple select input and enter',
|
tip: 'You can manually input the following multiple select input and enter',
|
||||||
manualInputPrompt: 'Type and press Enter to add',
|
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
successTip: 'Save Success',
|
successTip: 'Save Success',
|
||||||
address_block_list: 'Address Block Keywords for Users(Admin can skip)',
|
address_block_list: 'Address Block Keywords for Users(Admin can skip)',
|
||||||
@@ -39,7 +38,6 @@ const { t } = useI18n({
|
|||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
tip: '您可以手动输入以下多选输入框, 回车增加',
|
tip: '您可以手动输入以下多选输入框, 回车增加',
|
||||||
manualInputPrompt: '输入后按回车键添加',
|
|
||||||
save: '保存',
|
save: '保存',
|
||||||
successTip: '保存成功',
|
successTip: '保存成功',
|
||||||
address_block_list: '邮件地址屏蔽关键词(管理员可跳过检查)',
|
address_block_list: '邮件地址屏蔽关键词(管理员可跳过检查)',
|
||||||
@@ -211,55 +209,25 @@ onMounted(async () => {
|
|||||||
</n-flex>
|
</n-flex>
|
||||||
<n-form-item-row :label="t('address_block_list')">
|
<n-form-item-row :label="t('address_block_list')">
|
||||||
<n-select v-model:value="addressBlockList" filterable multiple tag
|
<n-select v-model:value="addressBlockList" filterable multiple tag
|
||||||
:placeholder="t('address_block_list_placeholder')">
|
:placeholder="t('address_block_list_placeholder')" />
|
||||||
<template #empty>
|
|
||||||
<n-text depth="3">
|
|
||||||
{{ t('manualInputPrompt') }}
|
|
||||||
</n-text>
|
|
||||||
</template>
|
|
||||||
</n-select>
|
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('send_address_block_list')">
|
<n-form-item-row :label="t('send_address_block_list')">
|
||||||
<n-select v-model:value="sendAddressBlockList" filterable multiple tag
|
<n-select v-model:value="sendAddressBlockList" filterable multiple tag
|
||||||
:placeholder="t('address_block_list_placeholder')">
|
:placeholder="t('address_block_list_placeholder')" />
|
||||||
<template #empty>
|
|
||||||
<n-text depth="3">
|
|
||||||
{{ t('manualInputPrompt') }}
|
|
||||||
</n-text>
|
|
||||||
</template>
|
|
||||||
</n-select>
|
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('noLimitSendAddressList')">
|
<n-form-item-row :label="t('noLimitSendAddressList')">
|
||||||
<n-select v-model:value="noLimitSendAddressList" filterable multiple tag
|
<n-select v-model:value="noLimitSendAddressList" filterable multiple tag
|
||||||
:placeholder="t('noLimitSendAddressList')">
|
:placeholder="t('noLimitSendAddressList')" />
|
||||||
<template #empty>
|
|
||||||
<n-text depth="3">
|
|
||||||
{{ t('manualInputPrompt') }}
|
|
||||||
</n-text>
|
|
||||||
</template>
|
|
||||||
</n-select>
|
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('verified_address_list')">
|
<n-form-item-row :label="t('verified_address_list')">
|
||||||
<n-select v-model:value="verifiedAddressList" filterable multiple tag
|
<n-select v-model:value="verifiedAddressList" filterable multiple tag
|
||||||
:placeholder="t('verified_address_list')">
|
:placeholder="t('verified_address_list')" />
|
||||||
<template #empty>
|
|
||||||
<n-text depth="3">
|
|
||||||
{{ t('manualInputPrompt') }}
|
|
||||||
</n-text>
|
|
||||||
</template>
|
|
||||||
</n-select>
|
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('fromBlockList')">
|
<n-form-item-row :label="t('fromBlockList')">
|
||||||
<n-select v-model:value="fromBlockList" filterable multiple tag :placeholder="t('fromBlockList')">
|
<n-select v-model:value="fromBlockList" filterable multiple tag :placeholder="t('fromBlockList')" />
|
||||||
<template #empty>
|
|
||||||
<n-text depth="3">
|
|
||||||
{{ t('manualInputPrompt') }}
|
|
||||||
</n-text>
|
|
||||||
</template>
|
|
||||||
</n-select>
|
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('block_receive_unknow_address_email')">
|
<n-form-item-row :label="t('block_receive_unknow_address_email')">
|
||||||
<n-switch v-model:value="emailRuleSettings.blockReceiveUnknowAddressEmail" :round="false" />
|
<n-checkbox v-model:checked="emailRuleSettings.blockReceiveUnknowAddressEmail" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('email_forwarding_config')">
|
<n-form-item-row :label="t('email_forwarding_config')">
|
||||||
<n-button @click="openEmailForwardingModal">{{ t('config') }}</n-button>
|
<n-button @click="openEmailForwardingModal">{{ t('config') }}</n-button>
|
||||||
|
|||||||
@@ -15,13 +15,10 @@ const { t } = useI18n({
|
|||||||
en: {
|
en: {
|
||||||
address: 'Address',
|
address: 'Address',
|
||||||
enablePrefix: 'If enable Prefix',
|
enablePrefix: 'If enable Prefix',
|
||||||
creatNewEmail: 'Create New Email',
|
creatNewEmail: 'Get New Email',
|
||||||
fillInAllFields: 'Please fill in all fields',
|
fillInAllFields: 'Please fill in all fields',
|
||||||
successTip: 'Success Created',
|
successTip: 'Success Created',
|
||||||
addressCredential: 'Mail Address Credential',
|
addressCredential: 'Mail Address Credential',
|
||||||
addressCredentialTip: 'Please copy the Mail Address Credential and you can use it to login to your email account.',
|
|
||||||
addressPassword: 'Address Password',
|
|
||||||
linkWithAddressCredential: 'Open to auto login email link',
|
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
address: '地址',
|
address: '地址',
|
||||||
@@ -30,9 +27,6 @@ const { t } = useI18n({
|
|||||||
fillInAllFields: '请填写完整信息',
|
fillInAllFields: '请填写完整信息',
|
||||||
successTip: '创建成功',
|
successTip: '创建成功',
|
||||||
addressCredential: '邮箱地址凭证',
|
addressCredential: '邮箱地址凭证',
|
||||||
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
|
||||||
addressPassword: '地址密码',
|
|
||||||
linkWithAddressCredential: '打开即可自动登录邮箱的链接',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -42,8 +36,6 @@ const emailName = ref("")
|
|||||||
const emailDomain = ref("")
|
const emailDomain = ref("")
|
||||||
const showReultModal = ref(false)
|
const showReultModal = ref(false)
|
||||||
const result = ref("")
|
const result = ref("")
|
||||||
const addressPassword = ref("")
|
|
||||||
const createdAddress = ref("")
|
|
||||||
|
|
||||||
const newEmail = async () => {
|
const newEmail = async () => {
|
||||||
if (!emailName.value || !emailDomain.value) {
|
if (!emailName.value || !emailDomain.value) {
|
||||||
@@ -60,8 +52,6 @@ const newEmail = async () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
result.value = res["jwt"];
|
result.value = res["jwt"];
|
||||||
addressPassword.value = res["password"] || '';
|
|
||||||
createdAddress.value = res["address"] || '';
|
|
||||||
message.success(t('successTip'))
|
message.success(t('successTip'))
|
||||||
showReultModal.value = true
|
showReultModal.value = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -69,10 +59,6 @@ const newEmail = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUrlWithJwt = () => {
|
|
||||||
return `${window.location.origin}/?jwt=${result.value}`
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (openSettings.prefix) {
|
if (openSettings.prefix) {
|
||||||
enablePrefix.value = true
|
enablePrefix.value = true
|
||||||
@@ -84,29 +70,14 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-modal v-model:show="showReultModal" preset="dialog" :title="t('addressCredential')">
|
<n-modal v-model:show="showReultModal" preset="dialog" :title="t('addressCredential')">
|
||||||
<span>
|
<p>{{ t('addressCredential') }}</p>
|
||||||
<p>{{ t("addressCredentialTip") }}</p>
|
<n-card :bordered="false" embedded>
|
||||||
</span>
|
|
||||||
<n-card embedded>
|
|
||||||
<b>{{ result }}</b>
|
<b>{{ result }}</b>
|
||||||
</n-card>
|
</n-card>
|
||||||
<n-card embedded v-if="addressPassword">
|
|
||||||
<p><b>{{ createdAddress }}</b></p>
|
|
||||||
<p>{{ t('addressPassword') }}: <b>{{ addressPassword }}</b></p>
|
|
||||||
</n-card>
|
|
||||||
<n-card embedded>
|
|
||||||
<n-collapse>
|
|
||||||
<n-collapse-item :title='t("linkWithAddressCredential")'>
|
|
||||||
<n-card embedded>
|
|
||||||
<b>{{ getUrlWithJwt() }}</b>
|
|
||||||
</n-card>
|
|
||||||
</n-collapse-item>
|
|
||||||
</n-collapse>
|
|
||||||
</n-card>
|
|
||||||
</n-modal>
|
</n-modal>
|
||||||
<n-card :bordered="false" embedded style="max-width: 600px;">
|
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||||
<n-form-item-row v-if="openSettings.prefix" :label="t('enablePrefix')">
|
<n-form-item-row v-if="openSettings.prefix" :label="t('enablePrefix')">
|
||||||
<n-switch v-model:value="enablePrefix" :round="false" />
|
<n-checkbox v-model:checked="enablePrefix" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('address')">
|
<n-form-item-row :label="t('address')">
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ const cleanupModel = ref({
|
|||||||
cleanAddressDays: 30,
|
cleanAddressDays: 30,
|
||||||
enableInactiveAddressAutoCleanup: false,
|
enableInactiveAddressAutoCleanup: false,
|
||||||
cleanInactiveAddressDays: 30,
|
cleanInactiveAddressDays: 30,
|
||||||
enableUnboundAddressAutoCleanup: false,
|
|
||||||
cleanUnboundAddressDays: 30,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const { t } = useI18n({
|
const { t } = useI18n({
|
||||||
@@ -30,7 +28,6 @@ const { t } = useI18n({
|
|||||||
sendBoxLabel: "Cleanup the sendbox before n days",
|
sendBoxLabel: "Cleanup the sendbox before n days",
|
||||||
addressCreateLabel: "Cleanup the address created before n days",
|
addressCreateLabel: "Cleanup the address created before n days",
|
||||||
inactiveAddressLabel: "Cleanup the inactive address before n days",
|
inactiveAddressLabel: "Cleanup the inactive address before n days",
|
||||||
unboundAddressLabel: "Cleanup the unbound address before n days",
|
|
||||||
cleanupNow: "Cleanup now",
|
cleanupNow: "Cleanup now",
|
||||||
autoCleanup: "Auto cleanup",
|
autoCleanup: "Auto cleanup",
|
||||||
cleanupSuccess: "Cleanup success",
|
cleanupSuccess: "Cleanup success",
|
||||||
@@ -44,7 +41,6 @@ const { t } = useI18n({
|
|||||||
sendBoxLabel: "清理 n 天前的发件箱",
|
sendBoxLabel: "清理 n 天前的发件箱",
|
||||||
addressCreateLabel: "清理 n 天前创建的地址",
|
addressCreateLabel: "清理 n 天前创建的地址",
|
||||||
inactiveAddressLabel: "清理 n 天前的未活跃地址",
|
inactiveAddressLabel: "清理 n 天前的未活跃地址",
|
||||||
unboundAddressLabel: "清理 n 天前的未绑定用户地址",
|
|
||||||
autoCleanup: "自动清理",
|
autoCleanup: "自动清理",
|
||||||
cleanupSuccess: "清理成功",
|
cleanupSuccess: "清理成功",
|
||||||
cleanupNow: "立即清理",
|
cleanupNow: "立即清理",
|
||||||
@@ -165,18 +161,6 @@ onMounted(async () => {
|
|||||||
{{ t('cleanupNow') }}
|
{{ t('cleanupNow') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('unboundAddressLabel')">
|
|
||||||
<n-checkbox v-model:checked="cleanupModel.enableUnboundAddressAutoCleanup">
|
|
||||||
{{ t('autoCleanup') }}
|
|
||||||
</n-checkbox>
|
|
||||||
<n-input-number v-model:value="cleanupModel.cleanUnboundAddressDays" :placeholder="t('tip')" />
|
|
||||||
<n-button @click="cleanup('unboundAddress', cleanupModel.cleanUnboundAddressDays)">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="CleaningServicesFilled" />
|
|
||||||
</template>
|
|
||||||
{{ t('cleanupNow') }}
|
|
||||||
</n-button>
|
|
||||||
</n-form-item-row>
|
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-card>
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,153 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, onMounted, h } from 'vue';
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { NInputNumber, NTag, NSpace, NButton } from 'naive-ui';
|
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
|
||||||
import { api } from '../../api'
|
|
||||||
|
|
||||||
const { loading } = useGlobalState()
|
|
||||||
const message = useMessage()
|
|
||||||
|
|
||||||
const { t } = useI18n({
|
|
||||||
messages: {
|
|
||||||
en: {
|
|
||||||
role: 'Role',
|
|
||||||
maxAddressCount: 'Max Address Count',
|
|
||||||
save: 'Save',
|
|
||||||
successTip: 'Success',
|
|
||||||
noRolesAvailable: 'No roles available in system config',
|
|
||||||
roleConfigDesc: 'Configure maximum address count for each user role. Role-based limits take priority over global settings.',
|
|
||||||
notConfigured: 'Not Configured (Use Global Settings)',
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
role: '角色',
|
|
||||||
maxAddressCount: '最大地址数量',
|
|
||||||
save: '保存',
|
|
||||||
successTip: '成功',
|
|
||||||
noRolesAvailable: '系统配置中没有可用的角色',
|
|
||||||
roleConfigDesc: '为每个用户角色配置最大地址数量。角色配置优先于全局设置。',
|
|
||||||
notConfigured: '未配置(使用全局设置)',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const systemRoles = ref([])
|
|
||||||
const tableData = ref([])
|
|
||||||
|
|
||||||
const fetchUserRoles = async () => {
|
|
||||||
try {
|
|
||||||
const results = await api.fetch(`/admin/user_roles`);
|
|
||||||
systemRoles.value = results;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchRoleConfigs = async () => {
|
|
||||||
try {
|
|
||||||
const { configs } = await api.fetch(`/admin/role_address_config`);
|
|
||||||
tableData.value = systemRoles.value.map(roleObj => ({
|
|
||||||
role: roleObj.role,
|
|
||||||
max_address_count: configs[roleObj.role]?.maxAddressCount ?? null,
|
|
||||||
}));
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveConfig = async () => {
|
|
||||||
try {
|
|
||||||
// convert tableData to object with nested structure
|
|
||||||
const configs = {};
|
|
||||||
tableData.value.forEach(row => {
|
|
||||||
if (row.max_address_count !== null && row.max_address_count !== undefined) {
|
|
||||||
configs[row.role] = { maxAddressCount: row.max_address_count };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await api.fetch(`/admin/role_address_config`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ configs })
|
|
||||||
});
|
|
||||||
message.success(t('successTip'));
|
|
||||||
await fetchRoleConfigs();
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: t('role'),
|
|
||||||
key: 'role',
|
|
||||||
width: 200,
|
|
||||||
render(row) {
|
|
||||||
return h(NTag, {
|
|
||||||
type: 'info',
|
|
||||||
bordered: false
|
|
||||||
}, {
|
|
||||||
default: () => row.role
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('maxAddressCount'),
|
|
||||||
key: 'max_address_count',
|
|
||||||
render(row) {
|
|
||||||
return h(NInputNumber, {
|
|
||||||
value: row.max_address_count,
|
|
||||||
min: 0,
|
|
||||||
max: 999,
|
|
||||||
clearable: true,
|
|
||||||
placeholder: t('notConfigured'),
|
|
||||||
style: 'width: 200px;',
|
|
||||||
onUpdateValue: (value) => {
|
|
||||||
row.max_address_count = value;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await fetchUserRoles();
|
|
||||||
await fetchRoleConfigs();
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div style="margin-top: 10px;">
|
|
||||||
<n-alert type="info" :bordered="false" style="margin-bottom: 20px;">
|
|
||||||
{{ t('roleConfigDesc') }}
|
|
||||||
</n-alert>
|
|
||||||
|
|
||||||
<n-alert v-if="systemRoles.length === 0" type="warning" :bordered="false">
|
|
||||||
{{ t('noRolesAvailable') }}
|
|
||||||
</n-alert>
|
|
||||||
|
|
||||||
<div v-else>
|
|
||||||
<n-space justify="end" style="margin-bottom: 12px;">
|
|
||||||
<n-button :loading="loading" @click="saveConfig" type="primary">
|
|
||||||
{{ t('save') }}
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
|
|
||||||
<n-data-table
|
|
||||||
:columns="columns"
|
|
||||||
:data="tableData"
|
|
||||||
:bordered="false"
|
|
||||||
embedded
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.n-data-table {
|
|
||||||
min-width: 600px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -15,29 +15,25 @@ const { t } = useI18n({
|
|||||||
init: 'Init',
|
init: 'Init',
|
||||||
successTip: 'Success',
|
successTip: 'Success',
|
||||||
status: 'Check Status',
|
status: 'Check Status',
|
||||||
enableTelegramAllowList: 'Enable Telegram Allow List(Manually input Chat ID)',
|
enableTelegramAllowList: 'Enable Telegram Allow List(Manually input user ID)',
|
||||||
enable: 'Enable',
|
enable: 'Enable',
|
||||||
telegramAllowList: 'Telegram Allow List(Manually input telegram Chat ID)',
|
telegramAllowList: 'Telegram Allow List(Manually input telegram user ID)',
|
||||||
manualInputPrompt: 'Type and press Enter to add',
|
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
miniAppUrl: 'Telegram Mini App URL',
|
miniAppUrl: 'Telegram Mini App URL',
|
||||||
enableGlobalMailPush: 'Enable Global Mail Push(Manually input telegram Chat ID)',
|
enableGlobalMailPush: 'Enable Global Mail Push(Manually input telegram user ID)',
|
||||||
globalMailPushList: 'Global Mail Push Chat ID List',
|
globalMailPushList: 'Global Mail Push List',
|
||||||
globalMailPushListTip: 'Support chat_id of private chat/group/channel. You can send a message to your bot, then visit this link to see chat_id, https://api.telegram.org/bot<Replace with your BOT TOKEN>/getUpdates',
|
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
init: '初始化',
|
init: '初始化',
|
||||||
successTip: '成功',
|
successTip: '成功',
|
||||||
status: '查看状态',
|
status: '查看状态',
|
||||||
enableTelegramAllowList: '启用 Telegram 白名单(手动输入 Chat ID, 回车增加)',
|
enableTelegramAllowList: '启用 Telegram 白名单(手动输入用户 ID, 回车增加)',
|
||||||
enable: '启用',
|
enable: '启用',
|
||||||
telegramAllowList: 'Telegram 白名单(手动输入 Chat ID, 回车增加)',
|
telegramAllowList: 'Telegram 白名单(手动输入用户 ID, 回车增加)',
|
||||||
manualInputPrompt: '输入后按回车键添加',
|
|
||||||
save: '保存',
|
save: '保存',
|
||||||
miniAppUrl: '电报小程序 URL(请输入你部署的电报小程序网页地址)',
|
miniAppUrl: '电报小程序 URL(请输入你部署的电报小程序网页地址)',
|
||||||
enableGlobalMailPush: '启用全局邮件推送(手动输入邮箱管理员的 telegram Chat ID, 回车增加)',
|
enableGlobalMailPush: '启用全局邮件推送(手动输入邮箱管理员的 telegram 用户 ID, 回车增加)',
|
||||||
globalMailPushList: '全局邮件推送 Chat ID 列表',
|
globalMailPushList: '全局邮件推送用户列表',
|
||||||
globalMailPushListTip: '支持对话/群组/频道的 Chat ID, 您可以发送一条消息给您的机器人,然后访问此链接来查看 chat_id, https://api.telegram.org/bot<这里替换成您的 BOT TOKEN>/getUpdates',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -117,17 +113,6 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card :bordered="false" embedded style="max-width: 800px; overflow: auto;">
|
<n-card :bordered="false" embedded style="max-width: 800px; overflow: auto;">
|
||||||
<n-flex justify="end">
|
|
||||||
<n-button @click="fetchStatus" secondary>
|
|
||||||
{{ t('status') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button @click="init" type="primary">
|
|
||||||
{{ t('init') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button @click="saveSettings" type="primary">
|
|
||||||
{{ t('save') }}
|
|
||||||
</n-button>
|
|
||||||
</n-flex>
|
|
||||||
<n-card :bordered="false" embedded>
|
<n-card :bordered="false" embedded>
|
||||||
<n-form-item-row :label="t('enableTelegramAllowList')">
|
<n-form-item-row :label="t('enableTelegramAllowList')">
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
@@ -135,41 +120,31 @@ onMounted(async () => {
|
|||||||
{{ t('enable') }}
|
{{ t('enable') }}
|
||||||
</n-checkbox>
|
</n-checkbox>
|
||||||
<n-select v-model:value="settings.allowList" filterable multiple tag style="width: 80%;"
|
<n-select v-model:value="settings.allowList" filterable multiple tag style="width: 80%;"
|
||||||
:placeholder="t('telegramAllowList')">
|
:placeholder="t('telegramAllowList')" />
|
||||||
<template #empty>
|
|
||||||
<n-text depth="3">
|
|
||||||
{{ t('manualInputPrompt') }}
|
|
||||||
</n-text>
|
|
||||||
</template>
|
|
||||||
</n-select>
|
|
||||||
</n-input-group>
|
</n-input-group>
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<br />
|
|
||||||
<n-form-item-row :label="t('enableGlobalMailPush')">
|
<n-form-item-row :label="t('enableGlobalMailPush')">
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
<n-checkbox v-model:checked="settings.enableGlobalMailPush" style="width: 20%;">
|
<n-checkbox v-model:checked="settings.enableGlobalMailPush" style="width: 20%;">
|
||||||
{{ t('enable') }}
|
{{ t('enable') }}
|
||||||
</n-checkbox>
|
</n-checkbox>
|
||||||
<n-select v-model:value="settings.globalMailPushList" filterable multiple tag
|
<n-select v-model:value="settings.globalMailPushList" filterable multiple tag
|
||||||
style="width: 80%;" :placeholder="t('globalMailPushList')">
|
style="width: 80%;" :placeholder="t('globalMailPushList')" />
|
||||||
<template #empty>
|
|
||||||
<n-text depth="3">
|
|
||||||
{{ t('manualInputPrompt') }}
|
|
||||||
</n-text>
|
|
||||||
</template>
|
|
||||||
</n-select>
|
|
||||||
</n-input-group>
|
</n-input-group>
|
||||||
<template #feedback>
|
|
||||||
<n-text depth="3">
|
|
||||||
{{ t('globalMailPushListTip') }}
|
|
||||||
</n-text>
|
|
||||||
</template>
|
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<br />
|
|
||||||
<n-form-item-row :label="t('miniAppUrl')">
|
<n-form-item-row :label="t('miniAppUrl')">
|
||||||
<n-input v-model:value="settings.miniAppUrl"></n-input>
|
<n-input v-model:value="settings.miniAppUrl"></n-input>
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
|
<n-button @click="saveSettings" type="primary" block>
|
||||||
|
{{ t('save') }}
|
||||||
|
</n-button>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
<n-button @click="init" type="primary" block>
|
||||||
|
{{ t('init') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="fetchStatus" secondary block>
|
||||||
|
{{ t('status') }}
|
||||||
|
</n-button>
|
||||||
<pre v-if="status.fetched">{{ JSON.stringify(status, null, 2) }}</pre>
|
<pre v-if="status.fetched">{{ JSON.stringify(status, null, 2) }}</pre>
|
||||||
</n-card>
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -182,4 +157,8 @@ onMounted(async () => {
|
|||||||
place-items: center;
|
place-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.n-button {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ const { t } = useI18n({
|
|||||||
successTip: 'Save Success',
|
successTip: 'Save Success',
|
||||||
enable: 'Enable',
|
enable: 'Enable',
|
||||||
enableMailAllowList: 'Enable Mail Address Allow List(Manually enterable)',
|
enableMailAllowList: 'Enable Mail Address Allow List(Manually enterable)',
|
||||||
manualInputPrompt: 'Type and press Enter to add',
|
|
||||||
mailAllowList: 'Mail Address Allow List',
|
mailAllowList: 'Mail Address Allow List',
|
||||||
addOauth2: 'Add Oauth2',
|
addOauth2: 'Add Oauth2',
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
@@ -34,7 +33,6 @@ const { t } = useI18n({
|
|||||||
successTip: '保存成功',
|
successTip: '保存成功',
|
||||||
enable: '启用',
|
enable: '启用',
|
||||||
enableMailAllowList: '启用邮件地址白名单(可手动输入, 回车增加)',
|
enableMailAllowList: '启用邮件地址白名单(可手动输入, 回车增加)',
|
||||||
manualInputPrompt: '输入后按回车键添加',
|
|
||||||
mailAllowList: '邮件地址白名单',
|
mailAllowList: '邮件地址白名单',
|
||||||
addOauth2: '添加 Oauth2',
|
addOauth2: '添加 Oauth2',
|
||||||
name: '名称',
|
name: '名称',
|
||||||
@@ -186,7 +184,7 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
<n-card :bordered="false" embedded style="max-width: 600px;">
|
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||||
<n-alert :show-icon="false" :bordered="false" type="warning" closable style="margin-bottom: 10px;">
|
<n-alert :show-icon="false" type="warning" closable style="margin-bottom: 10px;">
|
||||||
{{ t("tip") }}
|
{{ t("tip") }}
|
||||||
</n-alert>
|
</n-alert>
|
||||||
<n-flex justify="end">
|
<n-flex justify="end">
|
||||||
@@ -248,13 +246,7 @@ onMounted(async () => {
|
|||||||
</n-checkbox>
|
</n-checkbox>
|
||||||
<n-select v-model:value="item.mailAllowList" v-if="item.enableMailAllowList" filterable
|
<n-select v-model:value="item.mailAllowList" v-if="item.enableMailAllowList" filterable
|
||||||
multiple tag style="width: 80%;" :options="mailAllowOptions"
|
multiple tag style="width: 80%;" :options="mailAllowOptions"
|
||||||
:placeholder="t('mailAllowList')">
|
:placeholder="t('mailAllowList')" />
|
||||||
<template #empty>
|
|
||||||
<n-text depth="3">
|
|
||||||
{{ t('manualInputPrompt') }}
|
|
||||||
</n-text>
|
|
||||||
</template>
|
|
||||||
</n-select>
|
|
||||||
</n-input-group>
|
</n-input-group>
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
</n-form>
|
</n-form>
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ const { t } = useI18n({
|
|||||||
enableMailVerify: 'Enable Mail Verify (Send address must be an address in the system with a balance and can send mail normally)',
|
enableMailVerify: 'Enable Mail Verify (Send address must be an address in the system with a balance and can send mail normally)',
|
||||||
verifyMailSender: 'Verify Mail Sender',
|
verifyMailSender: 'Verify Mail Sender',
|
||||||
enableMailAllowList: 'Enable Mail Address Allow List(Manually enterable)',
|
enableMailAllowList: 'Enable Mail Address Allow List(Manually enterable)',
|
||||||
manualInputPrompt: 'Type and press Enter to add',
|
|
||||||
mailAllowList: 'Mail Address Allow List',
|
mailAllowList: 'Mail Address Allow List',
|
||||||
maxAddressCount: 'Maximum number of email addresses that can be binded',
|
maxAddressCount: 'Maximum number of email addresses that can be binded',
|
||||||
},
|
},
|
||||||
@@ -30,7 +29,6 @@ const { t } = useI18n({
|
|||||||
enableMailVerify: '启用邮件验证(发送地址必须是系统中能有余额且能正常发送邮件的地址)',
|
enableMailVerify: '启用邮件验证(发送地址必须是系统中能有余额且能正常发送邮件的地址)',
|
||||||
verifyMailSender: '验证邮件发送地址',
|
verifyMailSender: '验证邮件发送地址',
|
||||||
enableMailAllowList: '启用邮件地址白名单(可手动输入, 回车增加)',
|
enableMailAllowList: '启用邮件地址白名单(可手动输入, 回车增加)',
|
||||||
manualInputPrompt: '输入后按回车键添加',
|
|
||||||
mailAllowList: '邮件地址白名单',
|
mailAllowList: '邮件地址白名单',
|
||||||
maxAddressCount: '可绑定最大邮箱地址数量',
|
maxAddressCount: '可绑定最大邮箱地址数量',
|
||||||
}
|
}
|
||||||
@@ -85,14 +83,9 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card :bordered="false" embedded style="max-width: 600px;">
|
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||||
<n-flex justify="end">
|
|
||||||
<n-button @click="save" type="primary" :loading="loading">
|
|
||||||
{{ t('save') }}
|
|
||||||
</n-button>
|
|
||||||
</n-flex>
|
|
||||||
<n-form :model="userSettings">
|
<n-form :model="userSettings">
|
||||||
<n-form-item-row :label="t('enableUserRegister')">
|
<n-form-item-row :label="t('enableUserRegister')">
|
||||||
<n-switch v-model:value="userSettings.enable" :round="false" />
|
<n-checkbox v-model:checked="userSettings.enable" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('enableMailVerify')">
|
<n-form-item-row :label="t('enableMailVerify')">
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
@@ -110,13 +103,7 @@ onMounted(async () => {
|
|||||||
</n-checkbox>
|
</n-checkbox>
|
||||||
<n-select v-model:value="userSettings.mailAllowList" v-if="userSettings.enableMailAllowList"
|
<n-select v-model:value="userSettings.mailAllowList" v-if="userSettings.enableMailAllowList"
|
||||||
filterable multiple tag style="width: 80%;" :options="mailAllowOptions"
|
filterable multiple tag style="width: 80%;" :options="mailAllowOptions"
|
||||||
:placeholder="t('mailAllowList')">
|
:placeholder="t('mailAllowList')" />
|
||||||
<template #empty>
|
|
||||||
<n-text depth="3">
|
|
||||||
{{ t('manualInputPrompt') }}
|
|
||||||
</n-text>
|
|
||||||
</template>
|
|
||||||
</n-select>
|
|
||||||
</n-input-group>
|
</n-input-group>
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('maxAddressCount')">
|
<n-form-item-row :label="t('maxAddressCount')">
|
||||||
@@ -125,6 +112,9 @@ onMounted(async () => {
|
|||||||
:placeholder="t('maxAddressCount')" />
|
:placeholder="t('maxAddressCount')" />
|
||||||
</n-input-group>
|
</n-input-group>
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
|
<n-button @click="save" type="primary" block :loading="loading">
|
||||||
|
{{ t('save') }}
|
||||||
|
</n-button>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-card>
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,17 +13,13 @@ const { t } = useI18n({
|
|||||||
messages: {
|
messages: {
|
||||||
en: {
|
en: {
|
||||||
successTip: 'Success',
|
successTip: 'Success',
|
||||||
enableAllowList: 'Enable Allow List (Restrict webhook access to specific users)',
|
webhookAllowList: 'Webhook Allow List(Enter the address that is allowed to use webhook and enter)',
|
||||||
webhookAllowList: 'Webhook Allow List(Enter the mail address that is allowed to use webhook and enter)',
|
|
||||||
manualInputPrompt: 'Type and press Enter to add',
|
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
notEnabled: 'Webhook is not enabled',
|
notEnabled: 'Webhook is not enabled',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
successTip: '成功',
|
successTip: '成功',
|
||||||
enableAllowList: '启用白名单 (限制 webhook 访问权限,只有白名单中的用户可以使用)',
|
webhookAllowList: 'Webhook 白名单(请输入允许使用webhook 的地址, 回车增加)',
|
||||||
webhookAllowList: 'Webhook 白名单(请输入允许使用webhook 的邮箱地址, 回车增加)',
|
|
||||||
manualInputPrompt: '输入后按回车键添加',
|
|
||||||
save: '保存',
|
save: '保存',
|
||||||
notEnabled: 'Webhook 未开启',
|
notEnabled: 'Webhook 未开启',
|
||||||
}
|
}
|
||||||
@@ -31,16 +27,14 @@ const { t } = useI18n({
|
|||||||
});
|
});
|
||||||
|
|
||||||
class WebhookSettings {
|
class WebhookSettings {
|
||||||
enableAllowList: boolean;
|
|
||||||
allowList: string[];
|
allowList: string[];
|
||||||
|
|
||||||
constructor(enableAllowList: boolean, allowList: string[]) {
|
constructor(allowList: string[]) {
|
||||||
this.enableAllowList = enableAllowList;
|
|
||||||
this.allowList = allowList;
|
this.allowList = allowList;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const webhookSettings = ref(new WebhookSettings(false, []))
|
const webhookSettings = ref(new WebhookSettings([]))
|
||||||
const webhookEnabled = ref(false)
|
const webhookEnabled = ref(false)
|
||||||
const errorInfo = ref('')
|
const errorInfo = ref('')
|
||||||
|
|
||||||
@@ -74,24 +68,13 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card v-if="webhookEnabled" :bordered="false" embedded style="max-width: 800px; overflow: auto;">
|
<n-card v-if="webhookEnabled" :bordered="false" embedded style="max-width: 800px; overflow: auto;">
|
||||||
<n-flex justify="end">
|
|
||||||
<n-button @click="saveSettings" type="primary">
|
|
||||||
{{ t('save') }}
|
|
||||||
</n-button>
|
|
||||||
</n-flex>
|
|
||||||
<n-form-item-row :label="t('enableAllowList')">
|
|
||||||
<n-switch v-model:value="webhookSettings.enableAllowList" :round="false" />
|
|
||||||
</n-form-item-row>
|
|
||||||
<n-form-item-row :label="t('webhookAllowList')">
|
<n-form-item-row :label="t('webhookAllowList')">
|
||||||
<n-select v-model:value="webhookSettings.allowList" filterable multiple tag
|
<n-select v-model:value="webhookSettings.allowList" filterable multiple tag
|
||||||
:placeholder="t('webhookAllowList')">
|
:placeholder="t('webhookAllowList')" />
|
||||||
<template #empty>
|
|
||||||
<n-text depth="3">
|
|
||||||
{{ t('manualInputPrompt') }}
|
|
||||||
</n-text>
|
|
||||||
</template>
|
|
||||||
</n-select>
|
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
|
<n-button @click="saveSettings" type="primary" block>
|
||||||
|
{{ t('save') }}
|
||||||
|
</n-button>
|
||||||
</n-card>
|
</n-card>
|
||||||
<n-result v-else status="404" :title="t('notEnabled')" :description="errorInfo" />
|
<n-result v-else status="404" :title="t('notEnabled')" :description="errorInfo" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card :bordered="false" embedded style="max-width: 800px; overflow: auto;">
|
<n-card :bordered="false" embedded style="max-width: 600px; overflow: auto;">
|
||||||
<pre>{{ JSON.stringify(settings, null, 2) }}</pre>
|
<pre>{{ JSON.stringify(settings, null, 2) }}</pre>
|
||||||
</n-card>
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import Turnstile from '../../components/Turnstile.vue'
|
|||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
import { useGlobalState } from '../../store'
|
||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
import { getRouterPathWithLang, hashPassword } from '../../utils'
|
import { getRouterPathWithLang } from '../../utils'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
bindUserAddress: {
|
bindUserAddress: {
|
||||||
@@ -39,7 +39,7 @@ const router = useRouter()
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
jwt, loading, openSettings,
|
jwt, loading, openSettings,
|
||||||
showAddressCredential, userSettings, addressPassword
|
showAddressCredential, userSettings
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
|
|
||||||
const tabValue = ref('signin')
|
const tabValue = ref('signin')
|
||||||
@@ -47,47 +47,8 @@ const credential = ref('')
|
|||||||
const emailName = ref("")
|
const emailName = ref("")
|
||||||
const emailDomain = ref("")
|
const emailDomain = ref("")
|
||||||
const cfToken = ref("")
|
const cfToken = ref("")
|
||||||
const loginMethod = ref('credential') // 'credential' or 'password'
|
|
||||||
const loginAddress = ref('')
|
|
||||||
const loginPassword = ref('')
|
|
||||||
|
|
||||||
// 根据 openSettings 初始化登录方式
|
|
||||||
const initLoginMethod = () => {
|
|
||||||
if (openSettings.value?.enableAddressPassword) {
|
|
||||||
loginMethod.value = 'password';
|
|
||||||
} else {
|
|
||||||
loginMethod.value = 'credential';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
if (loginMethod.value === 'password') {
|
|
||||||
// Password login
|
|
||||||
if (!loginAddress.value || !loginPassword.value) {
|
|
||||||
message.error(t('emailPasswordRequired'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await api.fetch('/api/address_login', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: loginAddress.value,
|
|
||||||
password: await hashPassword(loginPassword.value)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
jwt.value = res.jwt;
|
|
||||||
await api.getSettings();
|
|
||||||
try {
|
|
||||||
await props.bindUserAddress();
|
|
||||||
} catch (error) {
|
|
||||||
message.error(`${t('bindUserAddressError')}: ${error.message}`);
|
|
||||||
}
|
|
||||||
await router.push(getRouterPathWithLang("/", locale.value));
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!credential.value) {
|
if (!credential.value) {
|
||||||
message.error(t('credentialInput'));
|
message.error(t('credentialInput'));
|
||||||
return;
|
return;
|
||||||
@@ -124,11 +85,6 @@ const { locale, t } = useI18n({
|
|||||||
bindUserInfo: 'Logged in user, login without binding email or create new email address will bind to current user',
|
bindUserInfo: 'Logged in user, login without binding email or create new email address will bind to current user',
|
||||||
bindUserAddressError: 'Error when bind email address to user',
|
bindUserAddressError: 'Error when bind email address to user',
|
||||||
autoGeneratedName: 'Auto-generated name',
|
autoGeneratedName: 'Auto-generated name',
|
||||||
passwordLogin: 'Password Login',
|
|
||||||
credentialLogin: 'Credential Login',
|
|
||||||
email: 'Email',
|
|
||||||
password: 'Password',
|
|
||||||
emailPasswordRequired: 'Email and password are required',
|
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
login: '登录',
|
login: '登录',
|
||||||
@@ -146,11 +102,6 @@ const { locale, t } = useI18n({
|
|||||||
bindUserInfo: '已登录用户, 登录未绑定邮箱或创建新邮箱地址将绑定到当前用户',
|
bindUserInfo: '已登录用户, 登录未绑定邮箱或创建新邮箱地址将绑定到当前用户',
|
||||||
bindUserAddressError: '绑定邮箱地址到用户时错误',
|
bindUserAddressError: '绑定邮箱地址到用户时错误',
|
||||||
autoGeneratedName: '自动生成名称',
|
autoGeneratedName: '自动生成名称',
|
||||||
passwordLogin: '密码登录',
|
|
||||||
credentialLogin: '凭据登录',
|
|
||||||
email: '邮箱',
|
|
||||||
password: '密码',
|
|
||||||
emailPasswordRequired: '邮箱和密码不能为空',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -206,7 +157,6 @@ const newEmail = async () => {
|
|||||||
cfToken.value
|
cfToken.value
|
||||||
);
|
);
|
||||||
jwt.value = res["jwt"];
|
jwt.value = res["jwt"];
|
||||||
addressPassword.value = res["password"] || '';
|
|
||||||
await api.getSettings();
|
await api.getSettings();
|
||||||
await router.push(getRouterPathWithLang("/", locale.value));
|
await router.push(getRouterPathWithLang("/", locale.value));
|
||||||
showAddressCredential.value = true;
|
showAddressCredential.value = true;
|
||||||
@@ -262,7 +212,6 @@ onMounted(async () => {
|
|||||||
await api.getOpenSettings(message, notification);
|
await api.getOpenSettings(message, notification);
|
||||||
}
|
}
|
||||||
emailDomain.value = domainsOptions.value ? domainsOptions.value[0]?.value : "";
|
emailDomain.value = domainsOptions.value ? domainsOptions.value[0]?.value : "";
|
||||||
initLoginMethod();
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -274,29 +223,9 @@ onMounted(async () => {
|
|||||||
<n-tabs v-if="openSettings.fetched" v-model:value="tabValue" size="large" justify-content="space-evenly">
|
<n-tabs v-if="openSettings.fetched" v-model:value="tabValue" size="large" justify-content="space-evenly">
|
||||||
<n-tab-pane name="signin" :tab="loginAndBindTag">
|
<n-tab-pane name="signin" :tab="loginAndBindTag">
|
||||||
<n-form>
|
<n-form>
|
||||||
<div v-if="loginMethod === 'password'">
|
|
||||||
<n-form-item-row :label="t('email')" required>
|
|
||||||
<n-input v-model:value="loginAddress" />
|
|
||||||
</n-form-item-row>
|
|
||||||
<n-form-item-row :label="t('password')" required>
|
|
||||||
<n-input v-model:value="loginPassword" type="password" show-password-on="click" />
|
|
||||||
</n-form-item-row>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else>
|
|
||||||
<n-form-item-row :label="t('credential')" required>
|
<n-form-item-row :label="t('credential')" required>
|
||||||
<n-input v-model:value="credential" type="textarea" :autosize="{ minRows: 3 }" />
|
<n-input v-model:value="credential" type="textarea" :autosize="{ minRows: 3 }" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="switch-login-button">
|
|
||||||
<n-button v-if="openSettings?.enableAddressPassword"
|
|
||||||
@click="loginMethod === 'password' ? loginMethod = 'credential' : loginMethod = 'password'"
|
|
||||||
type="info" quaternary size="tiny">
|
|
||||||
{{ loginMethod === 'password' ? t('credentialLogin') : t('passwordLogin') }}
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<n-button @click="login" :loading="loading" type="primary" block secondary strong>
|
<n-button @click="login" :loading="loading" type="primary" block secondary strong>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon :component="EmailOutlined" />
|
<n-icon :component="EmailOutlined" />
|
||||||
@@ -315,21 +244,19 @@ onMounted(async () => {
|
|||||||
<n-spin :show="generateNameLoading">
|
<n-spin :show="generateNameLoading">
|
||||||
<n-form>
|
<n-form>
|
||||||
<span>
|
<span>
|
||||||
<p v-if="!openSettings.disableCustomAddressName">{{ t("getNewEmailTip1") +
|
<p v-if="!openSettings.disableCustomAddressName">{{ t("getNewEmailTip1") + addressRegex.source }}</p>
|
||||||
addressRegex.source }}</p>
|
|
||||||
<p v-if="!openSettings.disableCustomAddressName">{{ t("getNewEmailTip2") }}</p>
|
<p v-if="!openSettings.disableCustomAddressName">{{ t("getNewEmailTip2") }}</p>
|
||||||
<p>{{ t("getNewEmailTip3") }}</p>
|
<p>{{ t("getNewEmailTip3") }}</p>
|
||||||
</span>
|
</span>
|
||||||
<n-button v-if="!openSettings.disableCustomAddressName" @click="generateName"
|
<n-button v-if="!openSettings.disableCustomAddressName" @click="generateName" style="margin-bottom: 10px;">
|
||||||
style="margin-bottom: 10px;">
|
|
||||||
{{ t('generateName') }}
|
{{ t('generateName') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
<n-input-group-label v-if="addressPrefix">
|
<n-input-group-label v-if="addressPrefix">
|
||||||
{{ addressPrefix }}
|
{{ addressPrefix }}
|
||||||
</n-input-group-label>
|
</n-input-group-label>
|
||||||
<n-input v-if="!openSettings.disableCustomAddressName" v-model:value="emailName" show-count
|
<n-input v-if="!openSettings.disableCustomAddressName" v-model:value="emailName" show-count :minlength="openSettings.minAddressLen"
|
||||||
:minlength="openSettings.minAddressLen" :maxlength="openSettings.maxAddressLen" />
|
:maxlength="openSettings.maxAddressLen" />
|
||||||
<n-input v-else :value="t('autoGeneratedName')" disabled />
|
<n-input v-else :value="t('autoGeneratedName')" disabled />
|
||||||
<n-input-group-label>@</n-input-group-label>
|
<n-input-group-label>@</n-input-group-label>
|
||||||
<n-select v-model:value="emailDomain" :consistent-menu-width="false"
|
<n-select v-model:value="emailDomain" :consistent-menu-width="false"
|
||||||
@@ -367,12 +294,6 @@ onMounted(async () => {
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch-login-button {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.n-form {
|
.n-form {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,22 +5,16 @@ import { useRouter } from 'vue-router'
|
|||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
import { useGlobalState } from '../../store'
|
||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
import { hashPassword } from '../../utils'
|
|
||||||
import { getRouterPathWithLang } from '../../utils'
|
import { getRouterPathWithLang } from '../../utils'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
jwt, settings, showAddressCredential, loading, openSettings
|
jwt, settings, showAddressCredential, loading
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
const showLogout = ref(false)
|
const showLogout = ref(false)
|
||||||
const showDeleteAccount = ref(false)
|
const showDeleteAccount = ref(false)
|
||||||
const showClearInbox = ref(false)
|
|
||||||
const showClearSentItems = ref(false)
|
|
||||||
const showChangePassword = ref(false)
|
|
||||||
const newPassword = ref('')
|
|
||||||
const confirmPassword = ref('')
|
|
||||||
const { locale, t } = useI18n({
|
const { locale, t } = useI18n({
|
||||||
messages: {
|
messages: {
|
||||||
en: {
|
en: {
|
||||||
@@ -30,16 +24,6 @@ const { locale, t } = useI18n({
|
|||||||
logoutConfirm: 'Are you sure to logout?',
|
logoutConfirm: 'Are you sure to logout?',
|
||||||
deleteAccount: "Delete Account",
|
deleteAccount: "Delete Account",
|
||||||
deleteAccountConfirm: "Are you sure to delete your account and all emails for this account?",
|
deleteAccountConfirm: "Are you sure to delete your account and all emails for this account?",
|
||||||
clearInbox: "Clear Inbox",
|
|
||||||
clearSentItems: "Clear Sent Items",
|
|
||||||
clearInboxConfirm: "Are you sure to clear all emails in your inbox?",
|
|
||||||
clearSentItemsConfirm: "Are you sure to clear all emails in your sent items?",
|
|
||||||
success: "Success",
|
|
||||||
changePassword: "Change Password",
|
|
||||||
newPassword: "New Password",
|
|
||||||
confirmPassword: "Confirm Password",
|
|
||||||
passwordMismatch: "Passwords do not match",
|
|
||||||
passwordChanged: "Password changed successfully",
|
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
logout: '退出登录',
|
logout: '退出登录',
|
||||||
@@ -48,16 +32,6 @@ const { locale, t } = useI18n({
|
|||||||
logoutConfirm: '确定要退出登录吗?',
|
logoutConfirm: '确定要退出登录吗?',
|
||||||
deleteAccount: "删除账户",
|
deleteAccount: "删除账户",
|
||||||
deleteAccountConfirm: "确定要删除你的账户和其中的所有邮件吗?",
|
deleteAccountConfirm: "确定要删除你的账户和其中的所有邮件吗?",
|
||||||
clearInbox: "清空收件箱",
|
|
||||||
clearSentItems: "清空发件箱",
|
|
||||||
clearInboxConfirm: "确定要清空你收件箱中的所有邮件吗?",
|
|
||||||
clearSentItemsConfirm: "确定要清空你发件箱中的所有邮件吗?",
|
|
||||||
success: "成功",
|
|
||||||
changePassword: "修改密码",
|
|
||||||
newPassword: "新密码",
|
|
||||||
confirmPassword: "确认密码",
|
|
||||||
passwordMismatch: "密码不匹配",
|
|
||||||
passwordChanged: "密码修改成功",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -80,53 +54,6 @@ const deleteAccount = async () => {
|
|||||||
message.error(error.message || "error");
|
message.error(error.message || "error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearInbox = async () => {
|
|
||||||
try {
|
|
||||||
await api.fetch(`/api/clear_inbox`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
message.success(t("success"));
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
} finally {
|
|
||||||
showClearInbox.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearSentItems = async () => {
|
|
||||||
try {
|
|
||||||
await api.fetch(`/api/clear_sent_items`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
message.success(t("success"));
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
} finally {
|
|
||||||
showClearSentItems.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const changePassword = async () => {
|
|
||||||
if (newPassword.value !== confirmPassword.value) {
|
|
||||||
message.error(t("passwordMismatch"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await api.fetch(`/api/address_change_password`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
new_password: await hashPassword(newPassword.value)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
message.success(t("passwordChanged"));
|
|
||||||
newPassword.value = '';
|
|
||||||
confirmPassword.value = '';
|
|
||||||
showChangePassword.value = false;
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -135,22 +62,10 @@ const changePassword = async () => {
|
|||||||
<n-button @click="showAddressCredential = true" type="primary" secondary block strong>
|
<n-button @click="showAddressCredential = true" type="primary" secondary block strong>
|
||||||
{{ t('showAddressCredential') }}
|
{{ t('showAddressCredential') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button v-if="openSettings?.enableAddressPassword" @click="showChangePassword = true" type="info" secondary block strong>
|
|
||||||
{{ t('changePassword') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button v-if="openSettings.enableUserDeleteEmail" @click="showClearInbox = true" type="warning" secondary
|
|
||||||
block strong>
|
|
||||||
{{ t('clearInbox') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button v-if="openSettings.enableUserDeleteEmail" @click="showClearSentItems = true" type="warning"
|
|
||||||
secondary block strong>
|
|
||||||
{{ t('clearSentItems') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button @click="showLogout = true" secondary block strong>
|
<n-button @click="showLogout = true" secondary block strong>
|
||||||
{{ t('logout') }}
|
{{ t('logout') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button v-if="openSettings.enableUserDeleteEmail" @click="showDeleteAccount = true" type="error" secondary
|
<n-button @click="showDeleteAccount = true" type="error" secondary block strong>
|
||||||
block strong>
|
|
||||||
{{ t('deleteAccount') }}
|
{{ t('deleteAccount') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-card>
|
</n-card>
|
||||||
@@ -170,38 +85,6 @@ const changePassword = async () => {
|
|||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
<n-modal v-model:show="showClearInbox" preset="dialog" :title="t('clearInbox')">
|
|
||||||
<p>{{ t('clearInboxConfirm') }}</p>
|
|
||||||
<template #action>
|
|
||||||
<n-button :loading="loading" @click="clearInbox" size="small" tertiary type="warning">
|
|
||||||
{{ t('clearInbox') }}
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
<n-modal v-model:show="showClearSentItems" preset="dialog" :title="t('clearSentItems')">
|
|
||||||
<p>{{ t('clearSentItemsConfirm') }}</p>
|
|
||||||
<template #action>
|
|
||||||
<n-button :loading="loading" @click="clearSentItems" size="small" tertiary type="warning">
|
|
||||||
{{ t('clearSentItems') }}
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
|
|
||||||
<n-modal v-model:show="showChangePassword" preset="dialog" :title="t('changePassword')">
|
|
||||||
<n-form :model="{ newPassword, confirmPassword }">
|
|
||||||
<n-form-item :label="t('newPassword')">
|
|
||||||
<n-input v-model:value="newPassword" type="password" placeholder="" show-password-on="click" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item :label="t('confirmPassword')">
|
|
||||||
<n-input v-model:value="confirmPassword" type="password" placeholder="" show-password-on="click" />
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
<template #action>
|
|
||||||
<n-button :loading="loading" @click="changePassword" size="small" tertiary type="info">
|
|
||||||
{{ t('changePassword') }}
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const router = useRouter()
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
jwt, settings, showAddressCredential, userJwt,
|
jwt, settings, showAddressCredential, userJwt,
|
||||||
isTelegram, openSettings, addressPassword
|
isTelegram, openSettings
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
|
|
||||||
const { locale, t } = useI18n({
|
const { locale, t } = useI18n({
|
||||||
@@ -34,7 +34,6 @@ const { locale, t } = useI18n({
|
|||||||
addressCredential: 'Mail Address Credential',
|
addressCredential: 'Mail Address Credential',
|
||||||
linkWithAddressCredential: 'Open to auto login email link',
|
linkWithAddressCredential: 'Open to auto login email link',
|
||||||
addressCredentialTip: 'Please copy the Mail Address Credential and you can use it to login to your email account.',
|
addressCredentialTip: 'Please copy the Mail Address Credential and you can use it to login to your email account.',
|
||||||
addressPassword: 'Address Password',
|
|
||||||
userLogin: 'User Login',
|
userLogin: 'User Login',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
@@ -47,7 +46,6 @@ const { locale, t } = useI18n({
|
|||||||
addressCredential: '邮箱地址凭证',
|
addressCredential: '邮箱地址凭证',
|
||||||
linkWithAddressCredential: '打开即可自动登录邮箱的链接',
|
linkWithAddressCredential: '打开即可自动登录邮箱的链接',
|
||||||
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
||||||
addressPassword: '地址密码',
|
|
||||||
userLogin: '用户登录',
|
userLogin: '用户登录',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,10 +149,6 @@ onMounted(async () => {
|
|||||||
<n-card embedded>
|
<n-card embedded>
|
||||||
<b>{{ jwt }}</b>
|
<b>{{ jwt }}</b>
|
||||||
</n-card>
|
</n-card>
|
||||||
<n-card embedded v-if="addressPassword">
|
|
||||||
<p><b>{{ settings.address }}</b></p>
|
|
||||||
<p>{{ t('addressPassword') }}: <b>{{ addressPassword }}</b></p>
|
|
||||||
</n-card>
|
|
||||||
<n-card embedded>
|
<n-card embedded>
|
||||||
<n-collapse>
|
<n-collapse>
|
||||||
<n-collapse-item :title='t("linkWithAddressCredential")'>
|
<n-collapse-item :title='t("linkWithAddressCredential")'>
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "temp-email-pages",
|
"name": "temp-email-pages",
|
||||||
"version": "1.0.6",
|
"version": "1.0.5",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"wrangler": "^4.42.2"
|
"wrangler": "^4.33.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,6 @@
|
|||||||
| `DOMAIN_LABELS` | JSON | 对于中文域名,可以使用 DOMAIN_LABELS 显示域名的中文展示名称 | `["中文.awsl.uk", "dreamhunter2333.xyz"]` |
|
| `DOMAIN_LABELS` | JSON | 对于中文域名,可以使用 DOMAIN_LABELS 显示域名的中文展示名称 | `["中文.awsl.uk", "dreamhunter2333.xyz"]` |
|
||||||
| `ENABLE_AUTO_REPLY` | 文本/JSON | 允许自动回复邮件 | `true` |
|
| `ENABLE_AUTO_REPLY` | 文本/JSON | 允许自动回复邮件 | `true` |
|
||||||
| `DEFAULT_SEND_BALANCE` | 文本/JSON | 默认发送邮件余额,如果不设置,将为 0 | `1` |
|
| `DEFAULT_SEND_BALANCE` | 文本/JSON | 默认发送邮件余额,如果不设置,将为 0 | `1` |
|
||||||
| `ENABLE_ADDRESS_PASSWORD` | 文本/JSON | 启用邮箱地址密码功能,启用后创建新地址时会自动生成密码,并支持密码登录和修改 | `true` |
|
|
||||||
|
|
||||||
## 接受邮件相关变量
|
## 接受邮件相关变量
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "temp-mail-docs",
|
"name": "temp-mail-docs",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.6",
|
"version": "1.0.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.7.2",
|
"@types/node": "^24.3.0",
|
||||||
"vitepress": "^1.6.4",
|
"vitepress": "^1.6.4",
|
||||||
"wrangler": "^4.42.2"
|
"wrangler": "^4.33.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vitepress dev docs",
|
"dev": "vitepress dev docs",
|
||||||
|
|||||||
Generated
+392
-410
File diff suppressed because it is too large
Load Diff
+9
-9
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cloudflare_temp_email",
|
"name": "cloudflare_temp_email",
|
||||||
"version": "1.0.6",
|
"version": "1.0.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -11,23 +11,23 @@
|
|||||||
"build": "wrangler deploy --dry-run --outdir dist --minify"
|
"build": "wrangler deploy --dry-run --outdir dist --minify"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "^4.20251011.0",
|
"@cloudflare/workers-types": "^4.20250826.0",
|
||||||
"@eslint/js": "9.18.0",
|
"@eslint/js": "9.18.0",
|
||||||
"@simplewebauthn/types": "10.0.0",
|
"@simplewebauthn/types": "10.0.0",
|
||||||
"@types/node": "^22.18.10",
|
"@types/node": "^22.18.0",
|
||||||
"eslint": "9.18.0",
|
"eslint": "9.18.0",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"typescript-eslint": "^8.46.0",
|
"typescript-eslint": "^8.41.0",
|
||||||
"wrangler": "^4.42.2"
|
"wrangler": "^4.33.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.888.0",
|
"@aws-sdk/client-s3": "^3.876.0",
|
||||||
"@aws-sdk/s3-request-presigner": "3.888.0",
|
"@aws-sdk/s3-request-presigner": "^3.876.0",
|
||||||
"@simplewebauthn/server": "10.0.1",
|
"@simplewebauthn/server": "10.0.1",
|
||||||
"hono": "^4.9.11",
|
"hono": "^4.9.4",
|
||||||
"jsonpath-plus": "^10.3.0",
|
"jsonpath-plus": "^10.3.0",
|
||||||
"mimetext": "^3.0.27",
|
"mimetext": "^3.0.27",
|
||||||
"postal-mime": "^2.5.0",
|
"postal-mime": "^2.4.4",
|
||||||
"resend": "^4.8.0",
|
"resend": "^4.8.0",
|
||||||
"telegraf": "4.16.3",
|
"telegraf": "4.16.3",
|
||||||
"worker-mailer": "^1.1.5"
|
"worker-mailer": "^1.1.5"
|
||||||
|
|||||||
Generated
+891
-897
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@ import { Context } from 'hono';
|
|||||||
|
|
||||||
import { CONSTANTS } from '../constants';
|
import { CONSTANTS } from '../constants';
|
||||||
import { getJsonSetting, saveSetting, checkUserPassword, getDomains, getUserRoles } from '../utils';
|
import { getJsonSetting, saveSetting, checkUserPassword, getDomains, getUserRoles } from '../utils';
|
||||||
import { UserSettings, GeoData, UserInfo, RoleAddressConfig } from "../models";
|
import { UserSettings, GeoData, UserInfo } from "../models";
|
||||||
import { handleListQuery } from '../common'
|
import { handleListQuery } from '../common'
|
||||||
import UserBindAddressModule from '../user_api/bind_address';
|
import UserBindAddressModule from '../user_api/bind_address';
|
||||||
import i18n from '../i18n';
|
import i18n from '../i18n';
|
||||||
@@ -166,14 +166,4 @@ export default {
|
|||||||
results: results,
|
results: results,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getRoleAddressConfig: async (c: Context<HonoCustomType>) => {
|
|
||||||
const value = await getJsonSetting<RoleAddressConfig>(c, CONSTANTS.ROLE_ADDRESS_CONFIG_KEY);
|
|
||||||
const configs = value || {};
|
|
||||||
return c.json({ configs });
|
|
||||||
},
|
|
||||||
saveRoleAddressConfig: async (c: Context<HonoCustomType>) => {
|
|
||||||
const { configs } = await c.req.json<{ configs: RoleAddressConfig }>();
|
|
||||||
await saveSetting(c, CONSTANTS.ROLE_ADDRESS_CONFIG_KEY, JSON.stringify(configs));
|
|
||||||
return c.json({ success: true });
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,22 +14,15 @@ CREATE TABLE IF NOT EXISTS raw_mails (
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_raw_mails_address ON raw_mails(address);
|
CREATE INDEX IF NOT EXISTS idx_raw_mails_address ON raw_mails(address);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_raw_mails_created_at ON raw_mails(created_at);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS address (
|
CREATE TABLE IF NOT EXISTS address (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT UNIQUE,
|
name TEXT UNIQUE,
|
||||||
password TEXT,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_address_name ON address(name);
|
CREATE INDEX IF NOT EXISTS idx_address_name ON address(name);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_address_created_at ON address(created_at);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_address_updated_at ON address(updated_at);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS auto_reply_mails (
|
CREATE TABLE IF NOT EXISTS auto_reply_mails (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
source_prefix TEXT,
|
source_prefix TEXT,
|
||||||
@@ -61,7 +54,6 @@ CREATE TABLE IF NOT EXISTS sendbox (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sendbox_address ON sendbox(address);
|
CREATE INDEX IF NOT EXISTS idx_sendbox_address ON sendbox(address);
|
||||||
CREATE INDEX IF NOT EXISTS idx_sendbox_created_at ON sendbox(created_at);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
@@ -137,20 +129,9 @@ export default {
|
|||||||
},
|
},
|
||||||
migrate: async (c: Context<HonoCustomType>) => {
|
migrate: async (c: Context<HonoCustomType>) => {
|
||||||
const version = await utils.getSetting(c, CONSTANTS.DB_VERSION_KEY);
|
const version = await utils.getSetting(c, CONSTANTS.DB_VERSION_KEY);
|
||||||
if (version == "v0.0.2") {
|
|
||||||
// example migration from v0.0.2 to v0.0.3
|
|
||||||
const query = `ALTER TABLE address ADD password TEXT;`
|
|
||||||
await c.env.DB.exec(query);
|
|
||||||
}
|
|
||||||
if (version != CONSTANTS.DB_VERSION) {
|
if (version != CONSTANTS.DB_VERSION) {
|
||||||
// TODO: Perform migration logic here
|
// TODO: Perform migration logic here
|
||||||
// remove all \r and \n characters from the query string
|
|
||||||
// split by ; and join with a ;\n
|
|
||||||
const query = DB_INIT_QUERIES.replace(/[\r\n]/g, "")
|
|
||||||
.split(";")
|
|
||||||
.map((query) => query.trim())
|
|
||||||
.join(";\n");
|
|
||||||
await c.env.DB.exec(query);
|
|
||||||
// Update the version in the settings table
|
// Update the version in the settings table
|
||||||
await utils.saveSetting(c, CONSTANTS.DB_VERSION_KEY, CONSTANTS.DB_VERSION);
|
await utils.saveSetting(c, CONSTANTS.DB_VERSION_KEY, CONSTANTS.DB_VERSION);
|
||||||
return c.json({
|
return c.json({
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Hono } from 'hono'
|
|||||||
import { Jwt } from 'hono/utils/jwt'
|
import { Jwt } from 'hono/utils/jwt'
|
||||||
|
|
||||||
import i18n from '../i18n'
|
import i18n from '../i18n'
|
||||||
import { sendAdminInternalMail, getJsonSetting, saveSetting, getUserRoles, getBooleanValue, hashPassword } from '../utils'
|
import { sendAdminInternalMail, getJsonSetting, saveSetting, getUserRoles } from '../utils'
|
||||||
import { newAddress, handleListQuery } from '../common'
|
import { newAddress, handleListQuery } from '../common'
|
||||||
import { CONSTANTS } from '../constants'
|
import { CONSTANTS } from '../constants'
|
||||||
import cleanup_api from './cleanup_api'
|
import cleanup_api from './cleanup_api'
|
||||||
@@ -56,7 +56,6 @@ api.post('/admin/new_address', async (c) => {
|
|||||||
checkAllowDomains: false,
|
checkAllowDomains: false,
|
||||||
enableCheckNameRegex: false,
|
enableCheckNameRegex: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
return c.json(res);
|
return c.json(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return c.text(`${msgs.FailedCreateAddressMsg}: ${(e as Error).message}`, 400)
|
return c.text(`${msgs.FailedCreateAddressMsg}: ${(e as Error).message}`, 400)
|
||||||
@@ -90,34 +89,6 @@ api.delete('/admin/delete_address/:id', async (c) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
api.delete('/admin/clear_inbox/:id', async (c) => {
|
|
||||||
const { id } = c.req.param();
|
|
||||||
const { success: mailSuccess } = await c.env.DB.prepare(
|
|
||||||
`DELETE FROM raw_mails WHERE address IN`
|
|
||||||
+ ` (select name from address where id = ?) `
|
|
||||||
).bind(id).run();
|
|
||||||
if (!mailSuccess) {
|
|
||||||
return c.text("Failed to clear inbox", 500)
|
|
||||||
}
|
|
||||||
return c.json({
|
|
||||||
success: mailSuccess
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
api.delete('/admin/clear_sent_items/:id', async (c) => {
|
|
||||||
const { id } = c.req.param();
|
|
||||||
const { success: sendboxSuccess } = await c.env.DB.prepare(
|
|
||||||
`DELETE FROM sendbox WHERE address IN`
|
|
||||||
+ ` (select name from address where id = ?) `
|
|
||||||
).bind(id).run();
|
|
||||||
if (!sendboxSuccess) {
|
|
||||||
return c.text("Failed to clear sent items", 500)
|
|
||||||
}
|
|
||||||
return c.json({
|
|
||||||
success: sendboxSuccess
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
api.get('/admin/show_password/:id', async (c) => {
|
api.get('/admin/show_password/:id', async (c) => {
|
||||||
const { id } = c.req.param();
|
const { id } = c.req.param();
|
||||||
const name = await c.env.DB.prepare(
|
const name = await c.env.DB.prepare(
|
||||||
@@ -132,30 +103,6 @@ api.get('/admin/show_password/:id', async (c) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
api.post('/admin/address/:id/reset_password', async (c) => {
|
|
||||||
const { id } = c.req.param();
|
|
||||||
const { password } = await c.req.json();
|
|
||||||
// 检查功能是否启用
|
|
||||||
if (!getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)) {
|
|
||||||
return c.text("Password management is disabled", 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!password) {
|
|
||||||
return c.text("Password is required", 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
const hashedPassword = await hashPassword(password);
|
|
||||||
const { success } = await c.env.DB.prepare(
|
|
||||||
`UPDATE address SET password = ?, updated_at = datetime('now') WHERE id = ?`
|
|
||||||
).bind(hashedPassword, id).run();
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
return c.text("Failed to reset password", 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json({ success: true });
|
|
||||||
})
|
|
||||||
|
|
||||||
// mail api
|
// mail api
|
||||||
api.get('/admin/mails', admin_mail_api.getMails);
|
api.get('/admin/mails', admin_mail_api.getMails);
|
||||||
api.get('/admin/mails_unknow', admin_mail_api.getUnknowMails);
|
api.get('/admin/mails_unknow', admin_mail_api.getUnknowMails);
|
||||||
@@ -344,8 +291,6 @@ api.post('/admin/users', admin_user_api.createUser)
|
|||||||
api.post('/admin/users/:user_id/reset_password', admin_user_api.resetPassword)
|
api.post('/admin/users/:user_id/reset_password', admin_user_api.resetPassword)
|
||||||
api.get('/admin/user_roles', async (c) => c.json(getUserRoles(c)))
|
api.get('/admin/user_roles', async (c) => c.json(getUserRoles(c)))
|
||||||
api.post('/admin/user_roles', admin_user_api.updateUserRoles)
|
api.post('/admin/user_roles', admin_user_api.updateUserRoles)
|
||||||
api.get('/admin/role_address_config', admin_user_api.getRoleAddressConfig)
|
|
||||||
api.post('/admin/role_address_config', admin_user_api.saveRoleAddressConfig)
|
|
||||||
api.get('/admin/users/bind_address/:user_id', admin_user_api.getBindedAddresses)
|
api.get('/admin/users/bind_address/:user_id', admin_user_api.getBindedAddresses)
|
||||||
api.post('/admin/users/bind_address', admin_user_api.bindAddress)
|
api.post('/admin/users/bind_address', admin_user_api.bindAddress)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { AdminWebhookSettings } from "../models";
|
|||||||
|
|
||||||
async function getWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
async function getWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||||
const settings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
|
const settings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
|
||||||
return c.json(settings || new AdminWebhookSettings(false, []));
|
return c.json(settings || new AdminWebhookSettings([]));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
async function saveWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||||
|
|||||||
@@ -40,8 +40,7 @@ api.get('/open_api/settings', async (c) => {
|
|||||||
"isS3Enabled": isS3Enabled(c),
|
"isS3Enabled": isS3Enabled(c),
|
||||||
"version": CONSTANTS.VERSION,
|
"version": CONSTANTS.VERSION,
|
||||||
"showGithub": !utils.getBooleanValue(c.env.DISABLE_SHOW_GITHUB),
|
"showGithub": !utils.getBooleanValue(c.env.DISABLE_SHOW_GITHUB),
|
||||||
"disableAdminPasswordCheck": utils.getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK),
|
"disableAdminPasswordCheck": utils.getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK)
|
||||||
"enableAddressPassword": utils.getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+6
-46
@@ -1,7 +1,7 @@
|
|||||||
import { Context } from 'hono';
|
import { Context } from 'hono';
|
||||||
import { Jwt } from 'hono/utils/jwt'
|
import { Jwt } from 'hono/utils/jwt'
|
||||||
|
|
||||||
import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting, getAnotherWorkerList, hashPassword } from './utils';
|
import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting, getAnotherWorkerList } from './utils';
|
||||||
import { unbindTelegramByAddress } from './telegram_api/common';
|
import { unbindTelegramByAddress } from './telegram_api/common';
|
||||||
import { CONSTANTS } from './constants';
|
import { CONSTANTS } from './constants';
|
||||||
import { AdminWebhookSettings, WebhookMail, WebhookSettings } from './models';
|
import { AdminWebhookSettings, WebhookMail, WebhookSettings } from './models';
|
||||||
@@ -82,37 +82,6 @@ export async function updateAddressUpdatedAt(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateRandomPassword = (): string => {
|
|
||||||
const charset = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
||||||
let password = "";
|
|
||||||
for (let i = 0; i < 8; i++) {
|
|
||||||
password += charset.charAt(Math.floor(Math.random() * charset.length));
|
|
||||||
}
|
|
||||||
return password;
|
|
||||||
}
|
|
||||||
|
|
||||||
const generatePasswordForAddress = async (
|
|
||||||
c: Context<HonoCustomType>,
|
|
||||||
address: string
|
|
||||||
): Promise<string | null> => {
|
|
||||||
if (!getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const plainPassword = generateRandomPassword();
|
|
||||||
const hashedPassword = await hashPassword(plainPassword);
|
|
||||||
const { success } = await c.env.DB.prepare(
|
|
||||||
`UPDATE address SET password = ?, updated_at = datetime('now') WHERE name = ?`
|
|
||||||
).bind(hashedPassword, address).run();
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
console.warn("Failed to set generated password for address:", address);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return plainPassword;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const newAddress = async (
|
export const newAddress = async (
|
||||||
c: Context<HonoCustomType>,
|
c: Context<HonoCustomType>,
|
||||||
{
|
{
|
||||||
@@ -131,7 +100,7 @@ export const newAddress = async (
|
|||||||
checkAllowDomains?: boolean,
|
checkAllowDomains?: boolean,
|
||||||
enableCheckNameRegex?: boolean,
|
enableCheckNameRegex?: boolean,
|
||||||
}
|
}
|
||||||
): Promise<{ address: string, jwt: string, password?: string | null }> => {
|
): Promise<{ address: string, jwt: string }> => {
|
||||||
// trim whitespace and remove special characters
|
// trim whitespace and remove special characters
|
||||||
name = name.trim().replace(getNameRegex(c), '')
|
name = name.trim().replace(getNameRegex(c), '')
|
||||||
// check name
|
// check name
|
||||||
@@ -197,10 +166,6 @@ export const newAddress = async (
|
|||||||
const address_id = await c.env.DB.prepare(
|
const address_id = await c.env.DB.prepare(
|
||||||
`SELECT id FROM address where name = ?`
|
`SELECT id FROM address where name = ?`
|
||||||
).bind(name).first<number>("id");
|
).bind(name).first<number>("id");
|
||||||
|
|
||||||
// 如果启用地址密码功能,自动生成密码
|
|
||||||
const generatedPassword = await generatePasswordForAddress(c, name);
|
|
||||||
|
|
||||||
// create jwt
|
// create jwt
|
||||||
const jwt = await Jwt.sign({
|
const jwt = await Jwt.sign({
|
||||||
address: name,
|
address: name,
|
||||||
@@ -209,7 +174,6 @@ export const newAddress = async (
|
|||||||
return {
|
return {
|
||||||
jwt: jwt,
|
jwt: jwt,
|
||||||
address: name,
|
address: name,
|
||||||
password: generatedPassword,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,12 +215,6 @@ export const cleanup = async (
|
|||||||
`created_at < datetime('now', '-${cleanDays} day')`
|
`created_at < datetime('now', '-${cleanDays} day')`
|
||||||
)
|
)
|
||||||
break;
|
break;
|
||||||
case "unboundAddress":
|
|
||||||
await batchDeleteAddressWithData(
|
|
||||||
c,
|
|
||||||
`id NOT IN (SELECT address_id FROM users_address) AND created_at < datetime('now', '-${cleanDays} day')`
|
|
||||||
)
|
|
||||||
break;
|
|
||||||
case "mails":
|
case "mails":
|
||||||
await c.env.DB.prepare(`
|
await c.env.DB.prepare(`
|
||||||
DELETE FROM raw_mails WHERE created_at < datetime('now', '-${cleanDays} day')`
|
DELETE FROM raw_mails WHERE created_at < datetime('now', '-${cleanDays} day')`
|
||||||
@@ -310,7 +268,9 @@ const batchDeleteAddressWithData = async (
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: need senbox delete?
|
||||||
|
*/
|
||||||
export const deleteAddressWithData = async (
|
export const deleteAddressWithData = async (
|
||||||
c: Context<HonoCustomType>,
|
c: Context<HonoCustomType>,
|
||||||
address: string | undefined | null,
|
address: string | undefined | null,
|
||||||
@@ -520,7 +480,7 @@ export async function triggerWebhook(
|
|||||||
|
|
||||||
// user mail webhook
|
// user mail webhook
|
||||||
const adminSettings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
|
const adminSettings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
|
||||||
if (!adminSettings?.enableAllowList || adminSettings?.allowList.includes(address)) {
|
if (adminSettings?.allowList.includes(address)) {
|
||||||
const settings = await c.env.KV.get<WebhookSettings>(
|
const settings = await c.env.KV.get<WebhookSettings>(
|
||||||
`${CONSTANTS.WEBHOOK_KV_USER_SETTINGS_KEY}:${address}`, "json"
|
`${CONSTANTS.WEBHOOK_KV_USER_SETTINGS_KEY}:${address}`, "json"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
export const CONSTANTS = {
|
export const CONSTANTS = {
|
||||||
VERSION: 'v' + '1.0.6',
|
VERSION: 'v' + '1.0.5',
|
||||||
|
|
||||||
// DB Version
|
// DB Version
|
||||||
DB_VERSION_KEY: 'db_version',
|
DB_VERSION_KEY: 'db_version',
|
||||||
DB_VERSION: "v0.0.3",
|
DB_VERSION: "v0.0.1",
|
||||||
|
|
||||||
// DB settings
|
// DB settings
|
||||||
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
||||||
@@ -14,7 +14,6 @@ export const CONSTANTS = {
|
|||||||
VERIFIED_ADDRESS_LIST_KEY: 'verified_address_list',
|
VERIFIED_ADDRESS_LIST_KEY: 'verified_address_list',
|
||||||
NO_LIMIT_SEND_ADDRESS_LIST_KEY: 'no_limit_send_address_list',
|
NO_LIMIT_SEND_ADDRESS_LIST_KEY: 'no_limit_send_address_list',
|
||||||
EMAIL_RULE_SETTINGS_KEY: 'email_rule_settings',
|
EMAIL_RULE_SETTINGS_KEY: 'email_rule_settings',
|
||||||
ROLE_ADDRESS_CONFIG_KEY: 'role_address_config',
|
|
||||||
|
|
||||||
// KV
|
// KV
|
||||||
TG_KV_PREFIX: "temp-mail-telegram",
|
TG_KV_PREFIX: "temp-mail-telegram",
|
||||||
|
|||||||
@@ -38,14 +38,6 @@ const messages: LocaleMessages = {
|
|||||||
Oauth2FailedGetUserInfoMsg: "Failed to get user info from Oauth2 provider",
|
Oauth2FailedGetUserInfoMsg: "Failed to get user info from Oauth2 provider",
|
||||||
Oauth2FailedGetAccessTokenMsg: "Failed to get access token from Oauth2 provider",
|
Oauth2FailedGetAccessTokenMsg: "Failed to get access token from Oauth2 provider",
|
||||||
Oauth2FailedGetUserEmailMsg: "Failed to get user email from Oauth2 provider",
|
Oauth2FailedGetUserEmailMsg: "Failed to get user email from Oauth2 provider",
|
||||||
|
|
||||||
PasswordChangeDisabledMsg: "Password change is disabled",
|
|
||||||
NewPasswordRequiredMsg: "New password is required",
|
|
||||||
InvalidAddressTokenMsg: "Invalid address token",
|
|
||||||
FailedUpdatePasswordMsg: "Failed to update password",
|
|
||||||
PasswordLoginDisabledMsg: "Password login is disabled",
|
|
||||||
EmailPasswordRequiredMsg: "Email and password are required",
|
|
||||||
AddressNotFoundMsg: "Address not found",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default messages;
|
export default messages;
|
||||||
|
|||||||
@@ -36,12 +36,4 @@ export type LocaleMessages = {
|
|||||||
Oauth2FailedGetUserInfoMsg: string
|
Oauth2FailedGetUserInfoMsg: string
|
||||||
Oauth2FailedGetAccessTokenMsg: string
|
Oauth2FailedGetAccessTokenMsg: string
|
||||||
Oauth2FailedGetUserEmailMsg: string
|
Oauth2FailedGetUserEmailMsg: string
|
||||||
|
|
||||||
PasswordChangeDisabledMsg: string
|
|
||||||
NewPasswordRequiredMsg: string
|
|
||||||
InvalidAddressTokenMsg: string
|
|
||||||
FailedUpdatePasswordMsg: string
|
|
||||||
PasswordLoginDisabledMsg: string
|
|
||||||
EmailPasswordRequiredMsg: string
|
|
||||||
AddressNotFoundMsg: string
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,14 +38,6 @@ const messages: LocaleMessages = {
|
|||||||
Oauth2FailedGetUserInfoMsg: "从 Oauth2 提供商获取用户信息失败",
|
Oauth2FailedGetUserInfoMsg: "从 Oauth2 提供商获取用户信息失败",
|
||||||
Oauth2FailedGetAccessTokenMsg: "从 Oauth2 提供商获取访问令牌失败",
|
Oauth2FailedGetAccessTokenMsg: "从 Oauth2 提供商获取访问令牌失败",
|
||||||
Oauth2FailedGetUserEmailMsg: "从 Oauth2 提供商获取用户邮箱失败",
|
Oauth2FailedGetUserEmailMsg: "从 Oauth2 提供商获取用户邮箱失败",
|
||||||
|
|
||||||
PasswordChangeDisabledMsg: "密码修改已禁用",
|
|
||||||
NewPasswordRequiredMsg: "新密码不能为空",
|
|
||||||
InvalidAddressTokenMsg: "无效的地址令牌",
|
|
||||||
FailedUpdatePasswordMsg: "更新密码失败",
|
|
||||||
PasswordLoginDisabledMsg: "密码登录已禁用",
|
|
||||||
EmailPasswordRequiredMsg: "邮箱和密码不能为空",
|
|
||||||
AddressNotFoundMsg: "邮箱地址不存在",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default messages;
|
export default messages;
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import { Context } from 'hono';
|
|
||||||
import i18n from '../i18n';
|
|
||||||
import { getBooleanValue, hashPassword } from '../utils';
|
|
||||||
import { Jwt } from 'hono/utils/jwt';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
// 修改地址密码
|
|
||||||
changePassword: async (c: Context<HonoCustomType>) => {
|
|
||||||
const { new_password } = await c.req.json();
|
|
||||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
|
||||||
const msgs = i18n.getMessages(lang);
|
|
||||||
const { address, address_id } = c.get("jwtPayload");
|
|
||||||
|
|
||||||
// 检查功能是否启用
|
|
||||||
if (!getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)) {
|
|
||||||
return c.text(msgs.PasswordChangeDisabledMsg, 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!new_password) {
|
|
||||||
return c.text(msgs.NewPasswordRequiredMsg, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!address || !address_id) {
|
|
||||||
return c.text(msgs.InvalidAddressTokenMsg, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新密码
|
|
||||||
const { success } = await c.env.DB.prepare(
|
|
||||||
`UPDATE address SET password = ?, updated_at = datetime('now') WHERE id = ?`
|
|
||||||
).bind(new_password, address_id).run();
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
return c.text(msgs.FailedUpdatePasswordMsg, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json({ success: true });
|
|
||||||
},
|
|
||||||
|
|
||||||
// 地址密码登录
|
|
||||||
login: async (c: Context<HonoCustomType>) => {
|
|
||||||
const { email, password, cf_token } = await c.req.json();
|
|
||||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
|
||||||
const msgs = i18n.getMessages(lang);
|
|
||||||
|
|
||||||
// 检查功能是否启用
|
|
||||||
if (!getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)) {
|
|
||||||
return c.text(msgs.PasswordLoginDisabledMsg, 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!email || !password) {
|
|
||||||
return c.text(msgs.EmailPasswordRequiredMsg, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查找地址
|
|
||||||
const address = await c.env.DB.prepare(
|
|
||||||
`SELECT * FROM address WHERE name = ?`
|
|
||||||
).bind(email).first();
|
|
||||||
|
|
||||||
if (!address) {
|
|
||||||
return c.text(msgs.AddressNotFoundMsg, 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证密码
|
|
||||||
if (address.password !== password) {
|
|
||||||
return c.text(msgs.InvalidEmailOrPasswordMsg, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建JWT
|
|
||||||
const jwt = await Jwt.sign({
|
|
||||||
address: address.name,
|
|
||||||
address_id: address.id
|
|
||||||
}, c.env.JWT_SECRET, "HS256");
|
|
||||||
|
|
||||||
return c.json({
|
|
||||||
jwt: jwt,
|
|
||||||
address: address.name
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -7,7 +7,6 @@ import { CONSTANTS } from '../constants'
|
|||||||
import auto_reply from './auto_reply'
|
import auto_reply from './auto_reply'
|
||||||
import webhook_settings from './webhook_settings';
|
import webhook_settings from './webhook_settings';
|
||||||
import s3_attachment from './s3_attachment';
|
import s3_attachment from './s3_attachment';
|
||||||
import address_auth from './address_auth';
|
|
||||||
|
|
||||||
export const api = new Hono<HonoCustomType>()
|
export const api = new Hono<HonoCustomType>()
|
||||||
|
|
||||||
@@ -163,42 +162,3 @@ api.delete('/api/delete_address', async (c) => {
|
|||||||
success: success
|
success: success
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
api.delete('/api/clear_inbox', async (c) => {
|
|
||||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
|
||||||
const msgs = i18n.getMessages(lang);
|
|
||||||
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
|
||||||
return c.text(msgs.UserDeleteEmailDisabledMsg, 403)
|
|
||||||
}
|
|
||||||
const { address } = c.get("jwtPayload")
|
|
||||||
const { success } = await c.env.DB.prepare(
|
|
||||||
`DELETE FROM raw_mails WHERE address = ?`
|
|
||||||
).bind(address).run();
|
|
||||||
if (!success) {
|
|
||||||
return c.text("Failed to clear inbox", 500)
|
|
||||||
}
|
|
||||||
return c.json({
|
|
||||||
success: success
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
api.delete('/api/clear_sent_items', async (c) => {
|
|
||||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
|
||||||
const msgs = i18n.getMessages(lang);
|
|
||||||
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
|
||||||
return c.text(msgs.UserDeleteEmailDisabledMsg, 403)
|
|
||||||
}
|
|
||||||
const { address } = c.get("jwtPayload")
|
|
||||||
const { success } = await c.env.DB.prepare(
|
|
||||||
`DELETE FROM sendbox WHERE address = ?`
|
|
||||||
).bind(address).run();
|
|
||||||
if (!success) {
|
|
||||||
return c.text("Failed to clear sent items", 500)
|
|
||||||
}
|
|
||||||
return c.json({
|
|
||||||
success: success
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
api.post('/api/address_change_password', address_auth.changePassword)
|
|
||||||
api.post('/api/address_login', address_auth.login)
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { commonParseMail, sendWebhook } from "../common";
|
|||||||
async function getWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
async function getWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||||
const { address } = c.get("jwtPayload")
|
const { address } = c.get("jwtPayload")
|
||||||
const adminSettings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
|
const adminSettings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
|
||||||
if (adminSettings?.enableAllowList && !adminSettings?.allowList.includes(address)) {
|
if (!adminSettings?.allowList.includes(address)) {
|
||||||
return c.text("Webhook settings is not allowed for this user", 403);
|
return c.text("Webhook settings is not allowed for this user", 403);
|
||||||
}
|
}
|
||||||
const settings = await c.env.KV.get<WebhookSettings>(
|
const settings = await c.env.KV.get<WebhookSettings>(
|
||||||
@@ -20,7 +20,7 @@ async function getWebhookSettings(c: Context<HonoCustomType>): Promise<Response>
|
|||||||
async function saveWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
async function saveWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||||
const { address } = c.get("jwtPayload")
|
const { address } = c.get("jwtPayload")
|
||||||
const adminSettings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
|
const adminSettings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
|
||||||
if (adminSettings?.enableAllowList && !adminSettings?.allowList.includes(address)) {
|
if (!adminSettings?.allowList.includes(address)) {
|
||||||
return c.text("Webhook settings is not allowed for this user", 403);
|
return c.text("Webhook settings is not allowed for this user", 403);
|
||||||
}
|
}
|
||||||
const settings = await c.req.json<WebhookSettings>();
|
const settings = await c.req.json<WebhookSettings>();
|
||||||
|
|||||||
@@ -14,11 +14,9 @@ export type Passkey = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class AdminWebhookSettings {
|
export class AdminWebhookSettings {
|
||||||
enableAllowList: boolean;
|
|
||||||
allowList: string[];
|
allowList: string[];
|
||||||
|
|
||||||
constructor(enableAllowList: boolean, allowList: string[]) {
|
constructor(allowList: string[]) {
|
||||||
this.enableAllowList = enableAllowList;
|
|
||||||
this.allowList = allowList;
|
this.allowList = allowList;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,8 +44,6 @@ export type CleanupSettings = {
|
|||||||
cleanAddressDays: number;
|
cleanAddressDays: number;
|
||||||
enableInactiveAddressAutoCleanup: boolean | undefined;
|
enableInactiveAddressAutoCleanup: boolean | undefined;
|
||||||
cleanInactiveAddressDays: number;
|
cleanInactiveAddressDays: number;
|
||||||
enableUnboundAddressAutoCleanup: boolean | undefined;
|
|
||||||
cleanUnboundAddressDays: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GeoData {
|
export class GeoData {
|
||||||
@@ -154,10 +150,3 @@ export type EmailRuleSettings = {
|
|||||||
blockReceiveUnknowAddressEmail: boolean;
|
blockReceiveUnknowAddressEmail: boolean;
|
||||||
emailForwardingList: SubdomainForwardAddressList[]
|
emailForwardingList: SubdomainForwardAddressList[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RoleConfig = {
|
|
||||||
maxAddressCount?: number;
|
|
||||||
// future configs can be added here
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RoleAddressConfig = Record<string, RoleConfig>;
|
|
||||||
|
|||||||
@@ -50,11 +50,4 @@ export async function scheduled(event: ScheduledEvent, env: Bindings, ctx: any)
|
|||||||
autoCleanupSetting.cleanAddressDays
|
autoCleanupSetting.cleanAddressDays
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (autoCleanupSetting.enableUnboundAddressAutoCleanup) {
|
|
||||||
await cleanup(
|
|
||||||
{ env: env, } as Context<HonoCustomType>,
|
|
||||||
"unboundAddress",
|
|
||||||
autoCleanupSetting.cleanUnboundAddressDays
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { deleteAddressWithData, newAddress, generateRandomName } from "../common
|
|||||||
|
|
||||||
export const tgUserNewAddress = async (
|
export const tgUserNewAddress = async (
|
||||||
c: Context<HonoCustomType>, userId: string, address: string
|
c: Context<HonoCustomType>, userId: string, address: string
|
||||||
): Promise<{ address: string, jwt: string, password?: string | null }> => {
|
): Promise<{ address: string, jwt: string }> => {
|
||||||
if (c.env.RATE_LIMITER) {
|
if (c.env.RATE_LIMITER) {
|
||||||
const { success } = await c.env.RATE_LIMITER.limit(
|
const { success } = await c.env.RATE_LIMITER.limit(
|
||||||
{ key: `${CONSTANTS.TG_KV_PREFIX}:${userId}` }
|
{ key: `${CONSTANTS.TG_KV_PREFIX}:${userId}` }
|
||||||
|
|||||||
@@ -101,7 +101,6 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
|||||||
const res = await tgUserNewAddress(c, userId.toString(), address);
|
const res = await tgUserNewAddress(c, userId.toString(), address);
|
||||||
return await ctx.reply(`创建地址成功:\n`
|
return await ctx.reply(`创建地址成功:\n`
|
||||||
+ `地址: ${res.address}\n`
|
+ `地址: ${res.address}\n`
|
||||||
+ (res.password ? `密码: \`${res.password}\`\n` : '')
|
|
||||||
+ `凭证: \`${res.jwt}\`\n`,
|
+ `凭证: \`${res.jwt}\`\n`,
|
||||||
{
|
{
|
||||||
parse_mode: "Markdown"
|
parse_mode: "Markdown"
|
||||||
|
|||||||
Vendored
-1
@@ -40,7 +40,6 @@ type Bindings = {
|
|||||||
ENABLE_USER_CREATE_EMAIL: string | boolean | undefined
|
ENABLE_USER_CREATE_EMAIL: string | boolean | undefined
|
||||||
DISABLE_ANONYMOUS_USER_CREATE_EMAIL: string | boolean | undefined
|
DISABLE_ANONYMOUS_USER_CREATE_EMAIL: string | boolean | undefined
|
||||||
ENABLE_USER_DELETE_EMAIL: string | boolean | undefined
|
ENABLE_USER_DELETE_EMAIL: string | boolean | undefined
|
||||||
ENABLE_ADDRESS_PASSWORD: string | boolean | undefined
|
|
||||||
ENABLE_INDEX_ABOUT: string | boolean | undefined
|
ENABLE_INDEX_ABOUT: string | boolean | undefined
|
||||||
DEFAULT_SEND_BALANCE: number | string | undefined
|
DEFAULT_SEND_BALANCE: number | string | undefined
|
||||||
NO_LIMIT_SEND_ROLE: string | undefined | null
|
NO_LIMIT_SEND_ROLE: string | undefined | null
|
||||||
|
|||||||
@@ -1,26 +1,12 @@
|
|||||||
import { Context } from 'hono';
|
import { Context } from 'hono';
|
||||||
import { Jwt } from 'hono/utils/jwt'
|
import { Jwt } from 'hono/utils/jwt'
|
||||||
|
|
||||||
import { UserSettings, RoleAddressConfig } from "../models";
|
import { UserSettings } from "../models";
|
||||||
import { getJsonSetting } from "../utils"
|
import { getJsonSetting } from "../utils"
|
||||||
import { CONSTANTS } from "../constants";
|
import { CONSTANTS } from "../constants";
|
||||||
import { unbindTelegramByAddress } from '../telegram_api/common';
|
import { unbindTelegramByAddress } from '../telegram_api/common';
|
||||||
import i18n from '../i18n';
|
import i18n from '../i18n';
|
||||||
import { updateAddressUpdatedAt, commonGetUserRole } from '../common';
|
import { updateAddressUpdatedAt } from '../common';
|
||||||
|
|
||||||
const getMaxAddressCount = async (
|
|
||||||
c: Context<HonoCustomType>,
|
|
||||||
userRole: string | null | undefined,
|
|
||||||
settings: UserSettings
|
|
||||||
): Promise<number> => {
|
|
||||||
if (!userRole) return settings.maxAddressCount;
|
|
||||||
const roleConfigs = await getJsonSetting<RoleAddressConfig>(c, CONSTANTS.ROLE_ADDRESS_CONFIG_KEY);
|
|
||||||
if (!roleConfigs) return settings.maxAddressCount;
|
|
||||||
const roleMaxCount = roleConfigs[userRole]?.maxAddressCount;
|
|
||||||
if (typeof roleMaxCount !== 'number') return settings.maxAddressCount;
|
|
||||||
if (roleMaxCount <= 0) return settings.maxAddressCount;
|
|
||||||
return roleMaxCount;
|
|
||||||
};
|
|
||||||
|
|
||||||
const UserBindAddressModule = {
|
const UserBindAddressModule = {
|
||||||
bind: async (c: Context<HonoCustomType>) => {
|
bind: async (c: Context<HonoCustomType>) => {
|
||||||
@@ -57,15 +43,11 @@ const UserBindAddressModule = {
|
|||||||
// check if binded address count
|
// check if binded address count
|
||||||
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
|
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
|
||||||
const settings = new UserSettings(value);
|
const settings = new UserSettings(value);
|
||||||
// get user role
|
if (settings.maxAddressCount > 0) {
|
||||||
const userRole = c.get("userRolePayload");
|
|
||||||
// check role-based max address count first, fallback to global settings
|
|
||||||
const maxAddressCount = await getMaxAddressCount(c, userRole, settings);
|
|
||||||
if (maxAddressCount > 0) {
|
|
||||||
const { count } = await c.env.DB.prepare(
|
const { count } = await c.env.DB.prepare(
|
||||||
`SELECT COUNT(*) as count FROM users_address where user_id = ?`
|
`SELECT COUNT(*) as count FROM users_address where user_id = ?`
|
||||||
).bind(user_id).first<{ count: number }>() || { count: 0 };
|
).bind(user_id).first<{ count: number }>() || { count: 0 };
|
||||||
if (count >= maxAddressCount) {
|
if (count >= settings.maxAddressCount) {
|
||||||
return c.text("Max address count reached", 400)
|
return c.text("Max address count reached", 400)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,22 +194,18 @@ const UserBindAddressModule = {
|
|||||||
// check if target user exists
|
// check if target user exists
|
||||||
const target_user_id = await c.env.DB.prepare(
|
const target_user_id = await c.env.DB.prepare(
|
||||||
`SELECT id FROM users where user_email = ?`
|
`SELECT id FROM users where user_email = ?`
|
||||||
).bind(target_user_email).first<number>("id");
|
).bind(target_user_email).first("id");
|
||||||
if (!target_user_id) {
|
if (!target_user_id) {
|
||||||
return c.text("Target user not found", 400)
|
return c.text("Target user not found", 400)
|
||||||
}
|
}
|
||||||
// check target user binded address count
|
// check target user binded address count
|
||||||
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
|
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
|
||||||
const settings = new UserSettings(value);
|
const settings = new UserSettings(value);
|
||||||
// get target user role
|
if (settings.maxAddressCount > 0) {
|
||||||
const userRoleObj = await commonGetUserRole(c, target_user_id);
|
|
||||||
// check role-based max address count first, fallback to global settings
|
|
||||||
const maxAddressCount = await getMaxAddressCount(c, userRoleObj?.role, settings);
|
|
||||||
if (maxAddressCount > 0) {
|
|
||||||
const { count } = await c.env.DB.prepare(
|
const { count } = await c.env.DB.prepare(
|
||||||
`SELECT COUNT(*) as count FROM users_address where user_id = ?`
|
`SELECT COUNT(*) as count FROM users_address where user_id = ?`
|
||||||
).bind(target_user_id).first<{ count: number }>() || { count: 0 };
|
).bind(target_user_id).first<{ count: number }>() || { count: 0 };
|
||||||
if (count >= maxAddressCount) {
|
if (count >= settings.maxAddressCount) {
|
||||||
return c.text("Target User Max address count reached", 400)
|
return c.text("Target User Max address count reached", 400)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -296,13 +296,6 @@ export const checkUserPassword = (password: string) => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const hashPassword = async (password: string): Promise<string> => {
|
|
||||||
// use crypto to hash password
|
|
||||||
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(password));
|
|
||||||
const hashArray = Array.from(new Uint8Array(digest));
|
|
||||||
return hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getJsonObjectValue,
|
getJsonObjectValue,
|
||||||
getSetting,
|
getSetting,
|
||||||
|
|||||||
@@ -148,10 +148,6 @@ app.use('/api/*', async (c, next) => {
|
|||||||
) {
|
) {
|
||||||
await checkoutUserRolePayload(c);
|
await checkoutUserRolePayload(c);
|
||||||
}
|
}
|
||||||
if (c.req.path.startsWith("/api/address_login")) {
|
|
||||||
await next();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await jwt({ secret: c.env.JWT_SECRET, alg: "HS256" })(c, next);
|
return await jwt({ secret: c.env.JWT_SECRET, alg: "HS256" })(c, next);
|
||||||
@@ -194,9 +190,6 @@ app.use('/user_api/*', async (c, next) => {
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
return c.text(msgs.UserTokenExpiredMsg, 401)
|
return c.text(msgs.UserTokenExpiredMsg, 401)
|
||||||
}
|
}
|
||||||
if (c.req.path.startsWith("/user_api/bind_address")) {
|
|
||||||
await checkoutUserRolePayload(c);
|
|
||||||
}
|
|
||||||
if (c.req.path.startsWith('/user_api/bind_address')
|
if (c.req.path.startsWith('/user_api/bind_address')
|
||||||
&& c.req.method === 'POST'
|
&& c.req.method === 'POST'
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -70,8 +70,6 @@ ENABLE_USER_DELETE_EMAIL = true
|
|||||||
ENABLE_AUTO_REPLY = false
|
ENABLE_AUTO_REPLY = false
|
||||||
# Allow webhook
|
# Allow webhook
|
||||||
# ENABLE_WEBHOOK = true
|
# ENABLE_WEBHOOK = true
|
||||||
# Enable address password feature, if set true, will generate password for new address and support password login and change
|
|
||||||
# ENABLE_ADDRESS_PASSWORD = false
|
|
||||||
# Footer text
|
# Footer text
|
||||||
# COPYRIGHT = "Dream Hunter"
|
# COPYRIGHT = "Dream Hunter"
|
||||||
# DISABLE_SHOW_GITHUB = true
|
# DISABLE_SHOW_GITHUB = true
|
||||||
|
|||||||
Reference in New Issue
Block a user