mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-05 23:47:50 +08:00
feat: add adminContact && DEFAULT_SEND_BALANCE (#162)
This commit is contained in:
@@ -132,9 +132,13 @@ PREFIX = "tmp" # 要处理的邮箱名称前缀
|
|||||||
# PASSWORDS = ["123", "456"]
|
# PASSWORDS = ["123", "456"]
|
||||||
# admin 控制台密码, 不配置则不允许访问控制台
|
# admin 控制台密码, 不配置则不允许访问控制台
|
||||||
# ADMIN_PASSWORDS = ["123", "456"]
|
# ADMIN_PASSWORDS = ["123", "456"]
|
||||||
|
# admin 联系方式,不配置则不显示,可配置任意字符串
|
||||||
|
# ADMIN_CONTACT = "xx@xx.xxx"
|
||||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名
|
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名
|
||||||
JWT_SECRET = "xxx" # 用于生成 jwt 的密钥
|
JWT_SECRET = "xxx" # 用于生成 jwt 的密钥
|
||||||
BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔
|
BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔
|
||||||
|
# 默认发送邮件余额,如果不设置,将为 0
|
||||||
|
# DEFAULT_SEND_BALANCE = 1
|
||||||
# dkim config
|
# dkim config
|
||||||
# DKIM_SELECTOR = "mailchannels" # 参考 DKIM 部分 mailchannels._domainkey 的 mailchannels
|
# DKIM_SELECTOR = "mailchannels" # 参考 DKIM 部分 mailchannels._domainkey 的 mailchannels
|
||||||
# DKIM_PRIVATE_KEY = "" # 参考 DKIM 部分 priv_key.txt 的内容
|
# DKIM_PRIVATE_KEY = "" # 参考 DKIM 部分 priv_key.txt 的内容
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ const getOpenSettings = async (message) => {
|
|||||||
label: domain,
|
label: domain,
|
||||||
value: domain
|
value: domain
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
adminContact: res["adminContact"] || "",
|
||||||
};
|
};
|
||||||
if (openSettings.value.needAuth) {
|
if (openSettings.value.needAuth) {
|
||||||
showAuth.value = true;
|
showAuth.value = true;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const useGlobalState = createGlobalState(
|
|||||||
const openSettings = ref({
|
const openSettings = ref({
|
||||||
prefix: '',
|
prefix: '',
|
||||||
needAuth: false,
|
needAuth: false,
|
||||||
|
adminContact: '',
|
||||||
domains: [{
|
domains: [{
|
||||||
label: 'test.com',
|
label: 'test.com',
|
||||||
value: 'test.com'
|
value: 'test.com'
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import MailsUnknow from './admin/MailsUnknow.vue';
|
|||||||
import Maintenance from './admin/Maintenance.vue';
|
import Maintenance from './admin/Maintenance.vue';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
localeCache, adminAuth, showAdminAuth, adminTab
|
localeCache, adminAuth, showAdminAuth, adminTab, loading
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ onMounted(async () => {
|
|||||||
<p>{{ t('accessTip') }}</p>
|
<p>{{ t('accessTip') }}</p>
|
||||||
<n-input v-model:value="adminAuth" type="textarea" :autosize="{ minRows: 3 }" />
|
<n-input v-model:value="adminAuth" type="textarea" :autosize="{ minRows: 3 }" />
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button @click="authFunc" type="primary">
|
<n-button @click="authFunc" type="primary" :loading="loading">
|
||||||
{{ t('ok') }}
|
{{ t('ok') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import { useIsMobile } from '../utils/composables'
|
import { useIsMobile } from '../utils/composables'
|
||||||
import { DarkModeFilled, LightModeFilled, MenuFilled, AdminPanelSettingsFilled, SendFilled } from '@vicons/material'
|
import { DarkModeFilled, LightModeFilled, MenuFilled, AdminPanelSettingsFilled, SendFilled } from '@vicons/material'
|
||||||
import { GithubAlt, Language, User, Home, Copy } from '@vicons/fa'
|
import { GithubAlt, Language, User, Home, Copy } from '@vicons/fa'
|
||||||
|
import AdminContact from './admin/AdminContact.vue'
|
||||||
|
|
||||||
import { useGlobalState } from '../store'
|
import { useGlobalState } from '../store'
|
||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
@@ -14,7 +15,7 @@ const message = useMessage()
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
jwt, localeCache, toggleDark, isDark,
|
jwt, localeCache, toggleDark, isDark,
|
||||||
showAuth, adminAuth, auth
|
showAuth, adminAuth, auth, loading
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
const { showLogin, openSettings, settings } = useGlobalState()
|
const { showLogin, openSettings, settings } = useGlobalState()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -22,6 +23,7 @@ const router = useRouter()
|
|||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
const isAdminRoute = computed(() => route.path.includes('admin'))
|
const isAdminRoute = computed(() => route.path.includes('admin'))
|
||||||
|
|
||||||
|
const showMobileMenu = ref(false)
|
||||||
const showNewEmail = ref(false)
|
const showNewEmail = ref(false)
|
||||||
const showLogout = ref(false)
|
const showLogout = ref(false)
|
||||||
const showDelteAccount = ref(false)
|
const showDelteAccount = ref(false)
|
||||||
@@ -85,7 +87,8 @@ const { t } = useI18n({
|
|||||||
getNewEmailTip3: 'You can choose a domain from the dropdown list.',
|
getNewEmailTip3: 'You can choose a domain from the dropdown list.',
|
||||||
yourAddress: 'Your email address is',
|
yourAddress: 'Your email address is',
|
||||||
password: 'Password',
|
password: 'Password',
|
||||||
passwordTip: 'Please copy the password and you can use it to login to your email account.', cancel: 'Cancel',
|
passwordTip: 'Please copy the password and you can use it to login to your email account.',
|
||||||
|
cancel: 'Cancel',
|
||||||
ok: 'OK',
|
ok: 'OK',
|
||||||
copy: 'Copy',
|
copy: 'Copy',
|
||||||
copied: 'Copied',
|
copied: 'Copied',
|
||||||
@@ -138,10 +141,10 @@ const menuOptions = computed(() => [
|
|||||||
label: () => h(
|
label: () => h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
bordered: false,
|
text: true,
|
||||||
ghost: true,
|
|
||||||
size: "small",
|
size: "small",
|
||||||
onClick: () => router.push('/')
|
style: "width: 100%",
|
||||||
|
onClick: () => { router.push('/'); showMobileMenu.value = false; }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: () => t('home'),
|
default: () => t('home'),
|
||||||
@@ -154,10 +157,10 @@ const menuOptions = computed(() => [
|
|||||||
label: () => h(
|
label: () => h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
bordered: false,
|
text: true,
|
||||||
ghost: true,
|
|
||||||
size: "small",
|
size: "small",
|
||||||
onClick: () => router.push('/admin')
|
style: "width: 100%",
|
||||||
|
onClick: () => { router.push('/admin'); showMobileMenu.value = false; }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: () => "Admin",
|
default: () => "Admin",
|
||||||
@@ -171,9 +174,9 @@ const menuOptions = computed(() => [
|
|||||||
label: () => h(
|
label: () => h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
bordered: false,
|
text: true,
|
||||||
ghost: true,
|
|
||||||
size: "small",
|
size: "small",
|
||||||
|
style: "width: 100%",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: () => t('user'),
|
default: () => t('user'),
|
||||||
@@ -187,10 +190,10 @@ const menuOptions = computed(() => [
|
|||||||
label: () => h(
|
label: () => h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
bordered: false,
|
text: true,
|
||||||
ghost: true,
|
|
||||||
size: "small",
|
size: "small",
|
||||||
onClick: () => router.push('/sendbox')
|
style: "width: 100%",
|
||||||
|
onClick: () => { router.push('/sendbox'); showMobileMenu.value = false; }
|
||||||
},
|
},
|
||||||
{ default: () => t('sendbox') }
|
{ default: () => t('sendbox') }
|
||||||
),
|
),
|
||||||
@@ -200,10 +203,10 @@ const menuOptions = computed(() => [
|
|||||||
label: () => h(
|
label: () => h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
bordered: false,
|
text: true,
|
||||||
ghost: true,
|
|
||||||
size: "small",
|
size: "small",
|
||||||
onClick: () => { showPassword.value = true }
|
style: "width: 100%",
|
||||||
|
onClick: () => { showPassword.value = true; showMobileMenu.value = false; }
|
||||||
},
|
},
|
||||||
{ default: () => t('showPassword') }
|
{ default: () => t('showPassword') }
|
||||||
),
|
),
|
||||||
@@ -213,10 +216,10 @@ const menuOptions = computed(() => [
|
|||||||
label: () => h(
|
label: () => h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
bordered: false,
|
text: true,
|
||||||
ghost: true,
|
|
||||||
size: "small",
|
size: "small",
|
||||||
onClick: () => { router.push('/settings') }
|
style: "width: 100%",
|
||||||
|
onClick: () => { router.push('/settings'); showMobileMenu.value = false; }
|
||||||
},
|
},
|
||||||
{ default: () => t('settings') }
|
{ default: () => t('settings') }
|
||||||
),
|
),
|
||||||
@@ -226,10 +229,10 @@ const menuOptions = computed(() => [
|
|||||||
label: () => h(
|
label: () => h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
bordered: false,
|
text: true,
|
||||||
ghost: true,
|
|
||||||
size: "small",
|
size: "small",
|
||||||
onClick: () => { showLogout.value = true }
|
style: "width: 100%",
|
||||||
|
onClick: () => { showLogout.value = true; showMobileMenu.value = false; }
|
||||||
},
|
},
|
||||||
{ default: () => t('logout') }
|
{ default: () => t('logout') }
|
||||||
),
|
),
|
||||||
@@ -239,10 +242,10 @@ const menuOptions = computed(() => [
|
|||||||
label: () => h(
|
label: () => h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
bordered: false,
|
text: true,
|
||||||
ghost: true,
|
|
||||||
size: "small",
|
size: "small",
|
||||||
onClick: () => { showDelteAccount.value = true }
|
style: "width: 100%",
|
||||||
|
onClick: () => { showDelteAccount.value = true; showMobileMenu.value = false; }
|
||||||
},
|
},
|
||||||
{ default: () => t('delteAccount') }
|
{ default: () => t('delteAccount') }
|
||||||
),
|
),
|
||||||
@@ -254,10 +257,10 @@ const menuOptions = computed(() => [
|
|||||||
label: () => h(
|
label: () => h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
bordered: false,
|
text: true,
|
||||||
ghost: true,
|
|
||||||
size: "small",
|
size: "small",
|
||||||
onClick: () => toggleDark()
|
style: "width: 100%",
|
||||||
|
onClick: () => { toggleDark(); showMobileMenu.value = false; }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: () => isDark.value ? t('light') : t('dark'),
|
default: () => isDark.value ? t('light') : t('dark'),
|
||||||
@@ -272,10 +275,13 @@ const menuOptions = computed(() => [
|
|||||||
label: () => h(
|
label: () => h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
bordered: false,
|
text: true,
|
||||||
ghost: true,
|
|
||||||
size: "small",
|
size: "small",
|
||||||
onClick: () => localeCache.value == 'zh' ? changeLocale('en') : changeLocale('zh')
|
style: "width: 100%",
|
||||||
|
onClick: () => {
|
||||||
|
localeCache.value == 'zh' ? changeLocale('en') : changeLocale('zh');
|
||||||
|
showMobileMenu.value = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: () => localeCache.value == 'zh' ? "English" : "中文",
|
default: () => localeCache.value == 'zh' ? "English" : "中文",
|
||||||
@@ -290,9 +296,9 @@ const menuOptions = computed(() => [
|
|||||||
label: () => h(
|
label: () => h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
bordered: !isMobile.value,
|
text: true,
|
||||||
ghost: true,
|
|
||||||
size: "small",
|
size: "small",
|
||||||
|
style: "width: 100%",
|
||||||
tag: "a",
|
tag: "a",
|
||||||
target: "_blank",
|
target: "_blank",
|
||||||
href: "https://github.com/dreamhunter2333/cloudflare_temp_email",
|
href: "https://github.com/dreamhunter2333/cloudflare_temp_email",
|
||||||
@@ -306,21 +312,6 @@ const menuOptions = computed(() => [
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const menuOptionsMobile = computed(() => [
|
|
||||||
{
|
|
||||||
label: t('menu'),
|
|
||||||
icon: () => h(
|
|
||||||
NIcon,
|
|
||||||
{
|
|
||||||
component: MenuFilled
|
|
||||||
}
|
|
||||||
),
|
|
||||||
key: "menu",
|
|
||||||
children: menuOptions.value
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
|
||||||
const copy = async () => {
|
const copy = async () => {
|
||||||
try {
|
try {
|
||||||
await toClipboard(settings.value.address)
|
await toClipboard(settings.value.address)
|
||||||
@@ -384,13 +375,30 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n-layout-header>
|
<n-page-header>
|
||||||
<h2 style="display: inline-block; margin-left: 10px;">{{ t('title') }}</h2>
|
<template #title>
|
||||||
<div>
|
<h3>{{ t('title') }}</h3>
|
||||||
<n-menu v-if="!isMobile" mode="horizontal" :options="menuOptions" />
|
</template>
|
||||||
<n-menu v-else mode="horizontal" :options="menuOptionsMobile" />
|
<template #avatar>
|
||||||
</div>
|
<n-avatar style="margin-left: 10px;" src="/logo.png" />
|
||||||
</n-layout-header>
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<n-space>
|
||||||
|
<n-menu v-if="!isMobile" mode="horizontal" :options="menuOptions" />
|
||||||
|
<n-button v-else :text="true" @click="showMobileMenu = !showMobileMenu" style="margin-right: 10px;">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="MenuFilled" />
|
||||||
|
</template>
|
||||||
|
{{ t('menu') }}
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
</n-page-header>
|
||||||
|
<n-drawer v-model:show="showMobileMenu" placement="top" style="height: 100vh;">
|
||||||
|
<n-drawer-content :title="t('menu')" closable>
|
||||||
|
<n-menu :options="menuOptions" />
|
||||||
|
</n-drawer-content>
|
||||||
|
</n-drawer>
|
||||||
<div v-if="!isAdminRoute">
|
<div v-if="!isAdminRoute">
|
||||||
<n-card v-if="!settings.fetched">
|
<n-card v-if="!settings.fetched">
|
||||||
<n-skeleton style="height: 50vh" />
|
<n-skeleton style="height: 50vh" />
|
||||||
@@ -460,7 +468,7 @@ onMounted(async () => {
|
|||||||
<n-button @click="showNewEmail = false">
|
<n-button @click="showNewEmail = false">
|
||||||
{{ t('cancel') }}
|
{{ t('cancel') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button @click="newEmail" type="primary">
|
<n-button @click="newEmail" type="primary" :loading="loading">
|
||||||
{{ t('ok') }}
|
{{ t('ok') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -482,11 +490,12 @@ onMounted(async () => {
|
|||||||
<template #header>
|
<template #header>
|
||||||
<div>{{ t('login') }}</div>
|
<div>{{ t('login') }}</div>
|
||||||
</template>
|
</template>
|
||||||
|
<AdminContact />
|
||||||
<n-input v-model:value="password" type="textarea" :autosize="{
|
<n-input v-model:value="password" type="textarea" :autosize="{
|
||||||
minRows: 3
|
minRows: 3
|
||||||
}" />
|
}" />
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button @click="login" size="small" tertiary round type="primary">
|
<n-button @click="login" :loading="loading" size="small" tertiary round type="primary">
|
||||||
{{ t('login') }}
|
{{ t('login') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -497,7 +506,7 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
<p>{{ t('logoutConfirm') }}</p>
|
<p>{{ t('logoutConfirm') }}</p>
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button @click="logout" size="small" tertiary round type="primary">
|
<n-button :loading="loading" @click="logout" size="small" tertiary round type="primary">
|
||||||
{{ t('logout') }}
|
{{ t('logout') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -508,7 +517,7 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
<p>{{ t('delteAccountConfirm') }}</p>
|
<p>{{ t('delteAccountConfirm') }}</p>
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button @click="deleteAccount" size="small" tertiary round type="error">
|
<n-button :loading="loading" @click="deleteAccount" size="small" tertiary round type="error">
|
||||||
{{ t('delteAccount') }}
|
{{ t('delteAccount') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -523,7 +532,7 @@ onMounted(async () => {
|
|||||||
minRows: 3
|
minRows: 3
|
||||||
}" />
|
}" />
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button @click="authFunc" type="primary">
|
<n-button :loading="loading" @click="authFunc" type="primary">
|
||||||
{{ t('ok') }}
|
{{ t('ok') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -538,6 +547,11 @@ onMounted(async () => {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-menu-button {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
.n-alert {
|
.n-alert {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ onMounted(async () => {
|
|||||||
<n-layout v-if="settings.address">
|
<n-layout v-if="settings.address">
|
||||||
<n-split class="left" v-if="!isMobile" direction="horizontal" :max="0.75" :min="0.25" :default-size="0.25">
|
<n-split class="left" v-if="!isMobile" direction="horizontal" :max="0.75" :min="0.25" :default-size="0.25">
|
||||||
<template #1>
|
<template #1>
|
||||||
<div>
|
<div class="center">
|
||||||
<div style="display: inline-block; margin-top: 10px; margin-bottom: 10px;">
|
<div style="display: inline-block; margin-top: 10px; margin-bottom: 10px;">
|
||||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
||||||
</div>
|
</div>
|
||||||
@@ -138,7 +138,7 @@ onMounted(async () => {
|
|||||||
<template #unchecked>
|
<template #unchecked>
|
||||||
{{ t('autoRefresh') }}
|
{{ t('autoRefresh') }}
|
||||||
</template></n-switch>
|
</template></n-switch>
|
||||||
<n-button class="center" @click="refresh" size="small" type="primary">
|
<n-button @click="refresh" size="small" type="primary">
|
||||||
{{ t('refresh') }}
|
{{ t('refresh') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -146,7 +146,7 @@ onMounted(async () => {
|
|||||||
<n-list hoverable clickable>
|
<n-list hoverable clickable>
|
||||||
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
|
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
|
||||||
:class="mailItemClass(row)">
|
:class="mailItemClass(row)">
|
||||||
<n-thing class="center" :title="row.subject">
|
<n-thing :title="row.subject">
|
||||||
<template #description>
|
<template #description>
|
||||||
<n-tag type="info">
|
<n-tag type="info">
|
||||||
ID: {{ row.id }}
|
ID: {{ row.id }}
|
||||||
@@ -200,7 +200,7 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
</n-split>
|
</n-split>
|
||||||
<div class="left" v-else>
|
<div class="left" v-else>
|
||||||
<div>
|
<div class="center">
|
||||||
<div style="display: inline-block; margin-top: 10px; margin-bottom: 10px;">
|
<div style="display: inline-block; margin-top: 10px; margin-bottom: 10px;">
|
||||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
||||||
</div>
|
</div>
|
||||||
@@ -211,14 +211,14 @@ onMounted(async () => {
|
|||||||
<template #unchecked>
|
<template #unchecked>
|
||||||
{{ t('autoRefresh') }}
|
{{ t('autoRefresh') }}
|
||||||
</template></n-switch>
|
</template></n-switch>
|
||||||
<n-button class="center" @click="refresh" size="small" type="primary">
|
<n-button @click="refresh" size="small" type="primary">
|
||||||
{{ t('refresh') }}
|
{{ t('refresh') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
<div id="drawer-target" style="overflow: auto; height: 80vh;">
|
<div id="drawer-target" style="overflow: auto; height: 80vh;">
|
||||||
<n-list hoverable clickable>
|
<n-list hoverable clickable>
|
||||||
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)">
|
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)">
|
||||||
<n-thing class="center" :title="row.subject">
|
<n-thing :title="row.subject">
|
||||||
<template #description>
|
<template #description>
|
||||||
<n-tag type="info">
|
<n-tag type="info">
|
||||||
ID: {{ row.id }}
|
ID: {{ row.id }}
|
||||||
@@ -303,6 +303,10 @@ onMounted(async () => {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.overlay {
|
.overlay {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { NMenu } from 'naive-ui';
|
|||||||
import { MenuFilled } from '@vicons/material'
|
import { MenuFilled } from '@vicons/material'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
localeCache, adminAuth, showAdminAuth,
|
localeCache, adminAuth, showAdminAuth, loading,
|
||||||
adminTab, adminMailTabAddress, adminSendBoxTabAddress
|
adminTab, adminMailTabAddress, adminSendBoxTabAddress
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
@@ -218,7 +218,7 @@ onMounted(async () => {
|
|||||||
<n-modal v-model:show="showDelteAccount" preset="dialog" :title="t('delteAccount')">
|
<n-modal v-model:show="showDelteAccount" preset="dialog" :title="t('delteAccount')">
|
||||||
<p>{{ t('deleteTip') }}</p>
|
<p>{{ t('deleteTip') }}</p>
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button @click="deleteEmail" size="small" tertiary round type="error">
|
<n-button :loading="loading" @click="deleteEmail" size="small" tertiary round type="error">
|
||||||
{{ t('delteAccount') }}
|
{{ t('delteAccount') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useGlobalState } from '../../store'
|
||||||
|
const { localeCache, openSettings } = useGlobalState()
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
locale: localeCache.value || 'zh',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
adminContact: 'If you need help, please contact the administrator ({msg})',
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
adminContact: '如果你需要帮助,请联系管理员 ({msg})',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-alert v-if="openSettings.adminContact" type="info" show-icon>
|
||||||
|
<span>{{ t('adminContact', { msg: openSettings.adminContact }) }}</span>
|
||||||
|
</n-alert>
|
||||||
|
</template>
|
||||||
@@ -5,7 +5,7 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import { useGlobalState } from '../../store'
|
import { useGlobalState } from '../../store'
|
||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
|
|
||||||
const { localeCache } = useGlobalState()
|
const { localeCache, loading } = useGlobalState()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
const { t } = useI18n({
|
const { t } = useI18n({
|
||||||
@@ -161,7 +161,7 @@ onMounted(async () => {
|
|||||||
<n-input-number v-model:value="senderBalance" :min="0" :max="1000" />
|
<n-input-number v-model:value="senderBalance" :min="0" :max="1000" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button @click="updateData()" size="small" tertiary round type="primary">
|
<n-button :loading="loading" @click="updateData()" size="small" tertiary round type="primary">
|
||||||
{{ t('ok') }}
|
{{ t('ok') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -59,34 +59,36 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-row>
|
<n-card>
|
||||||
<n-col :span="6">
|
<n-row>
|
||||||
<n-statistic :label="t('userCount')" :value="statistics.userCount">
|
<n-col :span="6">
|
||||||
<template #prefix>
|
<n-statistic :label="t('userCount')" :value="statistics.userCount">
|
||||||
<n-icon :component="User" />
|
<template #prefix>
|
||||||
</template>
|
<n-icon :component="User" />
|
||||||
</n-statistic>
|
</template>
|
||||||
</n-col>
|
</n-statistic>
|
||||||
<n-col :span="6">
|
</n-col>
|
||||||
<n-statistic :label="t('activeUser')" :value="statistics.activeUserCount7days">
|
<n-col :span="6">
|
||||||
<template #prefix>
|
<n-statistic :label="t('activeUser')" :value="statistics.activeUserCount7days">
|
||||||
<n-icon :component="UserCheck" />
|
<template #prefix>
|
||||||
</template>
|
<n-icon :component="UserCheck" />
|
||||||
</n-statistic>
|
</template>
|
||||||
</n-col>
|
</n-statistic>
|
||||||
<n-col :span="6">
|
</n-col>
|
||||||
<n-statistic :label="t('mailCount')" :value="statistics.mailCount">
|
<n-col :span="6">
|
||||||
<template #prefix>
|
<n-statistic :label="t('mailCount')" :value="statistics.mailCount">
|
||||||
<n-icon :component="MailBulk" />
|
<template #prefix>
|
||||||
</template>
|
<n-icon :component="MailBulk" />
|
||||||
</n-statistic>
|
</template>
|
||||||
</n-col>
|
</n-statistic>
|
||||||
<n-col :span="6">
|
</n-col>
|
||||||
<n-statistic :label="t('sendMailCount')" :value="statistics.sendMailCount">
|
<n-col :span="6">
|
||||||
<template #prefix>
|
<n-statistic :label="t('sendMailCount')" :value="statistics.sendMailCount">
|
||||||
<n-icon :component="SendOutlined" />
|
<template #prefix>
|
||||||
</template>
|
<n-icon :component="SendOutlined" />
|
||||||
</n-statistic>
|
</template>
|
||||||
</n-col>
|
</n-statistic>
|
||||||
</n-row>
|
</n-col>
|
||||||
|
</n-row>
|
||||||
|
</n-card>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { DomEditor } from '@wangeditor/editor'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { onMounted, onBeforeUnmount, ref, shallowRef } from 'vue'
|
import { onMounted, onBeforeUnmount, ref, shallowRef } from 'vue'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
|
import AdminContact from '../admin/AdminContact.vue'
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
import { useGlobalState } from '../../store'
|
||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
@@ -143,7 +144,6 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
const handleCreated = (editor) => {
|
const handleCreated = (editor) => {
|
||||||
editorRef.value = editor;
|
editorRef.value = editor;
|
||||||
console.log(editor.getAllMenuKeys())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -160,6 +160,7 @@ onMounted(async () => {
|
|||||||
<n-button type="primary" ghost @click="requestAccess">{{ t('requestAccess') }}</n-button>
|
<n-button type="primary" ghost @click="requestAccess">{{ t('requestAccess') }}</n-button>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
<br />
|
<br />
|
||||||
|
<AdminContact />
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<n-alert type="info" show-icon>
|
<n-alert type="info" show-icon>
|
||||||
@@ -208,7 +209,6 @@ onMounted(async () => {
|
|||||||
<n-input v-else type="textarea" v-model:value="mailModel.content" :autosize="{
|
<n-input v-else type="textarea" v-model:value="mailModel.content" :autosize="{
|
||||||
minRows: 3
|
minRows: 3
|
||||||
}" />
|
}" />
|
||||||
|
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -68,9 +68,13 @@ PREFIX = "tmp" # The mailbox name prefix to be processed
|
|||||||
# PASSWORDS = ["123", "456"]
|
# PASSWORDS = ["123", "456"]
|
||||||
# admin console password, if not configured, access to the console is not allowed
|
# admin console password, if not configured, access to the console is not allowed
|
||||||
# ADMIN_PASSWORDS = ["123", "456"]
|
# ADMIN_PASSWORDS = ["123", "456"]
|
||||||
|
# admin contact information. If not configured, it will not be displayed. Any string can be configured.
|
||||||
|
# ADMIN_CONTACT = "xx@xx.xxx"
|
||||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # your domain name
|
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # your domain name
|
||||||
JWT_SECRET = "xxx" # Key used to generate jwt
|
JWT_SECRET = "xxx" # Key used to generate jwt
|
||||||
BLACK_LIST = "" # Blacklist, used to filter senders, comma separated
|
BLACK_LIST = "" # Blacklist, used to filter senders, comma separated
|
||||||
|
# default send balance, if not set, it will be 0
|
||||||
|
# DEFAULT_SEND_BALANCE = 1
|
||||||
# dkim config
|
# dkim config
|
||||||
# DKIM_SELECTOR = "mailchannels" # Refer to the DKIM section mailchannels._domainkey for mailchannels
|
# DKIM_SELECTOR = "mailchannels" # Refer to the DKIM section mailchannels._domainkey for mailchannels
|
||||||
# DKIM_PRIVATE_KEY = "" # Refer to the contents of priv_key.txt in the DKIM section
|
# DKIM_PRIVATE_KEY = "" # Refer to the contents of priv_key.txt in the DKIM section
|
||||||
|
|||||||
@@ -26,9 +26,13 @@ PREFIX = "tmp" # 要处理的邮箱名称前缀,不需要后缀可配置为空
|
|||||||
# PASSWORDS = ["123", "456"]
|
# PASSWORDS = ["123", "456"]
|
||||||
# admin 控制台密码, 不配置则不允许访问控制台
|
# admin 控制台密码, 不配置则不允许访问控制台
|
||||||
# ADMIN_PASSWORDS = ["123", "456"]
|
# ADMIN_PASSWORDS = ["123", "456"]
|
||||||
|
# admin 联系方式,不配置则不显示,可配置任意字符串
|
||||||
|
# ADMIN_CONTACT = "xx@xx.xxx"
|
||||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名, 支持多个域名
|
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名, 支持多个域名
|
||||||
JWT_SECRET = "xxx" # 用于生成 jwt 的密钥
|
JWT_SECRET = "xxx" # 用于生成 jwt 的密钥, jwt 用于给用户登录以及鉴权
|
||||||
BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔
|
BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔
|
||||||
|
# 默认发送邮件余额,如果不设置,将为 0
|
||||||
|
# DEFAULT_SEND_BALANCE = 1
|
||||||
# dkim config
|
# dkim config
|
||||||
# DKIM_SELECTOR = "mailchannels" # 参考 DKIM 部分 mailchannels._domainkey 的 mailchannels
|
# DKIM_SELECTOR = "mailchannels" # 参考 DKIM 部分 mailchannels._domainkey 的 mailchannels
|
||||||
# DKIM_PRIVATE_KEY = "" # 参考 DKIM 部分 priv_key.txt 的内容
|
# DKIM_PRIVATE_KEY = "" # 参考 DKIM 部分 priv_key.txt 的内容
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ api.get('/open_api/settings', async (c) => {
|
|||||||
"prefix": c.env.PREFIX,
|
"prefix": c.env.PREFIX,
|
||||||
"domains": getDomains(c),
|
"domains": getDomains(c),
|
||||||
"needAuth": needAuth,
|
"needAuth": needAuth,
|
||||||
|
"adminContact": c.env.ADMIN_CONTACT,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ api.post('/api/requset_send_mail_access', async (c) => {
|
|||||||
return c.text("No address", 400)
|
return c.text("No address", 400)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
const default_balance = c.env.DEFAULT_SEND_BALANCE || 0;
|
||||||
const { success } = await c.env.DB.prepare(
|
const { success } = await c.env.DB.prepare(
|
||||||
`INSERT INTO address_sender (address, balance, enabled) VALUES (?, 1, 1)`
|
`INSERT INTO address_sender (address, balance, enabled) VALUES (?, ?, ?)`
|
||||||
).bind(address).run();
|
).bind(
|
||||||
|
address, default_balance, default_balance > 0 ? 1 : 0
|
||||||
|
).run();
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return c.text("Failed to request send mail access", 500)
|
return c.text("Failed to request send mail access", 500)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -23,13 +23,14 @@ export const getPasswords = (c) => {
|
|||||||
// check if PASSWORDS is an array, if not use json.parse
|
// check if PASSWORDS is an array, if not use json.parse
|
||||||
if (!Array.isArray(c.env.PASSWORDS)) {
|
if (!Array.isArray(c.env.PASSWORDS)) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(c.env.PASSWORDS);
|
let res = JSON.parse(c.env.PASSWORDS);
|
||||||
|
return res.filter((item) => item.length > 0);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to parse PASSWORDS", e);
|
console.error("Failed to parse PASSWORDS", e);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return c.env.PASSWORDS;
|
return c.env.PASSWORDS.filter((item) => item.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAdminPasswords = (c) => {
|
export const getAdminPasswords = (c) => {
|
||||||
|
|||||||
@@ -13,9 +13,12 @@ PREFIX = "tmp"
|
|||||||
# PASSWORDS = ["123", "456"]
|
# PASSWORDS = ["123", "456"]
|
||||||
# For admin panel
|
# For admin panel
|
||||||
# ADMIN_PASSWORDS = ["123", "456"]
|
# ADMIN_PASSWORDS = ["123", "456"]
|
||||||
|
# ADMIN CONTACT, CAN BE ANY STRING
|
||||||
|
# ADMIN_CONTACT = "xx@xx.xxx"
|
||||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"]
|
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"]
|
||||||
JWT_SECRET = "xxx"
|
JWT_SECRET = "xxx"
|
||||||
BLACK_LIST = ""
|
BLACK_LIST = ""
|
||||||
|
# DEFAULT_SEND_BALANCE = 1 # default send balance, if not set, it will be 0
|
||||||
# dkim config
|
# dkim config
|
||||||
# DKIM_SELECTOR = ""
|
# DKIM_SELECTOR = ""
|
||||||
# DKIM_PRIVATE_KEY = ""
|
# DKIM_PRIVATE_KEY = ""
|
||||||
|
|||||||
Reference in New Issue
Block a user