Files

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>