mirror of https://github.com/wg-easy/wg-easy
47 changed files with 1015 additions and 224 deletions
@ -0,0 +1,37 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import { |
||||
|
ToastAction, |
||||
|
ToastClose, |
||||
|
ToastDescription, |
||||
|
ToastRoot, |
||||
|
ToastTitle, |
||||
|
} from 'radix-vue'; |
||||
|
|
||||
|
defineProps<{ |
||||
|
title: string; |
||||
|
content: string; |
||||
|
}>(); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<ToastRoot |
||||
|
class="bg-white rounded-md shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] p-[15px] grid [grid-template-areas:_'title_action'_'description_action'] grid-cols-[auto_max-content] gap-x-[15px] items-center data-[state=open]:animate-slideIn data-[state=closed]:animate-hide data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=cancel]:transition-[transform_200ms_ease-out] data-[swipe=end]:animate-swipeOut" |
||||
|
> |
||||
|
<ToastTitle |
||||
|
v-if="title" |
||||
|
class="[grid-area:_title] mb-[5px] font-medium text-slate12 text-[15px]" |
||||
|
> |
||||
|
{{ title }} |
||||
|
</ToastTitle> |
||||
|
<ToastDescription |
||||
|
class="[grid-area:_description] m-0 text-slate11 text-[13px] leading-[1.3]" |
||||
|
>{{ content }}</ToastDescription |
||||
|
> |
||||
|
<ToastAction as-child alt-text="toast" class="[grid-area:_action]"> |
||||
|
<slot /> |
||||
|
</ToastAction> |
||||
|
<ToastClose aria-label="Close"> |
||||
|
<span aria-hidden>×</span> |
||||
|
</ToastClose> |
||||
|
</ToastRoot> |
||||
|
</template> |
@ -0,0 +1,92 @@ |
|||||
|
<template> |
||||
|
<DropdownMenuRoot v-model:open="toggleState"> |
||||
|
<DropdownMenuTrigger> |
||||
|
<button |
||||
|
class="flex items-center pe-1 font-medium text-sm text-gray-400 rounded-full hover:text-red-800 dark:hover:text-red-800 md:me-0 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:text-neutral-400" |
||||
|
type="button" |
||||
|
> |
||||
|
<AvatarRoot |
||||
|
class="inline-flex h-8 w-8 select-none items-center justify-center overflow-hidden rounded-full align-middle mr-2" |
||||
|
> |
||||
|
<AvatarFallback |
||||
|
class="text-grass11 leading-1 flex h-full w-full items-center justify-center bg-white text-[15px] font-medium" |
||||
|
:delay-ms="600" |
||||
|
> |
||||
|
{{ fallbackName }} |
||||
|
</AvatarFallback> |
||||
|
</AvatarRoot> |
||||
|
{{ authStore.userData?.name }} |
||||
|
</button> |
||||
|
</DropdownMenuTrigger> |
||||
|
|
||||
|
<DropdownMenuPortal> |
||||
|
<DropdownMenuContent |
||||
|
:side-offset="5" |
||||
|
class="z-10 bg-white divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-neutral-700 dark:divide-neutral-800 text-gray-700 dark:text-gray-200" |
||||
|
> |
||||
|
<DropdownMenuItem> |
||||
|
<div class="truncate">{{ authStore.userData?.name }}</div> |
||||
|
<div class="truncate">@{{ authStore.userData?.username }}</div> |
||||
|
</DropdownMenuItem> |
||||
|
<DropdownMenuItem> |
||||
|
<NuxtLink |
||||
|
to="/" |
||||
|
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white" |
||||
|
> |
||||
|
Clients |
||||
|
</NuxtLink> |
||||
|
</DropdownMenuItem> |
||||
|
<DropdownMenuItem> |
||||
|
<NuxtLink |
||||
|
to="/me" |
||||
|
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white" |
||||
|
> |
||||
|
Account |
||||
|
</NuxtLink> |
||||
|
</DropdownMenuItem> |
||||
|
<DropdownMenuItem v-if="authStore.userData?.role === 'ADMIN'"> |
||||
|
<NuxtLink |
||||
|
to="/admin" |
||||
|
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white" |
||||
|
> |
||||
|
Admin Panel |
||||
|
</NuxtLink> |
||||
|
</DropdownMenuItem> |
||||
|
<DropdownMenuItem> |
||||
|
<button |
||||
|
class="flex items-center gap-2 w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white" |
||||
|
@click.prevent="logout" |
||||
|
> |
||||
|
<IconsLogout class="h-5" /> |
||||
|
{{ $t('logout') }} |
||||
|
</button> |
||||
|
</DropdownMenuItem> |
||||
|
</DropdownMenuContent> |
||||
|
</DropdownMenuPortal> |
||||
|
</DropdownMenuRoot> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
const authStore = useAuthStore(); |
||||
|
const toggleState = ref(false); |
||||
|
|
||||
|
async function logout() { |
||||
|
try { |
||||
|
await authStore.logout(); |
||||
|
navigateTo('/login'); |
||||
|
} catch (err) { |
||||
|
if (err instanceof Error) { |
||||
|
// TODO: better ui |
||||
|
alert(err.message || err.toString()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const fallbackName = computed(() => { |
||||
|
return authStore.userData?.name |
||||
|
.split(' ') |
||||
|
.map((word) => word.charAt(0).toUpperCase()) |
||||
|
.slice(0, 2) |
||||
|
.join(''); |
||||
|
}); |
||||
|
</script> |
@ -0,0 +1,52 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<div class="container mx-auto p-4"> |
||||
|
<div class="flex"> |
||||
|
<div class="w-64 bg-white dark:bg-neutral-700 rounded-lg p-4 mr-4"> |
||||
|
<NuxtLink to="/admin"> |
||||
|
<h2 class="text-xl font-bold dark:text-neutral-200 mb-4"> |
||||
|
Admin Panel |
||||
|
</h2> |
||||
|
</NuxtLink> |
||||
|
<div class="space-y-2 flex flex-col"> |
||||
|
<NuxtLink |
||||
|
v-for="(item, index) in menuItems" |
||||
|
:key="index" |
||||
|
:to="`/admin/${item.id}`" |
||||
|
> |
||||
|
<BaseButton |
||||
|
class="font-medium dark:text-neutral-200 p-2 rounded cursor-pointer hover:bg-red-800 transition-colors duration-200 w-full" |
||||
|
> |
||||
|
{{ item.name }} |
||||
|
</BaseButton> |
||||
|
</NuxtLink> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div |
||||
|
class="flex-1 bg-white dark:text-neutral-200 dark:bg-neutral-700 rounded-lg p-6" |
||||
|
> |
||||
|
<h1 class="text-3xl font-bold mb-6">{{ activeMenuItem?.name }}</h1> |
||||
|
<NuxtPage /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
const authStore = useAuthStore(); |
||||
|
authStore.update(); |
||||
|
|
||||
|
const route = useRoute(); |
||||
|
|
||||
|
const menuItems = [ |
||||
|
{ id: 'features', name: 'Features' }, |
||||
|
{ id: 'statistics', name: 'Statistics' }, |
||||
|
{ id: 'metrics', name: 'Metrics' }, |
||||
|
]; |
||||
|
|
||||
|
const activeMenuItem = computed(() => { |
||||
|
return menuItems.find((item) => route.path === `/admin/${item.id}`); |
||||
|
}); |
||||
|
</script> |
@ -0,0 +1,83 @@ |
|||||
|
<template> |
||||
|
<div class="flex flex-col"> |
||||
|
<div v-for="(feature, key) in featuresData" :key="key" class="space-y-2"> |
||||
|
<div class="flex items-center justify-between"> |
||||
|
<div> |
||||
|
<h3 class="text-lg font-medium text-gray-900 dark:text-neutral-200"> |
||||
|
{{ feature.name }} |
||||
|
</h3> |
||||
|
<p class="text-sm text-gray-500 dark:text-neutral-300"> |
||||
|
{{ feature.description }} |
||||
|
</p> |
||||
|
</div> |
||||
|
<SwitchRoot |
||||
|
:checked="feature.enabled" |
||||
|
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-800" |
||||
|
:class="feature.enabled ? 'bg-red-800' : 'bg-gray-200'" |
||||
|
@update:checked="toggleFeature(key)" |
||||
|
> |
||||
|
<SwitchThumb |
||||
|
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform" |
||||
|
:class="feature.enabled ? 'translate-x-6' : 'translate-x-1'" |
||||
|
/> |
||||
|
</SwitchRoot> |
||||
|
</div> |
||||
|
</div> |
||||
|
<BaseButton class="self-end" @click="submit">Save</BaseButton> |
||||
|
<UiToast |
||||
|
v-model:open="open" |
||||
|
title="Saved successfully" |
||||
|
content="Features saved successfully" |
||||
|
/> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
const globalStore = useGlobalStore(); |
||||
|
const open = ref(false); |
||||
|
|
||||
|
type ExtendedFeatures = { |
||||
|
[K in keyof (typeof globalStore)['features']]: { |
||||
|
name: string; |
||||
|
description: string; |
||||
|
enabled: boolean; |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
const featuresData = ref({ |
||||
|
sortClients: { |
||||
|
name: 'Sort Clients', |
||||
|
description: 'Be able to sort Clients by Name', |
||||
|
enabled: globalStore.features.sortClients.enabled, |
||||
|
}, |
||||
|
oneTimeLinks: { |
||||
|
name: 'One Time Links', |
||||
|
description: 'Be able to generate One Time Link to download Config', |
||||
|
enabled: globalStore.features.oneTimeLinks.enabled, |
||||
|
}, |
||||
|
clientExpiration: { |
||||
|
name: 'Client Expiration', |
||||
|
description: 'Be able to set Date when Client will be disabled', |
||||
|
enabled: globalStore.features.clientExpiration.enabled, |
||||
|
}, |
||||
|
} satisfies ExtendedFeatures); |
||||
|
|
||||
|
function toggleFeature(key: keyof ExtendedFeatures) { |
||||
|
const feat = featuresData.value[key]; |
||||
|
if (!feat) { |
||||
|
return; |
||||
|
} |
||||
|
feat.enabled = !feat.enabled; |
||||
|
} |
||||
|
|
||||
|
async function submit() { |
||||
|
const response = await $fetch('/api/admin/features', { |
||||
|
method: 'post', |
||||
|
body: { features: featuresData.value }, |
||||
|
}); |
||||
|
if (response.success) { |
||||
|
open.value = true; |
||||
|
} |
||||
|
globalStore.fetchFeatures(); |
||||
|
} |
||||
|
</script> |
@ -0,0 +1,10 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
This is the Admin Panel. Your are running wg-easy |
||||
|
{{ globalStore.currentRelease }} |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
const globalStore = useGlobalStore(); |
||||
|
</script> |
@ -0,0 +1,115 @@ |
|||||
|
<template> |
||||
|
<div class="flex flex-col"> |
||||
|
<div class="flex items-center justify-between"> |
||||
|
<div> |
||||
|
<h3 class="text-lg font-medium text-gray-900 dark:text-neutral-200"> |
||||
|
Traffic Stats |
||||
|
</h3> |
||||
|
<p class="text-sm text-gray-500 dark:text-neutral-300"> |
||||
|
Show more concise Stats about Traffic Usage |
||||
|
</p> |
||||
|
</div> |
||||
|
<SwitchRoot |
||||
|
v-model:checked="enabled" |
||||
|
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-800" |
||||
|
:class="enabled ? 'bg-red-800' : 'bg-gray-200'" |
||||
|
> |
||||
|
<SwitchThumb |
||||
|
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform" |
||||
|
:class="enabled ? 'translate-x-6' : 'translate-x-1'" |
||||
|
/> |
||||
|
</SwitchRoot> |
||||
|
</div> |
||||
|
<div class="flex items-center justify-between"> |
||||
|
<div> |
||||
|
<h3 class="text-lg font-medium text-gray-900 dark:text-neutral-200"> |
||||
|
Chart Type |
||||
|
</h3> |
||||
|
<p class="text-sm text-gray-500 dark:text-neutral-300"> |
||||
|
Select Type of Chart you want to show |
||||
|
</p> |
||||
|
</div> |
||||
|
<SelectRoot v-model="chartType"> |
||||
|
<SelectTrigger |
||||
|
class="inline-flex min-w-[160px] items-center justify-between rounded px-[15px] text-[13px] leading-none h-[35px] gap-[5px] bg-white text-grass11 shadow-[0_2px_10px] shadow-black/10 hover:bg-mauve3 focus:shadow-[0_0_0_2px] focus:shadow-black data-[placeholder]:text-green9 outline-none" |
||||
|
aria-label="Customize options" |
||||
|
> |
||||
|
<SelectValue placeholder="Select a fruit..." /> |
||||
|
<IconsArrowDown class="h-3.5 w-3.5" /> |
||||
|
</SelectTrigger> |
||||
|
|
||||
|
<SelectPortal> |
||||
|
<SelectContent |
||||
|
class="min-w-[160px] bg-white rounded shadow-[0px_10px_38px_-10px_rgba(22,_23,_24,_0.35),_0px_10px_20px_-15px_rgba(22,_23,_24,_0.2)] will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade z-[100]" |
||||
|
:side-offset="5" |
||||
|
> |
||||
|
<SelectScrollUpButton |
||||
|
class="flex items-center justify-center h-[25px] bg-white text-violet11 cursor-default" |
||||
|
> |
||||
|
<IconsArrowUp /> |
||||
|
</SelectScrollUpButton> |
||||
|
<SelectViewport class="p-[5px]"> |
||||
|
<SelectItem |
||||
|
v-for="(option, index) in options" |
||||
|
:key="index" |
||||
|
class="text-[13px] leading-none text-grass11 rounded-[3px] flex items-center h-[25px] pr-[35px] pl-[25px] relative select-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-green9 data-[highlighted]:text-green1" |
||||
|
:value="option" |
||||
|
> |
||||
|
<SelectItemText> |
||||
|
{{ option }} |
||||
|
</SelectItemText> |
||||
|
</SelectItem> |
||||
|
</SelectViewport> |
||||
|
<SelectScrollDownButton |
||||
|
class="flex items-center justify-center h-[25px] bg-white text-violet11 cursor-default" |
||||
|
> |
||||
|
<IconsArrowDown /> |
||||
|
</SelectScrollDownButton> |
||||
|
</SelectContent> |
||||
|
</SelectPortal> |
||||
|
</SelectRoot> |
||||
|
</div> |
||||
|
<BaseButton class="self-end" @click="submit">Save</BaseButton> |
||||
|
<UiToast |
||||
|
v-model:open="open" |
||||
|
title="Saved successfully" |
||||
|
content="Statistics saved successfully" |
||||
|
/> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
const globalStore = useGlobalStore(); |
||||
|
const open = ref(false); |
||||
|
const enabled = ref(globalStore.statistics.enabled); |
||||
|
const options: Record<number, string> = { |
||||
|
0: 'None', |
||||
|
1: 'Line', |
||||
|
2: 'Area', |
||||
|
3: 'Bar', |
||||
|
}; |
||||
|
const stringToIndex = Object.entries(options).reduce( |
||||
|
(obj, [k, v]) => { |
||||
|
obj[v] = Number.parseInt(k); |
||||
|
return obj; |
||||
|
}, |
||||
|
{} as Record<string, number> |
||||
|
); |
||||
|
const chartType = ref(options[globalStore.statistics.chartType]); |
||||
|
|
||||
|
async function submit() { |
||||
|
const response = await $fetch('/api/admin/statistics', { |
||||
|
method: 'post', |
||||
|
body: { |
||||
|
statistics: { |
||||
|
enabled: enabled.value, |
||||
|
chartType: stringToIndex[chartType.value!], |
||||
|
}, |
||||
|
}, |
||||
|
}); |
||||
|
if (response.success) { |
||||
|
open.value = true; |
||||
|
} |
||||
|
globalStore.fetchStatistics(); |
||||
|
} |
||||
|
</script> |
@ -0,0 +1,54 @@ |
|||||
|
<template> |
||||
|
<main> |
||||
|
<div class="container mx-auto max-w-3xl px-3 md:px-0"> |
||||
|
<div |
||||
|
class="shadow-md rounded-lg bg-white dark:bg-neutral-700 overflow-hidden" |
||||
|
> |
||||
|
<div |
||||
|
class="flex flex-row flex-auto items-center p-3 px-5 border-b-2 border-gray-100 dark:border-neutral-600" |
||||
|
> |
||||
|
<div class="flex-grow"> |
||||
|
<p class="text-2xl font-medium dark:text-neutral-200">Account</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="space-y-2"> |
||||
|
<div class="flex flex-wrap items-center gap-[15px] px-5"> |
||||
|
<Label class="font-semibold dark:text-neutral-200" for="username"> |
||||
|
Username |
||||
|
</Label> |
||||
|
<input id="username" v-model.trim="username" type="text" /> |
||||
|
</div> |
||||
|
<div class="flex flex-wrap items-center gap-[15px] px-5"> |
||||
|
<Label class="font-semibold dark:text-neutral-200" for="name"> |
||||
|
Name |
||||
|
</Label> |
||||
|
<input id="name" v-model.trim="name" type="text" /> |
||||
|
</div> |
||||
|
<div class="flex flex-wrap items-center gap-[15px] px-5"> |
||||
|
<Label class="font-semibold dark:text-neutral-200" for="name"> |
||||
|
E-Mail |
||||
|
</Label> |
||||
|
<input id="name" v-model.trim="email" type="text" /> |
||||
|
</div> |
||||
|
<BaseButton class="self-end" @click="openPasswordModal"> |
||||
|
Change Password |
||||
|
</BaseButton> |
||||
|
<BaseButton class="self-end" @click="submit">Save</BaseButton> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</main> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
const authStore = useAuthStore(); |
||||
|
authStore.update(); |
||||
|
|
||||
|
const username = ref(authStore.userData?.username); |
||||
|
const name = ref(authStore.userData?.name); |
||||
|
const email = ref(authStore.userData?.email); |
||||
|
|
||||
|
function submit() {} |
||||
|
|
||||
|
function openPasswordModal() {} |
||||
|
</script> |
@ -0,0 +1,8 @@ |
|||||
|
export default defineEventHandler(async (event) => { |
||||
|
const { features } = await readValidatedBody( |
||||
|
event, |
||||
|
validateZod(featuresType) |
||||
|
); |
||||
|
await Database.system.updateFeatures(features); |
||||
|
return { success: true }; |
||||
|
}); |
@ -0,0 +1,8 @@ |
|||||
|
export default defineEventHandler(async (event) => { |
||||
|
const { statistics } = await readValidatedBody( |
||||
|
event, |
||||
|
validateZod(statisticsType) |
||||
|
); |
||||
|
await Database.system.updateStatistics(statistics); |
||||
|
return { success: true }; |
||||
|
}); |
@ -1,9 +1,4 @@ |
|||||
export default defineEventHandler(async () => { |
export default defineEventHandler(async () => { |
||||
const system = await Database.system.get(); |
const system = await Database.system.get(); |
||||
return { |
return system.features; |
||||
trafficStats: system.trafficStats, |
|
||||
sortClients: system.sortClients, |
|
||||
clientExpiration: system.clientExpiration, |
|
||||
oneTimeLinks: system.oneTimeLinks, |
|
||||
}; |
|
||||
}); |
}); |
||||
|
@ -1,5 +1,5 @@ |
|||||
export default defineEventHandler(async (event) => { |
export default defineEventHandler(async (event) => { |
||||
setHeader(event, 'Content-Type', 'application/json'); |
setHeader(event, 'Content-Type', 'application/json'); |
||||
const system = await Database.system.get(); |
const system = await Database.system.get(); |
||||
return system.lang; |
return system.general.lang; |
||||
}); |
}); |
||||
|
@ -1,9 +1,24 @@ |
|||||
export default defineEventHandler(async (event) => { |
export default defineEventHandler(async (event) => { |
||||
const session = await useWGSession(event); |
const session = await useWGSession(event); |
||||
const authenticated = session.data.authenticated; |
|
||||
|
if (!session.data.userId) { |
||||
|
throw createError({ |
||||
|
statusCode: 401, |
||||
|
statusMessage: 'Not logged in', |
||||
|
}); |
||||
|
} |
||||
|
const user = await Database.user.findById(session.data.userId); |
||||
|
if (!user) { |
||||
|
throw createError({ |
||||
|
statusCode: 404, |
||||
|
statusMessage: 'Not found in Database', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
return { |
return { |
||||
requiresPassword: true, |
role: user.role, |
||||
authenticated, |
username: user.username, |
||||
|
name: user.name, |
||||
|
email: user.email, |
||||
}; |
}; |
||||
}); |
}); |
||||
|
@ -0,0 +1,4 @@ |
|||||
|
export default defineEventHandler(async () => { |
||||
|
const system = await Database.system.get(); |
||||
|
return system.statistics; |
||||
|
}); |
@ -1,14 +1,35 @@ |
|||||
export default defineEventHandler(async (event) => { |
export default defineEventHandler(async (event) => { |
||||
const url = getRequestURL(event); |
const url = getRequestURL(event); |
||||
const session = await useWGSession(event); |
const session = await useWGSession(event); |
||||
|
|
||||
|
// Api handled by session, Setup handled with setup middleware
|
||||
|
if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/setup')) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
if (url.pathname === '/login') { |
if (url.pathname === '/login') { |
||||
if (session.data.authenticated) { |
if (session.data.userId) { |
||||
return sendRedirect(event, '/', 302); |
return sendRedirect(event, '/', 302); |
||||
} |
} |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Require auth for every page other than Login
|
||||
|
// TODO: investigate /__nuxt_error (error page when unauthenticated)
|
||||
|
if (!session.data.userId) { |
||||
|
return sendRedirect(event, '/login', 302); |
||||
} |
} |
||||
if (url.pathname === '/') { |
|
||||
if (!session.data.authenticated) { |
if (url.pathname.startsWith('/admin')) { |
||||
|
const user = await Database.user.findById(session.data.userId); |
||||
|
if (!user) { |
||||
return sendRedirect(event, '/login', 302); |
return sendRedirect(event, '/login', 302); |
||||
} |
} |
||||
|
if (user.role !== 'ADMIN') { |
||||
|
throw createError({ |
||||
|
statusCode: 403, |
||||
|
statusMessage: 'Not allowed to access Admin Panel', |
||||
|
}); |
||||
|
} |
||||
} |
} |
||||
}); |
}); |
||||
|
@ -1,10 +1,10 @@ |
|||||
import type { H3Event } from 'h3'; |
import type { H3Event } from 'h3'; |
||||
|
|
||||
export type WGSession = { |
export type WGSession = Partial<{ |
||||
authenticated: boolean; |
userId: string; |
||||
}; |
}>; |
||||
|
|
||||
export async function useWGSession(event: H3Event) { |
export async function useWGSession(event: H3Event) { |
||||
const system = await Database.system.get(); |
const system = await Database.system.get(); |
||||
return useSession<Partial<WGSession>>(event, system.sessionConfig); |
return useSession<WGSession>(event, system.sessionConfig); |
||||
} |
} |
||||
|
Loading…
Reference in new issue