mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-09 09:27:04 +08:00
Compare commits
10
Commits
6ae90be3bf
...
v1.0.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5c229b6c4 | ||
|
|
2728e9667b | ||
|
|
6109ab9e82 | ||
|
|
09a6cac8fe | ||
|
|
5f752c94f9 | ||
|
|
a2f3634c7e | ||
|
|
b62a3cbc3e | ||
|
|
8edb75587e | ||
|
|
de48661d0d | ||
|
|
a905ba5f06 |
@@ -60,7 +60,7 @@ jobs:
|
|||||||
echo "Applied mail-parser-wasm-worker patch"
|
echo "Applied mail-parser-wasm-worker patch"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$debug_mode" ]; then
|
if [ "$debug_mode" = "true" ]; then
|
||||||
pnpm run deploy
|
pnpm run deploy
|
||||||
else
|
else
|
||||||
output=$(pnpm run deploy 2>&1)
|
output=$(pnpm run deploy 2>&1)
|
||||||
|
|||||||
+6
-1
@@ -1,9 +1,14 @@
|
|||||||
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
|
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
|
||||||
# CHANGE LOG
|
# CHANGE LOG
|
||||||
|
|
||||||
## main(v1.0.6)
|
## v1.0.6
|
||||||
|
|
||||||
- feat: |DB| update db schema add index
|
- 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
|
## v1.0.5
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
- 🆓 **完全免费** - 基于 Cloudflare 免费服务构建,零成本运行
|
- 🆓 **完全免费** - 基于 Cloudflare 免费服务构建,零成本运行
|
||||||
- ⚡ **高性能** - Rust WASM 邮件解析,响应速度极快
|
- ⚡ **高性能** - Rust WASM 邮件解析,响应速度极快
|
||||||
- 🎨 **现代化界面** - 响应式设计,支持多语言,操作简便
|
- 🎨 **现代化界面** - 响应式设计,支持多语言,操作简便
|
||||||
|
- 🔐 **地址密码** - 支持为邮箱地址设置独立密码,增强安全性 (通过 `ENABLE_ADDRESS_PASSWORD` 启用)
|
||||||
|
|
||||||
## 📚 部署文档 - 快速开始
|
## 📚 部署文档 - 快速开始
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
ALTER TABLE
|
||||||
|
address
|
||||||
|
ADD
|
||||||
|
password TEXT;
|
||||||
@@ -14,6 +14,7 @@ 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
|
||||||
);
|
);
|
||||||
@@ -55,6 +56,7 @@ 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 INDEX IF NOT EXISTS idx_sendbox_created_at ON sendbox(created_at);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
|||||||
@@ -29,9 +29,9 @@
|
|||||||
"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.43.1",
|
||||||
"postal-mime": "^2.4.4",
|
"postal-mime": "^2.5.0",
|
||||||
"vooks": "^0.2.12",
|
"vooks": "^0.2.12",
|
||||||
"vue": "^3.5.21",
|
"vue": "^3.5.22",
|
||||||
"vue-clipboard3": "^2.0.0",
|
"vue-clipboard3": "^2.0.0",
|
||||||
"vue-i18n": "^11.1.12",
|
"vue-i18n": "^11.1.12",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
"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.37.0"
|
"wrangler": "^4.42.2"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+492
-483
File diff suppressed because it is too large
Load Diff
@@ -86,6 +86,7 @@ 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,6 +36,7 @@ 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,
|
||||||
@@ -63,6 +64,7 @@ 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("");
|
||||||
@@ -145,6 +147,7 @@ export const useGlobalState = createGlobalState(
|
|||||||
userOauth2SessionState,
|
userOauth2SessionState,
|
||||||
userOauth2SessionClientID,
|
userOauth2SessionClientID,
|
||||||
useSimpleIndex,
|
useSimpleIndex,
|
||||||
|
addressPassword,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ 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';
|
||||||
@@ -61,6 +62,7 @@ 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',
|
||||||
@@ -88,6 +90,7 @@ const { t } = useI18n({
|
|||||||
user_management: '用户管理',
|
user_management: '用户管理',
|
||||||
user_settings: '用户设置',
|
user_settings: '用户设置',
|
||||||
userOauth2Settings: 'Oauth2 设置',
|
userOauth2Settings: 'Oauth2 设置',
|
||||||
|
roleAddressConfig: '角色地址配置',
|
||||||
unknow: '无收件人邮件',
|
unknow: '无收件人邮件',
|
||||||
senderAccess: '发件权限控制',
|
senderAccess: '发件权限控制',
|
||||||
sendBox: '发件箱',
|
sendBox: '发件箱',
|
||||||
@@ -173,6 +176,9 @@ 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 } from 'vue';
|
import { ref, h, onMounted, watch, computed } from 'vue';
|
||||||
import { NBadge } from 'naive-ui'
|
import { NBadge, useMessage } 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,
|
loading, adminTab, openSettings,
|
||||||
adminMailTabAddress, adminSendBoxTabAddress
|
adminMailTabAddress, adminSendBoxTabAddress
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
@@ -39,6 +39,19 @@ const { t } = useI18n({
|
|||||||
clearSentItemsTip: 'Are you sure to clear sent items for this email?',
|
clearSentItemsTip: 'Are you sure to clear sent items for this email?',
|
||||||
actions: 'Actions',
|
actions: 'Actions',
|
||||||
success: 'Success',
|
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: '名称',
|
||||||
@@ -63,6 +76,19 @@ const { t } = useI18n({
|
|||||||
clearSentItemsTip: '确定要清空这个邮箱的发件箱吗?',
|
clearSentItemsTip: '确定要清空这个邮箱的发件箱吗?',
|
||||||
actions: '操作',
|
actions: '操作',
|
||||||
success: '成功',
|
success: '成功',
|
||||||
|
resetPassword: '重置密码',
|
||||||
|
newPassword: '新密码',
|
||||||
|
passwordResetSuccess: '密码重置成功',
|
||||||
|
selectAll: '全选本页',
|
||||||
|
unselectAll: '取消全选',
|
||||||
|
pleaseSelectAddress: '请选择地址',
|
||||||
|
selectedItems: '已选择',
|
||||||
|
multiDelete: '批量删除',
|
||||||
|
multiDeleteTip: '确定要删除选中的邮箱吗?',
|
||||||
|
multiClearInbox: '批量清空收件箱',
|
||||||
|
multiClearInboxTip: '确定要清空选中邮箱的收件箱吗?',
|
||||||
|
multiClearSentItems: '批量清空发件箱',
|
||||||
|
multiClearSentItemsTip: '确定要清空选中邮箱的发件箱吗?',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -72,6 +98,18 @@ const curEmailCredential = ref("")
|
|||||||
const curDeleteAddressId = ref(0);
|
const curDeleteAddressId = ref(0);
|
||||||
const curClearInboxAddressId = ref(0);
|
const curClearInboxAddressId = ref(0);
|
||||||
const curClearSentItemsAddressId = 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("")
|
||||||
|
|
||||||
@@ -134,6 +172,114 @@ const clearSentItems = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
@@ -148,12 +294,15 @@ const fetchData = async () => {
|
|||||||
count.value = addressCount;
|
count.value = addressCount;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.error(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"
|
||||||
@@ -296,6 +445,19 @@ const columns = [
|
|||||||
),
|
),
|
||||||
show: row.send_count > 0
|
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,
|
||||||
{
|
{
|
||||||
@@ -365,13 +527,54 @@ onMounted(async () => {
|
|||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
<n-input-group>
|
|
||||||
|
<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"
|
||||||
@@ -381,8 +584,21 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
</n-pagination>
|
</n-pagination>
|
||||||
</div>
|
</div>
|
||||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
<n-data-table v-model:checked-row-keys="checkedRowKeys" :columns="columns" :data="data" :bordered="false"
|
||||||
|
: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>
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,13 @@ const { t } = useI18n({
|
|||||||
en: {
|
en: {
|
||||||
address: 'Address',
|
address: 'Address',
|
||||||
enablePrefix: 'If enable Prefix',
|
enablePrefix: 'If enable Prefix',
|
||||||
creatNewEmail: 'Get New Email',
|
creatNewEmail: 'Create 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: '地址',
|
||||||
@@ -27,6 +30,9 @@ const { t } = useI18n({
|
|||||||
fillInAllFields: '请填写完整信息',
|
fillInAllFields: '请填写完整信息',
|
||||||
successTip: '创建成功',
|
successTip: '创建成功',
|
||||||
addressCredential: '邮箱地址凭证',
|
addressCredential: '邮箱地址凭证',
|
||||||
|
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
||||||
|
addressPassword: '地址密码',
|
||||||
|
linkWithAddressCredential: '打开即可自动登录邮箱的链接',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -36,6 +42,8 @@ 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) {
|
||||||
@@ -52,6 +60,8 @@ 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) {
|
||||||
@@ -59,6 +69,10 @@ 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
|
||||||
@@ -70,10 +84,25 @@ 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')">
|
||||||
<p>{{ t('addressCredential') }}</p>
|
<span>
|
||||||
<n-card :bordered="false" embedded>
|
<p>{{ t("addressCredentialTip") }}</p>
|
||||||
|
</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')">
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ 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({
|
||||||
@@ -28,6 +30,7 @@ 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",
|
||||||
@@ -41,6 +44,7 @@ const { t } = useI18n({
|
|||||||
sendBoxLabel: "清理 n 天前的发件箱",
|
sendBoxLabel: "清理 n 天前的发件箱",
|
||||||
addressCreateLabel: "清理 n 天前创建的地址",
|
addressCreateLabel: "清理 n 天前创建的地址",
|
||||||
inactiveAddressLabel: "清理 n 天前的未活跃地址",
|
inactiveAddressLabel: "清理 n 天前的未活跃地址",
|
||||||
|
unboundAddressLabel: "清理 n 天前的未绑定用户地址",
|
||||||
autoCleanup: "自动清理",
|
autoCleanup: "自动清理",
|
||||||
cleanupSuccess: "清理成功",
|
cleanupSuccess: "清理成功",
|
||||||
cleanupNow: "立即清理",
|
cleanupNow: "立即清理",
|
||||||
@@ -161,6 +165,18 @@ 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>
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
<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>
|
||||||
@@ -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 } from '../../utils'
|
import { getRouterPathWithLang, hashPassword } 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
|
showAddressCredential, userSettings, addressPassword
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
|
|
||||||
const tabValue = ref('signin')
|
const tabValue = ref('signin')
|
||||||
@@ -47,8 +47,47 @@ 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;
|
||||||
@@ -85,6 +124,11 @@ 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: '登录',
|
||||||
@@ -102,6 +146,11 @@ const { locale, t } = useI18n({
|
|||||||
bindUserInfo: '已登录用户, 登录未绑定邮箱或创建新邮箱地址将绑定到当前用户',
|
bindUserInfo: '已登录用户, 登录未绑定邮箱或创建新邮箱地址将绑定到当前用户',
|
||||||
bindUserAddressError: '绑定邮箱地址到用户时错误',
|
bindUserAddressError: '绑定邮箱地址到用户时错误',
|
||||||
autoGeneratedName: '自动生成名称',
|
autoGeneratedName: '自动生成名称',
|
||||||
|
passwordLogin: '密码登录',
|
||||||
|
credentialLogin: '凭据登录',
|
||||||
|
email: '邮箱',
|
||||||
|
password: '密码',
|
||||||
|
emailPasswordRequired: '邮箱和密码不能为空',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -157,6 +206,7 @@ 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;
|
||||||
@@ -212,6 +262,7 @@ 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>
|
||||||
|
|
||||||
@@ -223,9 +274,29 @@ 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>
|
||||||
<n-form-item-row :label="t('credential')" required>
|
<div v-if="loginMethod === 'password'">
|
||||||
<n-input v-model:value="credential" type="textarea" :autosize="{ minRows: 3 }" />
|
<n-form-item-row :label="t('email')" required>
|
||||||
</n-form-item-row>
|
<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-input v-model:value="credential" type="textarea" :autosize="{ minRows: 3 }" />
|
||||||
|
</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" />
|
||||||
@@ -244,19 +315,21 @@ onMounted(async () => {
|
|||||||
<n-spin :show="generateNameLoading">
|
<n-spin :show="generateNameLoading">
|
||||||
<n-form>
|
<n-form>
|
||||||
<span>
|
<span>
|
||||||
<p v-if="!openSettings.disableCustomAddressName">{{ t("getNewEmailTip1") + addressRegex.source }}</p>
|
<p v-if="!openSettings.disableCustomAddressName">{{ t("getNewEmailTip1") +
|
||||||
|
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" style="margin-bottom: 10px;">
|
<n-button v-if="!openSettings.disableCustomAddressName" @click="generateName"
|
||||||
|
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 :minlength="openSettings.minAddressLen"
|
<n-input v-if="!openSettings.disableCustomAddressName" v-model:value="emailName" show-count
|
||||||
:maxlength="openSettings.maxAddressLen" />
|
:minlength="openSettings.minAddressLen" :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"
|
||||||
@@ -294,6 +367,12 @@ 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,6 +5,7 @@ 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 {
|
||||||
@@ -17,6 +18,9 @@ const showLogout = ref(false)
|
|||||||
const showDeleteAccount = ref(false)
|
const showDeleteAccount = ref(false)
|
||||||
const showClearInbox = ref(false)
|
const showClearInbox = ref(false)
|
||||||
const showClearSentItems = 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: {
|
||||||
@@ -31,6 +35,11 @@ const { locale, t } = useI18n({
|
|||||||
clearInboxConfirm: "Are you sure to clear all emails in your inbox?",
|
clearInboxConfirm: "Are you sure to clear all emails in your inbox?",
|
||||||
clearSentItemsConfirm: "Are you sure to clear all emails in your sent items?",
|
clearSentItemsConfirm: "Are you sure to clear all emails in your sent items?",
|
||||||
success: "Success",
|
success: "Success",
|
||||||
|
changePassword: "Change Password",
|
||||||
|
newPassword: "New Password",
|
||||||
|
confirmPassword: "Confirm Password",
|
||||||
|
passwordMismatch: "Passwords do not match",
|
||||||
|
passwordChanged: "Password changed successfully",
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
logout: '退出登录',
|
logout: '退出登录',
|
||||||
@@ -44,6 +53,11 @@ const { locale, t } = useI18n({
|
|||||||
clearInboxConfirm: "确定要清空你收件箱中的所有邮件吗?",
|
clearInboxConfirm: "确定要清空你收件箱中的所有邮件吗?",
|
||||||
clearSentItemsConfirm: "确定要清空你发件箱中的所有邮件吗?",
|
clearSentItemsConfirm: "确定要清空你发件箱中的所有邮件吗?",
|
||||||
success: "成功",
|
success: "成功",
|
||||||
|
changePassword: "修改密码",
|
||||||
|
newPassword: "新密码",
|
||||||
|
confirmPassword: "确认密码",
|
||||||
|
passwordMismatch: "密码不匹配",
|
||||||
|
passwordChanged: "密码修改成功",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -92,6 +106,27 @@ const clearSentItems = async () => {
|
|||||||
showClearSentItems.value = false;
|
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>
|
||||||
@@ -100,6 +135,9 @@ const clearSentItems = 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
|
<n-button v-if="openSettings.enableUserDeleteEmail" @click="showClearInbox = true" type="warning" secondary
|
||||||
block strong>
|
block strong>
|
||||||
{{ t('clearInbox') }}
|
{{ t('clearInbox') }}
|
||||||
@@ -148,6 +186,22 @@ const clearSentItems = async () => {
|
|||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</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
|
isTelegram, openSettings, addressPassword
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
|
|
||||||
const { locale, t } = useI18n({
|
const { locale, t } = useI18n({
|
||||||
@@ -34,6 +34,7 @@ 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: {
|
||||||
@@ -46,6 +47,7 @@ const { locale, t } = useI18n({
|
|||||||
addressCredential: '邮箱地址凭证',
|
addressCredential: '邮箱地址凭证',
|
||||||
linkWithAddressCredential: '打开即可自动登录邮箱的链接',
|
linkWithAddressCredential: '打开即可自动登录邮箱的链接',
|
||||||
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
||||||
|
addressPassword: '地址密码',
|
||||||
userLogin: '用户登录',
|
userLogin: '用户登录',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,6 +151,10 @@ 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")'>
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"wrangler": "^4.37.0"
|
"wrangler": "^4.42.2"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
| `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` |
|
||||||
|
|
||||||
## 接受邮件相关变量
|
## 接受邮件相关变量
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.4.0",
|
"@types/node": "^24.7.2",
|
||||||
"vitepress": "^1.6.4",
|
"vitepress": "^1.6.4",
|
||||||
"wrangler": "^4.37.0"
|
"wrangler": "^4.42.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vitepress dev docs",
|
"dev": "vitepress dev docs",
|
||||||
|
|||||||
Generated
+356
-347
File diff suppressed because it is too large
Load Diff
+8
-8
@@ -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.20250913.0",
|
"@cloudflare/workers-types": "^4.20251011.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.3",
|
"@types/node": "^22.18.10",
|
||||||
"eslint": "9.18.0",
|
"eslint": "9.18.0",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"typescript-eslint": "^8.43.0",
|
"typescript-eslint": "^8.46.0",
|
||||||
"wrangler": "^4.37.0"
|
"wrangler": "^4.42.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.888.0",
|
"@aws-sdk/client-s3": "3.888.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.888.0",
|
"@aws-sdk/s3-request-presigner": "3.888.0",
|
||||||
"@simplewebauthn/server": "10.0.1",
|
"@simplewebauthn/server": "10.0.1",
|
||||||
"hono": "^4.9.7",
|
"hono": "^4.9.11",
|
||||||
"jsonpath-plus": "^10.3.0",
|
"jsonpath-plus": "^10.3.0",
|
||||||
"mimetext": "^3.0.27",
|
"mimetext": "^3.0.27",
|
||||||
"postal-mime": "^2.4.4",
|
"postal-mime": "^2.5.0",
|
||||||
"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
+607
-602
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 } from "../models";
|
import { UserSettings, GeoData, UserInfo, RoleAddressConfig } 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,4 +166,14 @@ 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 });
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ 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
|
||||||
);
|
);
|
||||||
@@ -136,6 +137,11 @@ 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
|
// remove all \r and \n characters from the query string
|
||||||
|
|||||||
@@ -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 } from '../utils'
|
import { sendAdminInternalMail, getJsonSetting, saveSetting, getUserRoles, getBooleanValue, hashPassword } 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,6 +56,7 @@ 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)
|
||||||
@@ -131,6 +132,30 @@ 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);
|
||||||
@@ -319,6 +344,8 @@ 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)
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ 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)
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+44
-2
@@ -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 } from './utils';
|
import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting, getAnotherWorkerList, hashPassword } 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,6 +82,37 @@ 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>,
|
||||||
{
|
{
|
||||||
@@ -100,7 +131,7 @@ export const newAddress = async (
|
|||||||
checkAllowDomains?: boolean,
|
checkAllowDomains?: boolean,
|
||||||
enableCheckNameRegex?: boolean,
|
enableCheckNameRegex?: boolean,
|
||||||
}
|
}
|
||||||
): Promise<{ address: string, jwt: string }> => {
|
): Promise<{ address: string, jwt: string, password?: string | null }> => {
|
||||||
// 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
|
||||||
@@ -166,6 +197,10 @@ 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,
|
||||||
@@ -174,6 +209,7 @@ export const newAddress = async (
|
|||||||
return {
|
return {
|
||||||
jwt: jwt,
|
jwt: jwt,
|
||||||
address: name,
|
address: name,
|
||||||
|
password: generatedPassword,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,6 +251,12 @@ 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')`
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ export const CONSTANTS = {
|
|||||||
|
|
||||||
// DB Version
|
// DB Version
|
||||||
DB_VERSION_KEY: 'db_version',
|
DB_VERSION_KEY: 'db_version',
|
||||||
DB_VERSION: "v0.0.2",
|
DB_VERSION: "v0.0.3",
|
||||||
|
|
||||||
// DB settings
|
// DB settings
|
||||||
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
||||||
@@ -14,6 +14,7 @@ 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,6 +38,14 @@ 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,4 +36,12 @@ 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,6 +38,14 @@ 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;
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
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,6 +7,7 @@ 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>()
|
||||||
|
|
||||||
@@ -198,3 +199,6 @@ api.delete('/api/clear_sent_items', async (c) => {
|
|||||||
success: success
|
success: success
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
api.post('/api/address_change_password', address_auth.changePassword)
|
||||||
|
api.post('/api/address_login', address_auth.login)
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ 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 {
|
||||||
@@ -152,3 +154,10 @@ 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,4 +50,11 @@ 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 }> => {
|
): Promise<{ address: string, jwt: string, password?: string | null }> => {
|
||||||
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,6 +101,7 @@ 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,6 +40,7 @@ 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,12 +1,26 @@
|
|||||||
import { Context } from 'hono';
|
import { Context } from 'hono';
|
||||||
import { Jwt } from 'hono/utils/jwt'
|
import { Jwt } from 'hono/utils/jwt'
|
||||||
|
|
||||||
import { UserSettings } from "../models";
|
import { UserSettings, RoleAddressConfig } 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 } from '../common';
|
import { updateAddressUpdatedAt, commonGetUserRole } 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>) => {
|
||||||
@@ -43,11 +57,15 @@ 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);
|
||||||
if (settings.maxAddressCount > 0) {
|
// get user role
|
||||||
|
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 >= settings.maxAddressCount) {
|
if (count >= maxAddressCount) {
|
||||||
return c.text("Max address count reached", 400)
|
return c.text("Max address count reached", 400)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,18 +212,22 @@ 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("id");
|
).bind(target_user_email).first<number>("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);
|
||||||
if (settings.maxAddressCount > 0) {
|
// get target user role
|
||||||
|
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 >= settings.maxAddressCount) {
|
if (count >= maxAddressCount) {
|
||||||
return c.text("Target User Max address count reached", 400)
|
return c.text("Target User Max address count reached", 400)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -296,6 +296,13 @@ 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,6 +148,10 @@ 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);
|
||||||
@@ -190,6 +194,9 @@ 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,6 +70,8 @@ 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