191 lines
4.8 KiB
Vue
191 lines
4.8 KiB
Vue
<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>
|