mirror of https://github.com/wg-easy/wg-easy
Browse Source
* update: starting split components * upd: rebase & continue splitting components - layouts: header & footer - components: basic buttton - pages: login page * update: login page * package.json: remove dev:pass scriptpull/1244/head
committed by
Bernd Storath
12 changed files with 476 additions and 343 deletions
@ -0,0 +1,45 @@ |
|||
<template> |
|||
<NuxtLayout> |
|||
<NuxtLayout name="header" /> |
|||
<main> |
|||
<NuxtPage /> |
|||
</main> |
|||
<NuxtLayout name="footer" /> |
|||
</NuxtLayout> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import '~/assets/css/app.css'; |
|||
|
|||
useHead({ |
|||
bodyAttrs: { |
|||
class: 'bg-gray-50 dark:bg-neutral-800', |
|||
}, |
|||
link: [ |
|||
{ |
|||
rel: 'manifest', |
|||
href: '/manifest.json', |
|||
}, |
|||
{ |
|||
rel: 'icon', |
|||
type: 'image/png', |
|||
href: '/favicon.png', |
|||
}, |
|||
{ |
|||
rel: 'apple-touch-icon', |
|||
href: '/apple-touch-icon.png', |
|||
}, |
|||
], |
|||
meta: [ |
|||
{ |
|||
name: 'apple-mobile-web-app-capable', |
|||
content: 'yes', |
|||
}, |
|||
{ |
|||
name: 'apple-mobile-web-app-status-bar-style', |
|||
content: 'black-translucent', |
|||
}, |
|||
], |
|||
title: 'WireGuard', |
|||
}); |
|||
</script> |
@ -0,0 +1,34 @@ |
|||
<template> |
|||
<component :is="elementType" role="button" :class="btnClasses" v-bind="attrs"> |
|||
<slot /> |
|||
</component> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const props = defineProps({ |
|||
as: { |
|||
type: String, |
|||
default: 'button', |
|||
}, |
|||
class: { |
|||
type: String, |
|||
default: '', |
|||
}, |
|||
}); |
|||
|
|||
const elementType = computed(() => props.as); |
|||
|
|||
const attrs = computed(() => { |
|||
const { as, class: _, ...attrs } = props; |
|||
return attrs; |
|||
}); |
|||
|
|||
const btnClasses = |
|||
props.class || |
|||
` |
|||
py-2 px-4 transition-colors |
|||
hover:bg-primary hover:border-primary |
|||
hover:text-white text-gray-700 dark:text-neutral-200 |
|||
max-md:border-l-0 border-2 border-gray-100 dark:border-neutral-600 |
|||
`.replaceAll('\n', ' '); |
|||
</script> |
@ -0,0 +1,42 @@ |
|||
<template> |
|||
<footer> |
|||
<p |
|||
v-cloak |
|||
class="text-center m-10 text-gray-300 dark:text-neutral-600 text-xs" |
|||
> |
|||
<a |
|||
class="hover:underline" |
|||
target="_blank" |
|||
href="https://github.com/wg-easy/wg-easy" |
|||
>WireGuard Easy</a |
|||
> |
|||
© 2021-2024 by |
|||
<a |
|||
class="hover:underline" |
|||
target="_blank" |
|||
href="https://emilenijssen.nl/?ref=wg-easy" |
|||
>Emile Nijssen</a |
|||
> |
|||
is licensed under |
|||
<a |
|||
class="hover:underline" |
|||
target="_blank" |
|||
href="http://creativecommons.org/licenses/by-nc-sa/4.0/" |
|||
>CC BY-NC-SA 4.0</a |
|||
> |
|||
· |
|||
<a |
|||
class="hover:underline" |
|||
href="https://github.com/sponsors/WeeJeWel" |
|||
target="_blank" |
|||
>{{ $t('donate') }}</a |
|||
> |
|||
</p> |
|||
</footer> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
definePageMeta({ |
|||
layout: 'footer', |
|||
}); |
|||
</script> |
@ -0,0 +1,218 @@ |
|||
<template> |
|||
<header class="container mx-auto max-w-3xl"> |
|||
<div |
|||
class="flex flex-col-reverse xxs:flex-row flex-auto items-center gap-3" |
|||
> |
|||
<h1 |
|||
class="text-4xl dark:text-neutral-200 font-medium flex-grow self-start mb-4" |
|||
> |
|||
<img |
|||
src="/logo.png" |
|||
width="32" |
|||
class="inline align-middle dark:bg mr-2" |
|||
/><span class="align-middle">WireGuard</span> |
|||
</h1> |
|||
<div class="flex items-center grow-0 gap-3 self-end xxs:self-center"> |
|||
<!-- Dark / light theme --> |
|||
<button |
|||
class="flex items-center justify-center w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600 transition" |
|||
:title="$t(`theme.${theme.preference}`)" |
|||
@click="toggleTheme" |
|||
> |
|||
<svg |
|||
v-if="theme.preference === 'light'" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
fill="none" |
|||
viewBox="0 0 24 24" |
|||
stroke-width="1.5" |
|||
stroke="currentColor" |
|||
class="w-5 h-5" |
|||
> |
|||
<path |
|||
stroke-linecap="round" |
|||
stroke-linejoin="round" |
|||
d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" |
|||
/> |
|||
</svg> |
|||
<svg |
|||
v-else-if="theme.preference === 'dark'" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
fill="none" |
|||
viewBox="0 0 24 24" |
|||
stroke-width="1.5" |
|||
stroke="currentColor" |
|||
class="w-5 h-5 text-neutral-400" |
|||
> |
|||
<path |
|||
stroke-linecap="round" |
|||
stroke-linejoin="round" |
|||
d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" |
|||
/> |
|||
</svg> |
|||
<svg |
|||
v-else |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
fill="currentColor" |
|||
viewBox="0 0 24 24" |
|||
class="w-5 h-5 fill-gray-600 dark:fill-neutral-400" |
|||
> |
|||
<path |
|||
d="M12,2.2c-5.4,0-9.8,4.4-9.8,9.8s4.4,9.8,9.8,9.8s9.8-4.4,9.8-9.8S17.4,2.2,12,2.2z M3.8,12c0-4.5,3.7-8.2,8.2-8.2v16.5C7.5,20.2,3.8,16.5,3.8,12z" |
|||
/> |
|||
</svg> |
|||
<path |
|||
stroke-linecap="round" |
|||
stroke-linejoin="round" |
|||
d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25" |
|||
/> |
|||
</button> |
|||
<!-- Show / hide charts --> |
|||
<label |
|||
v-if="uiChartType > 0" |
|||
class="inline-flex items-center justify-center cursor-pointer w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600 whitespace-nowrap transition group" |
|||
:title="$t('toggleCharts')" |
|||
> |
|||
<input |
|||
v-model="uiShowCharts" |
|||
type="checkbox" |
|||
value="" |
|||
class="sr-only peer" |
|||
@change="toggleCharts" |
|||
/> |
|||
<svg |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
viewBox="0 0 24 24" |
|||
stroke-width="1.5" |
|||
fill="currentColor" |
|||
class="w-5 h-5 peer fill-gray-400 peer-checked:fill-gray-600 dark:fill-neutral-600 peer-checked:dark:fill-neutral-400 group-hover:dark:fill-neutral-500 transition" |
|||
> |
|||
<path |
|||
d="M18.375 2.25c-1.035 0-1.875.84-1.875 1.875v15.75c0 1.035.84 1.875 1.875 1.875h.75c1.035 0 1.875-.84 1.875-1.875V4.125c0-1.036-.84-1.875-1.875-1.875h-.75ZM9.75 8.625c0-1.036.84-1.875 1.875-1.875h.75c1.036 0 1.875.84 1.875 1.875v11.25c0 1.035-.84 1.875-1.875 1.875h-.75a1.875 1.875 0 0 1-1.875-1.875V8.625ZM3 13.125c0-1.036.84-1.875 1.875-1.875h.75c1.036 0 1.875.84 1.875 1.875v6.75c0 1.035-.84 1.875-1.875 1.875h-.75A1.875 1.875 0 0 1 3 19.875v-6.75Z" |
|||
/> |
|||
</svg> |
|||
</label> |
|||
<span |
|||
v-if="requiresPassword && authenticated" |
|||
class="text-sm text-gray-400 dark:text-neutral-400 cursor-pointer hover:underline" |
|||
@click="logout" |
|||
> |
|||
{{ $t('logout') }} |
|||
<svg |
|||
class="h-3 inline" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
fill="none" |
|||
viewBox="0 0 24 24" |
|||
stroke="currentColor" |
|||
> |
|||
<path |
|||
stroke-linecap="round" |
|||
stroke-linejoin="round" |
|||
stroke-width="2" |
|||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" |
|||
/> |
|||
</svg> |
|||
</span> |
|||
</div> |
|||
</div> |
|||
<div class="text-sm text-gray-400 dark:text-neutral-400 mb-5" /> |
|||
<div |
|||
v-if="latestRelease" |
|||
class="bg-red-800 dark:bg-red-100 p-4 text-white dark:text-red-600 text-sm font-small mb-10 rounded-md shadow-lg" |
|||
:title="`v${currentRelease} → v${latestRelease.version}`" |
|||
> |
|||
<div class="container mx-auto flex flex-row flex-auto items-center"> |
|||
<div class="flex-grow"> |
|||
<p class="font-bold">{{ $t('updateAvailable') }}</p> |
|||
<p>{{ latestRelease.changelog }}</p> |
|||
</div> |
|||
|
|||
<a |
|||
href="https://github.com/wg-easy/wg-easy#updating" |
|||
target="_blank" |
|||
class="p-3 rounded-md bg-white dark:bg-red-100 float-right font-sm font-semibold text-red-800 dark:text-red-600 flex-shrink-0 border-2 border-red-800 dark:border-red-600 hover:border-white dark:hover:border-red-600 hover:text-white dark:hover:text-red-100 hover:bg-red-800 dark:hover:bg-red-600 transition-all" |
|||
> |
|||
{{ $t('update') }} → |
|||
</a> |
|||
</div> |
|||
</div> |
|||
</header> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
definePageMeta({ |
|||
layout: 'header', |
|||
}); |
|||
|
|||
type ClientPersist = { |
|||
transferRxHistory: number[]; |
|||
transferRxPrevious: number; |
|||
transferRxCurrent: number; |
|||
transferRxSeries: { name: string; data: number[] }[]; |
|||
hoverRx?: unknown; |
|||
transferTxHistory: number[]; |
|||
transferTxPrevious: number; |
|||
transferTxCurrent: number; |
|||
transferTxSeries: { name: string; data: number[] }[]; |
|||
hoverTx?: unknown; |
|||
}; |
|||
|
|||
type LocalClient = WGClient & { |
|||
avatar?: string; |
|||
transferMax?: number; |
|||
} & Omit<ClientPersist, 'transferRxPrevious' | 'transferTxPrevious'>; |
|||
|
|||
const authenticated = ref<null | boolean>(null); |
|||
const requiresPassword = ref<null | boolean>(null); |
|||
|
|||
const clients = ref<null | LocalClient[]>(null); |
|||
|
|||
const currentRelease = ref<null | number>(null); |
|||
const latestRelease = ref<null | { version: number; changelog: string }>(null); |
|||
|
|||
const theme = useTheme(); |
|||
|
|||
const uiChartType = ref(0); |
|||
const uiShowCharts = ref(getItem('uiShowCharts') === '1'); |
|||
|
|||
function toggleTheme() { |
|||
const themeCycle = { |
|||
system: 'light', |
|||
light: 'dark', |
|||
dark: 'system', |
|||
} as const; |
|||
|
|||
theme.preference = themeCycle[theme.preference]; |
|||
} |
|||
|
|||
function toggleCharts() { |
|||
setItem('uiShowCharts', uiShowCharts.value ? '1' : '0'); |
|||
} |
|||
|
|||
function logout(e: Event) { |
|||
e.preventDefault(); |
|||
|
|||
api |
|||
.deleteSession() |
|||
.then(() => { |
|||
authenticated.value = false; |
|||
clients.value = null; |
|||
|
|||
window.location.replace('/login'); |
|||
}) |
|||
.catch((err) => { |
|||
alert(err.message || err.toString()); |
|||
}); |
|||
} |
|||
|
|||
onMounted(() => { |
|||
api |
|||
.getSession() |
|||
.then((session) => { |
|||
authenticated.value = session.authenticated; |
|||
requiresPassword.value = session.requiresPassword; |
|||
}) |
|||
.catch((err) => { |
|||
alert(err.message || err.toString()); |
|||
}); |
|||
}); |
|||
</script> |
@ -0,0 +1,122 @@ |
|||
<template> |
|||
<section> |
|||
<h1 |
|||
class="text-4xl font-medium my-16 text-gray-700 dark:text-neutral-200 text-center" |
|||
> |
|||
<img src="/logo.png" width="32" class="inline align-middle dark:bg" /> |
|||
<span class="align-middle">WireGuard</span> |
|||
</h1> |
|||
|
|||
<form |
|||
class="shadow rounded-md bg-white dark:bg-neutral-700 mx-auto w-64 p-5 overflow-hidden mt-10" |
|||
@submit="login" |
|||
> |
|||
<!-- Avatar --> |
|||
<div |
|||
class="h-20 w-20 mb-10 mt-5 mx-auto rounded-full bg-red-800 dark:bg-red-800 relative overflow-hidden" |
|||
> |
|||
<svg |
|||
class="w-10 h-10 m-5 text-white dark:text-white" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
viewBox="0 0 20 20" |
|||
fill="currentColor" |
|||
> |
|||
<path |
|||
fill-rule="evenodd" |
|||
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" |
|||
clip-rule="evenodd" |
|||
/> |
|||
</svg> |
|||
</div> |
|||
|
|||
<input |
|||
v-model="password" |
|||
type="password" |
|||
name="password" |
|||
:placeholder="$t('password')" |
|||
autocomplete="current-password" |
|||
class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 outline-none" |
|||
/> |
|||
|
|||
<button |
|||
v-if="authenticating" |
|||
class="bg-red-800 dark:bg-red-800 w-full rounded shadow py-2 text-sm text-white dark:text-white cursor-not-allowed" |
|||
> |
|||
<svg |
|||
class="w-5 animate-spin mx-auto" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
viewBox="0 0 24 24" |
|||
fill="currentColor" |
|||
> |
|||
<circle |
|||
class="opacity-25" |
|||
cx="12" |
|||
cy="12" |
|||
r="10" |
|||
stroke="currentColor" |
|||
stroke-width="4" |
|||
/> |
|||
<path |
|||
class="opacity-75" |
|||
fill="currentColor" |
|||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" |
|||
/> |
|||
</svg> |
|||
</button> |
|||
<input |
|||
v-if="!authenticating && password" |
|||
type="submit" |
|||
class="bg-red-800 dark:bg-red-800 w-full rounded shadow py-2 text-sm text-white dark:text-white hover:bg-red-700 dark:hover:bg-red-700 transition cursor-pointer" |
|||
:value="$t('signIn')" |
|||
/> |
|||
<input |
|||
v-if="!authenticating && !password" |
|||
type="submit" |
|||
class="bg-gray-200 dark:bg-neutral-800 w-full rounded shadow py-2 text-sm text-white dark:text-white cursor-not-allowed" |
|||
:value="$t('signIn')" |
|||
/> |
|||
</form> |
|||
</section> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const authenticated = ref<null | boolean>(null); |
|||
const authenticating = ref(false); |
|||
const password = ref<null | string>(null); |
|||
const requiresPassword = ref<null | boolean>(null); |
|||
|
|||
function login(e: Event) { |
|||
e.preventDefault(); |
|||
|
|||
if (!password.value) return; |
|||
if (authenticating.value) return; |
|||
|
|||
authenticating.value = true; |
|||
api |
|||
.createSession({ |
|||
password: password.value, |
|||
}) |
|||
.then(async () => { |
|||
const session = await api.getSession(); |
|||
authenticated.value = session.authenticated; |
|||
requiresPassword.value = session.requiresPassword; |
|||
window.location.replace('/'); |
|||
}) |
|||
.catch((err) => { |
|||
// TODO: replace alert with actual ui error message |
|||
alert(err.message || err.toString()); |
|||
}) |
|||
.finally(() => { |
|||
authenticating.value = false; |
|||
password.value = null; |
|||
}); |
|||
} |
|||
|
|||
onMounted(() => { |
|||
api.getSession().then((session) => { |
|||
if (session.authenticated || !session.requiresPassword) { |
|||
window.location.replace('/'); |
|||
} |
|||
}); |
|||
}); |
|||
</script> |
Loading…
Reference in new issue