Add page and user monitoring in admin. Add permission editing in administration.
This commit is contained in:
parent
5062d52667
commit
429f1d4b38
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<CollapsibleRoot v-model:open="model" :disabled="disabled">
|
||||
<CollapsibleRoot v-model:open="model" :disabled="disabled" :defaultOpen="defaultOpen">
|
||||
<div class="flex flex-row justify-center items-center">
|
||||
<span v-if="!!label">{{ label }}</span>
|
||||
<CollapsibleTrigger class="ms-4" asChild>
|
||||
|
|
@ -18,9 +18,10 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
const { label, disabled = false } = defineProps<{
|
||||
const { label, disabled = false, defaultOpen = false } = defineProps<{
|
||||
label?: string
|
||||
disabled?: boolean
|
||||
defaultOpen?: boolean
|
||||
}>();
|
||||
const model = defineModel<boolean>();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
<template>
|
||||
<TagsInputRoot v-model="model" addOnPaste class="flex gap-2 items-center border p-2 w-full flex-wrap border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10" >
|
||||
<TagsInputItem v-for="item in model" :key="item" :value="item" class="text-light-100 dark:text-dark-100 flex items-center justify-center gap-2 bg-light-20 dark:bg-dark-20 hover:bg-light-35 dark:hover:bg-dark-35 p-1 border border-light-35 dark:border-dark-35">
|
||||
<TagsInputItemText class="text-sm pl-1" />
|
||||
<TagsInputItemDelete asChild>
|
||||
<Icon icon="radix-icons:cross-2" class="w-4 h-4 cursor-pointer" />
|
||||
</TagsInputItemDelete>
|
||||
</TagsInputItem>
|
||||
|
||||
<TagsInputInput :placeholder="placeholder" class="text-sm focus:outline-none flex-1 rounded text-green9 bg-transparent placeholder:text-mauve9 px-1" />
|
||||
</TagsInputRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
|
||||
const { placeholder } = defineProps<{
|
||||
placeholder?: string
|
||||
}>();
|
||||
const model = defineModel<string[]>();
|
||||
</script>
|
||||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -18,17 +18,7 @@
|
|||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip v-else :message="'Mon profil'" side="right">
|
||||
<DropdownMenu :options="[{
|
||||
type: 'item',
|
||||
label: 'Mon profil',
|
||||
icon: 'radix-icons:avatar',
|
||||
select: () => useRouter().push({ name: 'user-profile' }),
|
||||
}, {
|
||||
type: 'item',
|
||||
label: 'Deconnexion',
|
||||
icon: 'radix-icons:close',
|
||||
select: () => clear(),
|
||||
}]" side="right" align="start">
|
||||
<DropdownMenu :options="options" side="bottom" align="end">
|
||||
<div class="hover:border-opacity-70 flex">
|
||||
<Icon :icon="'radix-icons:avatar'" class="w-7 h-7 p-1" />
|
||||
</div>
|
||||
|
|
@ -55,15 +45,7 @@
|
|||
</NuxtLink>
|
||||
</Tooltip>
|
||||
<Tooltip v-else :message="'Mon profil'" side="right">
|
||||
<DropdownMenu :options="[{
|
||||
type: 'item',
|
||||
label: 'Mon profil',
|
||||
select: () => useRouter().push({ name: 'user-profile' }),
|
||||
}, {
|
||||
type: 'item',
|
||||
label: 'Deconnexion',
|
||||
select: () => clear(),
|
||||
}]" side="right" align="start">
|
||||
<DropdownMenu :options="options" side="right" align="start">
|
||||
<div class="bg-light-20 dark:bg-dark-20 hover:border-opacity-70 flex border p-px border-light-50 dark:border-dark-50">
|
||||
<Icon :icon="'radix-icons:avatar'" class="w-7 h-7 p-1" />
|
||||
</div>
|
||||
|
|
@ -104,9 +86,20 @@
|
|||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import type { NavigationTreeItem } from '~/server/api/navigation.get';
|
||||
import { iconByType } from '#shared/general.utils';
|
||||
import type { DropdownOption } from '~/components/base/DropdownMenu.vue';
|
||||
|
||||
const options = ref<DropdownOption[]>([{
|
||||
type: 'item',
|
||||
label: 'Mon profil',
|
||||
select: () => useRouter().push({ name: 'user-profile' }),
|
||||
}, {
|
||||
type: 'item',
|
||||
label: 'Deconnexion',
|
||||
select: () => clear(),
|
||||
}]);
|
||||
|
||||
const open = ref(false);
|
||||
const { loggedIn, clear } = useUserSession();
|
||||
const { loggedIn, user, clear } = useUserSession();
|
||||
|
||||
const route = useRouter().currentRoute;
|
||||
const path = computed(() => route.value.params.path ? Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path : undefined);
|
||||
|
|
|
|||
|
|
@ -1,23 +1,255 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Format bytes as human-readable text.
|
||||
*
|
||||
* @param bytes Number of bytes.
|
||||
* @param si True to use metric (SI) units, aka powers of 1000. False to use
|
||||
* binary (IEC), aka powers of 1024.
|
||||
* @param dp Number of decimal places to display.
|
||||
*
|
||||
* @return Formatted string.
|
||||
*/
|
||||
function textualFileSize(bytes: number, si: boolean = false, dp: number = 2) {
|
||||
const thresh = si ? 1000 : 1024;
|
||||
|
||||
if (Math.abs(bytes) < thresh) {
|
||||
return bytes + ' B';
|
||||
}
|
||||
|
||||
const units = ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
let u = -1;
|
||||
const r = 10**dp;
|
||||
|
||||
do {
|
||||
bytes /= thresh;
|
||||
++u;
|
||||
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
|
||||
|
||||
|
||||
return bytes.toFixed(dp) + ' ' + units[u];
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { format, iconByType } from '~/shared/general.utils';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
|
||||
interface File
|
||||
{
|
||||
path: string;
|
||||
owner: number;
|
||||
title: string;
|
||||
type: "file" | "canvas" | "markdown" | 'folder';
|
||||
size: number;
|
||||
navigable: boolean;
|
||||
private: boolean;
|
||||
order: number;
|
||||
visit: number;
|
||||
timestamp: string;
|
||||
}
|
||||
interface User
|
||||
{
|
||||
id: number;
|
||||
username: string;
|
||||
state: number;
|
||||
session: {
|
||||
id: number;
|
||||
}[];
|
||||
data: {
|
||||
id: number;
|
||||
signin: string;
|
||||
lastTimestamp: string;
|
||||
logCount: number;
|
||||
};
|
||||
permission: string[];
|
||||
}
|
||||
|
||||
definePageMeta({
|
||||
rights: ['admin'],
|
||||
});
|
||||
|
||||
const { data: users } = useFetch('/api/admin/users');
|
||||
const toaster = useToast();
|
||||
|
||||
const { data: users } = useFetch('/api/admin/users', {
|
||||
transform: (users) => {
|
||||
//@ts-ignore
|
||||
users.forEach(e => e.permission = e.permission.map(p => p.permission));
|
||||
//@ts-ignore
|
||||
return users as User[];
|
||||
},
|
||||
});
|
||||
const { data: pages } = useFetch('/api/admin/pages');
|
||||
|
||||
const sorter = ref<((a: File, b: File) => number) | null>(null);
|
||||
const sortField = ref<keyof File | null>(null), sortOrder = ref<null | 'asc' | 'desc'>('asc');
|
||||
const sortedPage = ref([...pages.value ?? []]);
|
||||
|
||||
const permissionCopy = ref<string[]>([]);
|
||||
|
||||
watch([sortField, sortOrder, sorter], () => {
|
||||
sortedPage.value = (sorter.value === null ? ([...pages.value ?? []]) : sortedPage.value.sort(sorter.value))
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
function sort(field: keyof File, type: 'string' | 'number')
|
||||
{
|
||||
if(sortField.value === field)
|
||||
{
|
||||
if(sortOrder.value === 'asc')
|
||||
{
|
||||
sortOrder.value = 'desc';
|
||||
sorter.value = type === 'string' ? (a: File, b: File) => (b[field] as string).localeCompare(a[field] as string) : (a: File, b: File) => (b[field] as number) - (a[field] as number);
|
||||
}
|
||||
else
|
||||
{
|
||||
sortOrder.value = null;
|
||||
sortField.value = null;
|
||||
sorter.value = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sortField.value = field;
|
||||
sortOrder.value = 'asc';
|
||||
sorter.value = type === 'string' ? (a: File, b: File) => (a[field] as string).localeCompare(b[field] as string) : (a: File, b: File) => (a[field] as number) - (b[field] as number);
|
||||
}
|
||||
}
|
||||
async function editPermissions(user: User)
|
||||
{
|
||||
try
|
||||
{
|
||||
await $fetch(`/api/admin/user/${user.id}/permissions`, {
|
||||
method: 'POST',
|
||||
body: permissionCopy.value,
|
||||
});
|
||||
user.permission = permissionCopy.value;
|
||||
toaster.add({
|
||||
duration: 10000, type: 'success', content: 'Permissions mises à jour.', timer: true,
|
||||
});
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
toaster.add({
|
||||
duration: 10000, type: 'error', content: (e as any).message, timer: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head>
|
||||
<Title>d[any] - Administration</Title>
|
||||
</Head>
|
||||
<div class="flex flex-1 flex-col p-4 justify-start">
|
||||
<div class="flex flex-1 flex-col p-4">
|
||||
<div class="flex flex-row justify-between items-center">
|
||||
<ProseH2 class="text-center flex-1">Administration</ProseH2>
|
||||
<Button><NuxtLink :to="{ name: 'admin-jobs' }">Jobs</NuxtLink></Button>
|
||||
</div>
|
||||
<div>
|
||||
<div>Utilisateurs ({{ users.length }})</div>
|
||||
<div class="flex flex-1 w-full justify-center items-stretch flex-row gap-4">
|
||||
<div class="flex-1">
|
||||
<Collapsible v-if=users :label="`Utilisateurs (${users.length})`">
|
||||
<div class="flex flex-1 mt-2">
|
||||
<table class="border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border border-light-35 dark:border-dark-35 px-2 py-1">Utilisateur</th>
|
||||
<th class="border border-light-35 dark:border-dark-35 px-2 py-1">Inscription</th>
|
||||
<th class="border border-light-35 dark:border-dark-35 px-2 py-1">Dernière connexion</th>
|
||||
<th class="border border-light-35 dark:border-dark-35 px-2 py-1">Mail</th>
|
||||
<th class="border border-light-35 dark:border-dark-35 px-2 py-1">Sessions</th>
|
||||
<th class="border border-light-35 dark:border-dark-35 px-2 py-1">Permissions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="font-normal">
|
||||
<tr v-for="user in users">
|
||||
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 max-w-32 truncate"><NuxtLink :to="{ name: 'user-id', params: { id: user.id } }" class="hover:text-accent-purple font-bold" :title="user.username">{{ user.username }}</NuxtLink></td>
|
||||
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 text-sm text-light-70 dark:text-dark-70 text-center">{{ format(new Date(user.data.signin), 'dd/MM/yyyy') }}</td>
|
||||
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 text-sm text-light-70 dark:text-dark-70 text-center">{{ format(new Date(user.data.lastTimestamp), 'dd/MM/yyyy HH:mm:ss') }}</td>
|
||||
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 text-center"><Icon :class="{ 'text-light-red dark:text-dark-red': user.state === 0, 'text-light-green dark:text-dark-green': user.state !== 0 }" :icon="user.state === 0 ? `radix-icons:cross-2` : `radix-icons:check`" /></td>
|
||||
<td class="border border-light-35 dark:border-dark-35 px-2 py-1">
|
||||
<DialogRoot>
|
||||
<DialogTrigger asChild><span class="text-accent-blue hover:text-accent-purple font-bold cursor-pointer">{{ user.session.length }}</span></DialogTrigger>
|
||||
<DialogPortal>
|
||||
<DialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
|
||||
<DialogContent
|
||||
class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[800px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100">
|
||||
<DialogTitle class="text-3xl font-light relative -top-2">Deconnecter l'utilisateur ?
|
||||
</DialogTitle>
|
||||
<div class="flex flex-1 justify-end gap-4">
|
||||
<DialogClose asChild><Button>Non</Button></DialogClose>
|
||||
<DialogClose asChild><Button class="border-light-green dark:border-dark-green hover:border-light-green dark:hover:border-dark-green hover:bg-light-greenBack dark:hover:bg-dark-greenBack text-light-green dark:text-dark-green focus:shadow-light-green dark:focus:shadow-dark-green">Oui</Button></DialogClose>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
</td>
|
||||
<td class="border border-light-35 dark:border-dark-35 px-2 py-1">
|
||||
<AlertDialogRoot>
|
||||
<AlertDialogTrigger asChild><span class="text-accent-blue hover:text-accent-purple font-bold" @click="permissionCopy = [...user.permission]">{{ user.permission.length }}</span></AlertDialogTrigger>
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
|
||||
<AlertDialogContent
|
||||
class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[800px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100">
|
||||
<AlertDialogTitle class="text-3xl font-light relative -top-2">Permissions de {{ user.username }}</AlertDialogTitle>
|
||||
<AlertDialogDescription><TagsInput v-model="permissionCopy" /></AlertDialogDescription>
|
||||
<div class="flex flex-1 justify-end gap-4">
|
||||
<AlertDialogCancel asChild><Button>Annuler</Button></AlertDialogCancel>
|
||||
<AlertDialogAction asChild><Button @click="() => editPermissions(user)" class="border-light-green dark:border-dark-green hover:border-light-green dark:hover:border-dark-green hover:bg-light-greenBack dark:hover:bg-dark-greenBack text-light-green dark:text-dark-green focus:shadow-light-green dark:focus:shadow-dark-green">Modifier</Button></AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<Collapsible v-if=pages :label="`Pages (${pages.length})`">
|
||||
<div class="flex flex-1 mt-2">
|
||||
<table class="border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border border-light-35 dark:border-dark-35 px-2 py-1"><div class="flex justify-center items-center gap-2"><span>Page</span><span @click="() => sort('title', 'string')"><Icon :icon="sortField === 'title' ? sortOrder === 'asc' ? 'radix-icons:chevron-down' : 'radix-icons:chevron-up' : 'radix-icons:caret-sort'" /></span></div></th>
|
||||
<th class="border border-light-35 dark:border-dark-35 px-2 py-1"><div class="flex justify-center items-center gap-2"><span>Type</span></div></th>
|
||||
<th class="border border-light-35 dark:border-dark-35 px-2 py-1"><div class="flex justify-center items-center gap-2"><span>Propriétaire</span><span @click="() => sort('owner', 'number')"><Icon :icon="sortField === 'owner' ? sortOrder === 'asc' ? 'radix-icons:chevron-down' : 'radix-icons:chevron-up' : 'radix-icons:caret-sort'" /></span></div></th>
|
||||
<th class="border border-light-35 dark:border-dark-35 px-2 py-1"><div class="flex justify-center items-center gap-2"><span>Status</span></div></th>
|
||||
<th class="border border-light-35 dark:border-dark-35 px-2 py-1"><div class="flex justify-center items-center gap-2"><span>Taille</span><span @click="() => sort('size', 'number')"><Icon :icon="sortField === 'size' ? sortOrder === 'asc' ? 'radix-icons:chevron-down' : 'radix-icons:chevron-up' : 'radix-icons:caret-sort'" /></span></div></th>
|
||||
<th class="border border-light-35 dark:border-dark-35 px-2 py-1"><div class="flex justify-center items-center gap-2"><span>Visites</span><span @click="() => sort('visit', 'number')"><Icon :icon="sortField === 'visit' ? sortOrder === 'asc' ? 'radix-icons:chevron-down' : 'radix-icons:chevron-up' : 'radix-icons:caret-sort'" /></span></div></th>
|
||||
<th class="border border-light-35 dark:border-dark-35 px-2 py-1"><div class="flex justify-center items-center gap-2"><span>Actions</span></div></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="font-normal">
|
||||
<DialogRoot>
|
||||
<tr v-for="page in sortedPage" :id="page.path">
|
||||
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 max-w-48 truncate"><NuxtLink :to="{ name: 'explore-path', params: { path: page.path } }" class="hover:text-accent-purple font-bold" :title="page.title">{{ page.title }}</NuxtLink></td>
|
||||
<td class="border border-light-35 dark:border-dark-35 px-2 py-1"><Icon :icon="iconByType[page.type]" /></td>
|
||||
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 text-sm text-light-70 dark:text-dark-70 text-center max-w-32 truncate"><span :title=" users?.find(e => e.id === page.owner)?.username ?? 'Inconnu'">{{ users?.find(e => e.id === page.owner)?.username ?? "Inconnu" }}</span></td>
|
||||
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 ">
|
||||
<div class="flex gap-2 justify-center">
|
||||
<span>
|
||||
<Icon v-if="page.private" icon="radix-icons:lock-closed" />
|
||||
<Icon v-else class="text-light-50 dark:text-dark-50" icon="radix-icons:lock-open-2" />
|
||||
</span>
|
||||
<span>
|
||||
<Icon v-if="page.navigable" icon="radix-icons:eye-open" />
|
||||
<Icon v-else class="text-light-50 dark:text-dark-50" icon="radix-icons:eye-none" />
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 text-center">{{ textualFileSize(page.size) }}</td>
|
||||
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 text-center">{{ page.visit }}</td>
|
||||
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 text-center"><div class="flex justify-center items-center"><NuxtLink :to="{ name: 'explore-edit-path', params: { path: page.path } }"><Icon icon="radix-icons:pencil-1" /></NuxtLink></div></td>
|
||||
</tr>
|
||||
</DialogRoot>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -7,8 +7,8 @@
|
|||
<div class="grid grid-cols-4 gap-x-2 gap-y-4">
|
||||
<div v-if="loggedIn && user && hasPermissions(user.permissions, ['admin'])" class="flex flex-col gap-2 justify-start">
|
||||
<ProseH3>Administration</ProseH3>
|
||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Dashboard de statistiques</span></Label>
|
||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Gestion de droits</span><ProseTag>prioritaire</ProseTag></Label>
|
||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Dashboard de statistiques</span></Label>
|
||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Editeur de permissions</span><ProseTag>prioritaire</ProseTag></Label>
|
||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Synchro project <-> GIT</span><ProseTag>prioritaire</ProseTag></Label>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 justify-start">
|
||||
|
|
|
|||
|
|
@ -79,19 +79,5 @@ async function deleteUser()
|
|||
</AlertDialogRoot>
|
||||
<NuxtLink v-if="hasPermissions(user.permissions, ['admin'])" :href="{ name: 'admin' }" class="flex" no-prefetch><Button class="flex-1">Administration</Button></NuxtLink>
|
||||
</div>
|
||||
<div class="flex" v-if="user.permissions">
|
||||
<ProseTable class="!m-0">
|
||||
<ProseThead>
|
||||
<ProseTr>
|
||||
<ProseTh>Permission</ProseTh>
|
||||
</ProseTr>
|
||||
</ProseThead>
|
||||
<ProseTbody>
|
||||
<ProseTr v-for="permission in user.permissions">
|
||||
<ProseTd>{{ permission }}</ProseTd>
|
||||
</ProseTr>
|
||||
</ProseTbody>
|
||||
</ProseTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,3 +1,50 @@
|
|||
export default defineEventHandler((e) => {
|
||||
return [];
|
||||
import { ne, sql } from 'drizzle-orm';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { explorerContentTable } from '~/db/schema';
|
||||
import { hasPermissions } from '~/shared/auth.util';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const session = await getUserSession(e);
|
||||
|
||||
if(!session || !session.user || !hasPermissions(session.user.permissions, ['admin']))
|
||||
{
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Unauthorized',
|
||||
});
|
||||
}
|
||||
|
||||
const db = useDatabase();
|
||||
const content = db.select({
|
||||
path: explorerContentTable.path,
|
||||
owner: explorerContentTable.owner,
|
||||
title: explorerContentTable.title,
|
||||
type: explorerContentTable.type,
|
||||
size: sql<number>`CASE WHEN ${explorerContentTable.content} IS NULL THEN 0 ELSE length(${explorerContentTable.content}) END`.as('size'),
|
||||
navigable: explorerContentTable.navigable,
|
||||
private: explorerContentTable.private,
|
||||
order: explorerContentTable.order,
|
||||
visit: explorerContentTable.visit,
|
||||
timestamp: explorerContentTable.timestamp,
|
||||
}).from(explorerContentTable).all();
|
||||
|
||||
content.sort((a, b) => {
|
||||
return a.path.split('/').length - b.path.split('/').length;
|
||||
});
|
||||
|
||||
for(let i = 0; i < content.length; i++)
|
||||
{
|
||||
const path = content[i].path.substring(0, content[i].path.lastIndexOf('/'));
|
||||
if(path !== '')
|
||||
{
|
||||
const parent = content.find(e => e.path === path);
|
||||
|
||||
if(parent)
|
||||
{
|
||||
content[i].private = content[i].private || parent.private;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return content.filter(e => e.type !== 'folder');
|
||||
})
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { hasPermissions } from "~/shared/auth.util";
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { and, eq, notInArray } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { userPermissionsTable } from "~/db/schema";
|
||||
|
||||
const schema = z.array(z.string());
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const session = await getUserSession(e);
|
||||
|
||||
if(!session || !session.user || !hasPermissions(session.user.permissions, ['admin']))
|
||||
{
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Unauthorized',
|
||||
});
|
||||
}
|
||||
|
||||
const param = getRouterParam(e, 'id');
|
||||
|
||||
if(!param)
|
||||
{
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Forbidden',
|
||||
});
|
||||
}
|
||||
|
||||
const body = await readValidatedBody(e, schema.safeParse);
|
||||
|
||||
if(!body.success)
|
||||
{
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Forbidden',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const id = parseInt(param, 10);
|
||||
|
||||
const db = useDatabase();
|
||||
const permissions = body.data.map(e => ({ id: id, permission: e }));
|
||||
|
||||
db.transaction((tx) => {
|
||||
tx.delete(userPermissionsTable).where(eq(userPermissionsTable.id, id)).run();
|
||||
tx.insert(userPermissionsTable).values(permissions).run();
|
||||
});
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
@ -1,3 +1,34 @@
|
|||
export default defineEventHandler((e) => {
|
||||
return [];
|
||||
import { sql } from 'drizzle-orm';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { userSessionsTable } from '~/db/schema';
|
||||
import { hasPermissions } from '~/shared/auth.util';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const session = await getUserSession(e);
|
||||
|
||||
if(!session || !session.user || !hasPermissions(session.user.permissions, ['admin']))
|
||||
{
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Unauthorized',
|
||||
});
|
||||
}
|
||||
|
||||
const db = useDatabase();
|
||||
return db.query.usersTable.findMany({
|
||||
columns: {
|
||||
email: false,
|
||||
hash: false,
|
||||
},
|
||||
with: {
|
||||
data: true,
|
||||
permission: true,
|
||||
session: {
|
||||
columns: {
|
||||
timestamp: false,
|
||||
user_id: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}).sync();
|
||||
})
|
||||
|
|
@ -26,9 +26,9 @@ export function format(date: Date, template: string): string
|
|||
"yyyy": (date: Date) => date.getUTCFullYear().toString(),
|
||||
"MM": (date: Date) => padRight((date.getUTCMonth() + 1).toString(), '0', 2),
|
||||
"dd": (date: Date) => padRight(date.getUTCDate().toString(), '0', 2),
|
||||
"mm": (date: Date) => padRight(date.getFullYear().toString(), '0', 2),
|
||||
"HH": (date: Date) => padRight(date.getFullYear().toString(), '0', 2),
|
||||
"ss": (date: Date) => padRight(date.getFullYear().toString(), '0', 2),
|
||||
"mm": (date: Date) => padRight(date.getUTCMinutes().toString(), '0', 2),
|
||||
"HH": (date: Date) => padRight(date.getUTCHours().toString(), '0', 2),
|
||||
"ss": (date: Date) => padRight(date.getUTCSeconds().toString(), '0', 2),
|
||||
};
|
||||
const keys = Object.keys(transforms);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue