mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-06-27 18:33:02 +08:00
feat: add ADMIN_USER_ROLE for user access admin panel (#363)
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
|
||||
# CHANGE LOG
|
||||
|
||||
## main(v0.7.0)
|
||||
|
||||
- Docs: Update new-address-api.md (#360)
|
||||
- feat: worker 增加 `ADMIN_USER_ROLE` 配置, 用于配置管理员用户角色,此角色的用户可访问 admin 管理页面 (#363)
|
||||
|
||||
## v0.6.1
|
||||
|
||||
- pages github actions && 修复清理邮件天数为 0 不生效 by @tqjason (#355)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cloudflare_temp_email",
|
||||
"version": "0.6.1",
|
||||
"version": "0.7.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -22,6 +22,7 @@ const apiFetch = async (path, options = {}) => {
|
||||
data: options.body || null,
|
||||
headers: {
|
||||
'x-user-token': userJwt.value,
|
||||
'x-user-access-token': userSettings.value.access_token,
|
||||
'x-custom-auth': auth.value,
|
||||
'x-admin-auth': adminAuth.value,
|
||||
'Authorization': `Bearer ${jwt.value}`,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ref } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { createGlobalState, useStorage, useDark, useToggle, useLocalStorage } from '@vueuse/core'
|
||||
|
||||
export const useGlobalState = createGlobalState(
|
||||
@@ -74,9 +74,14 @@ export const useGlobalState = createGlobalState(
|
||||
user_email: '',
|
||||
/** @type {number} */
|
||||
user_id: 0,
|
||||
/** @type {boolean} */
|
||||
is_admin: false,
|
||||
/** @type {string | null} */
|
||||
access_token: null,
|
||||
/** @type {null | {domains: string[] | undefined | null, role: string, prefix: string | undefined | null}} */
|
||||
user_role: null,
|
||||
});
|
||||
const showAdminPage = computed(() => !!adminAuth.value || userSettings.value.is_admin);
|
||||
const telegramApp = ref(window.Telegram?.WebApp || {});
|
||||
const isTelegram = ref(!!window.Telegram?.WebApp?.initData);
|
||||
return {
|
||||
@@ -108,6 +113,7 @@ export const useGlobalState = createGlobalState(
|
||||
useSideMargin,
|
||||
telegramApp,
|
||||
isTelegram,
|
||||
showAdminPage,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -21,7 +21,8 @@ import Telegram from './admin/Telegram.vue';
|
||||
import Webhook from './admin/Webhook.vue';
|
||||
|
||||
const {
|
||||
adminAuth, showAdminAuth, adminTab, loading, globalTabplacement
|
||||
adminAuth, showAdminAuth, adminTab, loading,
|
||||
globalTabplacement, showAdminPage
|
||||
} = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
@@ -81,7 +82,7 @@ const { t } = useI18n({
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
if (!adminAuth.value) {
|
||||
if (!showAdminPage.value) {
|
||||
showAdminAuth.value = true;
|
||||
return;
|
||||
}
|
||||
@@ -100,7 +101,7 @@ onMounted(async () => {
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-tabs type="card" v-model:value="adminTab" :placement="globalTabplacement">
|
||||
<n-tabs v-if="showAdminPage" type="card" v-model:value="adminTab" :placement="globalTabplacement">
|
||||
<n-tab-pane name="account" :tab="t('account')">
|
||||
<n-tabs type="bar" animated>
|
||||
<n-tab-pane name="account" :tab="t('account')">
|
||||
|
||||
@@ -17,8 +17,8 @@ import { getRouterPathWithLang } from '../utils'
|
||||
const message = useMessage()
|
||||
|
||||
const {
|
||||
toggleDark, isDark, isTelegram,
|
||||
showAuth, adminAuth, auth, loading, openSettings
|
||||
toggleDark, isDark, isTelegram, showAdminPage,
|
||||
showAuth, auth, loading, openSettings
|
||||
} = useGlobalState()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -134,7 +134,7 @@ const menuOptions = computed(() => [
|
||||
icon: () => h(NIcon, { component: AdminPanelSettingsFilled }),
|
||||
}
|
||||
),
|
||||
show: !!adminAuth.value,
|
||||
show: showAdminPage.value,
|
||||
key: "admin"
|
||||
},
|
||||
{
|
||||
@@ -223,6 +223,11 @@ const logoClick = async () => {
|
||||
|
||||
onMounted(async () => {
|
||||
await api.getOpenSettings(message);
|
||||
try {
|
||||
await api.getUserSettings(message);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ import { NButton, NMenu } from 'naive-ui';
|
||||
import { MenuFilled } from '@vicons/material'
|
||||
|
||||
const {
|
||||
adminAuth, showAdminAuth, loading,
|
||||
adminTab, adminMailTabAddress, adminSendBoxTabAddress
|
||||
showAdminAuth, loading, adminTab,
|
||||
adminMailTabAddress, adminSendBoxTabAddress
|
||||
} = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
@@ -252,10 +252,6 @@ watch([page, pageSize], async () => {
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (!adminAuth.value) {
|
||||
showAdminAuth.value = true;
|
||||
return;
|
||||
}
|
||||
await fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -6,10 +6,7 @@ import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import MailBox from '../../components/MailBox.vue';
|
||||
|
||||
const {
|
||||
adminAuth, showAdminAuth,
|
||||
adminMailTabAddress
|
||||
} = useGlobalState()
|
||||
const { adminMailTabAddress } = useGlobalState()
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
@@ -48,13 +45,6 @@ const fetchMailData = async (limit, offset) => {
|
||||
const deleteMail = async (curMailId) => {
|
||||
await api.fetch(`/admin/mails/${curMailId}`, { method: 'DELETE' });
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
if (!adminAuth.value) {
|
||||
showAdminAuth.value = true;
|
||||
return;
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import MailBox from '../../components/MailBox.vue';
|
||||
|
||||
const { adminAuth, showAdminAuth } = useGlobalState()
|
||||
|
||||
const fetchMailUnknowData = async (limit, offset) => {
|
||||
return await api.fetch(
|
||||
`/admin/mails_unknow`
|
||||
@@ -18,17 +13,10 @@ const fetchMailUnknowData = async (limit, offset) => {
|
||||
const deleteMail = async (curMailId) => {
|
||||
await api.fetch(`/api/mails/${curMailId}`, { method: 'DELETE' });
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
if (!adminAuth.value) {
|
||||
showAdminAuth.value = true;
|
||||
return;
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="adminAuth" style="margin-top: 10px;">
|
||||
<div style="margin-top: 10px;">
|
||||
<MailBox :enableUserDeleteEmail="true" :fetchMailData="fetchMailUnknowData" :deleteMail="deleteMail" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<script setup>
|
||||
import { ref, h, onMounted, watch } from 'vue';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { CleaningServicesFilled } from '@vicons/material'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const { adminAuth, showAdminAuth } = useGlobalState()
|
||||
const message = useMessage()
|
||||
const cleanupModel = ref({
|
||||
enableMailsAutoCleanup: false,
|
||||
@@ -80,10 +78,6 @@ const save = async () => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!adminAuth.value) {
|
||||
showAdminAuth.value = true;
|
||||
return;
|
||||
}
|
||||
await fetchData();
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -4,10 +4,8 @@ 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 { adminAuth } = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
@@ -60,9 +58,6 @@ const fetchStatistics = async () => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!adminAuth.value) {
|
||||
return;
|
||||
}
|
||||
await fetchStatistics()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -31,7 +31,8 @@ const { t } = useI18n({
|
||||
|
||||
onMounted(async () => {
|
||||
await api.getUserOpenSettings(message);
|
||||
await api.getUserSettings(message);
|
||||
// make sure user_id is fetched
|
||||
if (!userSettings.value.user_id) await api.getUserSettings(message);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # all your domain name
|
||||
# For chinese domain name, you can use DOMAIN_LABELS to show chinese domain name
|
||||
# DOMAIN_LABELS = ["中文.xxx", "xxx.xxx2"]
|
||||
# USER_DEFAULT_ROLE = "vip" # default role for new users(only when enable mail verification)
|
||||
# ADMIN_USER_ROLE = "admin" # the role which can access admin panel
|
||||
# User roles configuration, if domains is empty will use default_domains, if prefix is null will use default prefix, if prefix is empty string will not use prefix
|
||||
# USER_ROLES = [
|
||||
# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "vip", prefix = "vip" },
|
||||
|
||||
@@ -60,6 +60,8 @@ DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名, 支持多个域名
|
||||
# DOMAIN_LABELS = ["中文.xxx", "xxx.xxx2"]
|
||||
# 新用户默认角色, 仅在启用邮件验证时有效
|
||||
# USER_DEFAULT_ROLE = "vip"
|
||||
# admin 角色配置, 如果用户角色等于 ADMIN_USER_ROLE 则可以访问 admin 控制台
|
||||
# ADMIN_USER_ROLE = "admin" # the role which can access admin panel
|
||||
# 用户角色配置, 如果 domains 为空将使用 default_domains
|
||||
# 如果 prefix 为 null 将使用默认前缀, 如果 prefix 为空字符串将不使用前缀
|
||||
# USER_ROLES = [
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# Admin 用户相关
|
||||
|
||||
默认不允许用户注册,可通过
|
||||
|
||||
## 用户管理页面
|
||||
|
||||

|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
# Admin 控制台
|
||||
|
||||
部署前端应用之后,访问 `/admin` 路径即可进入管理控制台。
|
||||
> [!NOTE]
|
||||
> 需要配置 `ADMIN_PASSWORDS` 或者 `ADMIN_USER_ROLE` 才可以访问 admin 控制台
|
||||
> admin 角色配置, 如果用户角色等于 ADMIN_USER_ROLE 则可以访问 admin 控制台
|
||||
|
||||
需要在后端配置 `admin 控制台密码`, 不配置则不允许访问控制台。
|
||||
部署前端应用之后,点击 左上角 logo 5 次 或者访问 `/admin` 路径即可进入管理控制台。
|
||||
|
||||
需要在后端配置 `ADMIN_PASSWORDS` 或者当前用户角色为 `ADMIN_USER_ROLE`, 则不允许访问控制台。
|
||||
|
||||

|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const CONSTANTS = {
|
||||
VERSION: 'v0.6.1',
|
||||
VERSION: 'v0.7.0',
|
||||
|
||||
// DB settings
|
||||
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
||||
|
||||
1
worker/src/types.d.ts
vendored
1
worker/src/types.d.ts
vendored
@@ -19,6 +19,7 @@ export type Bindings = {
|
||||
MAX_ADDRESS_LEN: string | number | undefined
|
||||
DEFAULT_DOMAINS: string | string[] | undefined
|
||||
DOMAINS: string | string[] | undefined
|
||||
ADMIN_USER_ROLE: string | undefined
|
||||
USER_DEFAULT_ROLE: string | UserRole | undefined
|
||||
USER_ROLES: string | UserRole[] | undefined
|
||||
DOMAIN_LABELS: string | string[] | undefined
|
||||
|
||||
@@ -5,6 +5,7 @@ import { UserSettings } from "../models";
|
||||
import { getJsonSetting, getUserRoles } from "../utils"
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { commonGetUserRole } from "../common";
|
||||
import { Jwt } from "hono/utils/jwt";
|
||||
|
||||
export default {
|
||||
openSettings: async (c: Context<HonoCustomType>) => {
|
||||
@@ -25,8 +26,23 @@ export default {
|
||||
return c.text("User not found", 400);
|
||||
}
|
||||
const user_role = await commonGetUserRole(c, db_user_id);
|
||||
const is_admin = (
|
||||
c.env.ADMIN_USER_ROLE
|
||||
&&
|
||||
c.env.ADMIN_USER_ROLE === user_role?.role
|
||||
);
|
||||
const access_token = is_admin ? await Jwt.sign({
|
||||
user_email: user.user_email,
|
||||
user_id: user.user_id,
|
||||
user_role: user_role?.role,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
// 1 hour
|
||||
exp: Math.floor(Date.now() / 1000) + 3600,
|
||||
}, c.env.JWT_SECRET, "HS256") : null;
|
||||
return c.json({
|
||||
...user,
|
||||
is_admin: is_admin,
|
||||
access_token: access_token,
|
||||
user_role: user_role
|
||||
});
|
||||
},
|
||||
|
||||
@@ -136,6 +136,26 @@ app.use('/admin/*', async (c, next) => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// check if user is admin
|
||||
const access_token = c.req.raw.headers.get("x-user-access-token");
|
||||
if (c.env.ADMIN_USER_ROLE && access_token) {
|
||||
try {
|
||||
const payload = await Jwt.verify(access_token, c.env.JWT_SECRET, "HS256");
|
||||
// check expired
|
||||
if (!payload.exp) return c.text("Invalid Token", 401);
|
||||
// exp is in seconds
|
||||
if (payload.exp < Math.floor(Date.now() / 1000)) {
|
||||
return c.text("Token Expired", 401)
|
||||
}
|
||||
if (payload.user_role !== c.env.ADMIN_USER_ROLE) {
|
||||
return c.text("Need Admin Role", 401)
|
||||
}
|
||||
await next();
|
||||
return;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
return c.text("Need Admin Password", 401)
|
||||
});
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # all domain names
|
||||
# For chinese domain name, you can use DOMAIN_LABELS to show chinese domain name
|
||||
# DOMAIN_LABELS = ["中文.xxx", "xxx.xxx2"]
|
||||
# USER_DEFAULT_ROLE = "vip" # default role for new users(only when enable mail verification)
|
||||
# ADMIN_USER_ROLE = "admin" # the role which can access admin panel
|
||||
# User roles configuration, if domains is empty will use default_domains, if prefix is null will use default prefix, if prefix is empty string will not use prefix
|
||||
# USER_ROLES = [
|
||||
# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "vip", prefix = "vip" },
|
||||
|
||||
Reference in New Issue
Block a user