mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-06-28 02:42:44 +08:00
feat: admin cleanup tab && admin sendbox tab (#126)
This commit is contained in:
46
.github/workflows/docs_deploy.yml
vendored
Normal file
46
.github/workflows/docs_deploy.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Deploy Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "vitepress-docs/**"
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Deploy Docs for ${{github.ref_name}}
|
||||
run: |
|
||||
cd vitepress-docs/
|
||||
pnpm install --no-frozen-lockfile
|
||||
if [[ ${{github.ref}} == refs/tags/* ]]; then
|
||||
export TAG_NAME=${{github.ref_name}}
|
||||
else
|
||||
export TAG_NAME=$(git describe --tags --abbrev=0)
|
||||
fi
|
||||
echo "Deploying docs for tag $TAG_NAME"
|
||||
pnpm run deploy
|
||||
env:
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
@@ -30,9 +30,12 @@ export const useGlobalState = createGlobalState(
|
||||
const auth = useStorage('auth', '');
|
||||
const adminAuth = useStorage('adminAuth', '');
|
||||
const jwt = useStorage('jwt', '');
|
||||
const localeCache = useStorage('locale', 'zhCN');
|
||||
const localeCache = useStorage('locale', 'zh');
|
||||
const themeSwitch = useStorage('themeSwitch', false);
|
||||
const showLogin = ref(false);
|
||||
const adminTab = ref("account");
|
||||
const adminMailTabAddress = ref("");
|
||||
const adminSendBoxTabAddress = ref("");
|
||||
return {
|
||||
loading,
|
||||
settings,
|
||||
@@ -45,6 +48,9 @@ export const useGlobalState = createGlobalState(
|
||||
adminAuth,
|
||||
showAdminAuth,
|
||||
showLogin,
|
||||
adminTab,
|
||||
adminMailTabAddress,
|
||||
adminSendBoxTabAddress,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<script setup>
|
||||
import { ref, h, onMounted, watch } from 'vue';
|
||||
import { onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { User, UserCheck, MailBulk } from '@vicons/fa'
|
||||
|
||||
import { useGlobalState } from '../store'
|
||||
import { api } from '../api'
|
||||
import { processItem } from '../utils/email-parser'
|
||||
|
||||
import SenderAccess from './admin/SenderAccess.vue'
|
||||
import Statistics from "./admin/Statistics.vue"
|
||||
import SendBox from './admin/SendBox.vue';
|
||||
import Account from './admin/Account.vue';
|
||||
import Mails from './admin/Mails.vue';
|
||||
import MailsUnknow from './admin/MailsUnknow.vue';
|
||||
import Maintenance from './admin/Maintenance.vue';
|
||||
|
||||
const { localeCache, adminAuth, showAdminAuth } = useGlobalState()
|
||||
const router = useRouter()
|
||||
const {
|
||||
localeCache, adminAuth, showAdminAuth, adminTab
|
||||
} = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const showEmailPassword = ref(false)
|
||||
const curEmailPassword = ref("")
|
||||
const addressQuery = ref("")
|
||||
|
||||
const authFunc = async () => {
|
||||
try {
|
||||
location.reload()
|
||||
@@ -29,243 +29,34 @@ const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
title: 'Temp Email Admin',
|
||||
auth: 'Admin Auth',
|
||||
home: 'Home',
|
||||
authTip: 'Please enter the correct auth code',
|
||||
name: 'Name',
|
||||
created_at: 'Created At',
|
||||
showPass: 'Show Passwrod',
|
||||
password: 'Password',
|
||||
passwordTip: 'Please copy the password and you can use it to login to your email account.',
|
||||
delete: 'Delete',
|
||||
deleteTip: 'Are you sure to delete this email?',
|
||||
refresh: 'Refresh',
|
||||
mails: 'Emails',
|
||||
itemCount: 'itemCount',
|
||||
query: 'Query',
|
||||
userCount: 'User Count',
|
||||
activeUser: '7 days Active User',
|
||||
mailCount: 'Mail Count',
|
||||
account: 'Account',
|
||||
unknow: 'Unknow',
|
||||
addressQueryTip: 'Leave blank to query all addresses',
|
||||
unknow: 'Mails with unknow receiver',
|
||||
senderAccess: 'Sender Access Control',
|
||||
sendBox: 'Send Box',
|
||||
maintenance: 'Maintenance',
|
||||
},
|
||||
zh: {
|
||||
title: '临时邮件 Admin',
|
||||
auth: 'Admin 授权',
|
||||
home: '首页',
|
||||
authTip: '请输入正确的授权码',
|
||||
name: '名称',
|
||||
created_at: '创建时间',
|
||||
showPass: '显示密码',
|
||||
password: '密码',
|
||||
passwordTip: '请复制密码,你可以使用它登录你的邮箱。',
|
||||
delete: '删除',
|
||||
deleteTip: '确定要删除这个邮箱吗?',
|
||||
refresh: '刷新',
|
||||
mails: '邮件',
|
||||
itemCount: '总数',
|
||||
query: '查询',
|
||||
userCount: '用户总数',
|
||||
activeUser: '周活跃用户',
|
||||
mailCount: '邮件总数',
|
||||
account: '账号',
|
||||
unknow: '未知',
|
||||
addressQueryTip: '留空查询所有地址',
|
||||
unknow: '无收件人邮件',
|
||||
senderAccess: '发件权限控制',
|
||||
sendBox: '发件箱',
|
||||
maintenance: '维护',
|
||||
}
|
||||
}
|
||||
});
|
||||
const data = ref([])
|
||||
const count = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
|
||||
const showPassword = async (id) => {
|
||||
try {
|
||||
curEmailPassword.value = await api.adminShowPassword(id)
|
||||
showEmailPassword.value = true
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
showEmailPassword.value = false
|
||||
curEmailPassword.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
const deleteEmail = async (id) => {
|
||||
try {
|
||||
await api.adminDeleteAddress(id)
|
||||
message.success("success");
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const { results, count: addressCount } = await api.fetch(
|
||||
`/admin/address`
|
||||
+ `?limit=${pageSize.value}`
|
||||
+ `&offset=${(page.value - 1) * pageSize.value}`
|
||||
+ (addressQuery.value ? `&query=${addressQuery.value}` : "")
|
||||
);
|
||||
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('created_at'),
|
||||
key: "created_at"
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
key: 'actions',
|
||||
render(row) {
|
||||
return h('div', [
|
||||
h(NButton,
|
||||
{
|
||||
type: 'success',
|
||||
ghost: true,
|
||||
onClick: () => showPassword(row.id)
|
||||
},
|
||||
{ default: () => t('showPass') }
|
||||
),
|
||||
h(NButton,
|
||||
{
|
||||
type: 'success',
|
||||
ghost: true,
|
||||
onClick: () => {
|
||||
mailAddress.value = row.name
|
||||
tab.value = "mails"
|
||||
}
|
||||
},
|
||||
{ default: () => t('mails') }
|
||||
),
|
||||
h(NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => deleteEmail(row.id)
|
||||
},
|
||||
{
|
||||
trigger: () => h(NButton, { type: "error" }, () => t('delete')),
|
||||
default: () => t('deleteTip')
|
||||
}
|
||||
)
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
watch([page, pageSize], async () => {
|
||||
await fetchData()
|
||||
})
|
||||
|
||||
const statistics = ref({
|
||||
userCount: 0,
|
||||
mailCount: 0,
|
||||
activeUserCount7days: 0,
|
||||
})
|
||||
|
||||
const fetchStatistics = async () => {
|
||||
try {
|
||||
const { userCount, activeUserCount7days, mailCount } = await api.fetch(`/admin/statistics`);
|
||||
statistics.value.mailCount = mailCount || 0;
|
||||
statistics.value.userCount = userCount || 0;
|
||||
statistics.value.activeUserCount7days = activeUserCount7days || 0;
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!adminAuth.value) {
|
||||
showAdminAuth.value = true
|
||||
} else {
|
||||
await fetchData()
|
||||
await fetchStatistics()
|
||||
showAdminAuth.value = true;
|
||||
return;
|
||||
}
|
||||
})
|
||||
|
||||
const tab = ref("account")
|
||||
const mailAddress = ref("")
|
||||
const mailData = ref([])
|
||||
const mailCount = ref(0)
|
||||
const mailPage = ref(1)
|
||||
const mailPageSize = ref(20)
|
||||
|
||||
watch([mailPage, mailPageSize, mailAddress], async () => {
|
||||
await fetchMailData()
|
||||
})
|
||||
|
||||
const fetchMailData = async () => {
|
||||
if (!mailAddress.value) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const { results, count } = await api.fetch(
|
||||
`/admin/mails`
|
||||
+ `?address=${mailAddress.value}`
|
||||
+ `&limit=${mailPageSize.value}`
|
||||
+ `&offset=${(mailPage.value - 1) * mailPageSize.value}`
|
||||
);
|
||||
mailData.value = await Promise.all(results.map(async (item) => {
|
||||
return await processItem(item);
|
||||
}));
|
||||
if (count > 0) {
|
||||
mailCount.value = count;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const mailUnknowData = ref([])
|
||||
const mailUnknowCount = ref(0)
|
||||
const mailUnknowPage = ref(1)
|
||||
const mailUnknowPageSize = ref(20)
|
||||
|
||||
watch([mailUnknowPage, mailUnknowPageSize], async () => {
|
||||
await fetchMailUnknowData()
|
||||
})
|
||||
|
||||
const fetchMailUnknowData = async () => {
|
||||
try {
|
||||
const { results, count } = await api.fetch(
|
||||
`/admin/mails_unknow`
|
||||
+ `?limit=${mailPageSize.value}`
|
||||
+ `&offset=${(mailPage.value - 1) * mailPageSize.value}`
|
||||
);
|
||||
mailUnknowData.value = await Promise.all(results.map(async (item) => {
|
||||
return await processItem(item);
|
||||
}));
|
||||
if (count > 0) {
|
||||
mailUnknowCount.value = count;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -283,125 +74,26 @@ const fetchMailUnknowData = async () => {
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showEmailPassword" preset="dialog" title="Dialog">
|
||||
<template #header>
|
||||
<div>{{ t("password") }}</div>
|
||||
</template>
|
||||
<span>
|
||||
<p>{{ t("passwordTip") }}</p>
|
||||
</span>
|
||||
<n-card>
|
||||
<b>{{ curEmailPassword }}</b>
|
||||
</n-card>
|
||||
<template #action>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-row>
|
||||
<n-col :span="8">
|
||||
<n-statistic :label="t('userCount')" :value="statistics.userCount">
|
||||
<template #prefix>
|
||||
<n-icon :component="User" />
|
||||
</template>
|
||||
</n-statistic>
|
||||
</n-col>
|
||||
<n-col :span="8">
|
||||
<n-statistic :label="t('activeUser')" :value="statistics.activeUserCount7days">
|
||||
<template #prefix>
|
||||
<n-icon :component="UserCheck" />
|
||||
</template>
|
||||
</n-statistic>
|
||||
</n-col>
|
||||
<n-col :span="8">
|
||||
<n-statistic :label="t('mailCount')" :value="statistics.mailCount">
|
||||
<template #prefix>
|
||||
<n-icon :component="MailBulk" />
|
||||
</template>
|
||||
</n-statistic>
|
||||
</n-col>
|
||||
</n-row>
|
||||
<n-tabs type="segment" v-model:value="tab">
|
||||
<Statistics />
|
||||
<n-tabs type="card" v-model:value="adminTab">
|
||||
<n-tab-pane name="account" :tab="t('account')">
|
||||
<n-input-group>
|
||||
<n-input v-model:value="addressQuery" clearable :placeholder="t('addressQueryTip')" />
|
||||
<n-button @click="fetchData" type="primary" ghost>
|
||||
{{ 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>
|
||||
</n-pagination>
|
||||
</div>
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
||||
<Account />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="mails" :tab="t('mails')">
|
||||
<n-input-group>
|
||||
<n-input v-model:value="mailAddress" />
|
||||
<n-button @click="fetchMailData" type="primary" ghost>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<n-list hoverable clickable>
|
||||
<div style="display: inline-block; margin-bottom: 10px;">
|
||||
<n-pagination v-model:page="mailPage" v-model:page-size="mailPageSize" :item-count="mailCount" simple>
|
||||
<template #prefix="{ itemCount }">
|
||||
{{ t('itemCount') }}: {{ itemCount }}
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
<n-list-item v-for="row in mailData" v-bind:key="row.id">
|
||||
<n-thing class="center" :title="row.subject">
|
||||
<template #description>
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
FROM: {{ row.source }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
ID: {{ row.id }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
</template>
|
||||
<div v-html="row.message"></div>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
<Mails />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="unknow" :tab="t('unknow')">
|
||||
<n-button @click="fetchMailUnknowData" type="primary" ghost>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
<n-list hoverable clickable>
|
||||
<div style="display: inline-block; margin-bottom: 10px;">
|
||||
<n-pagination v-model:page="mailUnknowPage" v-model:page-size="mailUnknowPageSize"
|
||||
:item-count="mailUnknowCount" simple>
|
||||
<template #prefix="{ itemCount }">
|
||||
{{ t('itemCount') }}: {{ itemCount }}
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
<n-list-item v-for="row in mailUnknowData" v-bind:key="row.id">
|
||||
<n-thing class="center" :title="row.subject">
|
||||
<template #description>
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
FROM: {{ row.source }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
ID: {{ row.id }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
</template>
|
||||
<div v-html="row.message"></div>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
<MailsUnknow />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="senderAccess" :tab="t('senderAccess')">
|
||||
<SenderAccess />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="sendBox" :tab="t('sendBox')">
|
||||
<SendBox />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="maintenance" :tab="t('maintenance')">
|
||||
<Maintenance />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -184,7 +184,7 @@ const menuOptions = computed(() => [
|
||||
label: () => h(
|
||||
NButton,
|
||||
{
|
||||
tertiary: true,
|
||||
bordered: false,
|
||||
ghost: true,
|
||||
size: "small",
|
||||
onClick: () => router.push('/sendbox')
|
||||
@@ -197,7 +197,7 @@ const menuOptions = computed(() => [
|
||||
label: () => h(
|
||||
NButton,
|
||||
{
|
||||
tertiary: true,
|
||||
bordered: false,
|
||||
ghost: true,
|
||||
size: "small",
|
||||
onClick: () => { showPassword.value = true }
|
||||
@@ -210,7 +210,7 @@ const menuOptions = computed(() => [
|
||||
label: () => h(
|
||||
NButton,
|
||||
{
|
||||
tertiary: true,
|
||||
bordered: false,
|
||||
ghost: true,
|
||||
size: "small",
|
||||
onClick: () => { router.push('/settings') }
|
||||
@@ -223,7 +223,7 @@ const menuOptions = computed(() => [
|
||||
label: () => h(
|
||||
NButton,
|
||||
{
|
||||
tertiary: true,
|
||||
bordered: false,
|
||||
ghost: true,
|
||||
size: "small",
|
||||
onClick: () => { showLogout.value = true }
|
||||
@@ -236,7 +236,7 @@ const menuOptions = computed(() => [
|
||||
label: () => h(
|
||||
NButton,
|
||||
{
|
||||
tertiary: true,
|
||||
bordered: false,
|
||||
ghost: true,
|
||||
size: "small",
|
||||
onClick: () => { showDelteAccount.value = true }
|
||||
|
||||
248
frontend/src/views/admin/Account.vue
Normal file
248
frontend/src/views/admin/Account.vue
Normal file
@@ -0,0 +1,248 @@
|
||||
<script setup>
|
||||
import { ref, h, onMounted, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import { NMenu } from 'naive-ui';
|
||||
import { MenuFilled } from '@vicons/material'
|
||||
|
||||
const {
|
||||
localeCache, adminAuth, showAdminAuth,
|
||||
adminTab, adminMailTabAddress, adminSendBoxTabAddress
|
||||
} = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
name: 'Name',
|
||||
created_at: 'Created At',
|
||||
showPass: 'Show Passwrod',
|
||||
password: 'Password',
|
||||
passwordTip: 'Please copy the password and you can use it to login to your email account.',
|
||||
delete: 'Delete',
|
||||
deleteTip: 'Are you sure to delete this email?',
|
||||
delteAccount: 'Delete Account',
|
||||
viewMails: 'View Mails',
|
||||
viewSendBox: 'View SendBox',
|
||||
itemCount: 'itemCount',
|
||||
query: 'Query',
|
||||
addressQueryTip: 'Leave blank to query all addresses',
|
||||
actions: 'Actions'
|
||||
},
|
||||
zh: {
|
||||
name: '名称',
|
||||
created_at: '创建时间',
|
||||
showPass: '显示密码',
|
||||
password: '密码',
|
||||
passwordTip: '请复制密码,你可以使用它登录你的邮箱。',
|
||||
delete: '删除',
|
||||
deleteTip: '确定要删除这个邮箱吗?',
|
||||
delteAccount: '删除邮箱',
|
||||
viewMails: '查看邮件',
|
||||
viewSendBox: '查看发件箱',
|
||||
itemCount: '总数',
|
||||
query: '查询',
|
||||
addressQueryTip: '留空查询所有地址',
|
||||
actions: '操作',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const showEmailPassword = ref(false)
|
||||
const curEmailPassword = ref("")
|
||||
const curDeleteAddressId = ref(0);
|
||||
|
||||
const addressQuery = ref("")
|
||||
|
||||
const data = ref([])
|
||||
const count = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const showDelteAccount = ref(false)
|
||||
|
||||
const showPassword = async (id) => {
|
||||
try {
|
||||
curEmailPassword.value = await api.adminShowPassword(id)
|
||||
showEmailPassword.value = true
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
showEmailPassword.value = false
|
||||
curEmailPassword.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
const deleteEmail = async () => {
|
||||
try {
|
||||
await api.adminDeleteAddress(curDeleteAddressId.value)
|
||||
message.success("success");
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const { results, count: addressCount } = await api.fetch(
|
||||
`/admin/address`
|
||||
+ `?limit=${pageSize.value}`
|
||||
+ `&offset=${(page.value - 1) * pageSize.value}`
|
||||
+ (addressQuery.value ? `&query=${addressQuery.value}` : "")
|
||||
);
|
||||
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('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,
|
||||
{
|
||||
bordered: false,
|
||||
ghost: true,
|
||||
onClick: () => showPassword(row.id)
|
||||
},
|
||||
{ default: () => t('showPass') }
|
||||
),
|
||||
},
|
||||
{
|
||||
label: () => h(NButton,
|
||||
{
|
||||
bordered: false,
|
||||
ghost: true,
|
||||
onClick: () => {
|
||||
adminMailTabAddress.value = row.name;
|
||||
adminTab.value = "mails";
|
||||
}
|
||||
},
|
||||
{ default: () => t('viewMails') }
|
||||
)
|
||||
},
|
||||
{
|
||||
label: () => h(NButton,
|
||||
{
|
||||
bordered: false,
|
||||
ghost: true,
|
||||
onClick: () => {
|
||||
adminSendBoxTabAddress.value = row.name;
|
||||
adminTab.value = "sendBox";
|
||||
}
|
||||
},
|
||||
{ default: () => t('viewSendBox') }
|
||||
)
|
||||
},
|
||||
{
|
||||
label: () => h(NButton,
|
||||
{
|
||||
bordered: false,
|
||||
ghost: true,
|
||||
onClick: () => {
|
||||
curDeleteAddressId.value = row.id;
|
||||
showDelteAccount.value = true;
|
||||
}
|
||||
},
|
||||
{ default: () => t('delete') }
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
watch([page, pageSize], async () => {
|
||||
await fetchData()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (!adminAuth.value) {
|
||||
showAdminAuth.value = true;
|
||||
return;
|
||||
}
|
||||
await fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-modal v-model:show="showEmailPassword" preset="dialog" title="Dialog">
|
||||
<template #header>
|
||||
<div>{{ t("password") }}</div>
|
||||
</template>
|
||||
<span>
|
||||
<p>{{ t("passwordTip") }}</p>
|
||||
</span>
|
||||
<n-card>
|
||||
<b>{{ curEmailPassword }}</b>
|
||||
</n-card>
|
||||
<template #action>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showDelteAccount" preset="dialog" title="Dialog">
|
||||
<p>{{ t('deleteTip') }}</p>
|
||||
<template #action>
|
||||
<n-button @click="deleteEmail" size="small" tertiary round type="error">
|
||||
{{ t('delteAccount') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="addressQuery" clearable :placeholder="t('addressQueryTip')" />
|
||||
<n-button @click="fetchData" type="primary" ghost>
|
||||
{{ 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>
|
||||
</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>
|
||||
112
frontend/src/views/admin/Mails.vue
Normal file
112
frontend/src/views/admin/Mails.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import { processItem } from '../../utils/email-parser'
|
||||
|
||||
const {
|
||||
localeCache, adminAuth, showAdminAuth,
|
||||
adminMailTabAddress
|
||||
} = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
mails: 'Emails',
|
||||
itemCount: 'itemCount',
|
||||
query: 'Query',
|
||||
},
|
||||
zh: {
|
||||
mails: '邮件',
|
||||
itemCount: '总数',
|
||||
query: '查询',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const mailData = ref([])
|
||||
const mailCount = ref(0)
|
||||
const mailPage = ref(1)
|
||||
const mailPageSize = ref(20)
|
||||
|
||||
watch([mailPage, mailPageSize, adminMailTabAddress], async () => {
|
||||
await fetchMailData()
|
||||
})
|
||||
|
||||
const fetchMailData = async () => {
|
||||
if (!adminMailTabAddress.value) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const { results, count } = await api.fetch(
|
||||
`/admin/mails`
|
||||
+ `?address=${adminMailTabAddress.value}`
|
||||
+ `&limit=${mailPageSize.value}`
|
||||
+ `&offset=${(mailPage.value - 1) * mailPageSize.value}`
|
||||
);
|
||||
mailData.value = await Promise.all(results.map(async (item) => {
|
||||
return await processItem(item);
|
||||
}));
|
||||
if (count > 0) {
|
||||
mailCount.value = count;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!adminAuth.value) {
|
||||
showAdminAuth.value = true;
|
||||
return;
|
||||
}
|
||||
await fetchMailData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="adminMailTabAddress" />
|
||||
<n-button @click="fetchMailData" type="primary" ghost>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<n-list hoverable clickable>
|
||||
<div style="display: inline-block; margin-bottom: 10px;">
|
||||
<n-pagination v-model:page="mailPage" v-model:page-size="mailPageSize" :item-count="mailCount" simple>
|
||||
<template #prefix="{ itemCount }">
|
||||
{{ t('itemCount') }}: {{ itemCount }}
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
<n-list-item v-for="row in mailData" v-bind:key="row.id">
|
||||
<n-thing class="center" :title="row.subject">
|
||||
<template #description>
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
FROM: {{ row.source }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
ID: {{ row.id }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
</template>
|
||||
<div v-html="row.message"></div>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.n-pagination {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
103
frontend/src/views/admin/MailsUnknow.vue
Normal file
103
frontend/src/views/admin/MailsUnknow.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import { processItem } from '../../utils/email-parser'
|
||||
|
||||
const {
|
||||
localeCache, adminAuth, showAdminAuth
|
||||
} = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
itemCount: 'itemCount',
|
||||
refresh: 'Refresh'
|
||||
},
|
||||
zh: {
|
||||
itemCount: '总数',
|
||||
refresh: '刷新'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const mailUnknowData = ref([])
|
||||
const mailUnknowCount = ref(0)
|
||||
const mailUnknowPage = ref(1)
|
||||
const mailUnknowPageSize = ref(20)
|
||||
|
||||
watch([mailUnknowPage, mailUnknowPageSize], async () => {
|
||||
await fetchMailUnknowData()
|
||||
})
|
||||
|
||||
const fetchMailUnknowData = async () => {
|
||||
try {
|
||||
const { results, count } = await api.fetch(
|
||||
`/admin/mails_unknow`
|
||||
+ `?limit=${mailUnknowPageSize.value}`
|
||||
+ `&offset=${(mailUnknowPage.value - 1) * mailUnknowPage.value}`
|
||||
);
|
||||
mailUnknowData.value = await Promise.all(results.map(async (item) => {
|
||||
return await processItem(item);
|
||||
}));
|
||||
if (count > 0) {
|
||||
mailUnknowCount.value = count;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!adminAuth.value) {
|
||||
showAdminAuth.value = true;
|
||||
return;
|
||||
}
|
||||
await fetchMailUnknowData();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-button @click="fetchMailUnknowData" type="primary" ghost>
|
||||
{{ t('refresh') }}
|
||||
</n-button>
|
||||
<n-list hoverable clickable>
|
||||
<div style="display: inline-block; margin-bottom: 10px;">
|
||||
<n-pagination v-model:page="mailUnknowPage" v-model:page-size="mailUnknowPageSize"
|
||||
:item-count="mailUnknowCount" simple>
|
||||
<template #prefix="{ itemCount }">
|
||||
{{ t('itemCount') }}: {{ itemCount }}
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
<n-list-item v-for="row in mailUnknowData" v-bind:key="row.id">
|
||||
<n-thing class="center" :title="row.subject">
|
||||
<template #description>
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
FROM: {{ row.source }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
ID: {{ row.id }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
</template>
|
||||
<div v-html="row.message"></div>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.n-pagination {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
118
frontend/src/views/admin/Maintenance.vue
Normal file
118
frontend/src/views/admin/Maintenance.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<script setup>
|
||||
import { ref, h, onMounted, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { CleaningServicesFilled } from '@vicons/material'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const { localeCache, adminAuth, showAdminAuth } = useGlobalState()
|
||||
const message = useMessage()
|
||||
const cleanMailsDays = ref(30)
|
||||
const cleanUnknowMailsDays = ref(30)
|
||||
const cleanAddressDays = ref(30)
|
||||
const cleanSendBoxDays = ref(30)
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
tip: 'Please input the cleanup days',
|
||||
mailBoxTip: "Clean up {day} days ago mailbox",
|
||||
mailUnknowTip: "Clean up {day} days ago mails with unknow receiver",
|
||||
addressUnActiveTip: "Clean up {day} days ago unactive address",
|
||||
sendBoxTip: "Clean up {day} days ago sendbox",
|
||||
cleanupSuccess: "Cleanup success",
|
||||
},
|
||||
zh: {
|
||||
tip: '请输入清理天数',
|
||||
mailBoxTip: "清理{day}天前的收件箱",
|
||||
mailUnknowTip: "清理{day}天前的无收件人邮件",
|
||||
addressUnActiveTip: "清理{day}天前的未活动地址",
|
||||
sendBoxTip: "清理{day}天前的发件箱",
|
||||
cleanupSuccess: "清理成功",
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const cleanup = async (cleanType, cleanDays) => {
|
||||
try {
|
||||
await api.fetch('/admin/cleanup', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ cleanType, cleanDays })
|
||||
});
|
||||
message.success(t('cleanupSuccess'));
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!adminAuth.value) {
|
||||
showAdminAuth.value = true;
|
||||
return;
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card>
|
||||
<div class="item">
|
||||
<n-input-number v-model:value="cleanMailsDays" :placeholder="t('tip')" />
|
||||
<n-button @click="cleanup('mails', cleanMailsDays)">
|
||||
<template #icon>
|
||||
<n-icon :component="CleaningServicesFilled" />
|
||||
</template>
|
||||
{{ t('mailBoxTip', { day: cleanMailsDays }) }}
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="item">
|
||||
<n-input-number v-model:value="cleanUnknowMailsDays" :placeholder="t('tip')" />
|
||||
<n-button @click="cleanup('mails_unknow', cleanUnknowMailsDays)">
|
||||
<template #icon>
|
||||
<n-icon :component="CleaningServicesFilled" />
|
||||
</template>
|
||||
{{ t('mailUnknowTip', { day: cleanUnknowMailsDays }) }}
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="item">
|
||||
<n-input-number v-model:value="cleanAddressDays" :placeholder="t('tip')" />
|
||||
<n-button @click="cleanup('address', cleanAddressDays)">
|
||||
<template #icon>
|
||||
<n-icon :component="CleaningServicesFilled" />
|
||||
</template>
|
||||
{{ t('addressUnActiveTip', { day: cleanAddressDays }) }}
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="item">
|
||||
<n-input-number v-model:value="cleanSendBoxDays" :placeholder="t('tip')" />
|
||||
<n-button @click="cleanup('sendbox', cleanSendBoxDays)">
|
||||
<template #icon>
|
||||
<n-icon :component="CleaningServicesFilled" />
|
||||
</template>
|
||||
{{ t('sendBoxTip', { day: cleanSendBoxDays }) }}
|
||||
</n-button>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.n-card {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
margin: 10px;
|
||||
}
|
||||
</style>
|
||||
162
frontend/src/views/admin/SendBox.vue
Normal file
162
frontend/src/views/admin/SendBox.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<script setup>
|
||||
import { ref, h, onMounted, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const { localeCache, settings, adminAuth, adminSendBoxTabAddress } = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
address: 'Address',
|
||||
success: 'Success',
|
||||
to_mail: 'To Mail',
|
||||
subject: 'Subject',
|
||||
created_at: 'Created At',
|
||||
action: 'Action',
|
||||
query: 'Query',
|
||||
itemCount: 'itemCount',
|
||||
view: 'View',
|
||||
},
|
||||
zh: {
|
||||
address: '地址',
|
||||
success: '成功',
|
||||
to_mail: '收件人邮箱',
|
||||
subject: '主题',
|
||||
created_at: '创建时间',
|
||||
action: '操作',
|
||||
query: '查询',
|
||||
itemCount: '总数',
|
||||
view: '查看',
|
||||
}
|
||||
}
|
||||
});
|
||||
const data = ref([])
|
||||
const count = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
|
||||
const curRow = ref({})
|
||||
const showModal = ref(false)
|
||||
|
||||
const fetchData = async () => {
|
||||
if (!adminSendBoxTabAddress.value) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const { results, count: addressCount } = await api.fetch(
|
||||
`/admin/sendbox`
|
||||
+ `?address=${adminSendBoxTabAddress.value}`
|
||||
+ `&limit=${pageSize.value}`
|
||||
+ `&offset=${(page.value - 1) * pageSize.value}`
|
||||
);
|
||||
data.value = results.map((item) => {
|
||||
try {
|
||||
const data = JSON.parse(item.raw);
|
||||
item.to_mail = data?.personalizations?.map(
|
||||
(p) => p.to?.map((t) => t.email).join(',')
|
||||
).join(';');
|
||||
item.subject = data.subject;
|
||||
item.raw = JSON.stringify(data, null, 2);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
if (addressCount > 0) {
|
||||
count.value = addressCount;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "ID",
|
||||
key: "id"
|
||||
},
|
||||
{
|
||||
title: t('address'),
|
||||
key: "address"
|
||||
},
|
||||
{
|
||||
title: t('to_mail'),
|
||||
key: "to_mail"
|
||||
},
|
||||
{
|
||||
title: t('subject'),
|
||||
key: "subject"
|
||||
},
|
||||
{
|
||||
title: t('created_at'),
|
||||
key: "created_at"
|
||||
},
|
||||
{
|
||||
title: t('action'),
|
||||
key: 'actions',
|
||||
render(row) {
|
||||
return h('div', [
|
||||
h(NButton,
|
||||
{
|
||||
type: 'success',
|
||||
ghost: true,
|
||||
onClick: () => {
|
||||
showModal.value = true;
|
||||
curRow.value = row;
|
||||
}
|
||||
},
|
||||
{ default: () => t('view') }
|
||||
)
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
watch([page, pageSize], async () => {
|
||||
await fetchData()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (!adminAuth.value) {
|
||||
showAdminAuth.value = true;
|
||||
return;
|
||||
}
|
||||
await fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="settings.address">
|
||||
<n-modal v-model:show="showModal" preset="dialog">
|
||||
<pre>{{ curRow.raw }}</pre>
|
||||
</n-modal>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="adminSendBoxTabAddress" />
|
||||
<n-button @click="fetchData" type="primary" ghost>
|
||||
{{ 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>
|
||||
</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>
|
||||
92
frontend/src/views/admin/Statistics.vue
Normal file
92
frontend/src/views/admin/Statistics.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<script setup>
|
||||
import { ref, h, onMounted, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { User, UserCheck, MailBulk } from '@vicons/fa'
|
||||
import { SendOutlined } from '@vicons/material'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const { localeCache, adminAuth } = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
userCount: 'User Count',
|
||||
activeUser: '7 days Active User',
|
||||
mailCount: 'Mail Count',
|
||||
sendMailCount: 'Send Mail Count'
|
||||
},
|
||||
zh: {
|
||||
userCount: '用户总数',
|
||||
activeUser: '周活跃用户',
|
||||
mailCount: '邮件总数',
|
||||
sendMailCount: '发送邮件总数'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const statistics = ref({
|
||||
userCount: 0,
|
||||
mailCount: 0,
|
||||
activeUserCount7days: 0,
|
||||
sendMailCount: 0,
|
||||
})
|
||||
|
||||
const fetchStatistics = async () => {
|
||||
try {
|
||||
const {
|
||||
userCount, activeUserCount7days, mailCount, sendMailCount
|
||||
} = await api.fetch(`/admin/statistics`);
|
||||
statistics.value.mailCount = mailCount || 0;
|
||||
statistics.value.userCount = userCount || 0;
|
||||
statistics.value.activeUserCount7days = activeUserCount7days || 0;
|
||||
statistics.value.sendMailCount = sendMailCount || 0;
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!adminAuth.value) {
|
||||
return;
|
||||
}
|
||||
await fetchStatistics()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-row>
|
||||
<n-col :span="6">
|
||||
<n-statistic :label="t('userCount')" :value="statistics.userCount">
|
||||
<template #prefix>
|
||||
<n-icon :component="User" />
|
||||
</template>
|
||||
</n-statistic>
|
||||
</n-col>
|
||||
<n-col :span="6">
|
||||
<n-statistic :label="t('activeUser')" :value="statistics.activeUserCount7days">
|
||||
<template #prefix>
|
||||
<n-icon :component="UserCheck" />
|
||||
</template>
|
||||
</n-statistic>
|
||||
</n-col>
|
||||
<n-col :span="6">
|
||||
<n-statistic :label="t('mailCount')" :value="statistics.mailCount">
|
||||
<template #prefix>
|
||||
<n-icon :component="MailBulk" />
|
||||
</template>
|
||||
</n-statistic>
|
||||
</n-col>
|
||||
<n-col :span="6">
|
||||
<n-statistic :label="t('sendMailCount')" :value="statistics.sendMailCount">
|
||||
<template #prefix>
|
||||
<n-icon :component="SendOutlined" />
|
||||
</template>
|
||||
</n-statistic>
|
||||
</n-col>
|
||||
</n-row>
|
||||
</template>
|
||||
@@ -12,7 +12,7 @@
|
||||
"dev": "vitepress dev docs",
|
||||
"build": "vitepress build docs",
|
||||
"preview": "vitepress preview docs",
|
||||
"deploy": "npm run build && wrangler pages deploy ./docs/.vitepress/dist --branch production"
|
||||
"deploy": "npm run build && wrangler pages deploy ./docs/.vitepress/dist --project-name=temp-mail-docs --branch production"
|
||||
},
|
||||
"dependencies": {
|
||||
"jszip": "^3.10.1"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Hono } from 'hono'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
import { getSendbox } from './send_mail_api'
|
||||
|
||||
const api = new Hono()
|
||||
|
||||
@@ -179,6 +180,11 @@ api.post('/admin/address_sender', async (c) => {
|
||||
})
|
||||
})
|
||||
|
||||
api.get('/admin/sendbox', async (c) => {
|
||||
const { address, limit, offset } = c.req.query();
|
||||
return getSendbox(c, address, limit, offset);
|
||||
})
|
||||
|
||||
api.get('/admin/statistics', async (c) => {
|
||||
const { count: mailCountV1 } = await c.env.DB.prepare(`
|
||||
SELECT count(*) as count FROM mails`
|
||||
@@ -192,11 +198,51 @@ api.get('/admin/statistics', async (c) => {
|
||||
const { count: activeUserCount7days } = await c.env.DB.prepare(`
|
||||
SELECT count(*) as count FROM address where updated_at > datetime('now', '-7 day')`
|
||||
).first();
|
||||
const { count: sendMailCount } = await c.env.DB.prepare(`
|
||||
SELECT count(*) as count FROM sendbox`
|
||||
).first();
|
||||
return c.json({
|
||||
mailCount: (mailCountV1 || 0) + (mailCount || 0),
|
||||
userCount: addressCount,
|
||||
activeUserCount7days: activeUserCount7days
|
||||
activeUserCount7days: activeUserCount7days,
|
||||
sendMailCount: sendMailCount
|
||||
})
|
||||
});
|
||||
|
||||
api.post('/admin/cleanup', async (c) => {
|
||||
const { cleanType, cleanDays } = await c.req.json();
|
||||
if (!cleanType || !cleanDays || cleanDays < 0 || cleanDays > 30) {
|
||||
return c.text("Invalid cleanType or cleanDays", 400)
|
||||
}
|
||||
console.log(`Cleanup ${cleanType} before ${cleanDays} days`);
|
||||
switch (cleanType) {
|
||||
case "mails":
|
||||
await c.env.DB.prepare(`
|
||||
DELETE FROM raw_mails WHERE created_at < datetime('now', '-${cleanDays} day')`
|
||||
).run();
|
||||
break;
|
||||
case "mails_unknow":
|
||||
await c.env.DB.prepare(`
|
||||
DELETE FROM raw_mails WHERE address NOT IN
|
||||
(select concat('${c.env.PREFIX}', name) from address) AND created_at < datetime('now', '-${cleanDays} day')`
|
||||
).run();
|
||||
break;
|
||||
case "address":
|
||||
await c.env.DB.prepare(`
|
||||
DELETE FROM address WHERE updated_at < datetime('now', '-${cleanDays} day')`
|
||||
).run();
|
||||
break;
|
||||
case "sendbox":
|
||||
await c.env.DB.prepare(`
|
||||
DELETE FROM sendbox WHERE created_at < datetime('now', '-${cleanDays} day')`
|
||||
).run();
|
||||
break;
|
||||
default:
|
||||
return c.text("Invalid cleanType", 400)
|
||||
}
|
||||
return c.json({
|
||||
success: true
|
||||
})
|
||||
})
|
||||
|
||||
export { api }
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Hono } from 'hono'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
import { getDomains, getPasswords } from './utils';
|
||||
|
||||
const api = new Hono()
|
||||
|
||||
api.get('/api/mails', async (c) => {
|
||||
@@ -124,13 +126,14 @@ api.post('/api/settings', async (c) => {
|
||||
api.get('/open_api/settings', async (c) => {
|
||||
// check header x-custom-auth
|
||||
let needAuth = false;
|
||||
if (c.env.PASSWORDS && c.env.PASSWORDS.length > 0) {
|
||||
const passwords = getPasswords(c);
|
||||
if (passwords && passwords.length > 0) {
|
||||
const auth = c.req.raw.headers.get("x-custom-auth");
|
||||
needAuth = !c.env.PASSWORDS.includes(auth);
|
||||
needAuth = !passwords.includes(auth);
|
||||
}
|
||||
return c.json({
|
||||
"prefix": c.env.PREFIX,
|
||||
"domains": c.env.DOMAINS,
|
||||
"domains": getDomains(c),
|
||||
"needAuth": needAuth,
|
||||
});
|
||||
})
|
||||
@@ -151,8 +154,9 @@ api.get('/api/new_address', async (c) => {
|
||||
return c.text("Name too long (max 100)", 400)
|
||||
}
|
||||
// check domain, generate random domain
|
||||
if (!domain || !c.env.DOMAINS.includes(domain)) {
|
||||
domain = c.env.DOMAINS[Math.floor(Math.random() * c.env.DOMAINS.length)];
|
||||
const domains = getDomains(c);
|
||||
if (!domain || !domains.includes(domain)) {
|
||||
domain = domains[Math.floor(Math.random() * domains.length)];
|
||||
}
|
||||
// create address
|
||||
const emailAddress = c.env.PREFIX + name + "@" + domain
|
||||
|
||||
@@ -110,12 +110,10 @@ api.post('/api/send_mail', async (c) => {
|
||||
return c.json({ status: "ok" });
|
||||
})
|
||||
|
||||
api.get('/api/sendbox', async (c) => {
|
||||
const { address } = c.get("jwtPayload")
|
||||
const getSendbox = async (c, address, limit, offset) => {
|
||||
if (!address) {
|
||||
return c.json({ "error": "No address" }, 400)
|
||||
}
|
||||
const { limit, offset } = c.req.query();
|
||||
if (!limit || limit < 0 || limit > 100) {
|
||||
return c.text("Invalid limit", 400)
|
||||
}
|
||||
@@ -137,6 +135,12 @@ api.get('/api/sendbox', async (c) => {
|
||||
results: results,
|
||||
count: count
|
||||
})
|
||||
}
|
||||
|
||||
api.get('/api/sendbox', async (c) => {
|
||||
const { address } = c.get("jwtPayload")
|
||||
const { limit, offset } = c.req.query();
|
||||
return getSendbox(c, address, limit, offset);
|
||||
})
|
||||
|
||||
export { api }
|
||||
export { api, getSendbox }
|
||||
|
||||
47
worker/src/utils.js
Normal file
47
worker/src/utils.js
Normal file
@@ -0,0 +1,47 @@
|
||||
export const getDomains = (c) => {
|
||||
if (!c.env.DOMAINS) {
|
||||
return [];
|
||||
}
|
||||
// check if DOMAINS is an array, if not use json.parse
|
||||
if (!Array.isArray(c.env.DOMAINS)) {
|
||||
try {
|
||||
return JSON.parse(c.env.DOMAINS);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse DOMAINS", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return c.env.DOMAINS;
|
||||
}
|
||||
|
||||
export const getPasswords = (c) => {
|
||||
if (!c.env.PASSWORDS) {
|
||||
return [];
|
||||
}
|
||||
// check if PASSWORDS is an array, if not use json.parse
|
||||
if (!Array.isArray(c.env.PASSWORDS)) {
|
||||
try {
|
||||
return JSON.parse(c.env.PASSWORDS);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse PASSWORDS", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return c.env.PASSWORDS;
|
||||
}
|
||||
|
||||
export const getAdminPasswords = (c) => {
|
||||
if (!c.env.ADMIN_PASSWORDS) {
|
||||
return [];
|
||||
}
|
||||
// check if ADMIN_PASSWORDS is an array, if not use json.parse
|
||||
if (!Array.isArray(c.env.ADMIN_PASSWORDS)) {
|
||||
try {
|
||||
return JSON.parse(c.env.ADMIN_PASSWORDS);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse ADMIN_PASSWORDS", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return c.env.ADMIN_PASSWORDS;
|
||||
}
|
||||
@@ -7,14 +7,16 @@ import { api as adminApi } from './admin_api';
|
||||
import { api as apiV1 } from './api_v1';
|
||||
import { api as apiSendMail } from './send_mail_api'
|
||||
import { email } from './email';
|
||||
import { getAdminPasswords, getPasswords } from './utils';
|
||||
|
||||
const app = new Hono()
|
||||
app.use('/*', cors());
|
||||
app.use('/api/*', async (c, next) => {
|
||||
// check header x-custom-auth
|
||||
if (c.env.PASSWORDS && c.env.PASSWORDS.length > 0) {
|
||||
const passwords = getPasswords(c);
|
||||
if (passwords && passwords.length > 0) {
|
||||
const auth = c.req.raw.headers.get("x-custom-auth");
|
||||
if (!auth || !c.env.PASSWORDS.includes(auth)) {
|
||||
if (!auth || !passwords.includes(auth)) {
|
||||
return c.text("Need Password", 401)
|
||||
}
|
||||
}
|
||||
@@ -27,9 +29,10 @@ app.use('/api/*', async (c, next) => {
|
||||
|
||||
app.use('/admin/*', async (c, next) => {
|
||||
// check header x-admin-auth
|
||||
if (c.env.ADMIN_PASSWORDS && c.env.ADMIN_PASSWORDS.length > 0) {
|
||||
const adminPasswords = getAdminPasswords(c);
|
||||
if (adminPasswords && adminPasswords.length > 0) {
|
||||
const adminAuth = c.req.raw.headers.get("x-admin-auth");
|
||||
if (adminAuth && c.env.ADMIN_PASSWORDS.includes(adminAuth)) {
|
||||
if (adminAuth && adminPasswords.includes(adminAuth)) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user