From aea8b964bbb42f2f634218d456a0b503b5abd284 Mon Sep 17 00:00:00 2001 From: Dream Hunter Date: Sun, 14 Apr 2024 22:41:16 +0800 Subject: [PATCH] feat: admin cleanup tab && admin sendbox tab (#126) --- .github/workflows/docs_deploy.yml | 46 +++ frontend/src/store/index.js | 8 +- frontend/src/views/Admin.vue | 368 ++--------------------- frontend/src/views/Header.vue | 10 +- frontend/src/views/admin/Account.vue | 248 +++++++++++++++ frontend/src/views/admin/Mails.vue | 112 +++++++ frontend/src/views/admin/MailsUnknow.vue | 103 +++++++ frontend/src/views/admin/Maintenance.vue | 118 ++++++++ frontend/src/views/admin/SendBox.vue | 162 ++++++++++ frontend/src/views/admin/Statistics.vue | 92 ++++++ vitepress-docs/package.json | 2 +- worker/src/admin_api.js | 48 ++- worker/src/router.js | 14 +- worker/src/send_mail_api.js | 12 +- worker/src/utils.js | 47 +++ worker/src/worker.js | 11 +- 16 files changed, 1042 insertions(+), 359 deletions(-) create mode 100644 .github/workflows/docs_deploy.yml create mode 100644 frontend/src/views/admin/Account.vue create mode 100644 frontend/src/views/admin/Mails.vue create mode 100644 frontend/src/views/admin/MailsUnknow.vue create mode 100644 frontend/src/views/admin/Maintenance.vue create mode 100644 frontend/src/views/admin/SendBox.vue create mode 100644 frontend/src/views/admin/Statistics.vue create mode 100644 worker/src/utils.js diff --git a/.github/workflows/docs_deploy.yml b/.github/workflows/docs_deploy.yml new file mode 100644 index 00000000..60621799 --- /dev/null +++ b/.github/workflows/docs_deploy.yml @@ -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 }} diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index 7cdf94da..86f40cf0 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -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, } }, ) diff --git a/frontend/src/views/Admin.vue b/frontend/src/views/Admin.vue index 1f124bcf..9d31e40a 100644 --- a/frontend/src/views/Admin.vue +++ b/frontend/src/views/Admin.vue @@ -1,22 +1,22 @@ - - - -

{{ t("passwordTip") }}

-
- - {{ curEmailPassword }} - - -
- - - - - - - - - - - - - - - - - - + + - - - - {{ t('query') }} - - -
- - - -
- +
- - - - {{ t('query') }} - - - -
- - - -
- - - -
-
-
-
+
- - {{ t('query') }} - - -
- - - -
- - - -
-
-
-
+
+ + + + + +
diff --git a/frontend/src/views/Header.vue b/frontend/src/views/Header.vue index 431813ba..eec70f9d 100644 --- a/frontend/src/views/Header.vue +++ b/frontend/src/views/Header.vue @@ -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 } diff --git a/frontend/src/views/admin/Account.vue b/frontend/src/views/admin/Account.vue new file mode 100644 index 00000000..000cd63b --- /dev/null +++ b/frontend/src/views/admin/Account.vue @@ -0,0 +1,248 @@ + + + + + diff --git a/frontend/src/views/admin/Mails.vue b/frontend/src/views/admin/Mails.vue new file mode 100644 index 00000000..8d31f390 --- /dev/null +++ b/frontend/src/views/admin/Mails.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/frontend/src/views/admin/MailsUnknow.vue b/frontend/src/views/admin/MailsUnknow.vue new file mode 100644 index 00000000..11f6c8fc --- /dev/null +++ b/frontend/src/views/admin/MailsUnknow.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/frontend/src/views/admin/Maintenance.vue b/frontend/src/views/admin/Maintenance.vue new file mode 100644 index 00000000..c738abe0 --- /dev/null +++ b/frontend/src/views/admin/Maintenance.vue @@ -0,0 +1,118 @@ + + + + + + diff --git a/frontend/src/views/admin/SendBox.vue b/frontend/src/views/admin/SendBox.vue new file mode 100644 index 00000000..dcd0870f --- /dev/null +++ b/frontend/src/views/admin/SendBox.vue @@ -0,0 +1,162 @@ + + + + + diff --git a/frontend/src/views/admin/Statistics.vue b/frontend/src/views/admin/Statistics.vue new file mode 100644 index 00000000..5c88eebf --- /dev/null +++ b/frontend/src/views/admin/Statistics.vue @@ -0,0 +1,92 @@ + + + diff --git a/vitepress-docs/package.json b/vitepress-docs/package.json index 313ba864..58554a4a 100644 --- a/vitepress-docs/package.json +++ b/vitepress-docs/package.json @@ -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" diff --git a/worker/src/admin_api.js b/worker/src/admin_api.js index 15e5ad62..b1526436 100644 --- a/worker/src/admin_api.js +++ b/worker/src/admin_api.js @@ -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 } diff --git a/worker/src/router.js b/worker/src/router.js index f5b52b63..c310ce82 100644 --- a/worker/src/router.js +++ b/worker/src/router.js @@ -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 diff --git a/worker/src/send_mail_api.js b/worker/src/send_mail_api.js index 7bb82021..26f6436b 100644 --- a/worker/src/send_mail_api.js +++ b/worker/src/send_mail_api.js @@ -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 } diff --git a/worker/src/utils.js b/worker/src/utils.js new file mode 100644 index 00000000..624e9502 --- /dev/null +++ b/worker/src/utils.js @@ -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; +} diff --git a/worker/src/worker.js b/worker/src/worker.js index 8e630a46..258e3b1a 100644 --- a/worker/src/worker.js +++ b/worker/src/worker.js @@ -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; }