You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1022 lines
33 KiB

<script>
import { ref, reactive } from 'vue';
import sha256 from 'crypto-js/sha256';
import { useDateTime } from './composables/useDateTime';
import { useTimeAgo } from './composables/useTimeAgo';
import UpdateNotification from './components/UpdateNotification.vue';
import ClientNewButton from './components/ClientNewButton.vue';
import ClientName from './components/ClientName.vue';
import ClientAvatar from './components/ClientAvatar.vue';
import ClientAddress from './components/ClientAddress.vue';
import IconUpArrow from './components/icons/IconUpArrow.vue';
import IconDownArrow from './components/icons/IconDownArrow.vue';
import IconLogout from './components/icons/IconLogout.vue';
import IconQRCode from './components/icons/IconQRCode.vue';
import IconDownload from './components/icons/IconDownload.vue';
import IconDelete from './components/icons/IconDelete.vue';
import IconNew from './components/icons/IconNew.vue';
import LoadingSpinner from './components/LoadingSpinner.vue';
import IconClose from './components/icons/IconClose.vue';
import IconWarning from './components/icons/IconWarning.vue';
import IconAvatarDefault from './components/icons/IconAvatarDefault.vue';
import ClientTransfer from './components/ClientTransfer.vue';
class API {
async call({ method, path, body }) {
const res = await fetch(`./api${path}`, {
method,
headers: {
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
});
if (res.status === 204) {
return undefined;
}
const json = await res.json();
if (!res.ok) {
throw new Error(json.error || res.statusText);
}
return json;
}
async getRelease() {
return this.call({
method: 'get',
path: '/release',
});
}
async getSession() {
return this.call({
method: 'get',
path: '/session',
});
}
async createSession({ password }) {
return this.call({
method: 'post',
path: '/session',
body: { password },
});
}
async deleteSession() {
return this.call({
method: 'delete',
path: '/session',
});
}
async getClients() {
return this.call({
method: 'get',
path: '/wireguard/client',
}).then((clients) =>
clients.map((client) => ({
...client,
createdAt: new Date(client.createdAt),
updatedAt: new Date(client.updatedAt),
latestHandshakeAt:
client.latestHandshakeAt !== null
? new Date(client.latestHandshakeAt)
: null,
}))
);
}
async createClient({ name }) {
return this.call({
method: 'post',
path: '/wireguard/client',
body: { name },
});
}
async deleteClient({ clientId }) {
return this.call({
method: 'delete',
path: `/wireguard/client/${clientId}`,
});
}
async enableClient({ clientId }) {
return this.call({
method: 'post',
path: `/wireguard/client/${clientId}/enable`,
});
}
async disableClient({ clientId }) {
return this.call({
method: 'post',
path: `/wireguard/client/${clientId}/disable`,
});
}
async updateClientName({ clientId, name }) {
return this.call({
method: 'put',
path: `/wireguard/client/${clientId}/name/`,
body: { name },
});
}
async updateClientAddress({ clientId, address }) {
return this.call({
method: 'put',
path: `/wireguard/client/${clientId}/address/`,
body: { address },
});
}
}
export default {
setup() {
const authenticated = ref(null);
const authenticating = ref(false);
const password = ref(null);
const requiresPassword = ref(null);
const clients = ref(null);
const clientsPersist = reactive({});
const clientDelete = ref(null);
const clientCreate = ref(null);
const clientCreateName = ref('');
const clientEditName = ref(null);
const clientEditNameId = ref(null);
const clientEditAddress = ref(null);
const clientEditAddressId = ref(null);
const qrcode = ref(null);
const currentRelease = ref(null);
const latestRelease = ref(null);
const isDark = ref(null);
const chartOptions = reactive({
chart: {
background: 'transparent',
type: 'bar',
stacked: false,
toolbar: {
show: false,
},
animations: {
enabled: false,
},
},
colors: [
'#DDDDDD', // rx
'#EEEEEE', // tx
],
dataLabels: {
enabled: false,
},
plotOptions: {
bar: {
horizontal: false,
},
},
xaxis: {
labels: {
show: false,
},
axisTicks: {
show: true,
},
axisBorder: {
show: true,
},
},
yaxis: {
labels: {
show: false,
},
min: 0,
},
tooltip: {
enabled: false,
},
legend: {
show: false,
},
grid: {
show: false,
padding: {
left: -10,
right: 0,
bottom: -15,
top: -15,
},
column: {
opacity: 0,
},
xaxis: {
lines: {
show: false,
},
},
},
});
const api = new API();
const { dateTime } = useDateTime();
const { timeago } = useTimeAgo();
return {
authenticated,
authenticating,
password,
requiresPassword,
clients,
clientsPersist,
clientDelete,
clientCreate,
clientCreateName,
clientEditName,
clientEditNameId,
clientEditAddress,
clientEditAddressId,
qrcode,
currentRelease,
latestRelease,
isDark,
chartOptions,
dateTime,
timeago,
api,
};
},
mounted() {
this.isDark = false;
if (localStorage.theme === 'dark') {
this.isDark = true;
}
this.api
.getSession()
.then((session) => {
console.log(session);
this.authenticated = session.authenticated;
this.requiresPassword = session.requiresPassword;
this.refresh({
updateCharts: true,
}).catch((err) => {
alert(err.message || err.toString());
});
})
.catch((err) => {
alert(err.message || err.toString());
});
setInterval(() => {
this.refresh({
updateCharts: true,
}).catch(console.error);
}, 1000);
Promise.resolve()
.then(async () => {
const currentReleaseData = await this.api.getRelease();
const latestReleaseData = await fetch(
'https://wg-easy.github.io/wg-easy/changelog.json'
)
.then((res) => res.json())
.then((releases) => {
const releasesArray = Object.entries(releases).map(
([version, changelog]) => ({
version: parseInt(version, 10),
changelog,
})
);
releasesArray.sort((a, b) => {
return b.version - a.version;
});
return releasesArray[0];
});
console.log(`Current Release: ${currentReleaseData}`);
console.log(`Latest Release: ${latestReleaseData.version}`);
if (currentReleaseData >= latestReleaseData.version) return;
currentRelease.value = currentReleaseData;
latestRelease.value = latestReleaseData;
})
.catch(console.error);
},
components: {
UpdateNotification,
ClientNewButton,
ClientName,
ClientAvatar,
ClientAddress,
IconUpArrow,
IconDownArrow,
IconLogout,
IconQRCode,
IconDownload,
IconDelete,
IconNew,
LoadingSpinner,
IconClose,
IconWarning,
IconAvatarDefault,
ClientTransfer,
},
methods: {
handleNewClient() {
this.clientCreate = true;
this.clientCreateName = '';
},
async refresh({ updateCharts = false } = {}) {
if (!this.authenticated) return;
const clientsData = await this.api.getClients();
this.clients = clientsData.map((client) => {
if (client.name.includes('@') && client.name.includes('.')) {
client.avatar = `https://www.gravatar.com/avatar/${sha256(
client.name
)}?d=blank`;
}
if (!this.clientsPersist[client.id]) {
this.clientsPersist[client.id] = {};
this.clientsPersist[client.id].transferRxHistory = Array(50).fill(0);
this.clientsPersist[client.id].transferRxPrevious = client.transferRx;
this.clientsPersist[client.id].transferTxHistory = Array(50).fill(0);
this.clientsPersist[client.id].transferTxPrevious = client.transferTx;
}
// Debug
// client.transferRx = this.clientsPersist[client.id].transferRxPrevious + Math.random() * 1000;
// client.transferTx = this.clientsPersist[client.id].transferTxPrevious + Math.random() * 1000;
if (updateCharts) {
this.clientsPersist[client.id].transferRxCurrent =
client.transferRx -
this.clientsPersist[client.id].transferRxPrevious;
this.clientsPersist[client.id].transferRxPrevious = client.transferRx;
this.clientsPersist[client.id].transferTxCurrent =
client.transferTx -
this.clientsPersist[client.id].transferTxPrevious;
this.clientsPersist[client.id].transferTxPrevious = client.transferTx;
this.clientsPersist[client.id].transferRxHistory.push(
this.clientsPersist[client.id].transferRxCurrent
);
this.clientsPersist[client.id].transferRxHistory.shift();
this.clientsPersist[client.id].transferTxHistory.push(
this.clientsPersist[client.id].transferTxCurrent
);
this.clientsPersist[client.id].transferTxHistory.shift();
}
client.transferTxCurrent =
this.clientsPersist[client.id].transferTxCurrent;
client.transferRxCurrent =
this.clientsPersist[client.id].transferRxCurrent;
client.transferTxHistory =
this.clientsPersist[client.id].transferTxHistory;
client.transferRxHistory =
this.clientsPersist[client.id].transferRxHistory;
client.transferMax = Math.max(
...client.transferTxHistory,
...client.transferRxHistory
);
client.hoverTx = this.clientsPersist[client.id].hoverTx;
client.hoverRx = this.clientsPersist[client.id].hoverRx;
return client;
});
},
login(e) {
e.preventDefault();
if (!this.password) return;
if (this.authenticating) return;
this.authenticating = true;
this.api
.createSession({
password: this.password,
})
.then(async () => {
const session = await this.api.getSession();
this.authenticated = session.authenticated;
this.requiresPassword = session.requiresPassword;
return this.refresh();
})
.catch((err) => {
alert(err.message || err.toString());
})
.finally(() => {
this.authenticating = false;
this.password = null;
});
},
logout(e) {
e.preventDefault();
this.api
.deleteSession()
.then(() => {
this.authenticated = false;
this.clients = null;
})
.catch((err) => {
alert(err.message || err.toString());
});
},
createClient() {
const name = this.clientCreateName;
if (!name) return;
this.api
.createClient({ name })
.catch((err) => alert(err.message || err.toString()))
.finally(() => this.refresh().catch(console.error));
},
deleteClient(client) {
this.api
.deleteClient({ clientId: client.id })
.catch((err) => alert(err.message || err.toString()))
.finally(() => this.refresh().catch(console.error));
},
enableClient(client) {
this.api
.enableClient({ clientId: client.id })
.catch((err) => alert(err.message || err.toString()))
.finally(() => this.refresh().catch(console.error));
},
disableClient(client) {
this.api
.disableClient({ clientId: client.id })
.catch((err) => alert(err.message || err.toString()))
.finally(() => this.refresh().catch(console.error));
},
updateClientName(client, name) {
this.api
.updateClientName({ clientId: client.id, name })
.catch((err) => alert(err.message || err.toString()))
.finally(() => this.refresh().catch(console.error));
},
updateClientAddress(client, address) {
this.api
.updateClientAddress({ clientId: client.id, address })
.catch((err) => alert(err.message || err.toString()))
.finally(() => this.refresh().catch(console.error));
},
toggleTheme() {
if (this.isDark) {
localStorage.theme = 'light';
document.documentElement.classList.remove('dark');
} else {
localStorage.theme = 'dark';
document.documentElement.classList.add('dark');
}
this.isDark = !this.isDark;
},
},
};
</script>
<template>
<main>
<div v-cloak class="container mx-auto max-w-3xl px-5 md:px-0">
<div v-if="authenticated === true">
<span
v-if="requiresPassword"
class="text-sm text-gray-400 dark:text-neutral-400 mb-10 mr-2 mt-3 cursor-pointer hover:underline float-right"
@click="logout"
>
Logout
<IconLogout />
</span>
<h1 class="text-4xl dark:text-neutral-200 font-medium mt-2 mb-2">
<img
src="./assets/img/logo.png"
width="32"
class="inline align-middle dark:bg"
/>
<span class="align-middle">WireGuard</span>
</h1>
<h2 class="text-sm text-gray-400 dark:text-neutral-400 mb-10"></h2>
<UpdateNotification
:latestRelease="latestRelease"
:currentRelease="currentRelease"
/>
<div
class="shadow-md rounded-lg bg-white dark:bg-neutral-700 overflow-hidden"
>
<ClientNewButton @create-new-client="handleNewClient" />
<div>
<!-- Client -->
<div
v-if="clients && clients.length > 0"
v-for="client in clients"
:key="client.id"
class="relative overflow-hidden border-b last:border-b-0 border-gray-100 dark:border-neutral-600 border-solid"
>
<!-- Chart -->
<!-- FIXME: Not working yet -->
<!-- <div class="absolute z-0 bottom-0 left-0 right-0" style="top: 60%">
<apexchart
width="100%"
height="100%"
:options="client.chartOptions"
:series="client.transferTxSeries"
>
</apexchart>
</div>
<div class="absolute z-0 top-0 left-0 right-0" style="bottom: 60%">
<apexchart
width="100%"
height="100%"
:options="client.chartOptions"
:series="client.transferRxSeries"
style="transform: scaleY(-1)"
>
</apexchart>
</div> -->
<div
class="relative p-5 z-10 flex flex-col md:flex-row justify-between"
>
<div class="flex items-center pb-2 md:pb-0">
<ClientAvatar :client="client" />
<div class="flex-grow">
<!-- Name -->
<ClientName
:client="client"
:clientEditName="clientEditName"
:clientEditNameId="clientEditNameId"
@update-client-name="updateClientName"
/>
<!-- Info -->
<div class="text-gray-400 dark:text-neutral-400 text-xs">
<!-- Address -->
<ClientAddress
:client="client"
@update-address="updateClientAddress"
/>
<!-- Transfer TX -->
<!-- FIXME: add "bytes" -->
<ClientTransfer
:transferData="client.transferTx"
:transferDataCurrent="client.transferTxCurrent"
title="'Total Download: ' + client.transferTx"
>
<IconDownArrow />
</ClientTransfer>
<!-- Transfer RX -->
<!-- FIXME: add "bytes" -->
<ClientTransfer
:transferData="client.transferRx"
:transferDataCurrent="client.transferRxCurrent"
title="'Total Upload: ' + client.transferRx"
>
<IconUpArrow />
</ClientTransfer>
<!-- Last seen -->
<span
v-if="client.latestHandshakeAt"
:title="
'Last seen on ' +
dateTime(new Date(client.latestHandshakeAt))
"
>
<!-- FIXME: add "timeago" -->
<!-- · {{ new Date(client.latestHandshakeAt) | timeago }} -->
· {{ timeago(client.latestHandshakeAt) }}
</span>
</div>
</div>
</div>
<div class="flex items-center justify-end">
<div
class="text-gray-400 dark:text-neutral-400 flex gap-1 items-center justify-between"
>
<!-- Enable/Disable -->
<div
@click="disableClient(client)"
v-if="client.enabled === true"
title="Disable Client"
class="inline-block align-middle rounded-full w-10 h-6 mr-1 bg-red-800 cursor-pointer hover:bg-red-700 transition-all"
>
<div class="rounded-full w-4 h-4 m-1 ml-5 bg-white"></div>
</div>
<div
@click="enableClient(client)"
v-if="client.enabled === false"
title="Enable Client"
class="inline-block align-middle rounded-full w-10 h-6 mr-1 bg-gray-200 dark:bg-neutral-400 cursor-pointer hover:bg-gray-300 dark:hover:bg-neutral-500 transition-all"
>
<div class="rounded-full w-4 h-4 m-1 bg-white"></div>
</div>
<!-- Show QR-->
<button
class="align-middle bg-gray-100 dark:bg-neutral-600 dark:text-neutral-300 hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white p-2 rounded transition"
title="Show QR Code"
@click="
qrcode = `./api/wireguard/client/${client.id}/qrcode.svg`
"
>
<IconQRCode />
</button>
<!-- Download Config -->
<a
:href="
'./api/wireguard/client/' + client.id + '/configuration'
"
download
class="align-middle inline-block bg-gray-100 dark:bg-neutral-600 dark:text-neutral-300 hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white p-2 rounded transition"
title="Download Configuration"
>
<IconDownload />
</a>
<!-- Delete -->
<button
class="align-middle bg-gray-100 dark:bg-neutral-600 dark:text-neutral-300 hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white p-2 rounded transition"
title="Delete Client"
@click="clientDelete = client"
>
<IconDelete />
</button>
</div>
</div>
</div>
</div>
<div v-if="clients && clients.length === 0">
<p
class="text-center m-10 text-gray-400 dark:text-neutral-400 text-sm"
>
There are no clients yet.<br /><br />
<button
@click="
clientCreate = true;
clientCreateName = '';
"
class="bg-red-800 hover:bg-red-700 text-white border-2 border-none py-2 px-4 rounded inline-flex items-center transition"
>
<IconNew class="w-4 mr-2" />
<span class="text-sm">New Client</span>
</button>
</p>
</div>
<div
v-if="clients === null"
class="text-gray-200 dark:text-red-300 p-5"
>
<LoadingSpinner />
</div>
</div>
</div>
<!-- QR Code-->
<div v-if="qrcode">
<div
class="bg-black bg-opacity-50 fixed top-0 right-0 left-0 bottom-0 flex items-center justify-center z-20"
>
<div class="bg-white rounded-md shadow-lg relative p-8">
<button
@click="qrcode = null"
class="absolute right-4 top-4 text-gray-600 dark:text-neutral-500 hover:text-gray-800 dark:hover:text-neutral-700"
>
<IconClose />
</button>
<img :src="qrcode" />
</div>
</div>
</div>
<!-- Create Dialog -->
<div v-if="clientCreate" class="fixed z-10 inset-0 overflow-y-auto">
<div
class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"
>
<!--
Background overlay, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0"
To: "opacity-100"
Leaving: "ease-in duration-200"
From: "opacity-100"
To: "opacity-0"
-->
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div
class="absolute inset-0 bg-gray-500 dark:bg-black opacity-75 dark:opacity-50"
></div>
</div>
<!-- This element is to trick the browser into centering the modal contents. -->
<span
class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true"
>&#8203;</span
>
<!--
Modal panel, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0 tranneutral-y-4 sm:tranneutral-y-0 sm:scale-95"
To: "opacity-100 tranneutral-y-0 sm:scale-100"
Leaving: "ease-in duration-200"
From: "opacity-100 tranneutral-y-0 sm:scale-100"
To: "opacity-0 tranneutral-y-4 sm:tranneutral-y-0 sm:scale-95"
-->
<div
class="inline-block align-bottom bg-white dark:bg-neutral-700 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg w-full"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
>
<div
class="bg-white dark:bg-neutral-700 px-4 pt-5 pb-4 sm:p-6 sm:pb-4"
>
<div class="sm:flex sm:items-start">
<div
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-800 sm:mx-0 sm:h-10 sm:w-10"
>
<IconNew class="h-6 w-6 text-white" />
</div>
<div
class="flex-grow mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"
>
<h3
class="text-lg leading-6 font-medium text-gray-900 dark:text-neutral-200"
id="modal-headline"
>
New Client
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500">
<input
class="rounded p-2 border-2 dark:bg-neutral-700 dark:text-neutral-200 border-gray-100 dark:border-neutral-600 focus:border-gray-200 focus:dark:border-neutral-500 dark:placeholder:text-neutral-400 outline-none w-full"
type="text"
v-model.trim="clientCreateName"
placeholder="Name"
/>
</p>
</div>
</div>
</div>
</div>
<div
class="bg-gray-50 dark:bg-neutral-700 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"
>
<button
v-if="clientCreateName.length"
type="button"
@click="
createClient();
clientCreate = null;
"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-800 text-base font-medium text-white hover:bg-red-700 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm"
>
Create
</button>
<button
v-else
type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-gray-200 dark:bg-neutral-400 text-base font-medium text-white dark:text-neutral-300 sm:ml-3 sm:w-auto sm:text-sm cursor-not-allowed"
>
Create
</button>
<button
type="button"
@click="clientCreate = null"
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-neutral-500 shadow-sm px-4 py-2 bg-white dark:bg-neutral-500 text-base font-medium text-gray-700 dark:text-neutral-50 hover:bg-gray-50 dark:hover:bg-neutral-600 dark:hover:border-neutral-600 focus:outline-none sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
>
Cancel
</button>
</div>
</div>
</div>
</div>
<!-- Delete Dialog -->
<div v-if="clientDelete" class="fixed z-10 inset-0 overflow-y-auto">
<div
class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"
>
<!--
Background overlay, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0"
To: "opacity-100"
Leaving: "ease-in duration-200"
From: "opacity-100"
To: "opacity-0"
-->
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div
class="absolute inset-0 bg-gray-500 dark:bg-black opacity-75 dark:opacity-50"
></div>
</div>
<!-- This element is to trick the browser into centering the modal contents. -->
<!-- TODO: Check if still relevant? -->
<span
class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true"
>&#8203;</span
>
<!--
Modal panel, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0 tranneutral-y-4 sm:tranneutral-y-0 sm:scale-95"
To: "opacity-100 tranneutral-y-0 sm:scale-100"
Leaving: "ease-in duration-200"
From: "opacity-100 tranneutral-y-0 sm:scale-100"
To: "opacity-0 tranneutral-y-4 sm:tranneutral-y-0 sm:scale-95"
-->
<div
class="inline-block align-bottom bg-white dark:bg-neutral-700 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg w-full"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
>
<div
class="bg-white dark:bg-neutral-700 px-4 pt-5 pb-4 sm:p-6 sm:pb-4"
>
<div class="sm:flex sm:items-start">
<div
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10"
>
<!-- Heroicon name: outline/exclamation -->
<IconWarning />
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3
class="text-lg leading-6 font-medium text-gray-900 dark:text-neutral-200"
id="modal-headline"
>
Delete Client
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-neutral-300">
Are you sure you want to delete
<strong>{{ clientDelete.name }}</strong
>? This action cannot be undone.
</p>
</div>
</div>
</div>
</div>
<div
class="bg-gray-50 dark:bg-neutral-600 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"
>
<button
type="button"
@click="
deleteClient(clientDelete);
clientDelete = null;
"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 dark:bg-red-600 text-base font-medium text-white dark:text-white hover:bg-red-700 dark:hover:bg-red-700 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm"
>
Delete
</button>
<button
type="button"
@click="clientDelete = null"
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-neutral-500 shadow-sm px-4 py-2 bg-white dark:bg-neutral-500 text-base font-medium text-gray-700 dark:text-neutral-50 hover:bg-gray-50 dark:hover:bg-neutral-600 dark:hover:border-neutral-600 focus:outline-none sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
>
Cancel
</button>
</div>
</div>
</div>
</div>
</div>
<div v-if="authenticated === false">
<h1
class="text-4xl font-medium my-16 text-gray-700 dark:text-neutral-200 text-center"
>
WireGuard
</h1>
<form
@submit="login"
class="shadow rounded-md bg-white dark:bg-neutral-700 mx-auto w-64 p-5 overflow-hidden mt-10"
>
<!-- 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"
>
<IconAvatarDefault
class="w-10 h-10 m-5 text-white dark:text-white"
/>
</div>
<input
type="password"
name="password"
placeholder="Password"
v-model="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"
>
<LoadingSpinner />
</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="Sign In"
/>
<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="Sign In"
/>
</form>
</div>
<div
v-if="authenticated === null"
class="text-gray-300 dark:text-red-300 pt-24 pb-12"
>
<LoadingSpinner />
</div>
</div>
<p
v-cloak
class="text-center m-10 text-gray-300 dark:text-neutral-600 text-xs"
>
Made by
<a
target="_blank"
class="hover:underline"
href="https://emilenijssen.nl/?ref=wg-easy"
>Emile Nijssen</a
>
·
<a
class="hover:underline"
href="https://github.com/sponsors/WeeJeWel"
target="_blank"
>Donate</a
>
·
<a
class="hover:underline"
href="https://github.com/wg-easy/wg-easy"
target="_blank"
>GitHub</a
>
</p>
</main>
</template>