feat: add USER_ROLES && admin pages search when keybord enter && auto trim (#348)

* feat: add USER_ROLES

* feat: admin pages search when keybord enter && auto trim

* feat: update version to v0.6.0
This commit is contained in:
Dream Hunter
2024-07-14 19:57:43 +08:00
committed by GitHub
parent 26ccfdd6e0
commit 75c48beb3b
25 changed files with 370 additions and 49 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "cloudflare_temp_email",
"version": "0.5.5",
"version": "0.6.0",
"private": true,
"type": "module",
"scripts": {

View File

@@ -61,6 +61,7 @@ const getOpenSettings = async (message) => {
minAddressLen: res["minAddressLen"] || 1,
maxAddressLen: res["maxAddressLen"] || 30,
needAuth: res["needAuth"] || false,
defaultDomains: res["defaultDomains"] || [],
domains: res["domains"].map((domain, index) => {
return {
label: domainLabels.length > index ? domainLabels[index] : domain,

View File

@@ -15,6 +15,8 @@ export const useGlobalState = createGlobalState(
enableUserDeleteEmail: false,
enableAutoReply: false,
enableIndexAbout: false,
/** @type {string[]} */
defaultDomains: [],
/** @type {Array<{label: string, value: string}>} */
domains: [],
copyright: 'Dream Hunter',
@@ -70,6 +72,8 @@ export const useGlobalState = createGlobalState(
user_email: '',
/** @type {number} */
user_id: 0,
/** @type {null | {domains: string[] | undefined | null, role: string, prefix: string | undefined | null}} */
user_role: null,
});
const telegramApp = ref(window.Telegram?.WebApp || {});
const isTelegram = ref(!!window.Telegram?.WebApp?.initData);

View File

@@ -94,6 +94,7 @@ const deleteEmail = async () => {
const fetchData = async () => {
try {
addressQuery.value = addressQuery.value.trim()
const { results, count: addressCount } = await api.fetch(
`/admin/address`
+ `?limit=${pageSize.value}`
@@ -283,7 +284,8 @@ onMounted(async () => {
</template>
</n-modal>
<n-input-group>
<n-input v-model:value="addressQuery" clearable :placeholder="t('addressQueryTip')" />
<n-input v-model:value="addressQuery" clearable :placeholder="t('addressQueryTip')"
@keydown.enter="fetchData" />
<n-button @click="fetchData" type="primary" tertiary>
{{ t('query') }}
</n-button>

View File

@@ -29,12 +29,9 @@ const { t } = useI18n({
const mailBoxKey = ref("")
const mailKeyword = ref("")
watch([adminMailTabAddress, mailKeyword], () => {
const queryMail = () => {
adminMailTabAddress.value = adminMailTabAddress.value.trim();
mailKeyword.value = mailKeyword.value.trim();
});
const queryMail = () => {
mailBoxKey.value = Date.now();
}
@@ -63,8 +60,9 @@ onMounted(async () => {
<template>
<div style="margin-top: 10px;">
<n-input-group>
<n-input v-model:value="adminMailTabAddress" :placeholder="t('addressQueryTip')" />
<n-input v-model:value="mailKeyword" :placeholder="t('keywordQueryTip')" />
<n-input v-model:value="adminMailTabAddress" :placeholder="t('addressQueryTip')"
@keydown.enter="queryMail" />
<n-input v-model:value="mailKeyword" :placeholder="t('keywordQueryTip')" @keydown.enter="queryMail" />
<n-button @click="queryMail" type="primary" tertiary>
{{ t('query') }}
</n-button>

View File

@@ -21,6 +21,7 @@ const { t } = useI18n({
});
const fetchData = async (limit, offset) => {
adminSendBoxTabAddress.value = adminSendBoxTabAddress.value.trim();
return await api.fetch(
`/admin/sendbox?limit=${limit}&offset=${offset}`
+ (adminSendBoxTabAddress.value ? `&address=${adminSendBoxTabAddress.value}` : '')
@@ -35,7 +36,7 @@ const deleteSenboxMail = async (curMailId) => {
<template>
<div>
<n-input-group>
<n-input v-model:value="adminSendBoxTabAddress" :placeholder="t('queryTip')" />
<n-input v-model:value="adminSendBoxTabAddress" :placeholder="t('queryTip')" @keydown.enter="fetchData" />
<n-button @click="fetchData" type="primary" tertiary>
{{ t('query') }}
</n-button>

View File

@@ -79,6 +79,7 @@ const updateData = async () => {
const fetchData = async () => {
try {
addressQuery.value = addressQuery.value.trim();
const { results, count: addressCount } = await api.fetch(
`/admin/address_sender`
+ `?limit=${pageSize.value}`
@@ -192,7 +193,7 @@ onMounted(async () => {
</template>
</n-modal>
<n-input-group>
<n-input v-model:value="addressQuery" />
<n-input v-model:value="addressQuery" @keydown.enter="fetchData" />
<n-button @click="fetchData" type="primary" tertiary>
{{ t('query') }}
</n-button>

View File

@@ -1,14 +1,14 @@
<script setup>
import { ref, h, onMounted, watch } from 'vue';
import { ref, h, onMounted, watch, computed } from 'vue';
import { useI18n } from 'vue-i18n'
import { NMenu, NButton, NBadge } from 'naive-ui';
import { NMenu, NButton, NBadge, NTag } from 'naive-ui';
import { MenuFilled } from '@vicons/material'
import { useGlobalState } from '../../store'
import { api } from '../../api'
import { hashPassword } from '../../utils';
const { loading } = useGlobalState()
const { loading, openSettings } = useGlobalState()
const message = useMessage()
const { t } = useI18n({
@@ -16,6 +16,7 @@ const { t } = useI18n({
en: {
success: 'Success',
user_email: 'User Email',
role: 'Role',
address_count: 'Address Count',
created_at: 'Created At',
actions: 'Actions',
@@ -29,10 +30,15 @@ const { t } = useI18n({
createUser: 'Create User',
email: 'Email',
password: 'Password',
changeRole: 'Change Role',
prefix: 'Prefix',
domains: 'Domains',
roleDonotExist: 'Current Role does not exist',
},
zh: {
success: '成功',
user_email: '用户邮箱',
role: '角色',
address_count: '地址数量',
created_at: '创建时间',
actions: '操作',
@@ -46,6 +52,10 @@ const { t } = useI18n({
createUser: '创建用户',
email: '邮箱',
password: '密码',
changeRole: '更改角色',
prefix: '前缀',
domains: '域名',
roleDonotExist: '当前角色不存在',
}
}
});
@@ -64,9 +74,31 @@ const user = ref({
email: "",
password: ""
})
const showChangeRole = ref(false)
const userRoles = ref([])
const curUserRole = ref('')
const userRolesOptions = computed(() => {
return userRoles.value.map(role => {
return {
label: role.role,
value: role.role
}
});
})
const fetchUserRoles = async () => {
try {
const results = await api.fetch(`/admin/user_roles`);
userRoles.value = results;
} catch (error) {
console.log(error)
message.error(error.message || "error");
}
}
const fetchData = async () => {
try {
userQuery.value = userQuery.value.trim()
const { results, count: userCount } = await api.fetch(
`/admin/users`
+ `?limit=${pageSize.value}`
@@ -138,6 +170,24 @@ const deleteUser = async () => {
}
}
const changeRole = async () => {
try {
await api.fetch(`/admin/user_roles`, {
method: "POST",
body: JSON.stringify({
user_id: curUserId.value,
role_text: curUserRole.value
})
});
message.success(t('success'));
showChangeRole.value = false;
await fetchData();
} catch (error) {
console.log(error)
message.error(error.message || "error");
}
}
const columns = [
{
title: "ID",
@@ -147,6 +197,19 @@ const columns = [
title: t('user_email'),
key: "user_email"
},
{
title: t('role'),
key: "role_text",
render(row) {
if (!row.role_text) return null;
return h(NTag, {
bordered: false,
type: "info"
}, {
default: () => row.role_text
})
}
},
{
title: t('address_count'),
key: "address_count",
@@ -176,6 +239,19 @@ const columns = [
icon: () => h(MenuFilled),
key: "action",
children: [
{
label: () => h(NButton,
{
text: true,
onClick: () => {
curUserId.value = row.id;
curUserRole.value = row.role_text;
showChangeRole.value = true;
}
},
{ default: () => t('changeRole') }
),
},
{
label: () => h(NButton,
{
@@ -212,12 +288,29 @@ const columns = [
}
]
const getRolePrefix = (role) => {
const res = userRoles.value.find(r => r.role === role)?.prefix;
if (res === undefined || res === null) return openSettings.value.prefix;
return res;
}
const getRoleDomains = (role) => {
const res = userRoles.value.find(r => r.role === role)?.domains;
if (res === undefined || res === null || res.length == 0) return openSettings.value.defaultDomains;
return res;
}
const roleDonotExist = computed(() => {
return !userRoles.value.some(r => r.role === curUserRole.value);
})
watch([page, pageSize], async () => {
await fetchData()
})
onMounted(async () => {
await fetchData()
await fetchUserRoles();
await fetchData();
})
</script>
@@ -256,8 +349,21 @@ onMounted(async () => {
</n-button>
</template>
</n-modal>
<n-modal v-model:show="showChangeRole" preset="dialog" :title="t('changeRole')">
<n-alert type="error" :bordered="false" v-if="roleDonotExist">
<span>{{ t('roleDonotExist') }}</span>
</n-alert>
<p>{{ t('prefix') + ": " + getRolePrefix(curUserRole) }}</p>
<p>{{ t('domains') + ": " + JSON.stringify(getRoleDomains(curUserRole)) }}</p>
<n-select clearable v-model:value="curUserRole" :options="userRolesOptions" />
<template #action>
<n-button :loading="loading" @click="changeRole" size="small" tertiary type="primary">
{{ t('changeRole') }}
</n-button>
</template>
</n-modal>
<n-input-group>
<n-input v-model:value="userQuery" />
<n-input v-model:value="userQuery" @keydown.enter="fetchData" />
<n-button @click="fetchData" type="primary" tertiary>
{{ t('query') }}
</n-button>

View File

@@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { NewLabelOutlined, EmailOutlined } from '@vicons/material'
@@ -140,11 +140,39 @@ const newEmail = async () => {
}
};
const addressPrefix = computed(() => {
// if user has role, return role prefix
if (userSettings.value?.user_role) {
return userSettings.value.user_role.prefix || "";
}
// if user has no role, return default prefix
return openSettings.value.prefix;
});
const domainsOptions = computed(() => {
// if user has role, return role domains
if (userSettings.value.user_role) {
const allDomains = userSettings.value.user_role.domains;
if (!allDomains) return openSettings.value.domains;
return openSettings.value.domains.filter((domain) => {
return allDomains.includes(domain.value);
});
}
// if user has no role, return default domains
if (!openSettings.value.defaultDomains) {
return openSettings.value.domains;
}
// if user has no role and no default domains, return all domains
return openSettings.value.domains.filter((domain) => {
return openSettings.value.defaultDomains.includes(domain.value);
});
});
onMounted(async () => {
if (!openSettings.value.domains || openSettings.value.domains.length === 0) {
await api.getOpenSettings();
}
emailDomain.value = openSettings.value.domains ? openSettings.value.domains[0]?.value : "";
emailDomain.value = domainsOptions.value ? domainsOptions.value[0]?.value : "";
});
</script>
@@ -186,14 +214,14 @@ onMounted(async () => {
{{ t('generateName') }}
</n-button>
<n-input-group>
<n-input-group-label v-if="openSettings.prefix">
{{ openSettings.prefix }}
<n-input-group-label v-if="addressPrefix">
{{ addressPrefix }}
</n-input-group-label>
<n-input v-model:value="emailName" show-count :minlength="openSettings.minAddressLen"
:maxlength="openSettings.maxAddressLen" />
<n-input-group-label>@</n-input-group-label>
<n-select v-model:value="emailDomain" :consistent-menu-width="false"
:options="openSettings.domains" />
:options="domainsOptions" />
</n-input-group>
<Turnstile v-model:value="cfToken" />
<n-button type="primary" block secondary strong @click="newEmail" :loading="loading">