mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-05 07:27:27 +08:00
feat: admin portal user page add user address manangement (#623)
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
- feat: |UI| 使用 shadow DOM 防止样式污染
|
- feat: |UI| 使用 shadow DOM 防止样式污染
|
||||||
- feat: |UI| 支持 URL jwt 参数自动登录邮箱,jwt 参数会覆盖浏览器中的 jwt
|
- feat: |UI| 支持 URL jwt 参数自动登录邮箱,jwt 参数会覆盖浏览器中的 jwt
|
||||||
- fix: |CleanUP| 修复清理邮件时,清理时间超过 30 天报错的 bug
|
- fix: |CleanUP| 修复清理邮件时,清理时间超过 30 天报错的 bug
|
||||||
|
- feat: admin 用户管理页面: 增加 用户地址查看功能
|
||||||
|
|
||||||
## v0.9.0
|
## v0.9.0
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, onBeforeUnmount, defineProps } from 'vue';
|
import { ref, watch, onMounted, onBeforeUnmount } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
htmlContent: {
|
htmlContent: {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const useGlobalState = createGlobalState(
|
|||||||
const useIframeShowMail = useStorage('useIframeShowMail', false);
|
const useIframeShowMail = useStorage('useIframeShowMail', false);
|
||||||
const preferShowTextMail = useStorage('preferShowTextMail', false);
|
const preferShowTextMail = useStorage('preferShowTextMail', false);
|
||||||
const userJwt = useStorage('userJwt', '');
|
const userJwt = useStorage('userJwt', '');
|
||||||
const userTab = useSessionStorage('userTab', 'account_settings');
|
const userTab = useSessionStorage('userTab', 'address_management');
|
||||||
const indexTab = useSessionStorage('indexTab', 'mailbox');
|
const indexTab = useSessionStorage('indexTab', 'mailbox');
|
||||||
const globalTabplacement = useStorage('globalTabplacement', 'top');
|
const globalTabplacement = useStorage('globalTabplacement', 'top');
|
||||||
const useSideMargin = useStorage('useSideMargin', true);
|
const useSideMargin = useStorage('useSideMargin', true);
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, h, onMounted } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { NBadge } from 'naive-ui'
|
||||||
|
|
||||||
|
import { api } from '../../api'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
user_id: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const { locale, t } = useI18n({
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
success: 'success',
|
||||||
|
name: 'Name',
|
||||||
|
mail_count: 'Mail Count',
|
||||||
|
send_count: 'Send Count',
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
success: '成功',
|
||||||
|
name: '名称',
|
||||||
|
mail_count: '邮件数量',
|
||||||
|
send_count: '发送数量',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = ref([])
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const { results } = await api.fetch(
|
||||||
|
`/admin/users/bind_address/${props.user_id}`,
|
||||||
|
);
|
||||||
|
data.value = results;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t('name'),
|
||||||
|
key: "name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('mail_count'),
|
||||||
|
key: "mail_count",
|
||||||
|
render(row) {
|
||||||
|
return h(NBadge, {
|
||||||
|
value: row.mail_count,
|
||||||
|
'show-zero': true,
|
||||||
|
max: 99,
|
||||||
|
type: "success"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('send_count'),
|
||||||
|
key: "send_count",
|
||||||
|
render(row) {
|
||||||
|
return h(NBadge, {
|
||||||
|
value: row.send_count,
|
||||||
|
'show-zero': true,
|
||||||
|
max: 99,
|
||||||
|
type: "success"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div style="overflow: auto;">
|
||||||
|
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.n-data-table {
|
||||||
|
min-width: 700px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -8,6 +8,8 @@ import { useGlobalState } from '../../store'
|
|||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
import { hashPassword } from '../../utils';
|
import { hashPassword } from '../../utils';
|
||||||
|
|
||||||
|
import UserAddressManagement from './UserAddressManagement.vue'
|
||||||
|
|
||||||
const { loading, openSettings } = useGlobalState()
|
const { loading, openSettings } = useGlobalState()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
@@ -34,6 +36,7 @@ const { t } = useI18n({
|
|||||||
prefix: 'Prefix',
|
prefix: 'Prefix',
|
||||||
domains: 'Domains',
|
domains: 'Domains',
|
||||||
roleDonotExist: 'Current Role does not exist',
|
roleDonotExist: 'Current Role does not exist',
|
||||||
|
userAddressManagement: 'User Address Management',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
success: '成功',
|
success: '成功',
|
||||||
@@ -56,6 +59,7 @@ const { t } = useI18n({
|
|||||||
prefix: '前缀',
|
prefix: '前缀',
|
||||||
domains: '域名',
|
domains: '域名',
|
||||||
roleDonotExist: '当前角色不存在',
|
roleDonotExist: '当前角色不存在',
|
||||||
|
userAddressManagement: '用户地址管理',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -75,6 +79,7 @@ const user = ref({
|
|||||||
password: ""
|
password: ""
|
||||||
})
|
})
|
||||||
const showChangeRole = ref(false)
|
const showChangeRole = ref(false)
|
||||||
|
const showUserAddressManagement = ref(false)
|
||||||
const userRoles = ref([])
|
const userRoles = ref([])
|
||||||
const curUserRole = ref('')
|
const curUserRole = ref('')
|
||||||
const userRolesOptions = computed(() => {
|
const userRolesOptions = computed(() => {
|
||||||
@@ -239,6 +244,18 @@ const columns = [
|
|||||||
icon: () => h(MenuFilled),
|
icon: () => h(MenuFilled),
|
||||||
key: "action",
|
key: "action",
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
label: () => h(NButton,
|
||||||
|
{
|
||||||
|
text: true,
|
||||||
|
onClick: () => {
|
||||||
|
curUserId.value = row.id;
|
||||||
|
showUserAddressManagement.value = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ default: () => t('userAddressManagement') }
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: () => h(NButton,
|
label: () => h(NButton,
|
||||||
{
|
{
|
||||||
@@ -362,6 +379,9 @@ onMounted(async () => {
|
|||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
<n-modal v-model:show="showUserAddressManagement" preset="card" :title="t('userAddressManagement')">
|
||||||
|
<UserAddressManagement :user_id="curUserId" />
|
||||||
|
</n-modal>
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
<n-input v-model:value="userQuery" @keydown.enter="fetchData" />
|
<n-input v-model:value="userQuery" @keydown.enter="fetchData" />
|
||||||
<n-button @click="fetchData" type="primary" tertiary>
|
<n-button @click="fetchData" type="primary" tertiary>
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ const { locale, t } = useI18n({
|
|||||||
messages: {
|
messages: {
|
||||||
en: {
|
en: {
|
||||||
login: 'Login',
|
login: 'Login',
|
||||||
|
loginAndBind: 'Login and Bind',
|
||||||
pleaseGetNewEmail: 'Please login or click "Get New Email" button to get a new email address',
|
pleaseGetNewEmail: 'Please login or click "Get New Email" button to get a new email address',
|
||||||
getNewEmail: 'Create New Email',
|
getNewEmail: 'Create New Email',
|
||||||
getNewEmailTip1: 'Please input the email you want to use. only allow: ',
|
getNewEmailTip1: 'Please input the email you want to use. only allow: ',
|
||||||
@@ -86,6 +87,7 @@ const { locale, t } = useI18n({
|
|||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
login: '登录',
|
login: '登录',
|
||||||
|
loginAndBind: '登录并绑定',
|
||||||
pleaseGetNewEmail: '请"登录"或点击 "注册新邮箱" 按钮来获取一个新的邮箱地址',
|
pleaseGetNewEmail: '请"登录"或点击 "注册新邮箱" 按钮来获取一个新的邮箱地址',
|
||||||
getNewEmail: '创建新邮箱',
|
getNewEmail: '创建新邮箱',
|
||||||
getNewEmailTip1: '请输入你想要使用的邮箱地址, 只允许: ',
|
getNewEmailTip1: '请输入你想要使用的邮箱地址, 只允许: ',
|
||||||
@@ -102,6 +104,13 @@ const { locale, t } = useI18n({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const loginAndBindTag = computed(() => {
|
||||||
|
if (userSettings.value.user_email) {
|
||||||
|
return t('loginAndBind')
|
||||||
|
}
|
||||||
|
return t('login')
|
||||||
|
})
|
||||||
|
|
||||||
const addressRegex = computed(() => {
|
const addressRegex = computed(() => {
|
||||||
try {
|
try {
|
||||||
if (openSettings.value.addressRegex) {
|
if (openSettings.value.addressRegex) {
|
||||||
@@ -208,7 +217,7 @@ onMounted(async () => {
|
|||||||
<span>{{ t('bindUserInfo') }}</span>
|
<span>{{ t('bindUserInfo') }}</span>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
<n-tabs v-if="openSettings.fetched" v-model:value="tabValue" size="large" justify-content="space-evenly">
|
<n-tabs v-if="openSettings.fetched" v-model:value="tabValue" size="large" justify-content="space-evenly">
|
||||||
<n-tab-pane name="signin" :tab="t('login')">
|
<n-tab-pane name="signin" :tab="loginAndBindTag">
|
||||||
<n-form>
|
<n-form>
|
||||||
<n-form-item-row :label="t('credential')" required>
|
<n-form-item-row :label="t('credential')" required>
|
||||||
<n-input v-model:value="credential" type="textarea" :autosize="{ minRows: 3 }" />
|
<n-input v-model:value="credential" type="textarea" :autosize="{ minRows: 3 }" />
|
||||||
@@ -217,7 +226,7 @@ onMounted(async () => {
|
|||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon :component="EmailOutlined" />
|
<n-icon :component="EmailOutlined" />
|
||||||
</template>
|
</template>
|
||||||
{{ t('login') }}
|
{{ loginAndBindTag }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button v-if="showNewAddressTab" @click="tabValue = 'register'" block secondary strong>
|
<n-button v-if="showNewAddressTab" @click="tabValue = 'register'" block secondary strong>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { useGlobalState } from '../../store'
|
|||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
import { getRouterPathWithLang } from '../../utils'
|
import { getRouterPathWithLang } from '../../utils'
|
||||||
|
|
||||||
|
import Login from '../common/Login.vue';
|
||||||
|
|
||||||
const { jwt } = useGlobalState()
|
const { jwt } = useGlobalState()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -25,7 +27,9 @@ const { locale, t } = useI18n({
|
|||||||
unbindAddressTip: 'Before unbinding, please switch to this email address and save the email address credential.',
|
unbindAddressTip: 'Before unbinding, please switch to this email address and save the email address credential.',
|
||||||
transferAddress: 'Transfer Address',
|
transferAddress: 'Transfer Address',
|
||||||
targetUserEmail: 'Target User Email',
|
targetUserEmail: 'Target User Email',
|
||||||
transferAddressTip: 'Transfer address to another user will remove the address from your account and transfer it to another user. Are you sure to transfer the address?'
|
transferAddressTip: 'Transfer address to another user will remove the address from your account and transfer it to another user. Are you sure to transfer the address?',
|
||||||
|
address: 'Address',
|
||||||
|
create_or_bind: 'Create or Bind',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
success: '成功',
|
success: '成功',
|
||||||
@@ -38,7 +42,9 @@ const { locale, t } = useI18n({
|
|||||||
unbindAddressTip: '解绑前请切换到此邮箱地址并保存邮箱地址凭证。',
|
unbindAddressTip: '解绑前请切换到此邮箱地址并保存邮箱地址凭证。',
|
||||||
transferAddress: '转移地址',
|
transferAddress: '转移地址',
|
||||||
targetUserEmail: '目标用户邮箱',
|
targetUserEmail: '目标用户邮箱',
|
||||||
transferAddressTip: '转移地址到其他用户将会从你的账户中移除此地址并转移给其他用户。确定要转移地址吗?'
|
transferAddressTip: '转移地址到其他用户将会从你的账户中移除此地址并转移给其他用户。确定要转移地址吗?',
|
||||||
|
address: '地址',
|
||||||
|
create_or_bind: '创建或绑定',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -111,13 +117,10 @@ const transferAddress = async () => {
|
|||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const { results, count: addressCount } = await api.fetch(
|
const { results } = await api.fetch(
|
||||||
`/user_api/bind_address`
|
`/user_api/bind_address`
|
||||||
);
|
);
|
||||||
data.value = results;
|
data.value = results;
|
||||||
if (addressCount > 0) {
|
|
||||||
count.value = addressCount;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
message.error(error.message || "error");
|
message.error(error.message || "error");
|
||||||
@@ -211,20 +214,29 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-modal v-model:show="showTranferAddress" preset="dialog" :title="t('transferAddress')">
|
<div>
|
||||||
<span>
|
<n-modal v-model:show="showTranferAddress" preset="dialog" :title="t('transferAddress')">
|
||||||
<p>{{ t("transferAddressTip") }}</p>
|
<span>
|
||||||
<p>{{ t('transferAddress') + ": " + currentAddress }}</p>
|
<p>{{ t("transferAddressTip") }}</p>
|
||||||
<n-input v-model:value="targetUserEmail" :placeholder="t('targetUserEmail')" />
|
<p>{{ t('transferAddress') + ": " + currentAddress }}</p>
|
||||||
</span>
|
<n-input v-model:value="targetUserEmail" :placeholder="t('targetUserEmail')" />
|
||||||
<template #action>
|
</span>
|
||||||
<n-button :loading="loading" @click="transferAddress" size="small" tertiary type="error">
|
<template #action>
|
||||||
{{ t('transferAddress') }}
|
<n-button :loading="loading" @click="transferAddress" size="small" tertiary type="error">
|
||||||
</n-button>
|
{{ t('transferAddress') }}
|
||||||
</template>
|
</n-button>
|
||||||
</n-modal>
|
</template>
|
||||||
<div style="overflow: auto;">
|
</n-modal>
|
||||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
<n-tabs type="segment">
|
||||||
|
<n-tab-pane name="address" :tab="t('address')">
|
||||||
|
<div style="overflow: auto;">
|
||||||
|
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||||
|
</div>
|
||||||
|
</n-tab-pane>
|
||||||
|
<n-tab-pane name="create_or_bind" :tab="t('create_or_bind')">
|
||||||
|
<Login />
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -143,5 +143,23 @@ export default {
|
|||||||
return c.text("Failed to update user roles", 500)
|
return c.text("Failed to update user roles", 500)
|
||||||
}
|
}
|
||||||
return c.json({ success: true })
|
return c.json({ success: true })
|
||||||
}
|
},
|
||||||
|
getBindedAddresses: async (c: Context<HonoCustomType>) => {
|
||||||
|
const { user_id } = c.req.param();
|
||||||
|
if (!user_id) return c.text("Invalid user_id", 400);
|
||||||
|
// select binded address
|
||||||
|
const { results } = await c.env.DB.prepare(
|
||||||
|
`SELECT a.*,`
|
||||||
|
+ ` (SELECT COUNT(*) FROM raw_mails WHERE address = a.name) AS mail_count,`
|
||||||
|
+ ` (SELECT COUNT(*) FROM sendbox WHERE address = a.name) AS send_count`
|
||||||
|
+ ` FROM address a `
|
||||||
|
+ ` JOIN users_address ua `
|
||||||
|
+ ` ON ua.address_id = a.id `
|
||||||
|
+ ` WHERE ua.user_id = ?`
|
||||||
|
+ ` ORDER BY a.id DESC`
|
||||||
|
).bind(user_id).all();
|
||||||
|
return c.json({
|
||||||
|
results: results,
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -327,6 +327,7 @@ api.post('/admin/users', admin_user_api.createUser)
|
|||||||
api.post('/admin/users/:user_id/reset_password', admin_user_api.resetPassword)
|
api.post('/admin/users/:user_id/reset_password', admin_user_api.resetPassword)
|
||||||
api.get('/admin/user_roles', async (c) => c.json(getUserRoles(c)))
|
api.get('/admin/user_roles', async (c) => c.json(getUserRoles(c)))
|
||||||
api.post('/admin/user_roles', admin_user_api.updateUserRoles)
|
api.post('/admin/user_roles', admin_user_api.updateUserRoles)
|
||||||
|
api.get('/admin/users/bind_address/:user_id', admin_user_api.getBindedAddresses)
|
||||||
|
|
||||||
// user oauth2 settings
|
// user oauth2 settings
|
||||||
api.get('/admin/user_oauth2_settings', oauth2_settings.getUserOauth2Settings)
|
api.get('/admin/user_oauth2_settings', oauth2_settings.getUserOauth2Settings)
|
||||||
|
|||||||
Reference in New Issue
Block a user