Skip to content

Commit

Permalink
refine admin user detail page
Browse files Browse the repository at this point in the history
  • Loading branch information
arily committed Jul 6, 2024
1 parent eae9e9a commit e9170ee
Show file tree
Hide file tree
Showing 19 changed files with 392 additions and 193 deletions.
32 changes: 32 additions & 0 deletions src/common/utils/admin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { ComputedUserRole } from './roles'
import { type UserFull, UserRole, type UserSecrets } from '~/def/user'

// TODO server validation impl same logic
export function isRoleEditable(stat: Record<'admin' | 'owner' | 'staff', boolean>, role: UserRole) {
switch (role) {
case UserRole.Admin: {
return stat.owner
}
case UserRole.Staff: {
return stat.admin || stat.owner
}
case UserRole.Owner: {
return stat.owner
}
case UserRole.Moderator:
return stat.admin || stat.owner
default:
return true
}
}

export function isUserFieldEditable(field: keyof (UserFull<any> & UserSecrets), computedRole: ComputedUserRole) {
switch (field) {
case 'profile':
case 'preferredMode':
case 'roles':
return computedRole.staff || computedRole.admin || computedRole.owner
default:
return computedRole.admin
}
}
1 change: 1 addition & 0 deletions src/common/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * as localeKey from './locales'
export * from './locale-path'
export * from './map'
export * from './roles'
export * from './admin'

export function noop<T extends undefined | void = void>(): T
export function noop(): void {}
Expand Down
15 changes: 12 additions & 3 deletions src/common/utils/roles.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { UserRole } from '~/def/user'

