feat(admin): add user role management with promotion feature
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
<script setup lang="ts">
|
||||
import type { UserRole } from '#shared/generated/prisma/enums';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { user } = useUserSession();
|
||||
const { confirm } = useModal();
|
||||
const { init } = useToast();
|
||||
|
||||
type RoleUser = {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
role: UserRole;
|
||||
};
|
||||
|
||||
const isAdmin = computed(() => user.value?.role === 'ADMIN');
|
||||
|
||||
const rolesToOptions = (roles: UserRole[]) =>
|
||||
computed(() =>
|
||||
roles.map((role) => ({
|
||||
label: t(`enums.user_role.${role}`),
|
||||
value: role
|
||||
}))
|
||||
);
|
||||
|
||||
const roleOptions = rolesToOptions(['USER', 'CREATOR', 'ADMIN']);
|
||||
const promoteOptions = rolesToOptions(['CREATOR', 'ADMIN']);
|
||||
|
||||
const { data, refresh, pending } = await useFetch('/api/admin/user', {
|
||||
immediate: isAdmin.value,
|
||||
default: () => [] as RoleUser[]
|
||||
});
|
||||
|
||||
const roleLoading = ref(false);
|
||||
|
||||
async function changeRole(target: RoleUser, role: UserRole) {
|
||||
if (!role || target.role === role) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ok = await confirm({
|
||||
title: t('validation.confirm'),
|
||||
message: t('page.profile.role_confirm', {
|
||||
name: target.name,
|
||||
role: t(`enums.user_role.${role}`)
|
||||
}),
|
||||
okText: t('auth.accept')
|
||||
});
|
||||
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
roleLoading.value = true;
|
||||
try {
|
||||
await $fetch(`/api/admin/user/${target.id}/role`, {
|
||||
method: 'POST',
|
||||
body: { role }
|
||||
});
|
||||
init({ message: t('page.profile.role_changed'), color: 'success' });
|
||||
} catch (e) {
|
||||
init({
|
||||
title: t('page.profile.role_change_error'),
|
||||
message: t(parseError(e)),
|
||||
color: 'danger'
|
||||
});
|
||||
}
|
||||
await refresh();
|
||||
roleLoading.value = false;
|
||||
}
|
||||
|
||||
const search = ref('');
|
||||
const safeSearch = ref('');
|
||||
const promoteUser = ref<RoleUser>();
|
||||
const promoteRole = ref<UserRole>('CREATOR');
|
||||
|
||||
watchDebounced(
|
||||
search,
|
||||
(newSearch) => {
|
||||
if (newSearch) {
|
||||
safeSearch.value = newSearch;
|
||||
}
|
||||
},
|
||||
{ debounce: 500, maxWait: 1000 }
|
||||
);
|
||||
|
||||
const { data: searchUsers, pending: searchPending } = await useFetch(
|
||||
() => `/api/admin/user/search?email=${safeSearch.value}`,
|
||||
{
|
||||
immediate: false,
|
||||
lazy: true,
|
||||
default: () => [] as RoleUser[]
|
||||
}
|
||||
);
|
||||
|
||||
async function promote() {
|
||||
if (!promoteUser.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
roleLoading.value = true;
|
||||
try {
|
||||
await $fetch(`/api/admin/user/${promoteUser.value.id}/role`, {
|
||||
method: 'POST',
|
||||
body: { role: promoteRole.value }
|
||||
});
|
||||
init({ message: t('page.profile.role_changed'), color: 'success' });
|
||||
} catch (e) {
|
||||
init({
|
||||
title: t('page.profile.role_change_error'),
|
||||
message: t(parseError(e)),
|
||||
color: 'danger'
|
||||
});
|
||||
}
|
||||
|
||||
promoteUser.value = undefined;
|
||||
search.value = '';
|
||||
safeSearch.value = '';
|
||||
await refresh();
|
||||
roleLoading.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VaCard v-if="isAdmin" stripe stripe-color="warning">
|
||||
<VaCardTitle>{{ t('page.profile.admins_and_creators') }}</VaCardTitle>
|
||||
<VaCardContent>
|
||||
<div class="flex flex-col gap-4">
|
||||
<VaAlert color="info" dense>{{
|
||||
t('page.profile.own_role_hint')
|
||||
}}</VaAlert>
|
||||
<div v-for="u in data" :key="u.id" class="grid grid-cols-2 gap-1">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="truncate font-bold">{{ u.name }}</span>
|
||||
<span class="text-secondary truncate text-sm">{{ u.email }}</span>
|
||||
</div>
|
||||
<VaButtonToggle
|
||||
round
|
||||
preset="secondary"
|
||||
border-color="danger"
|
||||
color="danger"
|
||||
class="ml-auto"
|
||||
:model-value="u.role"
|
||||
:options="roleOptions"
|
||||
:disabled="u.id === user?.id || roleLoading"
|
||||
size="small"
|
||||
@update:model-value="changeRole(u, $event as UserRole)"
|
||||
/>
|
||||
</div>
|
||||
<VaAlert v-if="pending" color="info">{{
|
||||
t('vuestic.loading')
|
||||
}}</VaAlert>
|
||||
</div>
|
||||
</VaCardContent>
|
||||
</VaCard>
|
||||
<VaCard v-if="isAdmin" stripe stripe-color="warning">
|
||||
<VaCardTitle>{{ t('page.profile.promote_user') }}</VaCardTitle>
|
||||
<VaCardContent>
|
||||
<div class="flex flex-col gap-4">
|
||||
<VaSelect
|
||||
v-model="promoteUser"
|
||||
v-model:search="search"
|
||||
:label="t('page.profile.search_user')"
|
||||
autocomplete
|
||||
highlight-matched-text
|
||||
:options="searchUsers"
|
||||
:loading="searchPending"
|
||||
track-by="id"
|
||||
text-by="email"
|
||||
/>
|
||||
<VaButtonToggle
|
||||
round
|
||||
preset="secondary"
|
||||
border-color="danger"
|
||||
color="danger"
|
||||
:model-value="promoteRole"
|
||||
:options="promoteOptions"
|
||||
@update:model-value="promoteRole = $event as UserRole"
|
||||
/>
|
||||
<VaButton
|
||||
color="success"
|
||||
:disabled="!promoteUser || roleLoading"
|
||||
:loading="roleLoading"
|
||||
@click="promote"
|
||||
>{{ t('page.profile.promote') }}</VaButton
|
||||
>
|
||||
</div>
|
||||
</VaCardContent>
|
||||
</VaCard>
|
||||
</template>
|
||||
+16
-1
@@ -41,6 +41,11 @@
|
||||
"TEXT": "Freitext",
|
||||
"WORD": "Ein Wort",
|
||||
"CHAR": "Ein Zeichen"
|
||||
},
|
||||
"user_role": {
|
||||
"ADMIN": "Admin",
|
||||
"CREATOR": "Ersteller",
|
||||
"USER": "Spieler"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -131,10 +136,20 @@
|
||||
"filter_quest": "Filtere Bilder nach Quests"
|
||||
},
|
||||
"profile": {
|
||||
"admins_and_creators": "Admins & Ersteller",
|
||||
"new_password": "Neues Passwort",
|
||||
"no_pictures": "Noch keine Bilder hochgeladen.",
|
||||
"old_password": "Altes Passwort",
|
||||
"uploaded_pictures": "Hochgeladene Bilder"
|
||||
"own_role_hint": "Du kannst deine eigene Rolle nicht ändern.",
|
||||
"promote": "Befördern",
|
||||
"promote_user": "Benutzer befördern",
|
||||
"role": "Rolle",
|
||||
"role_changed": "Rolle aktualisiert!",
|
||||
"role_change_error": "Rolle konnte nicht geändert werden!",
|
||||
"role_confirm": "Möchtest du die Rolle von {name} zu {role} ändern?",
|
||||
"search_user": "Benutzer per E-Mail suchen...",
|
||||
"uploaded_pictures": "Hochgeladene Bilder",
|
||||
"user_management": "Benutzerverwaltung"
|
||||
},
|
||||
"quest": {
|
||||
"already_finalized": "Antwort ist bereits finalisiert, keine Änderung erlaubt!",
|
||||
|
||||
+16
-1
@@ -41,6 +41,11 @@
|
||||
"TEXT": "Free text",
|
||||
"WORD": "One word",
|
||||
"CHAR": "One character"
|
||||
},
|
||||
"user_role": {
|
||||
"ADMIN": "Admin",
|
||||
"CREATOR": "Creator",
|
||||
"USER": "Player"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -131,10 +136,20 @@
|
||||
"filter_quest": "Filter pictures by quests"
|
||||
},
|
||||
"profile": {
|
||||
"admins_and_creators": "Admins & Creators",
|
||||
"new_password": "New password",
|
||||
"no_pictures": "No pictures uploaded yet.",
|
||||
"old_password": "Old password",
|
||||
"uploaded_pictures": "Uploaded pictures"
|
||||
"own_role_hint": "You cannot change your own role.",
|
||||
"promote": "Promote",
|
||||
"promote_user": "Promote a user",
|
||||
"role": "Role",
|
||||
"role_changed": "Role updated!",
|
||||
"role_change_error": "Could not change role!",
|
||||
"role_confirm": "Do you want to change the role of {name} to {role}?",
|
||||
"search_user": "Search user by email...",
|
||||
"uploaded_pictures": "Uploaded pictures",
|
||||
"user_management": "User management"
|
||||
},
|
||||
"quest": {
|
||||
"already_finalized": "Answer is already finalized, no update allowed!",
|
||||
|
||||
@@ -266,6 +266,7 @@ async function submitPassword() {
|
||||
</VaCardContent>
|
||||
<VaCardContent v-else>{{ t('page.profile.no_pictures') }}</VaCardContent>
|
||||
</VaCard>
|
||||
<AdminUserRoles />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user