From 5cfc78d70d69658bad5970db35154f78e4db3a7a Mon Sep 17 00:00:00 2001 From: Dream Hunter Date: Mon, 9 Oct 2023 23:03:15 +0800 Subject: [PATCH] feat: add admin panel (#31) * feat: add admin panel * feature: update limit --- README.md | 2 + README_EN.md | 2 + frontend/package.json | 3 +- frontend/pnpm-lock.yaml | 12 ++ frontend/src/App.vue | 5 +- frontend/src/api/index.js | 37 ++++++- frontend/src/main.js | 2 + frontend/src/router/index.js | 19 ++++ frontend/src/store/index.js | 6 +- frontend/src/views/Admin.vue | 196 +++++++++++++++++++++++++++++++++ frontend/src/views/Content.vue | 3 +- frontend/src/views/Index.vue | 12 ++ worker/src/router.js | 40 ++++++- worker/src/worker.js | 12 ++ worker/wrangler.toml.template | 2 + 15 files changed, 343 insertions(+), 10 deletions(-) create mode 100644 frontend/src/router/index.js create mode 100644 frontend/src/views/Admin.vue create mode 100644 frontend/src/views/Index.vue diff --git a/README.md b/README.md index d72ceed9..9d010aa5 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,8 @@ node_compat = true PREFIX = "tmp" # 要处理的邮箱名称前缀 # 如果你想要你的网站私有,取消下面的注释,并修改密码 # PASSWORDS = ["123", "456"] +# admin 控制台密码 +# ADMIN_PASSWORDS = ["123", "456"] DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名 JWT_SECRET = "xxx" # 用于生成 jwt 的密钥 BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔 diff --git a/README_EN.md b/README_EN.md index a360646b..f5ab3fb9 100644 --- a/README_EN.md +++ b/README_EN.md @@ -44,6 +44,8 @@ pnpm install # PREFIX = "tmp" - the email create will be like tmp@DOMAIN # IF YOU WANT TO MAKE YOUR SITE PRIVATE, UNCOMMENT THE FOLLOWING LINES # PASSWORDS = ["123", "456"] +# For admin panel +# ADMIN_PASSWORDS = ["123", "456"] # DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] you domain name # JWT_SECRET = "xxx" # BLACK_LIST = "" diff --git a/frontend/package.json b/frontend/package.json index 0d2834bb..4d09599c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,8 @@ "naive-ui": "^2.34.3", "vue": "^3.3.4", "vue-clipboard3": "^2.0.0", - "vue-i18n": "9" + "vue-i18n": "9", + "vue-router": "^4.2.5" }, "devDependencies": { "@vitejs/plugin-vue": "^4.2.3", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 7be5be13..e9ef2da2 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -20,6 +20,9 @@ dependencies: vue-i18n: specifier: '9' version: 9.3.0(vue@3.3.4) + vue-router: + specifier: ^4.2.5 + version: 4.2.5(vue@3.3.4) devDependencies: '@vitejs/plugin-vue': @@ -735,6 +738,15 @@ packages: vue: 3.3.4 dev: false + /vue-router@4.2.5(vue@3.3.4): + resolution: {integrity: sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw==} + peerDependencies: + vue: ^3.2.0 + dependencies: + '@vue/devtools-api': 6.5.0 + vue: 3.3.4 + dev: false + /vue@3.3.4: resolution: {integrity: sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==} dependencies: diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 181400c8..7c7419ea 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -43,10 +43,7 @@ onMounted(async () => {
- -
- - +
diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 0822feac..4a0f6a97 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -1,7 +1,7 @@ import { useGlobalState } from '../store' const API_BASE = import.meta.env.VITE_API_BASE || ""; -const { loading, auth, jwt, openSettings, showAuth } = useGlobalState(); +const { loading, auth, jwt, openSettings, showAuth, adminAuth, showAdminAuth } = useGlobalState(); const apiFetch = async (path, options = {}) => { loading.value = true; @@ -10,6 +10,7 @@ const apiFetch = async (path, options = {}) => { method: options.method || 'GET', headers: { 'x-custom-auth': auth.value, + 'x-admin-auth': adminAuth.value, 'Authorization': `Bearer ${jwt.value}`, 'Content-Type': 'application/json', }, @@ -18,6 +19,10 @@ const apiFetch = async (path, options = {}) => { showAuth.value = true; throw new Error("Unauthorized"); } + if (response.status === 401 && path.startsWith("/admin")) { + showAdminAuth.value = true; + throw new Error("Unauthorized"); + } if (!response.ok) { throw new Error(`${response.status} ${await response.text()}` || "error"); } @@ -58,9 +63,39 @@ const getSettings = async () => { return res["address"]; } +const adminShowPassword = async (id) => { + try { + const { password } = await apiFetch(`/admin/show_password/${id}`); + return password; + } catch (error) { + throw error; + } +} + +const adminDeleteAddress = async (id) => { + try { + await apiFetch(`/admin/delete_address/${id}`, { + method: 'DELETE' + }); + } catch (error) { + throw error; + } +} + +const adminGetAddress = async () => { + try { + return await apiFetch("/admin/addresss"); + } catch (error) { + throw error; + } +} + export const api = { fetch: apiFetch, getSettings: getSettings, getOpenSettings: getOpenSettings, + adminShowPassword: adminShowPassword, + adminDeleteAddress: adminDeleteAddress, + adminGetAddress: adminGetAddress, } diff --git a/frontend/src/main.js b/frontend/src/main.js index 198b9fec..0c963549 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -1,6 +1,7 @@ import { createApp } from 'vue' import App from './App.vue' import { createI18n } from 'vue-i18n' +import router from './router' const i18n = createI18n({ legacy: false, // you must set `false`, to use Composition API @@ -15,4 +16,5 @@ const i18n = createI18n({ }) const app = createApp(App) app.use(i18n) +app.use(router) app.mount('#app') diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 00000000..7516cee5 --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,19 @@ +import { createRouter, createWebHistory } from 'vue-router' +import Index from '../views/Index.vue' +import Admin from '../views/Admin.vue' + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/', + component: Index + }, + { + path: '/admin', + component: Admin + } + ] +}) + +export default router diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index 604c5e4f..edbf6c5c 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -13,7 +13,9 @@ export const useGlobalState = createGlobalState( }] }) const showAuth = ref(false); + const showAdminAuth = ref(false); const auth = useStorage('auth', ''); + const adminAuth = useStorage('adminAuth', ''); const jwt = useStorage('jwt', ''); const localeCache = useStorage('locale', 'zhCN'); const themeSwitch = useStorage('themeSwitch', false); @@ -24,7 +26,9 @@ export const useGlobalState = createGlobalState( auth, jwt, localeCache, - themeSwitch + themeSwitch, + adminAuth, + showAdminAuth, } }, ) diff --git a/frontend/src/views/Admin.vue b/frontend/src/views/Admin.vue new file mode 100644 index 00000000..6db3821f --- /dev/null +++ b/frontend/src/views/Admin.vue @@ -0,0 +1,196 @@ + + + + + diff --git a/frontend/src/views/Content.vue b/frontend/src/views/Content.vue index bf95d253..129bf1df 100644 --- a/frontend/src/views/Content.vue +++ b/frontend/src/views/Content.vue @@ -13,10 +13,9 @@ const { toClipboard } = useClipboard() const message = useMessage() const address = ref("") -const { jwt, loading, openSettings } = useGlobalState() +const { jwt, openSettings } = useGlobalState() const autoRefresh = ref(false) const data = ref([]) -const API_BASE = import.meta.env.VITE_API_BASE || ""; const timer = ref(null) const showPassword = ref(false) const showNewEmail = ref(false) diff --git a/frontend/src/views/Index.vue b/frontend/src/views/Index.vue new file mode 100644 index 00000000..d94b8f2c --- /dev/null +++ b/frontend/src/views/Index.vue @@ -0,0 +1,12 @@ + + + diff --git a/worker/src/router.js b/worker/src/router.js index 7b8dfca1..1750ddbf 100644 --- a/worker/src/router.js +++ b/worker/src/router.js @@ -9,7 +9,7 @@ api.get('/api/mails', async (c) => { return c.json({ "error": "No address" }, 400) } const { results } = await c.env.DB.prepare( - `SELECT id, source, subject, message FROM mails where address = ? order by id desc limit 10` + `SELECT id, source, subject, message FROM mails where address = ? order by id desc limit 100` ).bind(address).all(); return c.json(results); }) @@ -66,4 +66,42 @@ api.get('/api/new_address', async (c) => { }) }) +api.get('/admin/addresss', async (c) => { + const { results } = await c.env.DB.prepare( + `SELECT * FROM address order by id desc` + ).all(); + return c.json(results.map((r) => { + r.name = c.env.PREFIX + r.name; + return r; + })); +}) + +api.delete('/admin/delete_address/:id', async (c) => { + const { id } = c.req.param(); + const { success } = await c.env.DB.prepare( + `DELETE FROM address WHERE id = ?` + ).bind(id).run(); + if (!success) { + return c.text("Failed to delete address", 500) + } + return c.json({ + success: success + }) +}) + +api.get('/admin/show_password/:id', async (c) => { + const { id } = c.req.param(); + const name = await c.env.DB.prepare( + `SELECT name FROM address WHERE id = ?` + ).bind(id).first("name"); + // compute address + const emailAddress = c.env.PREFIX + name + const jwt = await Jwt.sign({ + address: emailAddress + }, c.env.JWT_SECRET) + return c.json({ + password: jwt + }) +}) + export { api } diff --git a/worker/src/worker.js b/worker/src/worker.js index 6ac57a29..0a7580ff 100644 --- a/worker/src/worker.js +++ b/worker/src/worker.js @@ -22,6 +22,18 @@ app.use('/api/*', async (c, next) => { return jwt({ secret: c.env.JWT_SECRET })(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 adminAuth = c.req.headers.get("x-admin-auth"); + if (adminAuth && c.env.ADMIN_PASSWORDS.includes(adminAuth)) { + await next(); + return; + } + } + return c.text("Need Admin Password", 401) +}); + app.route('/', api) diff --git a/worker/wrangler.toml.template b/worker/wrangler.toml.template index 816c8a6d..beb7955f 100644 --- a/worker/wrangler.toml.template +++ b/worker/wrangler.toml.template @@ -7,6 +7,8 @@ node_compat = true PREFIX = "tmp" # IF YOU WANT TO MAKE YOUR SITE PRIVATE, UNCOMMENT THE FOLLOWING LINES # PASSWORDS = ["123", "456"] +# For admin panel +# ADMIN_PASSWORDS = ["123", "456"] DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] JWT_SECRET = "xxx" BLACK_LIST = ""