export function computeUserRoles(user: { roles: UserRole[] }) {
const admin = user.roles.includes(UserRole.Admin)
const owner = user.roles.includes(UserRole.Owner)
export type ComputedUserRole = Record<'admin' | 'owner' | 'staff', boolean>

export function computeUserRoles(user: { roles: UserRole[] }): ComputedUserRole {
const admin = isAdmin(user)
const staff = isStaff(user)
const owner = user.roles.includes(UserRole.Owner)

return {
admin,
Expand All @@ -19,3 +21,10 @@ export function isStaff(user: { roles: UserRole[] }) {
|| role === UserRole.Owner
)
}

export function isAdmin(user: { roles: UserRole[] }) {
return user.roles.some(role =>
role === UserRole.Admin
|| role === UserRole.Owner
)
}
2 changes: 1 addition & 1 deletion src/def/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ export enum UserRole {
// users that have privileges
TournamentStaff = 'tournamentStaff',
ChannelModerator = 'channelModerator',
Moderator = 'moderator',
BeatmapNominator = 'beatmapNominator',
Moderator = 'moderator',
Staff = 'staff',
Admin = 'admin',

Expand Down
2 changes: 1 addition & 1 deletion src/middleware/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useSession } from '~/store/session'

export default defineNuxtRouteMiddleware(() => {
const { $state } = useSession()
if (!$state.role.staff) {
if (!$state.role.admin) {
return navigateTo({
name: 'article-id',
params: {
Expand Down
13 changes: 13 additions & 0 deletions src/middleware/staff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useSession } from '~/store/session'

export default defineNuxtRouteMiddleware(() => {
const { $state } = useSession()
if (!$state.role.staff) {
return navigateTo({
name: 'article-id',
params: {
id: ['403'],
},
})
}
})
2 changes: 1 addition & 1 deletion src/pages/admin.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
definePageMeta({
middleware: ['auth', 'admin'],
middleware: ['auth', 'staff'],
})
</script>

Expand Down
4 changes: 4 additions & 0 deletions src/pages/admin/index.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<script lang="ts" setup>
import { useSession } from '~/store/session'
const app = useNuxtApp()
const session = useSession()
useHead({
title: () => app.$i18n.t(localeKey.title['admin-panel'].__path__),
titleTemplate: title => `${title} - ${app.$i18n.t(localeKey.server.name.__path__)}`,
Expand All @@ -17,6 +20,7 @@ useHead({
{{ $t(localeKey.title.articles.__path__) }}
</t-nuxt-link-button>
<t-nuxt-link-button
v-if="session.role.admin"
:to="{
name: 'admin-logs',
}"
Expand Down
4 changes: 4 additions & 0 deletions src/pages/admin/logs.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
<script setup lang="ts" async>
definePageMeta({
middleware: 'admin',
})
const app = useNuxtApp()
const last = ref(50)
const { data: logs } = await app.$client.admin.log.last.useQuery(last)
Expand Down
127 changes: 81 additions & 46 deletions src/pages/admin/users/[id].vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const DISALLOW_USER_EDIT_ITSELF_ROLE = new Set([
UserRole.Owner,
UserRole.Admin,
UserRole.Staff,
UserRole.Moderator,
])
const app = useNuxtApp()
Expand Down Expand Up @@ -42,7 +43,7 @@ const opts = computed(() =>
const attrs: HTMLAttributes & InputHTMLAttributes = {}
if (
(session.user?.id === detail.value.id && DISALLOW_USER_EDIT_ITSELF_ROLE.has(item.value)) // prevent self from removing its priv
|| !isEditable(session.role, item.value)
|| !isRoleEditable(session.role, item.value)
) {
attrs.disabled = true
}
Expand All @@ -63,12 +64,13 @@ async function save() {
try {
const send = {
...detail.value,
password: detail.value.password
? md5(detail.value.password)
: undefined,
password: detail.value.password ? md5(detail.value.password) : undefined,
}
const newValue = await app.$client.admin.userManagement.saveDetail.mutate([route.params.id, send])
const newValue = await app.$client.admin.userManagement.saveDetail.mutate([
route.params.id,
send,
])
status.value = Status.Succeed
detail.value = { ...newValue }
Expand All @@ -81,23 +83,6 @@ async function save() {
}
}
}
// TODO server validation impl same logic
function isEditable(stat: Record<'admin' | 'owner' | 'staff', boolean>, role: UserRole) {
switch (role) {
case UserRole.Admin: {
return stat.owner
}
case UserRole.Staff: {
return stat.admin || stat.owner
}
case UserRole.Owner: {
return stat.owner
}
default:
return true
}
}
</script>

<i18n lang="yaml">
Expand Down Expand Up @@ -146,9 +131,16 @@ de-DE:
<template>
<div v-if="detail" class="container custom-container">
<div v-if="error" class="overflow-x-auto text-left alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none" viewBox="0 0 24 24">
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 stroke-current shrink-0"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
Expand All @@ -157,67 +149,104 @@ de-DE:
<dl>
<div class="striped">
<dt class="striped-dt">
{{ t('id') }}
{{ t("id") }}
</dt>
<dd class="striped-text">
<input v-model="detail.id" type="text" class="w-full input input-sm">
<input
v-model="detail.id"
type="text"
class="w-full input input-sm"
:disabled="!isUserFieldEditable('id', session.role)"
>
</dd>
</div>

<div class="striped">
<dt class="striped-dt">
{{ t('stable-client-id') }}
{{ t("stable-client-id") }}
</dt>
<dd class="striped-text">
<input v-model="detail.stableClientId" disabled type="text" class="w-full input input-sm">
<input
v-model="detail.stableClientId"
disabled
type="text"
class="w-full input input-sm"
>
</dd>
</div>

<div class="striped">
<dt class="striped-dt">
{{ t('name') }}
{{ t("name") }}
</dt>
<dd class="striped-text">
<input v-model="detail.name" type="text" class="w-full input input-sm">
<input
v-model="detail.name"
type="text"
class="w-full input input-sm"
:disabled="!isUserFieldEditable('name', session.role)"
>
</dd>
</div>

<div class="striped">
<dt class="striped-dt">
{{ t('link-name') }}
{{ t("link-name") }}
</dt>
<dd class="striped-text">
<input v-model="detail.safeName" type="text" class="w-full input input-sm">
<input
v-model="detail.safeName"
type="text"
class="w-full input input-sm"
:disabled="!isUserFieldEditable('safeName', session.role)"
>
</dd>
</div>
<div class="striped">
<dt class="striped-dt">
{{ t('password') }}
{{ t("password") }}
</dt>
<dd class="striped-text">
<input v-model="detail.password" type="text" class="w-full input input-sm">
<input
v-model="detail.password"
type="text"
class="w-full input input-sm"
:disabled="!isUserFieldEditable('password', session.role)"
>
</dd>
</div>

<div class="striped">
<dt class="striped-dt">
{{ t('email') }}
{{ t("email") }}
</dt>
<dd class="striped-text">
<input v-model="detail.email" type="text" class="w-full input input-sm">
<input
v-model="detail.email"
type="text"
class="w-full input input-sm"
:disabled="!isUserFieldEditable('email', session.role)"
>
</dd>
</div>

<div class="striped">
<dt class="striped-dt">
{{ t('flag') }}
{{ t("flag") }}
</dt>
<dd class="flex items-bottom gap-2 striped-text">
<dd class="flex gap-2 items-bottom striped-text">
<img :alt="detail.flag" :src="getFlagURL(detail.flag)" class="w-6">
<select v-model="detail.flag" class="w-full select select-sm">
<select
v-model="detail.flag"
class="w-full select select-sm"
:disabled="!isUserFieldEditable('flag', session.role)"
>
<option
v-for="countryCode in CountryCode" :key="countryCode" :disabled="countryCode === detail.flag"
:selected="countryCode === detail.flag" :value="countryCode"
v-for="countryCode in CountryCode"
:key="countryCode"
:disabled="countryCode === detail.flag"
:selected="countryCode === detail.flag"
:value="countryCode"
>
<template v-if="countryCode === CountryCode.Unknown">
Expand All @@ -236,22 +265,28 @@ de-DE:

<div class="striped">
<dt class="striped-dt">
{{ t('roles') }}
{{ t("roles") }}
</dt>
<dd class="striped-text">
<t-multi-checkbox v-model="detail.roles" size="sm" :options="opts" />
</dd>
</div>
</dl>
<button
class="btn btn-shadow" :class="{
class="btn btn-shadow"
:class="{
'loading': status === Status.Pending,
'btn-success': status === Status.Succeed,
'btn-error': status === Status.Errored,
}" @click="save"
}"
@click="save"
>
{{ t('save-btn') }}
<Icon v-if="status !== Status.Pending" :name="icon[status]" class="w-5 h-5" />
{{ t("save-btn") }}
<Icon
v-if="status !== Status.Pending"
:name="icon[status]"
class="w-5 h-5"
/>
</button>
</div>
</template>
2 changes: 1 addition & 1 deletion src/pages/article/edit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ContentEditor } from '#components'
import type { ArticleProvider } from '$base/server'
definePageMeta({
middleware: ['auth', 'admin'],
middleware: ['auth', 'staff'],
})
const app = useNuxtApp()
const { t } = useI18n()
Expand Down
Loading

0 comments on commit e9170ee

Please sign in to comment.