mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-07 08:12:46 +08:00
feat: add UserLogin (#209)
This commit is contained in:
23
CHANGELOG.md
23
CHANGELOG.md
@@ -1,9 +1,30 @@
|
||||
# CHANGE LOG
|
||||
|
||||
## main branch
|
||||
|
||||
### DB Changes
|
||||
|
||||
新增 user 相关表,用于存储用户信息
|
||||
|
||||
- `db/2024-05-08-patch.sql`
|
||||
|
||||
### config changs
|
||||
|
||||
```toml
|
||||
# kv config for send email verification code
|
||||
# [[kv_namespaces]]
|
||||
# binding = "KV"
|
||||
# id = "xxxx"
|
||||
```
|
||||
|
||||
### function changs
|
||||
|
||||
- 增加用户注册功能,可绑定邮箱地址,绑定后可自动获取邮箱JWT凭证
|
||||
|
||||
## v0.3.3
|
||||
|
||||
- 修复 Admin 删除邮件报错
|
||||
- UI: 回复邮件按钮, 引用原始邮件文本 #186
|
||||
- UI: 回复邮件按钮, 引用原始邮件文本 #186
|
||||
- 添加发送邮件地址黑名单
|
||||
- 添加 `CF Turnstile` 人机验证配置
|
||||
- 添加 `/external/api/send_mail` 发送邮件 api, 使用 body 验证 #194
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
- [x] 支持 `DKIM`
|
||||
- [x] `admin` 后台创建无前缀邮箱
|
||||
- [x] 添加 `SMTP proxy server`,支持 SMTP 发送邮件
|
||||
- [x] 添加完整的用户注册登录功能,可绑定邮箱地址,绑定后可自动获取邮箱JWT凭证切换不同邮箱
|
||||
|
||||
## Reference
|
||||
|
||||
|
||||
21
db/2024-05-08-patch.sql
Normal file
21
db/2024-05-08-patch.sql
Normal file
@@ -0,0 +1,21 @@
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_email TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
user_info TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_user_email ON users(user_email);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users_address (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER,
|
||||
address_id INTEGER UNIQUE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_address_user_id ON users_address(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_address_address_id ON users_address(address_id);
|
||||
@@ -77,3 +77,25 @@ CREATE TABLE IF NOT EXISTS settings (
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_email TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
user_info TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_user_email ON users(user_email);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users_address (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER,
|
||||
address_id INTEGER UNIQUE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_address_user_id ON users_address(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_address_address_id ON users_address(address_id);
|
||||
|
||||
@@ -2,8 +2,11 @@ import { useGlobalState } from '../store'
|
||||
import axios from 'axios'
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || "";
|
||||
const { loading, auth, jwt, settings, openSettings } = useGlobalState();
|
||||
const { showAuth, adminAuth, showAdminAuth } = useGlobalState();
|
||||
const {
|
||||
loading, auth, jwt, settings, openSettings,
|
||||
userOpenSettings, userSettings,
|
||||
showAuth, adminAuth, showAdminAuth, userJwt
|
||||
} = useGlobalState();
|
||||
|
||||
const instance = axios.create({
|
||||
baseURL: API_BASE,
|
||||
@@ -17,6 +20,7 @@ const apiFetch = async (path, options = {}) => {
|
||||
method: options.method || 'GET',
|
||||
data: options.body || null,
|
||||
headers: {
|
||||
'x-user-token': userJwt.value,
|
||||
'x-custom-auth': auth.value,
|
||||
'x-admin-auth': adminAuth.value,
|
||||
'Authorization': `Bearer ${jwt.value}`,
|
||||
@@ -25,7 +29,7 @@ const apiFetch = async (path, options = {}) => {
|
||||
});
|
||||
if (response.status === 401 && openSettings.value.auth) {
|
||||
showAuth.value = true;
|
||||
throw new Error("Unauthorized, you password is wrong")
|
||||
throw new Error("Unauthorized, you access password is wrong")
|
||||
}
|
||||
if (response.status === 401 && path.startsWith("/admin")) {
|
||||
showAdminAuth.value = true;
|
||||
@@ -90,10 +94,32 @@ const getSettings = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const adminShowPassword = async (id) => {
|
||||
|
||||
const getUserOpenSettings = async (message) => {
|
||||
try {
|
||||
const { password } = await apiFetch(`/admin/show_password/${id}`);
|
||||
return password;
|
||||
const res = await api.fetch(`/user_api/open_settings`);
|
||||
Object.assign(userOpenSettings.value, res);
|
||||
} catch (error) {
|
||||
message.error(error.message || "fetch settings failed");
|
||||
}
|
||||
}
|
||||
|
||||
const getUserSettings = async (message) => {
|
||||
try {
|
||||
if (!userJwt.value) return;
|
||||
const res = await api.fetch("/user_api/settings")
|
||||
Object.assign(userSettings.value, res)
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
} finally {
|
||||
userSettings.value.fetched = true;
|
||||
}
|
||||
}
|
||||
|
||||
const adminShowAddressCredential = async (id) => {
|
||||
try {
|
||||
const { jwt: addressCredential } = await apiFetch(`/admin/show_password/${id}`);
|
||||
return addressCredential;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -109,10 +135,24 @@ const adminDeleteAddress = async (id) => {
|
||||
}
|
||||
}
|
||||
|
||||
const bindUserAddress = async () => {
|
||||
if (!userJwt.value) return;
|
||||
try {
|
||||
await apiFetch(`/user_api/bind_address`, {
|
||||
method: 'POST',
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const api = {
|
||||
fetch: apiFetch,
|
||||
getSettings: getSettings,
|
||||
getOpenSettings: getOpenSettings,
|
||||
adminShowPassword: adminShowPassword,
|
||||
adminDeleteAddress: adminDeleteAddress,
|
||||
getSettings,
|
||||
getOpenSettings,
|
||||
getUserOpenSettings,
|
||||
getUserSettings,
|
||||
adminShowAddressCredential,
|
||||
adminDeleteAddress,
|
||||
bindUserAddress,
|
||||
}
|
||||
|
||||
@@ -41,7 +41,8 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const {
|
||||
localeCache, isDark, mailboxSplitSize, useIframeShowMail, sendMailModel
|
||||
localeCache, isDark, mailboxSplitSize, indexTab,
|
||||
useIframeShowMail, sendMailModel, preferShowTextMail
|
||||
} = useGlobalState()
|
||||
const autoRefresh = ref(false)
|
||||
const autoRefreshInterval = ref(30)
|
||||
@@ -55,6 +56,7 @@ const pageSize = ref(20)
|
||||
const showAttachments = ref(false)
|
||||
const curAttachments = ref([])
|
||||
const curMail = ref(null);
|
||||
const showTextMail = ref(preferShowTextMail.value)
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
@@ -69,7 +71,9 @@ const { t } = useI18n({
|
||||
pleaseSelectMail: "Please select a mail to view.",
|
||||
delete: 'Delete',
|
||||
deleteMailTip: 'Are you sure you want to delete this mail?',
|
||||
reply: 'Reply'
|
||||
reply: 'Reply',
|
||||
showTextMail: 'Show Text Mail',
|
||||
showHtmlMail: 'Show Html Mail'
|
||||
},
|
||||
zh: {
|
||||
success: '成功',
|
||||
@@ -81,7 +85,9 @@ const { t } = useI18n({
|
||||
pleaseSelectMail: "请选择一封邮件查看。",
|
||||
delete: '删除',
|
||||
deleteMailTip: '确定要删除这封邮件吗?',
|
||||
reply: '回复'
|
||||
reply: '回复',
|
||||
showTextMail: '显示纯文本邮件',
|
||||
showHtmlMail: '显示HTML邮件'
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -173,7 +179,7 @@ const replyMail = async () => {
|
||||
contentType: 'rich',
|
||||
content: curMail.value.text ? `<p><br></p><blockquote>${curMail.value.text}</blockquote><p><br></p>` : '',
|
||||
});
|
||||
await router.push('/send');
|
||||
indexTab.value = 'sendmail';
|
||||
};
|
||||
|
||||
const onSpiltSizeChange = (size) => {
|
||||
@@ -206,7 +212,7 @@ onBeforeUnmount(() => {
|
||||
{{ t('autoRefresh') }}
|
||||
</template>
|
||||
</n-switch>
|
||||
<n-button @click="refresh" size="small" type="primary">
|
||||
<n-button @click="refresh" size="small" type="primary" tertiary>
|
||||
{{ t('refresh') }}
|
||||
</n-button>
|
||||
</div>
|
||||
@@ -272,8 +278,12 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
{{ t('reply') }}
|
||||
</n-button>
|
||||
<n-button size="small" tertiary type="info" @click="showTextMail = !showTextMail">
|
||||
{{ showTextMail ? t('showHtmlMail') : t('showTextMail') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
<iframe v-if="useIframeShowMail" :srcdoc="curMail.message"
|
||||
<pre v-if="showTextMail" style="margin-top: 10px;">{{ curMail.text }}</pre>
|
||||
<iframe v-else-if="useIframeShowMail" :srcdoc="curMail.message"
|
||||
style="margin-top: 10px;width: 100%; height: 100%;">
|
||||
</iframe>
|
||||
<div v-else v-html="curMail.message" style="margin-top: 10px;"></div>
|
||||
@@ -420,4 +430,9 @@ onBeforeUnmount(() => {
|
||||
.mail-item {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Index from '../views/Index.vue'
|
||||
import UserLogin from '../views/user/UserLogin.vue'
|
||||
import User from '../views/User.vue'
|
||||
import SendMail from '../views/send/SendMail.vue'
|
||||
import SendMail from '../views/index/SendMail.vue'
|
||||
import Admin from '../views/Admin.vue'
|
||||
|
||||
const router = createRouter({
|
||||
@@ -15,10 +16,6 @@ const router = createRouter({
|
||||
path: '/user',
|
||||
component: User
|
||||
},
|
||||
{
|
||||
path: '/send',
|
||||
component: SendMail
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
component: Admin
|
||||
|
||||
@@ -39,7 +39,7 @@ export const useGlobalState = createGlobalState(
|
||||
content: "",
|
||||
});
|
||||
const showAuth = ref(false);
|
||||
const showPassword = ref(false);
|
||||
const showAddressCredential = ref(false);
|
||||
const showAdminAuth = ref(false);
|
||||
const auth = useStorage('auth', '');
|
||||
const adminAuth = useStorage('adminAuth', '');
|
||||
@@ -50,6 +50,22 @@ export const useGlobalState = createGlobalState(
|
||||
const adminSendBoxTabAddress = ref("");
|
||||
const mailboxSplitSize = useStorage('mailboxSplitSize', 0.25);
|
||||
const useIframeShowMail = useStorage('useIframeShowMail', false);
|
||||
const preferShowTextMail = useStorage('preferShowTextMail', false);
|
||||
const userJwt = useStorage('userJwt', '');
|
||||
const userTab = useStorage('userTab', 'user_settings');
|
||||
const indexTab = useStorage('indexTab', 'mailbox');
|
||||
const userOpenSettings = ref({
|
||||
enable: false,
|
||||
enableMailVerify: false,
|
||||
});
|
||||
const userSettings = ref({
|
||||
/** @type {boolean} */
|
||||
fetched: false,
|
||||
/** @type {string} */
|
||||
user_email: '',
|
||||
/** @type {number} */
|
||||
user_id: 0,
|
||||
});
|
||||
return {
|
||||
isDark,
|
||||
toggleDark,
|
||||
@@ -58,7 +74,7 @@ export const useGlobalState = createGlobalState(
|
||||
sendMailModel,
|
||||
openSettings,
|
||||
showAuth,
|
||||
showPassword,
|
||||
showAddressCredential,
|
||||
auth,
|
||||
jwt,
|
||||
localeCache,
|
||||
@@ -69,6 +85,12 @@ export const useGlobalState = createGlobalState(
|
||||
adminSendBoxTabAddress,
|
||||
mailboxSplitSize,
|
||||
useIframeShowMail,
|
||||
preferShowTextMail,
|
||||
userJwt,
|
||||
userTab,
|
||||
indexTab,
|
||||
userOpenSettings,
|
||||
userSettings,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
6
frontend/src/utils/index.js
Normal file
6
frontend/src/utils/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export const hashPassword = async (password) => {
|
||||
// user 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('');
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import SendBox from './admin/SendBox.vue';
|
||||
import Account from './admin/Account.vue';
|
||||
import CreateAccount from './admin/CreateAccount.vue';
|
||||
import AccountSettings from './admin/AccountSettings.vue';
|
||||
import UserManagement from './admin/UserManagement.vue';
|
||||
import UserSettings from './admin/UserSettings.vue';
|
||||
import Mails from './admin/Mails.vue';
|
||||
import MailsUnknow from './admin/MailsUnknow.vue';
|
||||
import Maintenance from './admin/Maintenance.vue';
|
||||
@@ -37,6 +39,8 @@ const { t } = useI18n({
|
||||
account: 'Account',
|
||||
account_create: 'Create Account',
|
||||
account_settings: 'Account Settings',
|
||||
user_management: 'User Management',
|
||||
user_settings: 'User Settings',
|
||||
unknow: 'Mails with unknow receiver',
|
||||
senderAccess: 'Sender Access Control',
|
||||
sendBox: 'Send Box',
|
||||
@@ -50,6 +54,8 @@ const { t } = useI18n({
|
||||
account: '账号',
|
||||
account_create: '创建账号',
|
||||
account_settings: '账号设置',
|
||||
user_management: '用户管理',
|
||||
user_settings: '用户设置',
|
||||
unknow: '无收件人邮件',
|
||||
senderAccess: '发件权限控制',
|
||||
sendBox: '发件箱',
|
||||
@@ -90,6 +96,12 @@ onMounted(async () => {
|
||||
<n-tab-pane name="account_settings" :tab="t('account_settings')">
|
||||
<AccountSettings />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="user_management" :tab="t('user_management')">
|
||||
<UserManagement />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="user_settings" :tab="t('user_settings')">
|
||||
<UserSettings />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="mails" :tab="t('mails')">
|
||||
<Mails />
|
||||
</n-tab-pane>
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
<script setup>
|
||||
import useClipboard from 'vue-clipboard3'
|
||||
import { ref, h, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRoute, useRouter, RouterLink } from 'vue-router'
|
||||
import { useIsMobile } from '../utils/composables'
|
||||
import {
|
||||
DarkModeFilled, LightModeFilled, MenuFilled,
|
||||
AdminPanelSettingsFilled, SendFilled
|
||||
AdminPanelSettingsFilled
|
||||
} from '@vicons/material'
|
||||
import { GithubAlt, Language, User, Home, Copy } from '@vicons/fa'
|
||||
|
||||
import Login from './Login.vue'
|
||||
import { GithubAlt, Language, User, Home } from '@vicons/fa'
|
||||
|
||||
import { useGlobalState } from '../store'
|
||||
import { api } from '../api'
|
||||
const { toClipboard } = useClipboard()
|
||||
const message = useMessage()
|
||||
|
||||
const {
|
||||
jwt, localeCache, toggleDark, isDark, settings, showPassword,
|
||||
localeCache, toggleDark, isDark, openSettings,
|
||||
showAuth, adminAuth, auth, loading
|
||||
} = useGlobalState()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const isMobile = useIsMobile()
|
||||
const isAdminRoute = computed(() => route.path.includes('admin'))
|
||||
|
||||
const showMobileMenu = ref(false)
|
||||
const menuValue = computed(() => {
|
||||
if (route.path.includes("user")) return "user";
|
||||
if (route.path.includes("admin")) return "admin";
|
||||
return "home";
|
||||
});
|
||||
|
||||
const authFunc = async () => {
|
||||
try {
|
||||
@@ -49,19 +49,11 @@ const { t } = useI18n({
|
||||
dark: 'Dark',
|
||||
light: 'Light',
|
||||
accessHeader: 'Access Password',
|
||||
accessTip: 'Please enter the correct password',
|
||||
accessTip: 'Please enter the correct access password',
|
||||
home: 'Home',
|
||||
menu: 'Menu',
|
||||
user: 'User',
|
||||
sendMail: 'Send Mail',
|
||||
yourAddress: 'Your email address is',
|
||||
ok: 'OK',
|
||||
copy: 'Copy',
|
||||
copied: 'Copied',
|
||||
fetchAddressError: 'Login password is invalid or account not exist, it may be network connection issue, please try again later.',
|
||||
mailV1Alert: 'You have some mails in v1, please click here to login and visit your history mails.',
|
||||
password: 'Password',
|
||||
passwordTip: 'Please copy the password and you can use it to login to your email account.',
|
||||
},
|
||||
zh: {
|
||||
title: 'Cloudflare 临时邮件',
|
||||
@@ -72,36 +64,25 @@ const { t } = useI18n({
|
||||
home: '主页',
|
||||
menu: '菜单',
|
||||
user: '用户',
|
||||
sendMail: '发送邮件',
|
||||
yourAddress: '你的邮箱地址是',
|
||||
ok: '确定',
|
||||
copy: '复制',
|
||||
copied: '已复制',
|
||||
fetchAddressError: '登录密码无效或账号不存在,也可能是网络连接异常,请稍后再尝试。',
|
||||
mailV1Alert: '你有一些 v1 版本的邮件,请点击此处登录查看。',
|
||||
password: '密码',
|
||||
passwordTip: '请复制密码,你可以使用它登录你的邮箱。',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const showUserMenu = computed(() => !!settings.value.address)
|
||||
|
||||
const menuOptions = computed(() => [
|
||||
{
|
||||
label: () => h(
|
||||
NButton,
|
||||
label: () => h(NButton,
|
||||
{
|
||||
text: true,
|
||||
size: "small",
|
||||
type: menuValue.value == "home" ? "primary" : "default",
|
||||
style: "width: 100%",
|
||||
onClick: () => { router.push('/'); showMobileMenu.value = false; }
|
||||
onClick: async () => { await router.push('/'); showMobileMenu.value = false; }
|
||||
},
|
||||
{
|
||||
default: () => t('home'),
|
||||
icon: () => h(NIcon, { component: Home })
|
||||
}
|
||||
),
|
||||
}),
|
||||
key: "home"
|
||||
},
|
||||
{
|
||||
@@ -110,8 +91,26 @@ const menuOptions = computed(() => [
|
||||
{
|
||||
text: true,
|
||||
size: "small",
|
||||
type: menuValue.value == "user" ? "primary" : "default",
|
||||
style: "width: 100%",
|
||||
onClick: () => { router.push('/admin'); showMobileMenu.value = false; }
|
||||
onClick: async () => { await router.push("/user"); showMobileMenu.value = false; }
|
||||
},
|
||||
{
|
||||
default: () => t('user'),
|
||||
icon: () => h(NIcon, { component: User }),
|
||||
}
|
||||
),
|
||||
key: "user",
|
||||
},
|
||||
{
|
||||
label: () => h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
size: "small",
|
||||
type: menuValue.value == "admin" ? "primary" : "default",
|
||||
style: "width: 100%",
|
||||
onClick: async () => { await router.push('/admin'); showMobileMenu.value = false; }
|
||||
},
|
||||
{
|
||||
default: () => "Admin",
|
||||
@@ -121,23 +120,6 @@ const menuOptions = computed(() => [
|
||||
show: !!adminAuth.value,
|
||||
key: "admin"
|
||||
},
|
||||
{
|
||||
label: () => h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
size: "small",
|
||||
style: "width: 100%",
|
||||
onClick: () => { router.push("/user"); showMobileMenu.value = false; }
|
||||
},
|
||||
{
|
||||
default: () => t('user'),
|
||||
icon: () => h(NIcon, { component: User }),
|
||||
}
|
||||
),
|
||||
show: showUserMenu.value,
|
||||
key: "user",
|
||||
},
|
||||
{
|
||||
label: () => h(
|
||||
NButton,
|
||||
@@ -197,18 +179,8 @@ const menuOptions = computed(() => [
|
||||
}
|
||||
]);
|
||||
|
||||
const copy = async () => {
|
||||
try {
|
||||
await toClipboard(settings.value.address)
|
||||
message.success(t('copied'));
|
||||
} catch (e) {
|
||||
message.error(e.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await api.getOpenSettings(message);
|
||||
await api.getSettings();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -223,7 +195,7 @@ onMounted(async () => {
|
||||
</template>
|
||||
<template #extra>
|
||||
<n-space>
|
||||
<n-menu v-if="!isMobile" mode="horizontal" :options="menuOptions" />
|
||||
<n-menu v-if="!isMobile" mode="horizontal" :options="menuOptions" responsive />
|
||||
<n-button v-else :text="true" @click="showMobileMenu = !showMobileMenu" style="margin-right: 10px;">
|
||||
<template #icon>
|
||||
<n-icon :component="MenuFilled" />
|
||||
@@ -238,49 +210,6 @@ onMounted(async () => {
|
||||
<n-menu :options="menuOptions" />
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
<div v-if="!isAdminRoute">
|
||||
<n-card v-if="!settings.fetched">
|
||||
<n-skeleton style="height: 50vh" />
|
||||
</n-card>
|
||||
<div v-else-if="settings.address">
|
||||
<n-alert v-if="settings.has_v1_mails" type="warning" show-icon closable>
|
||||
<span>
|
||||
<n-button tag="a" target="_blank" tertiary type="info" size="small"
|
||||
href="https://mail-v1.awsl.uk">
|
||||
<b>{{ t('mailV1Alert') }} </b>
|
||||
</n-button>
|
||||
</span>
|
||||
</n-alert>
|
||||
<n-alert type="info" show-icon>
|
||||
<span>
|
||||
<b>{{ t('yourAddress') }} <b>{{ settings.address }}</b></b>
|
||||
<n-button style="margin-left: 10px" @click="router.push('/send')" size="small" tertiary
|
||||
type="primary">
|
||||
<n-icon :component="SendFilled" /> {{ t('sendMail') }}
|
||||
</n-button>
|
||||
<n-button style="margin-left: 10px" @click="copy" size="small" tertiary type="primary">
|
||||
<n-icon :component="Copy" /> {{ t('copy') }}
|
||||
</n-button>
|
||||
</span>
|
||||
</n-alert>
|
||||
</div>
|
||||
<div v-else class="center">
|
||||
<n-card style="max-width: 600px;">
|
||||
<n-alert v-if="jwt" type="warning" show-icon>
|
||||
<span>{{ t('fetchAddressError') }}</span>
|
||||
</n-alert>
|
||||
<Login />
|
||||
</n-card>
|
||||
</div>
|
||||
</div>
|
||||
<n-modal v-model:show="showPassword" preset="dialog" :title="t('password')">
|
||||
<span>
|
||||
<p>{{ t("passwordTip") }}</p>
|
||||
</span>
|
||||
<n-card>
|
||||
<b>{{ jwt }}</b>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showAuth" :closable="false" :closeOnEsc="false" :maskClosable="false" preset="dialog"
|
||||
:title="t('accessHeader')">
|
||||
<p>{{ t('accessTip') }}</p>
|
||||
|
||||
@@ -1,9 +1,37 @@
|
||||
<script setup>
|
||||
import MailBox from '../components/MailBox.vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useGlobalState } from '../store'
|
||||
import { api } from '../api'
|
||||
|
||||
const { settings, openSettings } = useGlobalState()
|
||||
import AddressBar from './index/AddressBar.vue';
|
||||
import MailBox from '../components/MailBox.vue';
|
||||
import AutoReply from './index/AutoReply.vue';
|
||||
import SendBox from './index/SendBox.vue';
|
||||
import SendMail from './index/SendMail.vue';
|
||||
import AccountSettings from './index/AccountSettings.vue';
|
||||
|
||||
const { localeCache, settings, openSettings, indexTab } = useGlobalState()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
mailbox: 'Mail Box',
|
||||
sendbox: 'Send Box',
|
||||
sendmail: 'Send Mail',
|
||||
auto_reply: 'Auto Reply',
|
||||
accountSettings: 'Account Settings',
|
||||
},
|
||||
zh: {
|
||||
mailbox: '收件箱',
|
||||
sendbox: '发件箱',
|
||||
sendmail: '发送邮件',
|
||||
auto_reply: '自动回复',
|
||||
accountSettings: '账户设置',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const fetchMailData = async (limit, offset) => {
|
||||
return await api.fetch(`/api/mails?limit=${limit}&offset=${offset}`);
|
||||
@@ -15,8 +43,25 @@ const deleteMail = async (curMailId) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="settings.address">
|
||||
<MailBox :showEMailTo="false" :showReply="true" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
||||
:fetchMailData="fetchMailData" :deleteMail="deleteMail" />
|
||||
<div>
|
||||
<AddressBar />
|
||||
<n-tabs v-if="settings.address" type="card" v-model:value="indexTab">
|
||||
<n-tab-pane name="mailbox" :tab="t('mailbox')">
|
||||
<MailBox :showEMailTo="false" :showReply="true" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
||||
:fetchMailData="fetchMailData" :deleteMail="deleteMail" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="sendbox" :tab="t('sendbox')">
|
||||
<SendBox />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="sendmail" :tab="t('sendmail')">
|
||||
<SendMail />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="accountSettings" :tab="t('accountSettings')">
|
||||
<AccountSettings />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="openSettings.enableAutoReply" name="auto_reply" :tab="t('auto_reply')">
|
||||
<AutoReply />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
|
||||
import { useGlobalState } from '../store'
|
||||
|
||||
import AutoReply from './user/AutoReply.vue';
|
||||
import SendBox from './send/SendBox.vue';
|
||||
import Account from './user/Account.vue';
|
||||
import AddressMangement from './user/AddressManagement.vue';
|
||||
import UserSettingsPage from './user/UserSettings.vue';
|
||||
import UserBar from './user/UserBar.vue';
|
||||
import BindAddress from './user/BindAddress.vue';
|
||||
|
||||
const { localeCache, settings, openSettings } = useGlobalState()
|
||||
const userTab = useStorage('userTab', 'account')
|
||||
const {
|
||||
localeCache, userTab, userOpenSettings, userSettings
|
||||
} = useGlobalState()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
sendbox: 'Send Box',
|
||||
auto_reply: 'Auto Reply',
|
||||
account: 'Account',
|
||||
address_management: 'Address Management',
|
||||
user_settings: 'User Settings',
|
||||
bind_address: 'Bind Mail Address',
|
||||
},
|
||||
zh: {
|
||||
sendbox: '发件箱',
|
||||
auto_reply: '自动回复',
|
||||
account: '账户',
|
||||
address_management: '地址管理',
|
||||
user_settings: '用户设置',
|
||||
bind_address: '绑定邮箱地址',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -30,16 +31,17 @@ const { t } = useI18n({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="settings.address">
|
||||
<n-tabs type="card" v-model:value="userTab">
|
||||
<n-tab-pane name="account" :tab="t('account')">
|
||||
<Account />
|
||||
<div>
|
||||
<UserBar />
|
||||
<n-tabs v-if="userSettings.user_email" type="card" v-model:value="userTab">
|
||||
<n-tab-pane name="address_management" :tab="t('address_management')">
|
||||
<AddressMangement />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="sendbox" :tab="t('sendbox')">
|
||||
<SendBox />
|
||||
<n-tab-pane name="user_settings" :tab="t('user_settings')">
|
||||
<UserSettingsPage />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="openSettings.enableAutoReply" name="auto_reply" :tab="t('auto_reply')">
|
||||
<AutoReply />
|
||||
<n-tab-pane name="bind_address" :tab="t('bind_address')">
|
||||
<BindAddress />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
|
||||
@@ -23,9 +23,9 @@ const { t } = useI18n({
|
||||
updated_at: 'Update At',
|
||||
mail_count: 'Mail Count',
|
||||
send_count: 'Send Count',
|
||||
showPass: 'Show Passwrod',
|
||||
password: 'Password',
|
||||
passwordTip: 'Please copy the password and you can use it to login to your email account.',
|
||||
showCredential: 'Show 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.',
|
||||
delete: 'Delete',
|
||||
deleteTip: 'Are you sure to delete this email?',
|
||||
delteAccount: 'Delete Account',
|
||||
@@ -42,9 +42,9 @@ const { t } = useI18n({
|
||||
updated_at: '更新时间',
|
||||
mail_count: '邮件数量',
|
||||
send_count: '发送数量',
|
||||
showPass: '显示密码',
|
||||
password: '密码',
|
||||
passwordTip: '请复制密码,你可以使用它登录你的邮箱。',
|
||||
showCredential: '查看邮箱地址凭证',
|
||||
addressCredential: '邮箱地址凭证',
|
||||
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
||||
delete: '删除',
|
||||
deleteTip: '确定要删除这个邮箱吗?',
|
||||
delteAccount: '删除邮箱',
|
||||
@@ -58,8 +58,8 @@ const { t } = useI18n({
|
||||
}
|
||||
});
|
||||
|
||||
const showEmailPassword = ref(false)
|
||||
const curEmailPassword = ref("")
|
||||
const showEmailCredential = ref(false)
|
||||
const curEmailCredential = ref("")
|
||||
const curDeleteAddressId = ref(0);
|
||||
|
||||
const addressQuery = ref("")
|
||||
@@ -68,16 +68,16 @@ const data = ref([])
|
||||
const count = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const showDelteAccount = ref(false)
|
||||
const showDeleteAccount = ref(false)
|
||||
|
||||
const showPassword = async (id) => {
|
||||
const showCredential = async (id) => {
|
||||
try {
|
||||
curEmailPassword.value = await api.adminShowPassword(id)
|
||||
showEmailPassword.value = true
|
||||
curEmailCredential.value = await api.adminShowAddressCredential(id)
|
||||
showEmailCredential.value = true
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
showEmailPassword.value = false
|
||||
curEmailPassword.value = ""
|
||||
showEmailCredential.value = false
|
||||
curEmailCredential.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ const deleteEmail = async () => {
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
} finally {
|
||||
showDelteAccount.value = false
|
||||
showDeleteAccount.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,9 +197,9 @@ const columns = [
|
||||
label: () => h(NButton,
|
||||
{
|
||||
text: true,
|
||||
onClick: () => showPassword(row.id)
|
||||
onClick: () => showCredential(row.id)
|
||||
},
|
||||
{ default: () => t('showPass') }
|
||||
{ default: () => t('showCredential') }
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -232,7 +232,7 @@ const columns = [
|
||||
text: true,
|
||||
onClick: () => {
|
||||
curDeleteAddressId.value = row.id;
|
||||
showDelteAccount.value = true;
|
||||
showDeleteAccount.value = true;
|
||||
}
|
||||
},
|
||||
{ default: () => t('delete') }
|
||||
@@ -262,20 +262,20 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-modal v-model:show="showEmailPassword" preset="dialog" title="Dialog">
|
||||
<n-modal v-model:show="showEmailCredential" preset="dialog" title="Dialog">
|
||||
<template #header>
|
||||
<div>{{ t("password") }}</div>
|
||||
<div>{{ t("addressCredential") }}</div>
|
||||
</template>
|
||||
<span>
|
||||
<p>{{ t("passwordTip") }}</p>
|
||||
<p>{{ t("addressCredentialTip") }}</p>
|
||||
</span>
|
||||
<n-card>
|
||||
<b>{{ curEmailPassword }}</b>
|
||||
<b>{{ curEmailCredential }}</b>
|
||||
</n-card>
|
||||
<template #action>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showDelteAccount" preset="dialog" :title="t('delteAccount')">
|
||||
<n-modal v-model:show="showDeleteAccount" preset="dialog" :title="t('delteAccount')">
|
||||
<p>{{ t('deleteTip') }}</p>
|
||||
<template #action>
|
||||
<n-button :loading="loading" @click="deleteEmail" size="small" tertiary type="error">
|
||||
@@ -285,7 +285,7 @@ onMounted(async () => {
|
||||
</n-modal>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="addressQuery" clearable :placeholder="t('addressQueryTip')" />
|
||||
<n-button @click="fetchData" type="primary" ghost>
|
||||
<n-button @click="fetchData" type="primary" tertiary>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
|
||||
@@ -17,7 +17,7 @@ const { t } = useI18n({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-alert v-if="openSettings.adminContact" type="info" show-icon>
|
||||
<n-alert v-if="openSettings.adminContact" :show-icon="false">
|
||||
<span>{{ t('adminContact', { msg: openSettings.adminContact }) }}</span>
|
||||
</n-alert>
|
||||
</template>
|
||||
|
||||
@@ -19,7 +19,7 @@ const { t } = useI18n({
|
||||
creatNewEmail: 'Get New Email',
|
||||
fillInAllFields: 'Please fill in all fields',
|
||||
successTip: 'Success Created',
|
||||
password: 'Password',
|
||||
addressCredential: 'Mail Address Credential',
|
||||
},
|
||||
zh: {
|
||||
address: '地址',
|
||||
@@ -27,7 +27,7 @@ const { t } = useI18n({
|
||||
creatNewEmail: '创建新邮箱',
|
||||
fillInAllFields: '请填写完整信息',
|
||||
successTip: '创建成功',
|
||||
password: '密码',
|
||||
addressCredential: '邮箱地址凭证',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -70,8 +70,8 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-modal v-model:show="showReultModal" preset="dialog" :title="t('password')">
|
||||
<p>{{ t('password') }}</p>
|
||||
<n-modal v-model:show="showReultModal" preset="dialog" :title="t('addressCredential')">
|
||||
<p>{{ t('addressCredential') }}</p>
|
||||
<n-card>
|
||||
<b>{{ result }}</b>
|
||||
</n-card>
|
||||
|
||||
@@ -52,7 +52,7 @@ onMounted(async () => {
|
||||
<div>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="adminMailTabAddress" :placeholder="t('addressQueryTip')" />
|
||||
<n-button @click="queryAddress" type="primary" ghost>
|
||||
<n-button @click="queryAddress" type="primary" tertiary>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const { localeCache, adminAuth, adminSendBoxTabAddress } = useGlobalState()
|
||||
const { localeCache, adminAuth, adminSendBoxTabAddress, showAdminAuth } = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
@@ -102,7 +102,7 @@ const columns = [
|
||||
h(NButton,
|
||||
{
|
||||
type: 'success',
|
||||
ghost: true,
|
||||
tertiary: true,
|
||||
onClick: () => {
|
||||
showModal.value = true;
|
||||
curRow.value = row;
|
||||
@@ -135,7 +135,7 @@ onMounted(async () => {
|
||||
</n-modal>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="adminSendBoxTabAddress" />
|
||||
<n-button @click="fetchData" type="primary" ghost>
|
||||
<n-button @click="fetchData" type="primary" tertiary>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
|
||||
@@ -14,6 +14,7 @@ const { t } = useI18n({
|
||||
en: {
|
||||
address: 'Address',
|
||||
success: 'Success',
|
||||
is_enabled: 'Is Enabled',
|
||||
enable: 'Enable',
|
||||
disable: 'Disable',
|
||||
modify: 'Modify',
|
||||
@@ -28,6 +29,7 @@ const { t } = useI18n({
|
||||
zh: {
|
||||
address: '地址',
|
||||
success: '成功',
|
||||
is_enabled: '是否启用',
|
||||
enable: '启用',
|
||||
disable: '禁用',
|
||||
modify: '修改',
|
||||
@@ -108,7 +110,7 @@ const columns = [
|
||||
key: "balance"
|
||||
},
|
||||
{
|
||||
title: "Enabled",
|
||||
title: t('is_enabled'),
|
||||
key: "enabled",
|
||||
render(row) {
|
||||
return h('div', [
|
||||
@@ -124,7 +126,7 @@ const columns = [
|
||||
h(NButton,
|
||||
{
|
||||
type: 'success',
|
||||
ghost: true,
|
||||
tertiary: true,
|
||||
onClick: () => {
|
||||
showModal.value = true;
|
||||
curRow.value = row;
|
||||
@@ -170,7 +172,7 @@ onMounted(async () => {
|
||||
</n-modal>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="addressQuery" />
|
||||
<n-button @click="fetchData" type="primary" ghost>
|
||||
<n-button @click="fetchData" type="primary" tertiary>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
|
||||
289
frontend/src/views/admin/UserManagement.vue
Normal file
289
frontend/src/views/admin/UserManagement.vue
Normal file
@@ -0,0 +1,289 @@
|
||||
<script setup>
|
||||
import { ref, h, onMounted, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { NMenu, NButton, NBadge } from 'naive-ui';
|
||||
import { MenuFilled } from '@vicons/material'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import { hashPassword } from '../../utils';
|
||||
|
||||
const { localeCache, loading } = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
success: 'Success',
|
||||
user_email: 'User Email',
|
||||
address_count: 'Address Count',
|
||||
created_at: 'Created At',
|
||||
actions: 'Actions',
|
||||
query: 'Query',
|
||||
itemCount: 'itemCount',
|
||||
deleteUser: 'Delete User',
|
||||
delete: 'Delete',
|
||||
deleteUserTip: 'Are you sure you want to delete this user?',
|
||||
resetPassword: 'Reset Password',
|
||||
pleaseInput: 'Please input complete information',
|
||||
createUser: 'Create User',
|
||||
email: 'Email',
|
||||
password: 'Password',
|
||||
},
|
||||
zh: {
|
||||
success: '成功',
|
||||
user_email: '用户邮箱',
|
||||
address_count: '地址数量',
|
||||
created_at: '创建时间',
|
||||
actions: '操作',
|
||||
query: '查询',
|
||||
itemCount: '总数',
|
||||
deleteUser: '删除用户',
|
||||
delete: '删除',
|
||||
deleteUserTip: '确定要删除此用户吗?',
|
||||
resetPassword: '重置密码',
|
||||
pleaseInput: '请输入完整信息',
|
||||
createUser: '创建用户',
|
||||
email: '邮箱',
|
||||
password: '密码',
|
||||
}
|
||||
}
|
||||
});
|
||||
const data = ref([])
|
||||
const count = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
|
||||
const userQuery = ref('')
|
||||
const showResetPassword = ref(false)
|
||||
const newResetPassword = ref('')
|
||||
const showDeleteUser = ref(false)
|
||||
const curUserId = ref(0)
|
||||
const showCreateUser = ref(false)
|
||||
const user = ref({
|
||||
email: "",
|
||||
password: ""
|
||||
})
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const { results, count: userCount } = await api.fetch(
|
||||
`/admin/users`
|
||||
+ `?limit=${pageSize.value}`
|
||||
+ `&offset=${(page.value - 1) * pageSize.value}`
|
||||
+ (userQuery.value ? `&query=${userQuery.value}` : '')
|
||||
);
|
||||
data.value = results;
|
||||
if (userCount > 0) {
|
||||
count.value = userCount;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const resetPassword = async () => {
|
||||
if (!newResetPassword.value) {
|
||||
message.error(t('pleaseInput'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api.fetch(`/admin/users/${curUserId.value}/reset_password`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
password: await hashPassword(newResetPassword.value)
|
||||
})
|
||||
});
|
||||
message.success(t('success'));
|
||||
showResetPassword.value = false;
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const createUser = async () => {
|
||||
if (!user.value.email || !user.value.password) {
|
||||
message.error(t('pleaseInput'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api.fetch(`/admin/users`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
email: user.value.email,
|
||||
password: await hashPassword(user.value.password)
|
||||
})
|
||||
});
|
||||
message.success(t('success'));
|
||||
await fetchData();
|
||||
showCreateUser.value = false;
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const deleteUser = async () => {
|
||||
try {
|
||||
await api.fetch(`/admin/users/${curUserId.value}`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
message.success(t('success'));
|
||||
showDeleteUser.value = false;
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "ID",
|
||||
key: "id"
|
||||
},
|
||||
{
|
||||
title: t('user_email'),
|
||||
key: "user_email"
|
||||
},
|
||||
{
|
||||
title: t('address_count'),
|
||||
key: "address_count",
|
||||
render(row) {
|
||||
return h(NBadge, {
|
||||
value: row.address_count,
|
||||
'show-zero': true,
|
||||
max: 99,
|
||||
type: "success"
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('created_at'),
|
||||
key: "created_at"
|
||||
},
|
||||
{
|
||||
title: t('actions'),
|
||||
key: 'actions',
|
||||
render(row) {
|
||||
return h('div', [
|
||||
h(NMenu, {
|
||||
mode: "horizontal",
|
||||
options: [
|
||||
{
|
||||
label: t('actions'),
|
||||
icon: () => h(MenuFilled),
|
||||
key: "action",
|
||||
children: [
|
||||
{
|
||||
label: () => h(NButton,
|
||||
{
|
||||
text: true,
|
||||
onClick: () => {
|
||||
curUserId.value = row.id;
|
||||
newResetPassword.value = '';
|
||||
showResetPassword.value = true;
|
||||
}
|
||||
},
|
||||
{ default: () => t('resetPassword') }
|
||||
),
|
||||
},
|
||||
{
|
||||
label: () => h(NButton,
|
||||
{
|
||||
text: true,
|
||||
onClick: () => {
|
||||
curUserId.value = row.id;
|
||||
user.value.email = '';
|
||||
user.value.password = '';
|
||||
showDeleteUser.value = true;
|
||||
}
|
||||
},
|
||||
{ default: () => t('delete') }
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
watch([page, pageSize], async () => {
|
||||
await fetchData()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-modal v-model:show="showCreateUser" preset="dialog" :title="t('createUser')">
|
||||
<n-form>
|
||||
<n-form-item-row :label="t('email')" required>
|
||||
<n-input v-model:value="user.email" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('password')" required>
|
||||
<n-input v-model:value="user.password" type="password" show-password-on="click" />
|
||||
</n-form-item-row>
|
||||
</n-form>
|
||||
<template #action>
|
||||
<n-button :loading="loading" @click="createUser" size="small" tertiary type="primary">
|
||||
{{ t('createUser') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showResetPassword" preset="dialog" :title="t('resetPassword')">
|
||||
<n-form-item-row :label="t('password')" required>
|
||||
<n-input v-model:value="newResetPassword" type="password" show-password-on="click" />
|
||||
</n-form-item-row>
|
||||
<template #action>
|
||||
<n-button :loading="loading" @click="resetPassword" size="small" tertiary type="primary">
|
||||
{{ t('resetPassword') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showDeleteUser" preset="dialog" :title="t('deleteUser')">
|
||||
<p>{{ t('deleteUserTip') }}</p>
|
||||
<template #action>
|
||||
<n-button :loading="loading" @click="deleteUser" size="small" tertiary type="error">
|
||||
{{ t('deleteUser') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="userQuery" />
|
||||
<n-button @click="fetchData" type="primary" tertiary>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<div style="display: inline-block;">
|
||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count"
|
||||
:page-sizes="[20, 50, 100]" show-size-picker>
|
||||
<template #prefix="{ itemCount }">
|
||||
{{ t('itemCount') }}: {{ itemCount }}
|
||||
</template>
|
||||
<template #suffix>
|
||||
<n-button @click="showCreateUser = true" size="small" tertiary type="primary"
|
||||
style="margin-left: 10px">
|
||||
{{ t('createUser') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.n-pagination {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
130
frontend/src/views/admin/UserSettings.vue
Normal file
130
frontend/src/views/admin/UserSettings.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const { localeCache, loading } = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
save: 'Save',
|
||||
successTip: 'Save Success',
|
||||
enable: 'Enable',
|
||||
enableUserRegister: 'Allow User Register',
|
||||
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',
|
||||
enableMailAllowList: 'Enable Mail Address Allow List(Manually enterable)',
|
||||
mailAllowList: 'Mail Address Allow List',
|
||||
maxAddressCount: 'Maximum number of email addresses that can be binded',
|
||||
},
|
||||
zh: {
|
||||
save: '保存',
|
||||
successTip: '保存成功',
|
||||
enable: '启用',
|
||||
enableUserRegister: "允许用户注册",
|
||||
enableMailVerify: '启用邮件验证(发送地址必须是系统中能有余额且能正常发送邮件的地址)',
|
||||
verifyMailSender: '验证邮件发送地址',
|
||||
enableMailAllowList: '启用邮件地址白名单(可手动输入)',
|
||||
mailAllowList: '邮件地址白名单',
|
||||
maxAddressCount: '可绑定最大邮箱地址数量',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const commonMail = [
|
||||
"gmail.com", "163.com", "126.com", "qq.com", "outlook.com", "hotmail.com",
|
||||
"icloud.com", "yahoo.com", "foxmail.com"
|
||||
]
|
||||
|
||||
const mailAllowOptions = commonMail.map((item) => {
|
||||
return { label: item, value: item }
|
||||
})
|
||||
|
||||
const userSettings = ref({
|
||||
enable: false,
|
||||
enableMailVerify: false,
|
||||
verifyMailSender: "",
|
||||
enableMailAllowList: false,
|
||||
mailAllowList: commonMail,
|
||||
maxAddressCount: 5,
|
||||
});
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await api.fetch(`/admin/user_settings`)
|
||||
Object.assign(userSettings.value, res)
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
try {
|
||||
await api.fetch(`/admin/user_settings`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(userSettings.value)
|
||||
})
|
||||
message.success(t('successTip'))
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card style="max-width: 600px;">
|
||||
<n-form :model="userSettings">
|
||||
<n-form-item-row :label="t('enableUserRegister')">
|
||||
<n-checkbox v-model:checked="userSettings.enable" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('enableMailVerify')">
|
||||
<n-input-group>
|
||||
<n-checkbox v-model:checked="userSettings.enableMailVerify" style="width: 20%;">
|
||||
{{ t('enable') }}
|
||||
</n-checkbox>
|
||||
<n-input v-model:value="userSettings.verifyMailSender" style="width: 80%;"
|
||||
:placeholder="t('verifyMailSender')" />
|
||||
</n-input-group>
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('enableMailAllowList')">
|
||||
<n-input-group>
|
||||
<n-checkbox v-model:checked="userSettings.enableMailAllowList" style="width: 20%;">
|
||||
{{ t('enable') }}
|
||||
</n-checkbox>
|
||||
<n-select v-model:value="userSettings.mailAllowList" filterable multiple tag style="width: 80%;"
|
||||
:options="mailAllowOptions" :placeholder="t('mailAllowList')" />
|
||||
</n-input-group>
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('maxAddressCount')">
|
||||
<n-input-group>
|
||||
<n-input-number v-model:value="userSettings.maxAddressCount"
|
||||
:placeholder="t('maxAddressCount')" />
|
||||
</n-input-group>
|
||||
</n-form-item-row>
|
||||
<n-button @click="save" type="primary" block :loading="loading">
|
||||
{{ t('save') }}
|
||||
</n-button>
|
||||
</n-form>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
display: flex;
|
||||
text-align: left;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -7,8 +7,8 @@ import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const {
|
||||
jwt, localeCache, settings, showPassword, loading,
|
||||
mailboxSplitSize, useIframeShowMail
|
||||
jwt, localeCache, settings, showAddressCredential, loading,
|
||||
mailboxSplitSize, useIframeShowMail, preferShowTextMail
|
||||
} = useGlobalState()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
@@ -20,20 +20,22 @@ const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
mailboxSplitSize: 'Mailbox Split Size',
|
||||
useIframeShowMail: 'Use iframe Show Mail',
|
||||
useIframeShowMail: 'Use iframe Show HTML Mail',
|
||||
preferShowTextMail: 'Display text Mail by default',
|
||||
logout: "Logout",
|
||||
delteAccount: "Delete Account",
|
||||
showPassword: 'Show Password',
|
||||
showAddressCredential: 'Show Address Credential',
|
||||
logoutConfirm: 'Are you sure to logout?',
|
||||
delteAccount: "Delete Account",
|
||||
delteAccountConfirm: "Are you sure to delete your account and all emails for this account?",
|
||||
},
|
||||
zh: {
|
||||
mailboxSplitSize: '邮箱界面分栏大小',
|
||||
useIframeShowMail: '使用iframe显示邮件',
|
||||
preferShowTextMail: '默认以文本显示邮件',
|
||||
useIframeShowMail: '使用iframe显示HTML邮件',
|
||||
logout: '退出登录',
|
||||
delteAccount: "删除账户",
|
||||
showPassword: '查看密码',
|
||||
showAddressCredential: '查看邮箱地址凭证',
|
||||
logoutConfirm: '确定要退出登录吗?',
|
||||
delteAccount: "删除账户",
|
||||
delteAccountConfirm: "确定要删除你的账户和其中的所有邮件吗?",
|
||||
@@ -72,12 +74,15 @@ const deleteAccount = async () => {
|
||||
0.75: '0.75'
|
||||
}" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('preferShowTextMail')">
|
||||
<n-switch v-model:value="preferShowTextMail" :round="false" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('useIframeShowMail')">
|
||||
<n-switch v-model:value="useIframeShowMail" :round="false" />
|
||||
</n-form-item-row>
|
||||
</n-card>
|
||||
<n-button @click="showPassword = true" type="primary" secondary block strong>
|
||||
{{ t('showPassword') }}
|
||||
<n-button @click="showAddressCredential = true" type="primary" secondary block strong>
|
||||
{{ t('showAddressCredential') }}
|
||||
</n-button>
|
||||
<n-button @click="showLogout = true" secondary block strong>
|
||||
{{ t('logout') }}
|
||||
128
frontend/src/views/index/AddressBar.vue
Normal file
128
frontend/src/views/index/AddressBar.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<script setup>
|
||||
import useClipboard from 'vue-clipboard3'
|
||||
import { onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Copy, User } from '@vicons/fa'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import Login from './Login.vue'
|
||||
|
||||
const { toClipboard } = useClipboard()
|
||||
const message = useMessage()
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
jwt, localeCache, settings, showAddressCredential, openSettings
|
||||
} = useGlobalState()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
yourAddress: 'Your email address is',
|
||||
ok: 'OK',
|
||||
copy: 'Copy',
|
||||
copied: 'Copied',
|
||||
fetchAddressError: 'Mail address credential is invalid or account not exist, it may be network connection issue, please try again later.',
|
||||
mailV1Alert: 'You have some mails in v1, please click here to login and visit your history mails.',
|
||||
addressCredential: 'Mail Address Credential',
|
||||
addressCredentialTip: 'Please copy the Mail Address Credential and you can use it to login to your email account.',
|
||||
userLogin: 'User Login',
|
||||
},
|
||||
zh: {
|
||||
yourAddress: '你的邮箱地址是',
|
||||
ok: '确定',
|
||||
copy: '复制',
|
||||
copied: '已复制',
|
||||
fetchAddressError: '邮箱地址凭证无效或邮箱地址不存在,也可能是网络连接异常,请稍后再尝试。',
|
||||
mailV1Alert: '你有一些 v1 版本的邮件,请点击此处登录查看。',
|
||||
addressCredential: '邮箱地址凭证',
|
||||
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
||||
userLogin: '用户登录',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const copy = async () => {
|
||||
try {
|
||||
await toClipboard(settings.value.address)
|
||||
message.success(t('copied'));
|
||||
} catch (e) {
|
||||
message.error(e.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await api.getSettings();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-card v-if="!settings.fetched">
|
||||
<n-skeleton style="height: 50vh" />
|
||||
</n-card>
|
||||
<div v-else-if="settings.address">
|
||||
<n-alert v-if="settings.has_v1_mails" type="warning" :show-icon="false" closable>
|
||||
<span>
|
||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" href="https://mail-v1.awsl.uk">
|
||||
<b>{{ t('mailV1Alert') }} </b>
|
||||
</n-button>
|
||||
</span>
|
||||
</n-alert>
|
||||
<n-alert type="info" :show-icon="false">
|
||||
<span>
|
||||
<b>{{ t('yourAddress') }} <b>{{ settings.address }}</b></b>
|
||||
<n-button style="margin-left: 10px" @click="copy" size="small" tertiary type="primary">
|
||||
<n-icon :component="Copy" /> {{ t('copy') }}
|
||||
</n-button>
|
||||
</span>
|
||||
</n-alert>
|
||||
</div>
|
||||
<div v-else class="center">
|
||||
<n-card style="max-width: 600px;">
|
||||
<n-alert v-if="jwt" type="warning" :show-icon="false" closable>
|
||||
<span>{{ t('fetchAddressError') }}</span>
|
||||
</n-alert>
|
||||
<Login />
|
||||
<n-divider />
|
||||
<n-button @click="router.push('/user')" type="primary" block secondary strong>
|
||||
<template #icon>
|
||||
<n-icon :component="User" />
|
||||
</template>
|
||||
{{ t('userLogin') }}
|
||||
</n-button>
|
||||
</n-card>
|
||||
</div>
|
||||
<n-modal v-model:show="showAddressCredential" preset="dialog" :title="t('addressCredential')">
|
||||
<span>
|
||||
<p>{{ t("addressCredentialTip") }}</p>
|
||||
</span>
|
||||
<n-card>
|
||||
<b>{{ jwt }}</b>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.n-alert {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.n-card {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
text-align: left;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
margin: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -2,33 +2,41 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import AdminContact from './admin/AdminContact.vue'
|
||||
import Turnstile from '../components/Turnstile.vue'
|
||||
import { NewLabelOutlined, EmailOutlined } from '@vicons/material'
|
||||
|
||||
import { useGlobalState } from '../store'
|
||||
import { api } from '../api'
|
||||
import AdminContact from '../admin/AdminContact.vue'
|
||||
import Turnstile from '../../components/Turnstile.vue'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
const message = useMessage()
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
jwt, localeCache, loading, openSettings, showPassword
|
||||
jwt, localeCache, loading, openSettings,
|
||||
showAddressCredential, userSettings
|
||||
} = useGlobalState()
|
||||
|
||||
const tabValue = ref('signin')
|
||||
const password = ref('')
|
||||
const credential = ref('')
|
||||
const emailName = ref("")
|
||||
const emailDomain = ref("")
|
||||
const cfToken = ref("")
|
||||
|
||||
const login = async () => {
|
||||
if (!password.value) {
|
||||
message.error(t('passwordInput'));
|
||||
if (!credential.value) {
|
||||
message.error(t('credentialInput'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
jwt.value = password.value;
|
||||
await api.getSettings()
|
||||
location.reload()
|
||||
jwt.value = credential.value;
|
||||
await api.getSettings();
|
||||
try {
|
||||
await api.bindUserAddress();
|
||||
} catch (error) {
|
||||
message.error(`${t('bindUserAddressError')}: ${error.message}`);
|
||||
}
|
||||
await router.push("/");
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
@@ -40,28 +48,32 @@ const { t } = useI18n({
|
||||
en: {
|
||||
login: 'Login',
|
||||
pleaseGetNewEmail: 'Please login or click "Get New Email" button to get a new email address',
|
||||
getNewEmail: 'Get New Email',
|
||||
getNewEmail: 'Create New Email',
|
||||
getNewEmailTip1: 'Please input the email you want to use. only allow ., a-z, A-Z and 0-9',
|
||||
getNewEmailTip2: 'Levaing it blank will generate a random email address.',
|
||||
getNewEmailTip3: 'You can choose a domain from the dropdown list.',
|
||||
password: 'Password',
|
||||
credential: 'Email Address Credential',
|
||||
ok: 'OK',
|
||||
generateName: 'Generate Fake Name',
|
||||
help: 'Help',
|
||||
passwordInput: 'Please input the password',
|
||||
credentialInput: 'Please input the Mail Address Credential',
|
||||
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',
|
||||
},
|
||||
zh: {
|
||||
login: '登录',
|
||||
pleaseGetNewEmail: '请"登录"或点击 "获取新邮箱" 按钮来获取一个新的邮箱地址',
|
||||
getNewEmail: '注册新邮箱',
|
||||
pleaseGetNewEmail: '请"登录"或点击 "注册新邮箱" 按钮来获取一个新的邮箱地址',
|
||||
getNewEmail: '创建新邮箱',
|
||||
getNewEmailTip1: '请输入你想要使用的邮箱地址, 只允许 ., a-z, A-Z, 0-9',
|
||||
getNewEmailTip2: '留空将会生成一个随机的邮箱地址。',
|
||||
getNewEmailTip3: '你可以从下拉列表中选择一个域名。',
|
||||
password: '密码',
|
||||
credential: '邮箱地址凭据',
|
||||
ok: '确定',
|
||||
generateName: '生成随机名字',
|
||||
help: '帮助',
|
||||
passwordInput: '请输入密码',
|
||||
credentialInput: '请输入邮箱地址凭据',
|
||||
bindUserInfo: '已登录用户, 登录未绑定邮箱或创建新邮箱地址将绑定到当前用户',
|
||||
bindUserAddressError: '绑定邮箱地址到用户时错误',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -95,7 +107,13 @@ const newEmail = async () => {
|
||||
});
|
||||
jwt.value = res["jwt"];
|
||||
await api.getSettings();
|
||||
showPassword.value = true;
|
||||
await router.push("/");
|
||||
showAddressCredential.value = true;
|
||||
try {
|
||||
await api.bindUserAddress();
|
||||
} catch (error) {
|
||||
message.error(`${t('bindUserAddressError')}: ${error.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
@@ -108,17 +126,26 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-alert v-if="userSettings.user_email" :show-icon="false" closable>
|
||||
<span>{{ t('bindUserInfo') }}</span>
|
||||
</n-alert>
|
||||
<n-tabs v-model:value="tabValue" size="large" justify-content="space-evenly">
|
||||
<n-tab-pane name="signin" :tab="t('login')">
|
||||
<n-form>
|
||||
<n-form-item-row :label="t('password')" required>
|
||||
<n-input v-model:value="password" type="textarea" :autosize="{ minRows: 3 }" />
|
||||
<n-form-item-row :label="t('credential')" required>
|
||||
<n-input v-model:value="credential" type="textarea" :autosize="{ minRows: 3 }" />
|
||||
</n-form-item-row>
|
||||
<n-button @click="login" :loading="loading" type="primary" block secondary strong>
|
||||
<template #icon>
|
||||
<n-icon :component="EmailOutlined" />
|
||||
</template>
|
||||
{{ t('login') }}
|
||||
</n-button>
|
||||
<n-button v-if="openSettings.enableUserCreateEmail" @click="tabValue = 'register'" block secondary
|
||||
strong>
|
||||
<template #icon>
|
||||
<n-icon :component="NewLabelOutlined" />
|
||||
</template>
|
||||
{{ t('getNewEmail') }}
|
||||
</n-button>
|
||||
</n-form>
|
||||
@@ -145,13 +172,16 @@ onMounted(async () => {
|
||||
</n-input-group>
|
||||
<Turnstile v-model:value="cfToken" />
|
||||
<n-button type="primary" block secondary strong @click="newEmail" :loading="loading">
|
||||
{{ t('ok') }}
|
||||
<template #icon>
|
||||
<n-icon :component="NewLabelOutlined" />
|
||||
</template>
|
||||
{{ t('getNewEmail') }}
|
||||
</n-button>
|
||||
</n-form>
|
||||
</n-spin>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="help" :tab="t('help')">
|
||||
<n-alert type="info" show-icon>
|
||||
<n-alert :show-icon="false">
|
||||
<span>{{ t('pleaseGetNewEmail') }}</span>
|
||||
</n-alert>
|
||||
<AdminContact />
|
||||
@@ -171,4 +201,8 @@ onMounted(async () => {
|
||||
.n-form .n-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.n-form {
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
@@ -138,7 +138,7 @@ onMounted(async () => {
|
||||
{{ t('itemCount') }}: {{ itemCount }}
|
||||
</template>
|
||||
<template #suffix>
|
||||
<n-button @click="fetchData" type="primary" size="small" ghost>
|
||||
<n-button @click="fetchData" type="primary" size="small" tertiary>
|
||||
{{ t('refresh') }}
|
||||
</n-button>
|
||||
</template>
|
||||
@@ -7,14 +7,13 @@ import AdminContact from '../admin/AdminContact.vue'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import router from '../../router'
|
||||
|
||||
const message = useMessage()
|
||||
const isPreview = ref(false)
|
||||
const editorRef = shallowRef()
|
||||
|
||||
|
||||
const { settings, sendMailModel } = useGlobalState()
|
||||
const { settings, sendMailModel, indexTab } = useGlobalState()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: 'zh',
|
||||
@@ -91,7 +90,7 @@ const send = async () => {
|
||||
message.error(error.message || "error");
|
||||
} finally {
|
||||
message.success(t("successSend"));
|
||||
router.push('/user');
|
||||
indexTab.value = 'sendbox'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,15 +144,16 @@ onMounted(async () => {
|
||||
<div class="center" v-if="settings.address">
|
||||
<n-card>
|
||||
<div v-if="!settings.send_balance || settings.send_balance <= 0">
|
||||
<n-alert type="warning" show-icon>
|
||||
<n-alert type="warning" :show-icon="false">
|
||||
{{ t('requestAccessTip') }}
|
||||
<n-button type="primary" ghost @click="requestAccess">{{ t('requestAccess') }}</n-button>
|
||||
<n-button type="primary" tertiary @click="requestAccess" size="small">{{ t('requestAccess')
|
||||
}}</n-button>
|
||||
</n-alert>
|
||||
<br />
|
||||
<AdminContact />
|
||||
</div>
|
||||
<div v-else>
|
||||
<n-alert type="info" show-icon>
|
||||
<n-alert type="info" :show-icon="false">
|
||||
{{ t('send_balance') }}: {{ settings.send_balance }}
|
||||
</n-alert>
|
||||
<div class="right">
|
||||
170
frontend/src/views/user/AddressManagement.vue
Normal file
170
frontend/src/views/user/AddressManagement.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<script setup>
|
||||
import { ref, h, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router';
|
||||
import { NBadge, NPopconfirm, NButton } from 'naive-ui'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const { localeCache, jwt } = useGlobalState()
|
||||
const message = useMessage()
|
||||
const router = useRouter()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
success: 'success',
|
||||
name: 'Name',
|
||||
mail_count: 'Mail Count',
|
||||
send_count: 'Send Count',
|
||||
actions: 'Actions',
|
||||
changeMailAddress: 'Change Mail Address',
|
||||
unbindAddress: 'Unbind Address',
|
||||
unbindAddressTip: 'Before unbinding, please switch to this email address and save the email address credential.',
|
||||
},
|
||||
zh: {
|
||||
success: '成功',
|
||||
name: '名称',
|
||||
mail_count: '邮件数量',
|
||||
send_count: '发送数量',
|
||||
actions: '操作',
|
||||
changeMailAddress: '切换邮箱地址',
|
||||
unbindAddress: '解绑地址',
|
||||
unbindAddressTip: '解绑前请切换到此邮箱地址并保存邮箱地址凭证。',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const data = ref([])
|
||||
|
||||
const changeMailAddress = async (address_id) => {
|
||||
try {
|
||||
const res = await api.fetch(`/user_api/bind_address_jwt/${address_id}`);
|
||||
message.success(t('changeMailAddress') + " " + t('success'));
|
||||
if (!res.jwt) {
|
||||
message.error("jwt not found");
|
||||
return;
|
||||
}
|
||||
jwt.value = res.jwt;
|
||||
await router.push('/');
|
||||
location.reload();
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const unbindAddress = async (address_id) => {
|
||||
try {
|
||||
const res = await api.fetch(`/user_api/unbind_address`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ address_id })
|
||||
});
|
||||
message.success(t('unbindAddress') + " " + t('success'));
|
||||
await fetchData();
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const { results, count: addressCount } = await api.fetch(
|
||||
`/user_api/bind_address`
|
||||
);
|
||||
data.value = results;
|
||||
if (addressCount > 0) {
|
||||
count.value = addressCount;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "ID",
|
||||
key: "id"
|
||||
},
|
||||
{
|
||||
title: t('name'),
|
||||
key: "name"
|
||||
},
|
||||
{
|
||||
title: t('mail_count'),
|
||||
key: "mail_count",
|
||||
render(row) {
|
||||
return h(NBadge, {
|
||||
value: row.mail_count,
|
||||
'show-zero': true,
|
||||
max: 99,
|
||||
type: "success"
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('send_count'),
|
||||
key: "send_count",
|
||||
render(row) {
|
||||
return h(NBadge, {
|
||||
value: row.send_count,
|
||||
'show-zero': true,
|
||||
max: 99,
|
||||
type: "success"
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('actions'),
|
||||
key: 'actions',
|
||||
render(row) {
|
||||
return h('div', [
|
||||
h(NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => changeMailAddress(row.id)
|
||||
},
|
||||
{
|
||||
trigger: () => h(NButton,
|
||||
{
|
||||
tertiary: true,
|
||||
type: "primary",
|
||||
},
|
||||
{ default: () => t('changeMailAddress') }
|
||||
),
|
||||
default: () => `${t('changeMailAddress')}?`
|
||||
}
|
||||
),
|
||||
h(NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => unbindAddress(row.id)
|
||||
},
|
||||
{
|
||||
trigger: () => h(NButton,
|
||||
{
|
||||
tertiary: true,
|
||||
type: "error",
|
||||
},
|
||||
{ default: () => t('unbindAddress') }
|
||||
),
|
||||
default: () => t('unbindAddressTip')
|
||||
}
|
||||
),
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
||||
</div>
|
||||
</template>
|
||||
46
frontend/src/views/user/BindAddress.vue
Normal file
46
frontend/src/views/user/BindAddress.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import Login from '../index/Login.vue'
|
||||
|
||||
const { userJwt, localeCache, userSettings, } = useGlobalState()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
logout: 'Logout',
|
||||
},
|
||||
zh: {
|
||||
logout: '退出登录',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const fetchData = async () => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="center" v-if="userSettings.user_email">
|
||||
<n-card style="max-width: 600px;">
|
||||
<Login />
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
76
frontend/src/views/user/UserBar.vue
Normal file
76
frontend/src/views/user/UserBar.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import UserLogin from './UserLogin.vue'
|
||||
|
||||
const message = useMessage()
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
localeCache, userSettings, userJwt, userOpenSettings
|
||||
} = useGlobalState()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
currentUser: 'Current Login User',
|
||||
fetchUserSettingsError: 'Login password is invalid or account not exist, it may be network connection issue, please try again later.',
|
||||
},
|
||||
zh: {
|
||||
currentUser: '当前登录用户',
|
||||
fetchUserSettingsError: '登录信息已过期或账号不存在,也可能是网络连接异常,请稍后再尝试。',
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
await api.getUserOpenSettings(message);
|
||||
await api.getUserSettings(message);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-card v-if="!userSettings.fetched">
|
||||
<n-skeleton style="height: 50vh" />
|
||||
</n-card>
|
||||
<div v-else-if="userSettings.user_email">
|
||||
<n-alert type="success" :show-icon="false">
|
||||
<span>
|
||||
<b>{{ t('currentUser') }} <b>{{ userSettings.user_email }}</b></b>
|
||||
</span>
|
||||
</n-alert>
|
||||
</div>
|
||||
<div v-else class="center">
|
||||
<n-card style="max-width: 600px;">
|
||||
<n-alert v-if="userJwt" type="warning" :show-icon="false" closable>
|
||||
<span>{{ t('fetchUserSettingsError') }}</span>
|
||||
</n-alert>
|
||||
<UserLogin />
|
||||
</n-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.n-alert {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
margin: 20px;
|
||||
}
|
||||
</style>
|
||||
248
frontend/src/views/user/UserLogin.vue
Normal file
248
frontend/src/views/user/UserLogin.vue
Normal file
@@ -0,0 +1,248 @@
|
||||
<script setup>
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { api } from '../../api';
|
||||
import { useGlobalState } from '../../store'
|
||||
import { hashPassword } from '../../utils';
|
||||
|
||||
import Turnstile from '../../components/Turnstile.vue';
|
||||
|
||||
const { userJwt, localeCache, userTab, userOpenSettings } = useGlobalState()
|
||||
const message = useMessage();
|
||||
const router = useRouter();
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
login: 'Login',
|
||||
register: 'Register',
|
||||
email: 'Email',
|
||||
password: 'Password',
|
||||
verifyCode: 'Verification Code',
|
||||
verifyCodeSent: 'Verification Code Sent, expires in {timeout} seconds',
|
||||
waitforVerifyCode: 'Wait for {timeout} seconds',
|
||||
sendVerificationCode: 'Send Verification Code',
|
||||
forgotPassword: 'Forgot Password',
|
||||
cannotForgotPassword: 'Mail verification is disabled or register is disabled, cannot reset password, please contact administrator',
|
||||
resetPassword: 'Reset Password',
|
||||
pleaseInput: 'Please input email and password',
|
||||
pleaseInputEmail: 'Please input email',
|
||||
pleaseInputCode: 'Please input code',
|
||||
pleaseCompleteTurnstile: 'Please complete turnstile',
|
||||
pleaseLogin: 'Please login',
|
||||
},
|
||||
zh: {
|
||||
login: '登录',
|
||||
register: '注册',
|
||||
email: '邮箱',
|
||||
password: '密码',
|
||||
verifyCode: '验证码',
|
||||
sendVerificationCode: '发送验证码',
|
||||
verifyCodeSent: '验证码已发送, {timeout} 秒后失效',
|
||||
waitforVerifyCode: '等待{timeout}秒',
|
||||
forgotPassword: '忘记密码',
|
||||
cannotForgotPassword: '未开启邮箱验证或未开启注册功能,无法重置密码,请联系管理员',
|
||||
resetPassword: '重置密码',
|
||||
pleaseInput: '请输入邮箱和密码',
|
||||
pleaseInputEmail: '请输入邮箱',
|
||||
pleaseInputCode: '请输入验证码',
|
||||
pleaseCompleteTurnstile: '请完成人机验证',
|
||||
pleaseLogin: '请登录',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const tabValue = ref("signin");
|
||||
const showModal = ref(false);
|
||||
const user = ref({
|
||||
email: "",
|
||||
password: "",
|
||||
code: ""
|
||||
});
|
||||
const cfToken = ref("")
|
||||
|
||||
const emailLogin = async () => {
|
||||
if (!user.value.email || !user.value.password) {
|
||||
message.error(t('pleaseInput'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await api.fetch(`/user_api/login`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
email: user.value.email,
|
||||
// hash password
|
||||
password: await hashPassword(user.value.password)
|
||||
})
|
||||
});
|
||||
userJwt.value = res.jwt;
|
||||
location.reload();
|
||||
} catch (error) {
|
||||
message.error(error.message || "login failed");
|
||||
}
|
||||
};
|
||||
|
||||
const verifyCodeExpire = ref(0);
|
||||
const verifyCodeTimeout = ref(0);
|
||||
|
||||
const getVerifyCodeTimeout = () => {
|
||||
if (!verifyCodeExpire.value || verifyCodeExpire.value < new Date().getTime()) return 0;
|
||||
return Math.round((verifyCodeExpire.value - new Date().getTime()) / 1000);
|
||||
};
|
||||
|
||||
const sendVerificationCode = async () => {
|
||||
if (!user.value.email) {
|
||||
message.error(t('pleaseInputEmail'));
|
||||
return;
|
||||
}
|
||||
if (!cfToken.value && userOpenSettings.value.enableMailVerify) {
|
||||
message.error(t('pleaseCompleteTurnstile'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await api.fetch(`/user_api/verify_code`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
email: user.value.email,
|
||||
cf_token: cfToken.value
|
||||
})
|
||||
});
|
||||
if (res && res.expirationTtl) {
|
||||
message.success(t('verifyCodeSent', { timeout: res.expirationTtl }));
|
||||
verifyCodeExpire.value = new Date().getTime() + res.expirationTtl * 1000;
|
||||
const intervalId = setInterval(() => {
|
||||
verifyCodeTimeout.value = getVerifyCodeTimeout();
|
||||
if (verifyCodeTimeout.value <= 0) {
|
||||
clearInterval(intervalId);
|
||||
verifyCodeTimeout.value = 0;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error.message || "send verification code failed");
|
||||
}
|
||||
};
|
||||
|
||||
const emailSignup = async () => {
|
||||
if (!user.value.email || !user.value.password) {
|
||||
message.error(t('pleaseInput'));
|
||||
return;
|
||||
}
|
||||
if (!user.value.code && userOpenSettings.value.enableMailVerify) {
|
||||
message.error(t('pleaseInputCode'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await api.fetch(`/user_api/register`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
email: user.value.email,
|
||||
// hash password
|
||||
password: await hashPassword(user.value.password),
|
||||
code: user.value.code
|
||||
}),
|
||||
message: message
|
||||
});
|
||||
if (res) {
|
||||
tabValue.value = "signin";
|
||||
message.success(t('pleaseLogin'));
|
||||
}
|
||||
showModal.value = false;
|
||||
} catch (error) {
|
||||
message.error(error.message || "register failed");
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-tabs v-model:value="tabValue" size="large" justify-content="space-evenly">
|
||||
<n-tab-pane name="signin" :tab="t('login')">
|
||||
<n-form>
|
||||
<n-form-item-row :label="t('email')" required>
|
||||
<n-input v-model:value="user.email" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('password')" required>
|
||||
<n-input v-model:value="user.password" type="password" show-password-on="click" />
|
||||
</n-form-item-row>
|
||||
<n-button @click="emailLogin" type="primary" block secondary strong>
|
||||
{{ t('login') }}
|
||||
</n-button>
|
||||
<n-button @click="showModal = true" type="info" quaternary size="tiny">
|
||||
{{ t('forgotPassword') }}
|
||||
</n-button>
|
||||
</n-form>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="userOpenSettings.enable" name="signup" :tab="t('register')">
|
||||
<n-form>
|
||||
<n-form-item-row :label="t('email')" required>
|
||||
<n-input v-model:value="user.email" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('password')" required>
|
||||
<n-input v-model:value="user.password" type="password" show-password-on="click" />
|
||||
</n-form-item-row>
|
||||
<Turnstile v-if="userOpenSettings.enableMailVerify" v-model:value="cfToken" />
|
||||
<n-form-item-row v-if="userOpenSettings.enableMailVerify" :label="t('verifyCode')" required>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="user.code" />
|
||||
<n-button @click="sendVerificationCode" style="margin-bottom: 0" type="primary" ghost
|
||||
:disabled="verifyCodeTimeout > 0">
|
||||
{{ verifyCodeTimeout > 0 ? t('waitforVerifyCode', { timeout: verifyCodeTimeout })
|
||||
: t('sendVerificationCode') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</n-form-item-row>
|
||||
</n-form>
|
||||
<n-button @click="emailSignup" type="primary" block secondary strong>
|
||||
{{ t('register') }}
|
||||
</n-button>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
<n-modal v-model:show="showModal" style="max-width: 600px;" preset="card" :title="t('forgotPassword')">
|
||||
<n-form v-if="userOpenSettings.enable && userOpenSettings.enableMailVerify">
|
||||
<n-form-item-row :label="t('email')" required>
|
||||
<n-input v-model:value="user.email" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('password')" required>
|
||||
<n-input v-model:value="user.password" type="password" show-password-on="click" />
|
||||
</n-form-item-row>
|
||||
<Turnstile v-model:value="cfToken" />
|
||||
<n-form-item-row :label="t('verifyCode')" required>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="user.code" />
|
||||
<n-button @click="sendVerificationCode" style="margin-bottom: 0" type="primary" ghost
|
||||
:disabled="verifyCodeTimeout > 0">
|
||||
{{ verifyCodeTimeout > 0 ? t('waitforVerifyCode', { timeout: verifyCodeTimeout })
|
||||
: t('sendVerificationCode') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</n-form-item-row>
|
||||
<n-button @click="emailSignup" type="primary" block secondary strong>
|
||||
{{ t('resetPassword') }}
|
||||
</n-button>
|
||||
</n-form>
|
||||
<n-alert v-else :show-icon="false">
|
||||
<span>
|
||||
{{ t('cannotForgotPassword') }}
|
||||
</span>
|
||||
</n-alert>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
83
frontend/src/views/user/UserSettings.vue
Normal file
83
frontend/src/views/user/UserSettings.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const { userJwt, localeCache, userSettings, } = useGlobalState()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
|
||||
const showLogout = ref(false)
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
logout: 'Logout',
|
||||
logoutConfirm: 'Are you sure you want to logout?',
|
||||
passordTip: 'The server will only receive the hash value of the password, and will not receive the plaintext password, so it cannot view or retrieve your password. If the administrator enables email verification, you can reset the password in incognito mode',
|
||||
},
|
||||
zh: {
|
||||
logout: '退出登录',
|
||||
logoutConfirm: '确定要退出登录吗?',
|
||||
passordTip: '服务器只会接收到密码的哈希值,不会接收到明文密码,因此无法查看或者找回您的密码, 如果管理员启用了邮件验证您可以在无痕模式重置密码',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const logout = async () => {
|
||||
userJwt.value = '';
|
||||
location.reload()
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="center" v-if="userSettings.user_email">
|
||||
<n-card>
|
||||
<n-alert :show-icon="false">
|
||||
<span>
|
||||
{{ t('passordTip') }}
|
||||
</span>
|
||||
</n-alert>
|
||||
<n-button @click="showLogout = true" secondary block strong>
|
||||
{{ t('logout') }}
|
||||
</n-button>
|
||||
</n-card>
|
||||
<n-modal v-model:show="showLogout" preset="dialog" :title="t('logout')">
|
||||
<p>{{ t('logoutConfirm') }}</p>
|
||||
<template #action>
|
||||
<n-button :loading="loading" @click="logout" size="small" tertiary type="primary">
|
||||
{{ t('logout') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
.n-card {
|
||||
max-width: 800px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.n-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -38,6 +38,8 @@ wrangler d1 execute dev --file=db/schema.sql
|
||||
# schema update, if you have initialized the database before this date, you can execute this command to update
|
||||
# wrangler d1 execute dev --file=db/2024-01-13-patch.sql
|
||||
# wrangler d1 execute dev --file=db/2024-04-03-patch.sql
|
||||
# create a namespace, and copy the output to wrangler.toml in the next step
|
||||
wrangler kv:namespace create DEV
|
||||
```
|
||||
|
||||

|
||||
@@ -99,6 +101,11 @@ binding = "DB"
|
||||
database_name = "xxx" # D1 database name
|
||||
database_id = "xxx" # D1 database ID
|
||||
|
||||
# kv config for send email verification code
|
||||
# [[kv_namespaces]]
|
||||
# binding = "KV"
|
||||
# id = "xxxx"
|
||||
|
||||
# Create a new address current limiting configuration
|
||||
# [[unsafe.bindings]]
|
||||
# name = "RATE_LIMITER"
|
||||
|
||||
BIN
vitepress-docs/docs/public/ui_install/worker-kv-bind.png
Normal file
BIN
vitepress-docs/docs/public/ui_install/worker-kv-bind.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
vitepress-docs/docs/public/ui_install/worker-kv.png
Normal file
BIN
vitepress-docs/docs/public/ui_install/worker-kv.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -5,9 +5,11 @@
|
||||
## 初始化数据库
|
||||
|
||||
```bash
|
||||
cd worker
|
||||
cp wrangler.toml.template wrangler.toml
|
||||
# 创建 D1 并执行 schema.sql
|
||||
wrangler d1 create dev
|
||||
wrangler d1 execute dev --file=db/schema.sql
|
||||
wrangler d1 execute dev --file=../db/schema.sql
|
||||
```
|
||||
|
||||
创建完成后,我们在 cloudflare 的控制台可以看到 D1 数据库
|
||||
@@ -22,6 +24,7 @@ wrangler d1 execute dev --file=db/schema.sql
|
||||
找到需要执行的 `patch` 文件, 执行, 例如:
|
||||
|
||||
```bash
|
||||
wrangler d1 execute dev --file=db/2024-01-13-patch.sql
|
||||
wrangler d1 execute dev --file=db/2024-04-03-patch.sql
|
||||
cd worker
|
||||
wrangler d1 execute dev --file=../db/2024-01-13-patch.sql
|
||||
wrangler d1 execute dev --file=../db/2024-04-03-patch.sql
|
||||
```
|
||||
|
||||
@@ -8,6 +8,17 @@ pnpm install
|
||||
cp wrangler.toml.template wrangler.toml
|
||||
```
|
||||
|
||||
## 创建 KV 缓存
|
||||
|
||||
> [!NOTE]
|
||||
> 如果你要启用注册用户功能,并需要发送邮件验证,则需要创建 `KV` 缓存, 不需要可跳过此步骤
|
||||
|
||||
通过命令行创建 KV 缓存,或者在 Cloudflare 控制台创建,然后复制对应配置到 `wrangler.toml` 文件中
|
||||
|
||||
```bash
|
||||
wrangler kv:namespace create DEV
|
||||
```
|
||||
|
||||
## 修改 `wrangler.toml` 配置文件
|
||||
|
||||
```toml
|
||||
@@ -58,6 +69,11 @@ binding = "DB"
|
||||
database_name = "xxx" # D1 数据库名称
|
||||
database_id = "xxx" # D1 数据库 ID
|
||||
|
||||
# kv config 用于用户注册发送邮件验证码,如果不启用用户注册或不启用注册验证,可以不配置
|
||||
# [[kv_namespaces]]
|
||||
# binding = "KV"
|
||||
# id = "xxxx"
|
||||
|
||||
# 新建地址限流配置 /api/new_address
|
||||
# [[unsafe.bindings]]
|
||||
# name = "RATE_LIMITER"
|
||||
|
||||
@@ -38,3 +38,10 @@
|
||||
7. 点击 `Settings` -> `Variables`, 下拉找到 `D1 Database`, 点击 `Add Binding`, 名称如图,选择刚刚创建的 D1 数据库,点击 `Deploy`
|
||||
|
||||

|
||||
|
||||
8. 如果你要启用注册用户功能,并需要发送邮件验证,则需要创建 `KV` 缓存, 不需要可跳过此步骤,点击 `Workers & Pages` -> `KV` -> `Create Namespace`, 如图,点击 `Create Namespace`,然后在 `Settings` -> `Variables`, 下拉找到 `KV`, 点击 `Add Binding`, 名称如图,选择刚刚创建的 `KV` 缓存,点击 `Deploy`
|
||||
> [!NOTE]
|
||||
> 如果你要启用注册用户功能,并需要发送邮件验证,则需要创建 `KV` 缓存, 不需要可跳过此步骤
|
||||
|
||||

|
||||

|
||||
|
||||
135
worker/src/admin_api/admin_user_api.js
Normal file
135
worker/src/admin_api/admin_user_api.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import { CONSTANTS } from '../constants';
|
||||
import { getJsonSetting, saveSetting, checkUserPassword, getDomains } from '../utils';
|
||||
import { UserSettings, GeoData, UserInfo } from "../models";
|
||||
|
||||
export default {
|
||||
getSetting: async (c) => {
|
||||
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
|
||||
const settings = new UserSettings(value);
|
||||
return c.json(settings)
|
||||
},
|
||||
saveSetting: async (c) => {
|
||||
const value = await c.req.json();
|
||||
const settings = new UserSettings(value);
|
||||
if (settings.enableMailVerify && !c.env.KV) {
|
||||
return c.text("Please enable KV first if you want to enable mail verify", 403)
|
||||
}
|
||||
if (settings.enableMailVerify) {
|
||||
const mailDomain = settings.verifyMailSender.split("@")[1];
|
||||
const domains = getDomains(c);
|
||||
if (!domains.includes(mailDomain)) {
|
||||
return c.text(`VerifyMailSender(${settings.verifyMailSender}) domain must in ${JSON.stringify(domains, null, 2)}`, 400)
|
||||
}
|
||||
}
|
||||
if (settings.maxAddressCount < 0) {
|
||||
return c.text("Invalid maxAddressCount", 400)
|
||||
}
|
||||
await saveSetting(c, CONSTANTS.USER_SETTINGS_KEY, JSON.stringify(settings));
|
||||
return c.json({ success: true })
|
||||
},
|
||||
getUsers: async (c) => {
|
||||
const { limit, offset, query } = c.req.query();
|
||||
if (!limit || limit < 0 || limit > 100) {
|
||||
return c.text("Invalid limit", 400)
|
||||
}
|
||||
if (!offset || offset < 0) {
|
||||
return c.text("Invalid offset", 400)
|
||||
}
|
||||
if (query) {
|
||||
const { results } = await c.env.DB.prepare(
|
||||
`SELECT u.id, u.user_email, u.created_at, u.updated_at,`
|
||||
+ ` (SELECT COUNT(*) FROM users_address WHERE user_id = u.id) AS address_count`
|
||||
+ ` FROM users u`
|
||||
+ ` where u.user_email like ?`
|
||||
+ ` order by u.id desc limit ? offset ?`
|
||||
).bind(`%${query}%`, limit, offset).all();
|
||||
let count = 0;
|
||||
if (offset == 0) {
|
||||
const { count: userCount } = await c.env.DB.prepare(
|
||||
`SELECT count(*) as count FROM users where user_email like ?`
|
||||
).bind(`%${query}%`).first();
|
||||
count = userCount;
|
||||
}
|
||||
return c.json({
|
||||
results: results,
|
||||
count: count
|
||||
})
|
||||
}
|
||||
const { results } = await c.env.DB.prepare(
|
||||
`SELECT u.id, u.user_email, u.created_at, u.updated_at,`
|
||||
+ ` (SELECT COUNT(*) FROM users_address WHERE user_id = u.id) AS address_count`
|
||||
+ ` FROM users u`
|
||||
+ ` order by u.id desc limit ? offset ?`
|
||||
).bind(limit, offset).all();
|
||||
let count = 0;
|
||||
if (offset == 0) {
|
||||
const { count: userCount } = await c.env.DB.prepare(
|
||||
`SELECT count(*) as count FROM users`
|
||||
).first();
|
||||
count = userCount;
|
||||
}
|
||||
return c.json({
|
||||
results: results,
|
||||
count: count
|
||||
})
|
||||
},
|
||||
createUser: async (c) => {
|
||||
const { email, password } = await c.req.json();
|
||||
if (!email || !password) {
|
||||
return c.text("Invalid email or password", 400)
|
||||
}
|
||||
// geo data
|
||||
const reqIp = c.req.raw.headers.get("cf-connecting-ip")
|
||||
const geoData = new GeoData(reqIp, c.req.raw.cf);
|
||||
const userInfo = new UserInfo(geoData);
|
||||
try {
|
||||
checkUserPassword(password);
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`INSERT INTO users (user_email, password, user_info)`
|
||||
+ ` VALUES (?, ?, ?)`
|
||||
).bind(
|
||||
email, password, JSON.stringify(userInfo)
|
||||
).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to register", 500)
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message && e.message.includes("UNIQUE")) {
|
||||
return c.text("User already exists", 400)
|
||||
}
|
||||
return c.text(`Failed to register: ${e.message}`, 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
deleteUser: async (c) => {
|
||||
const { user_id } = c.req.param();
|
||||
if (!user_id) return c.text("Invalid user_id", 400);
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM users WHERE id = ?`
|
||||
).bind(user_id).run();
|
||||
const { success: addressSuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM users_address WHERE user_id = ?`
|
||||
).bind(user_id).run();
|
||||
if (!success || !addressSuccess) {
|
||||
return c.text("Failed to delete user", 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
resetPassword: async (c) => {
|
||||
const { user_id } = c.req.param();
|
||||
const { password } = await c.req.json();
|
||||
if (!user_id) return c.text("Invalid user_id", 400);
|
||||
try {
|
||||
checkUserPassword(password);
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`UPDATE users SET password = ? WHERE id = ?`
|
||||
).bind(password, user_id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to reset password", 500)
|
||||
}
|
||||
} catch (e) {
|
||||
return c.text(`Failed to reset password: ${e.message}`, 500)
|
||||
}
|
||||
return c.json({ success: true });
|
||||
},
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { cleanup } from '../common';
|
||||
import { CONSTANTS } from '../constants';
|
||||
import { getJsonSetting, saveSetting } from '../utils';
|
||||
import { CleanupSettings } from '../models';
|
||||
|
||||
export default {
|
||||
cleanup: async (c) => {
|
||||
@@ -15,11 +16,13 @@ export default {
|
||||
},
|
||||
getCleanup: async (c) => {
|
||||
const value = await getJsonSetting(c, CONSTANTS.AUTO_CLEANUP_KEY);
|
||||
return c.json(value || {})
|
||||
const cleanupSetting = new CleanupSettings(value);
|
||||
return c.json(cleanupSetting)
|
||||
},
|
||||
saveCleanup: async (c) => {
|
||||
const value = await c.req.json();
|
||||
await saveSetting(c, CONSTANTS.AUTO_CLEANUP_KEY, JSON.stringify(value));
|
||||
const cleanupSetting = new CleanupSettings(value);
|
||||
await saveSetting(c, CONSTANTS.AUTO_CLEANUP_KEY, JSON.stringify(cleanupSetting));
|
||||
return c.json({ success: true })
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Hono } from 'hono'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
import { sendAdminInternalMail, getJsonSetting, saveSetting } from './utils'
|
||||
import { newAddress } from './common'
|
||||
import { CONSTANTS } from './constants'
|
||||
import cleanup_api from './admin/cleanup_api'
|
||||
import { sendAdminInternalMail, getJsonSetting, saveSetting } from '../utils'
|
||||
import { newAddress } from '../common'
|
||||
import { CONSTANTS } from '../constants'
|
||||
import cleanup_api from './cleanup_api'
|
||||
import admin_user_api from './admin_user_api'
|
||||
|
||||
const api = new Hono()
|
||||
|
||||
@@ -83,8 +84,11 @@ api.delete('/admin/delete_address/:id', async (c) => {
|
||||
`DELETE FROM address_sender WHERE address IN`
|
||||
+ ` (select name from address where id = ?) `
|
||||
).bind(id).run();
|
||||
const { success: usersAddressSuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM users_address WHERE address_id = ?`
|
||||
).bind(id).run();
|
||||
return c.json({
|
||||
success: success && mailSuccess && sendAccess
|
||||
success: success && mailSuccess && sendAccess && usersAddressSuccess
|
||||
})
|
||||
})
|
||||
|
||||
@@ -98,11 +102,10 @@ api.get('/admin/show_password/:id', async (c) => {
|
||||
address_id: id
|
||||
}, c.env.JWT_SECRET)
|
||||
return c.json({
|
||||
password: jwt
|
||||
jwt: jwt
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
api.get('/admin/mails', async (c) => {
|
||||
const { address, limit, offset } = c.req.query();
|
||||
if (!limit || limit < 0 || limit > 100) {
|
||||
@@ -296,14 +299,11 @@ api.get('/admin/statistics', async (c) => {
|
||||
})
|
||||
});
|
||||
|
||||
api.post('/admin/cleanup', cleanup_api.cleanup)
|
||||
api.get('/admin/auto_cleanup', cleanup_api.getCleanup)
|
||||
api.post('/admin/auto_cleanup', cleanup_api.saveCleanup)
|
||||
|
||||
|
||||
api.get('/admin/account_settings', async (c) => {
|
||||
try {
|
||||
/** @type {Array<string>|undefined|null} */
|
||||
const blockList = await getJsonSetting(c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY);
|
||||
/** @type {Array<string>|undefined|null} */
|
||||
const sendBlockList = await getJsonSetting(c, CONSTANTS.SEND_BLOCK_LIST_KEY);
|
||||
return c.json({
|
||||
blockList: blockList || [],
|
||||
@@ -316,6 +316,7 @@ api.get('/admin/account_settings', async (c) => {
|
||||
})
|
||||
|
||||
api.post('/admin/account_settings', async (c) => {
|
||||
/** @type {{ blockList: Array<string>, sendBlockList: Array<string> }} */
|
||||
const { blockList, sendBlockList } = await c.req.json();
|
||||
if (!blockList || !sendBlockList) {
|
||||
return c.text("Invalid blockList or sendBlockList", 400)
|
||||
@@ -333,4 +334,14 @@ api.post('/admin/account_settings', async (c) => {
|
||||
})
|
||||
})
|
||||
|
||||
api.post('/admin/cleanup', cleanup_api.cleanup)
|
||||
api.get('/admin/auto_cleanup', cleanup_api.getCleanup)
|
||||
api.post('/admin/auto_cleanup', cleanup_api.saveCleanup)
|
||||
api.get('/admin/user_settings', admin_user_api.getSetting)
|
||||
api.post('/admin/user_settings', admin_user_api.saveSetting)
|
||||
api.get('/admin/users', admin_user_api.getUsers)
|
||||
api.delete('/admin/users/:user_id', admin_user_api.deleteUser)
|
||||
api.post('/admin/users', admin_user_api.createUser)
|
||||
api.post('/admin/users/:user_id/reset_password', admin_user_api.resetPassword)
|
||||
|
||||
export { api }
|
||||
@@ -2,4 +2,5 @@ export const CONSTANTS = {
|
||||
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
||||
SEND_BLOCK_LIST_KEY: 'send_block_list',
|
||||
AUTO_CLEANUP_KEY: 'auto_cleanup',
|
||||
USER_SETTINGS_KEY: 'user_settings',
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createMimeMessage } from "mimetext";
|
||||
import { EmailMessage } from "cloudflare:email";
|
||||
import { getBooleanValue } from "./utils";
|
||||
|
||||
async function email(message, env, ctx) {
|
||||
@@ -40,7 +39,7 @@ async function email(message, env, ctx) {
|
||||
contentType: 'text/plain',
|
||||
data: results.message || "This is an auto-reply message, please reconact later."
|
||||
});
|
||||
|
||||
const { EmailMessage } = await import('cloudflare:email');
|
||||
const replyMessage = new EmailMessage(
|
||||
message.to,
|
||||
message.from,
|
||||
|
||||
@@ -25,7 +25,7 @@ export default {
|
||||
if (!getBooleanValue(c.env.ENABLE_AUTO_REPLY)) {
|
||||
return c.text("Auto reply is disabled", 403)
|
||||
}
|
||||
const { address } = c.get("jwtPayload")
|
||||
const { address } = c.get("jwtPayload");
|
||||
const { auto_reply } = await c.req.json();
|
||||
const { name, subject, source_prefix, message, enabled } = auto_reply;
|
||||
if ((!subject || !message) && enabled) {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Hono } from 'hono'
|
||||
|
||||
import auto_reply from './user/auto_reply'
|
||||
import auto_reply from './auto_reply'
|
||||
|
||||
const api = new Hono()
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Hono } from 'hono'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
import { CONSTANTS } from './constants'
|
||||
import { getJsonSetting } from './utils';
|
||||
import { CONSTANTS } from '../constants'
|
||||
import { getJsonSetting, getDomains } from '../utils';
|
||||
import { GeoData } from '../models'
|
||||
|
||||
const api = new Hono()
|
||||
|
||||
@@ -29,37 +30,43 @@ api.post('/api/requset_send_mail_access', async (c) => {
|
||||
return c.json({ status: "ok" })
|
||||
})
|
||||
|
||||
const sendMail = async (c, address) => {
|
||||
export const sendMail = async (c, address, reqJson) => {
|
||||
if (!address) {
|
||||
throw new Error("No address")
|
||||
}
|
||||
// check domain
|
||||
const mailDomain = address.split("@")[1];
|
||||
const domains = getDomains(c);
|
||||
if (!domains.includes(mailDomain)) {
|
||||
throw new Error("Invalid domain")
|
||||
}
|
||||
// check permission
|
||||
const balance = await c.env.DB.prepare(
|
||||
`SELECT balance FROM address_sender
|
||||
where address = ? and enabled = 1`
|
||||
).bind(address).first("balance");
|
||||
if (!balance || balance <= 0) {
|
||||
return c.text("No balance", 400);
|
||||
throw new Error("No balance")
|
||||
}
|
||||
let {
|
||||
from_name, to_mail, to_name,
|
||||
subject, content, is_html
|
||||
} = await c.req.json();
|
||||
if (!address) {
|
||||
return c.text("No address", 400)
|
||||
}
|
||||
} = reqJson;
|
||||
if (!to_mail) {
|
||||
return c.text("Invalid to mail", 400)
|
||||
throw new Error("Invalid to mail")
|
||||
}
|
||||
// check SEND_BLOCK_LIST_KEY
|
||||
const sendBlockList = await getJsonSetting(c, CONSTANTS.SEND_BLOCK_LIST_KEY);
|
||||
if (sendBlockList && sendBlockList.some((item) => to_mail.includes(item))) {
|
||||
return c.text("to_mail address is blocked", 400);
|
||||
throw new Error("to_mail address is blocked")
|
||||
}
|
||||
from_name = from_name || address;
|
||||
to_name = to_name || to_mail;
|
||||
if (!subject) {
|
||||
return c.text("Invalid subject", 400)
|
||||
throw new Error("Invalid subject")
|
||||
}
|
||||
if (!content) {
|
||||
return c.text("Invalid content", 400)
|
||||
throw new Error("Invalid content")
|
||||
}
|
||||
let dmikBody = {}
|
||||
if (c.env.DKIM_SELECTOR && c.env.DKIM_PRIVATE_KEY && address.includes("@")) {
|
||||
@@ -100,7 +107,7 @@ const sendMail = async (c, address) => {
|
||||
const respText = await resp.text();
|
||||
console.log(resp.status + " " + resp.statusText + ": " + respText);
|
||||
if (resp.status >= 300) {
|
||||
return c.text("Failed to send mail", 500)
|
||||
throw new Error(`Mailchannels error: ${resp.status} ${respText}`);
|
||||
}
|
||||
// update balance
|
||||
try {
|
||||
@@ -120,7 +127,8 @@ const sendMail = async (c, address) => {
|
||||
delete body.personalizations[0].dkim_private_key;
|
||||
}
|
||||
const reqIp = c.req.raw.headers.get("cf-connecting-ip")
|
||||
body.reqIp = reqIp;
|
||||
const geoData = new GeoData(reqIp, c.req.raw.cf);
|
||||
body.geoData = geoData;
|
||||
const { success: success2 } = await c.env.DB.prepare(
|
||||
`INSERT INTO sendbox (address, raw) VALUES (?, ?)`
|
||||
).bind(address, JSON.stringify(body)).run();
|
||||
@@ -130,12 +138,18 @@ const sendMail = async (c, address) => {
|
||||
} catch (e) {
|
||||
console.warn(`Failed to save to sendbox for ${address}`);
|
||||
}
|
||||
return c.json({ status: "ok" });
|
||||
}
|
||||
|
||||
api.post('/api/send_mail', async (c) => {
|
||||
const { address } = c.get("jwtPayload")
|
||||
return await sendMail(c, address);
|
||||
const reqJson = await c.req.json();
|
||||
try {
|
||||
await sendMail(c, address, reqJson);
|
||||
} catch (e) {
|
||||
console.error("Failed to send mail", e);
|
||||
return c.text(`Failed to send mail ${e.message}`, 400)
|
||||
}
|
||||
return c.json({ status: "ok" })
|
||||
})
|
||||
|
||||
api.post('/external/api/send_mail', async (c) => {
|
||||
@@ -145,10 +159,11 @@ api.post('/external/api/send_mail', async (c) => {
|
||||
if (!address) {
|
||||
return c.text("No address", 400)
|
||||
}
|
||||
return await sendMail(c, address);
|
||||
const reqJson = await c.req.json();
|
||||
return await sendMail(c, address, reqJson);
|
||||
} catch (e) {
|
||||
console.error("Failed to verify token", e);
|
||||
return c.text("Unauthorized", 401)
|
||||
console.error("Failed to send mail", e);
|
||||
return c.text(`Failed to send mail ${e.message}`, 400)
|
||||
}
|
||||
})
|
||||
|
||||
92
worker/src/models/index.js
Normal file
92
worker/src/models/index.js
Normal file
@@ -0,0 +1,92 @@
|
||||
export class UserSettings {
|
||||
/** @param {UserSettings|undefined|null} data */
|
||||
constructor(data) {
|
||||
if (data === null) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
enable, enableMailVerify, verifyMailSender,
|
||||
enableMailAllowList, mailAllowList, maxAddressCount
|
||||
} = data || {};
|
||||
/** @type {boolean|undefined} */
|
||||
this.enable = enable;
|
||||
/** @type {boolean|undefined} */
|
||||
this.enableMailVerify = enableMailVerify;
|
||||
/** @type {string|undefined} */
|
||||
this.verifyMailSender = verifyMailSender;
|
||||
/** @type {boolean|undefined} */
|
||||
this.enableMailAllowList = enableMailAllowList;
|
||||
/** @type {Array<string>|undefined} */
|
||||
this.mailAllowList = mailAllowList;
|
||||
/** @type {number|undefined} */
|
||||
this.maxAddressCount = maxAddressCount || 5;
|
||||
}
|
||||
}
|
||||
|
||||
export class CleanupSettings {
|
||||
/** @param {CleanupSettings|undefined|null} data */
|
||||
constructor(data) {
|
||||
const {
|
||||
enableMailsAutoCleanup, cleanMailsDays,
|
||||
enableUnknowMailsAutoCleanup, cleanUnknowMailsDays,
|
||||
enableAddressAutoCleanup, cleanAddressDays,
|
||||
enableSendBoxAutoCleanup, cleanSendBoxDays
|
||||
} = data || {};
|
||||
/** @type {boolean|undefined} */
|
||||
this.enableMailsAutoCleanup = enableMailsAutoCleanup;
|
||||
/** @type {number|undefined} */
|
||||
this.cleanMailsDays = cleanMailsDays;
|
||||
/** @type {boolean|undefined} */
|
||||
this.enableUnknowMailsAutoCleanup = enableUnknowMailsAutoCleanup;
|
||||
/** @type {number|undefined} */
|
||||
this.cleanUnknowMailsDays = cleanUnknowMailsDays;
|
||||
/** @type {boolean|undefined} */
|
||||
this.enableAddressAutoCleanup = enableAddressAutoCleanup;
|
||||
/** @type {number|undefined} */
|
||||
this.cleanAddressDays = cleanAddressDays;
|
||||
/** @type {boolean|undefined} */
|
||||
this.enableSendBoxAutoCleanup = enableSendBoxAutoCleanup;
|
||||
/** @type {number|undefined} */
|
||||
this.cleanSendBoxDays = cleanSendBoxDays;
|
||||
}
|
||||
}
|
||||
|
||||
export class GeoData {
|
||||
/** @param {string} ip @param {GeoData|undefined|null} data */
|
||||
constructor(ip, data) {
|
||||
const {
|
||||
country, city, timezone, postalCode, region,
|
||||
latitude, longitude, regionCode, asOrganization
|
||||
} = data || {};
|
||||
/** @type {string} */
|
||||
this.ip = ip;
|
||||
/** @type {string|undefined} */
|
||||
this.country = country;
|
||||
/** @type {string|undefined} */
|
||||
this.city = city;
|
||||
/** @type {string|undefined} */
|
||||
this.timezone = timezone;
|
||||
/** @type {string|undefined} */
|
||||
this.postalCode = postalCode;
|
||||
/** @type {string|undefined} */
|
||||
this.region = region;
|
||||
/** @type {number|undefined} */
|
||||
this.latitude = latitude;
|
||||
/** @type {number|undefined} */
|
||||
this.longitude = longitude;
|
||||
/** @type {string|undefined} */
|
||||
this.regionCode = regionCode;
|
||||
/** @type {string|undefined} */
|
||||
this.asOrganization = asOrganization;
|
||||
}
|
||||
}
|
||||
|
||||
export class UserInfo {
|
||||
/** @param {GeoData} geoData @param {string} userEmail */
|
||||
constructor(geoData, userEmail) {
|
||||
/** @type {geoData} */
|
||||
this.geoData = geoData;
|
||||
/** @type {string} */
|
||||
this.userEmail = userEmail;
|
||||
}
|
||||
}
|
||||
@@ -153,7 +153,7 @@ api.delete('/api/delete_address', async (c) => {
|
||||
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
||||
return c.text("User delete email is disabled", 403)
|
||||
}
|
||||
const { address } = c.get("jwtPayload")
|
||||
const { address, address_id } = c.get("jwtPayload")
|
||||
let name = address;
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM address WHERE name = ? `
|
||||
@@ -170,8 +170,11 @@ api.delete('/api/delete_address', async (c) => {
|
||||
const { success: sendAccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM address_sender WHERE address = ? `
|
||||
).bind(address).run();
|
||||
const { success: addressSuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM users_address WHERE address_id = ?`
|
||||
).bind(address_id).run();
|
||||
return c.json({
|
||||
success: success && mailSuccess && sendAccess
|
||||
success: success && mailSuccess && sendAccess && addressSuccess
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { cleanup } from './common'
|
||||
import { CONSTANTS } from './constants'
|
||||
import { getJsonSetting } from './utils';
|
||||
import { CleanupSettings } from './models';
|
||||
|
||||
export async function scheduled(event, env, ctx) {
|
||||
console.log("Scheduled event: ", event);
|
||||
let autoCleanupSetting = await getJsonSetting(
|
||||
const value = await getJsonSetting(
|
||||
{ env: env, },
|
||||
CONSTANTS.AUTO_CLEANUP_KEY
|
||||
);
|
||||
const autoCleanupSetting = new CleanupSettings(value);
|
||||
console.log("autoCleanupSetting:", JSON.stringify(autoCleanupSetting));
|
||||
autoCleanupSetting = autoCleanupSetting || {};
|
||||
if (autoCleanupSetting.enableMailsAutoCleanup && autoCleanupSetting.cleanMailsDays > 0) {
|
||||
await cleanup(
|
||||
{ env: env, },
|
||||
|
||||
139
worker/src/user_api/bind_address.js
Normal file
139
worker/src/user_api/bind_address.js
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
import { UserSettings } from "../models";
|
||||
import { getJsonSetting } from "../utils"
|
||||
import { CONSTANTS } from "../constants";
|
||||
|
||||
export default {
|
||||
bind: async (c) => {
|
||||
const { user_id } = c.get("userPayload");
|
||||
const { address_id } = c.get("jwtPayload");
|
||||
if (!address_id || !user_id) {
|
||||
return c.text("No address or user token", 400)
|
||||
}
|
||||
// check if address exists
|
||||
const db_address_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM address where id = ?`
|
||||
).bind(address_id).first("id");
|
||||
if (!db_address_id) {
|
||||
return c.text("Address not found", 400)
|
||||
}
|
||||
// check if user exists
|
||||
const db_user_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM users where id = ?`
|
||||
).bind(user_id).first("id");
|
||||
if (!db_user_id) {
|
||||
return c.text("User not found", 400)
|
||||
}
|
||||
// check if binded
|
||||
const db_user_address_id = await c.env.DB.prepare(
|
||||
`SELECT user_id FROM users_address where user_id = ? and address_id = ?`
|
||||
).bind(user_id, address_id).first("user_id");
|
||||
if (db_user_address_id) return c.json({ success: true })
|
||||
// check if binded address count
|
||||
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
|
||||
const settings = new UserSettings(value);
|
||||
if (settings.maxAddressCount > 0) {
|
||||
const { count } = await c.env.DB.prepare(
|
||||
`SELECT COUNT(*) as count FROM users_address where user_id = ?`
|
||||
).bind(user_id).first();
|
||||
if (count >= settings.maxAddressCount) {
|
||||
return c.text("Max address count reached", 400)
|
||||
}
|
||||
}
|
||||
// bind
|
||||
try {
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`INSERT INTO users_address (user_id, address_id) VALUES (?, ?)`
|
||||
).bind(user_id, address_id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to bind", 500)
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message && e.message.includes("UNIQUE")) {
|
||||
return c.text("Address already binded, please unbind first", 400)
|
||||
}
|
||||
return c.text("Failed to bind", 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
unbind: async (c) => {
|
||||
const { user_id } = c.get("userPayload");
|
||||
const { address_id } = await c.req.json();
|
||||
if (!address_id || !user_id) {
|
||||
return c.text("Invalid address or user token", 400)
|
||||
}
|
||||
// check if address exists
|
||||
const db_address_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM address where id = ?`
|
||||
).bind(address_id).first("id");
|
||||
if (!db_address_id) {
|
||||
return c.text("Address not found", 400)
|
||||
}
|
||||
// check if user exists
|
||||
const db_user_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM users where id = ?`
|
||||
).bind(user_id).first("id");
|
||||
if (!db_user_id) {
|
||||
return c.text("User not found", 400)
|
||||
}
|
||||
// unbind
|
||||
try {
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM users_address where user_id = ? and address_id = ?`
|
||||
).bind(user_id, address_id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to unbind", 500)
|
||||
}
|
||||
} catch (e) {
|
||||
return c.text("Invalid address token", 400)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
getBindedAddresses: async (c) => {
|
||||
const { user_id } = c.get("userPayload");
|
||||
if (!user_id) {
|
||||
return c.text("No user token", 400)
|
||||
}
|
||||
// select binded address
|
||||
const { results } = await c.env.DB.prepare(
|
||||
`SELECT a.*,`
|
||||
+ ` (SELECT COUNT(*) FROM raw_mails WHERE address = a.name) AS mail_count,`
|
||||
+ ` (SELECT COUNT(*) FROM sendbox WHERE address = a.name) AS send_count`
|
||||
+ ` FROM address a `
|
||||
+ ` JOIN users_address ua `
|
||||
+ ` ON ua.address_id = a.id `
|
||||
+ ` WHERE ua.user_id = ?`
|
||||
+ ` ORDER BY a.id DESC`
|
||||
).bind(user_id).all();
|
||||
return c.json({
|
||||
results: results,
|
||||
})
|
||||
},
|
||||
getBindedAddressJwt: async (c) => {
|
||||
const { address_id } = c.req.param();
|
||||
// check binded
|
||||
const { user_id } = c.get("userPayload");
|
||||
if (!address_id || !user_id) {
|
||||
return c.text("Invalid address or user token", 400)
|
||||
}
|
||||
// check users_address if address binded
|
||||
const db_user_id = await c.env.DB.prepare(
|
||||
`SELECT user_id FROM users_address WHERE address_id = ? and user_id = ?`
|
||||
).bind(address_id, user_id).first("user_id");
|
||||
if (!db_user_id) {
|
||||
return c.text("Address not binded", 400)
|
||||
}
|
||||
// generate jwt
|
||||
const name = await c.env.DB.prepare(
|
||||
`SELECT name FROM address WHERE id = ? `
|
||||
).bind(address_id).first("name");
|
||||
const jwt = await Jwt.sign({
|
||||
address: name,
|
||||
address_id: address_id
|
||||
}, c.env.JWT_SECRET)
|
||||
return c.json({
|
||||
jwt: jwt
|
||||
})
|
||||
},
|
||||
}
|
||||
19
worker/src/user_api/index.js
Normal file
19
worker/src/user_api/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Hono } from 'hono';
|
||||
|
||||
import settings from './settings';
|
||||
import user from './user';
|
||||
import bind_address from './bind_address';
|
||||
|
||||
const api = new Hono();
|
||||
|
||||
api.get('/user_api/open_settings', settings.openSettings);
|
||||
api.get('/user_api/settings', settings.settings);
|
||||
api.post('/user_api/login', user.login);
|
||||
api.post('/user_api/verify_code', user.verifyCode);
|
||||
api.post('/user_api/register', user.register);
|
||||
api.get('/user_api/bind_address', bind_address.getBindedAddresses);
|
||||
api.post('/user_api/bind_address', bind_address.bind);
|
||||
api.get('/user_api/bind_address_jwt/:address_id', bind_address.getBindedAddressJwt);
|
||||
api.post('/user_api/unbind_address', bind_address.unbind);
|
||||
|
||||
export { api }
|
||||
25
worker/src/user_api/settings.js
Normal file
25
worker/src/user_api/settings.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { UserSettings } from "../models";
|
||||
import { getJsonSetting } from "../utils"
|
||||
import { CONSTANTS } from "../constants";
|
||||
|
||||
export default {
|
||||
openSettings: async (c) => {
|
||||
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
|
||||
const settings = new UserSettings(value);
|
||||
return c.json({
|
||||
enable: settings.enable,
|
||||
enableMailVerify: settings.enableMailVerify,
|
||||
})
|
||||
},
|
||||
settings: async (c) => {
|
||||
const user = c.get("userPayload");
|
||||
// check if user exists
|
||||
const db_user_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM users where id = ?`
|
||||
).bind(user.user_id).first("id");
|
||||
if (!db_user_id) {
|
||||
return c.text("User not found", 400);
|
||||
}
|
||||
return c.json(user);
|
||||
},
|
||||
}
|
||||
141
worker/src/user_api/user.js
Normal file
141
worker/src/user_api/user.js
Normal file
@@ -0,0 +1,141 @@
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
import { checkCfTurnstile, getJsonSetting, checkUserPassword } from "../utils"
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { GeoData, UserInfo, UserSettings } from "../models";
|
||||
import { sendMail } from "../mails_api/send_mail_api";
|
||||
|
||||
export default {
|
||||
verifyCode: async (c) => {
|
||||
const { email, cf_token } = await c.req.json();
|
||||
// check cf turnstile
|
||||
try {
|
||||
await checkCfTurnstile(c, cf_token);
|
||||
} catch (error) {
|
||||
return c.text("Failed to check cf turnstile", 500)
|
||||
}
|
||||
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
|
||||
const settings = new UserSettings(value)
|
||||
// check mail domain allow list
|
||||
const mailDomain = email.split("@")[1];
|
||||
if (settings.enableMailAllowList
|
||||
&& settings.mailAllowList
|
||||
&& !settings.mailAllowList.includes(mailDomain)
|
||||
) {
|
||||
return c.text(`Mail domain must in ${JSON.stringify(settings.mailAllowList, null, 2)}`, 400)
|
||||
}
|
||||
// check if code exists in KV
|
||||
const tmpcode = await c.env.KV.get(`temp-mail:${email}`)
|
||||
if (tmpcode) {
|
||||
return c.text("Code already sent, please wait", 400)
|
||||
}
|
||||
// generate code 6 digits and convert to string
|
||||
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
// send code to email
|
||||
try {
|
||||
await sendMail(c, settings.verifyMailSender, {
|
||||
to_mail: email,
|
||||
subject: "Temp Mail Verify code",
|
||||
content: `Your verify code is ${code}`,
|
||||
})
|
||||
} catch (e) {
|
||||
return c.text(`Failed to send verify code: ${e.message}`, 500)
|
||||
}
|
||||
// save to KV
|
||||
await c.env.KV.put(`temp-mail:${email}`, code, { expirationTtl: 300 });
|
||||
return c.json({
|
||||
success: true,
|
||||
expirationTtl: 300
|
||||
})
|
||||
},
|
||||
register: async (c) => {
|
||||
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
|
||||
const settings = new UserSettings(value)
|
||||
// check enable
|
||||
if (!settings.enable) {
|
||||
return c.text("User registration is disabled");
|
||||
}
|
||||
// check request
|
||||
const { email, password, code } = await c.req.json();
|
||||
if (!email || !password) {
|
||||
return c.text("Invalid email or password", 400)
|
||||
}
|
||||
checkUserPassword(password);
|
||||
if (settings.enableMailVerify && !code) {
|
||||
return c.text("Need verify code", 400)
|
||||
}
|
||||
// check mail domain allow list
|
||||
const mailDomain = email.split("@")[1];
|
||||
if (settings.enableMailAllowList
|
||||
&& !settings.mailAllowList.includes(mailDomain)
|
||||
) {
|
||||
return c.text(`Mail domain must in ${JSON.stringify(settings.mailAllowList, null, 2)}`, 400)
|
||||
}
|
||||
// check code
|
||||
if (settings.enableMailVerify) {
|
||||
const verifyCode = await c.env.KV.get(`temp-mail:${email}`)
|
||||
if (verifyCode != code) {
|
||||
return c.text("Invalid verify code", 400)
|
||||
}
|
||||
}
|
||||
// geo data
|
||||
const reqIp = c.req.raw.headers.get("cf-connecting-ip")
|
||||
const geoData = new GeoData(reqIp, c.req.raw.cf);
|
||||
const userInfo = new UserInfo(geoData);
|
||||
// if not enable mail verify, do not on conflict update
|
||||
if (!settings.enableMailVerify) {
|
||||
try {
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`INSERT INTO users (user_email, password, user_info)`
|
||||
+ ` VALUES (?, ?, ?)`
|
||||
).bind(
|
||||
email, password, JSON.stringify(userInfo)
|
||||
).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to register", 500)
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message && e.message.includes("UNIQUE")) {
|
||||
return c.text("User already exists, please login", 400)
|
||||
}
|
||||
return c.text(`Failed to register: ${e.message}`, 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
}
|
||||
// if enable mail verify, on conflict update
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`INSERT INTO users (user_email, password, user_info)`
|
||||
+ ` VALUES (?, ?, ?)`
|
||||
+ ` ON CONFLICT(user_email) DO UPDATE SET password = ?, user_info = ?, updated_at = datetime('now')`
|
||||
).bind(
|
||||
email, password, JSON.stringify(userInfo),
|
||||
password, JSON.stringify(userInfo)
|
||||
).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to register", 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
login: async (c) => {
|
||||
const { email, password } = await c.req.json();
|
||||
if (!email || !password) return c.text("Invalid email or password", 400);
|
||||
const { id: user_id, password: dbPassword } = await c.env.DB.prepare(
|
||||
`SELECT id, password FROM users where user_email = ?`
|
||||
).bind(email).first() || {};
|
||||
if (!dbPassword) {
|
||||
return c.text("User not found", 400)
|
||||
}
|
||||
// TODO: need check password use random salt
|
||||
if (dbPassword != password) {
|
||||
return c.text("Invalid password", 400)
|
||||
}
|
||||
// create jwt
|
||||
const jwt = await Jwt.sign({
|
||||
user_email: email,
|
||||
user_id: user_id
|
||||
}, c.env.JWT_SECRET)
|
||||
return c.json({
|
||||
jwt: jwt
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -153,3 +153,10 @@ export const checkCfTurnstile = async (c, token) => {
|
||||
throw new Error("Captcha failed");
|
||||
}
|
||||
}
|
||||
|
||||
export const checkUserPassword = (password) => {
|
||||
if (!password || password.length < 1 || password.length > 100) {
|
||||
throw new Error("Invalid password")
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
import { Hono } from 'hono'
|
||||
import { cors } from 'hono/cors';
|
||||
import { jwt } from 'hono/jwt'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
import { api } from './router';
|
||||
import { api as MailsApi } from './mails_api'
|
||||
import { api as userApi } from './user_api';
|
||||
import { api as adminApi } from './admin_api';
|
||||
import { api as apiV1 } from './api_v1';
|
||||
import { api as apiSendMail } from './send_mail_api'
|
||||
import { api as apiV1 } from './deprecated';
|
||||
import { api as apiSendMail } from './mails_api/send_mail_api'
|
||||
|
||||
import { email } from './email';
|
||||
import { scheduled } from './scheduled';
|
||||
import { getAdminPasswords, getPasswords } from './utils';
|
||||
|
||||
const app = new Hono()
|
||||
//cors
|
||||
app.use('/*', cors());
|
||||
app.use('/api/*', async (c, next) => {
|
||||
// check header x-custom-auth
|
||||
const passwords = getPasswords(c);
|
||||
if (passwords && passwords.length > 0) {
|
||||
const auth = c.req.raw.headers.get("x-custom-auth");
|
||||
if (!auth || !passwords.includes(auth)) {
|
||||
return c.text("Need Password", 401)
|
||||
}
|
||||
}
|
||||
if (c.req.path.startsWith("/api/new_address") || c.req.path.startsWith("/api/send_mail")) {
|
||||
// rate limit
|
||||
app.use('/*', async (c, next) => {
|
||||
if (
|
||||
c.req.path.startsWith("/api/new_address")
|
||||
|| c.req.path.startsWith("/api/send_mail")
|
||||
|| c.req.path.startsWith("/external/api/send_mail")
|
||||
|| c.req.path.startsWith("/user_api/register")
|
||||
|| c.req.path.startsWith("/user_api/verify_code")
|
||||
) {
|
||||
const reqIp = c.req.raw.headers.get("cf-connecting-ip")
|
||||
if (reqIp && c.env.RATE_LIMITER) {
|
||||
const { success } = await c.env.RATE_LIMITER.limit(
|
||||
@@ -33,13 +36,51 @@ app.use('/api/*', async (c, next) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
await next()
|
||||
});
|
||||
// api auth
|
||||
app.use('/api/*', async (c, next) => {
|
||||
// check header x-custom-auth
|
||||
const passwords = getPasswords(c);
|
||||
if (passwords && passwords.length > 0) {
|
||||
const auth = c.req.raw.headers.get("x-custom-auth");
|
||||
if (!auth || !passwords.includes(auth)) {
|
||||
return c.text("Need Password", 401)
|
||||
}
|
||||
}
|
||||
if (c.req.path.startsWith("/api/new_address")) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
return jwt({ secret: c.env.JWT_SECRET })(c, next);
|
||||
});
|
||||
|
||||
// user_api auth
|
||||
app.use('/user_api/*', async (c, next) => {
|
||||
if (
|
||||
c.req.path.startsWith("/user_api/open_settings")
|
||||
|| c.req.path.startsWith("/user_api/register")
|
||||
|| c.req.path.startsWith("/user_api/login")
|
||||
|| c.req.path.startsWith("/user_api/verify_code")
|
||||
) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const token = c.req.raw.headers.get("x-user-token");
|
||||
const payload = await Jwt.verify(token, c.env.JWT_SECRET);
|
||||
c.set("userPayload", payload);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return c.text("Need User Token", 401)
|
||||
}
|
||||
if (c.req.path.startsWith('/user_api/bind_address')
|
||||
&& c.req.method === 'POST'
|
||||
) {
|
||||
return jwt({ secret: c.env.JWT_SECRET })(c, next);
|
||||
}
|
||||
await next();
|
||||
});
|
||||
// admin auth
|
||||
app.use('/admin/*', async (c, next) => {
|
||||
// check header x-admin-auth
|
||||
const adminPasswords = getAdminPasswords(c);
|
||||
@@ -55,6 +96,7 @@ app.use('/admin/*', async (c, next) => {
|
||||
|
||||
|
||||
app.route('/', api)
|
||||
app.route('/', MailsApi)
|
||||
app.route('/', userApi)
|
||||
app.route('/', adminApi)
|
||||
app.route('/', apiV1)
|
||||
|
||||
@@ -44,6 +44,11 @@ binding = "DB"
|
||||
database_name = "xxx"
|
||||
database_id = "xxx"
|
||||
|
||||
# kv config for send email verification code
|
||||
# [[kv_namespaces]]
|
||||
# binding = "KV"
|
||||
# id = "xxxx"
|
||||
|
||||
# ratelimit config for /api/new_address
|
||||
# [[unsafe.bindings]]
|
||||
# name = "RATE_LIMITER"
|
||||
|
||||
Reference in New Issue
Block a user