Browse Source

Update

pull/937/head
Sergei Birukov 2 years ago
parent
commit
00e7e27183
  1. 257
      webui/src/App.vue
  2. 7
      webui/src/components/Auth.vue
  3. 24
      webui/src/components/ClientControls.vue
  4. 15
      webui/src/services/api.js
  5. 70
      webui/src/store/store.js

257
webui/src/App.vue

@ -32,7 +32,7 @@
<button <button
class="bg-red-800 hover:bg-red-700 text-white border-2 border-none py-2 px-4 rounded inline-flex items-center transition" class="bg-red-800 hover:bg-red-700 text-white border-2 border-none py-2 px-4 rounded inline-flex items-center transition"
@click=" @click="
clientCreate = true; clientCreateShowModal = true;
clientCreateName = ''; clientCreateName = '';
" "
> >
@ -63,34 +63,15 @@
</div> </div>
<!-- Create Dialog --> <!-- Create Dialog -->
<div v-if="clientCreate" class="fixed z-10 inset-0 overflow-y-auto"> <div v-if="clientCreateShowModal" 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"> <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="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 class="absolute inset-0 bg-gray-500 dark:bg-black opacity-75 dark:opacity-50"></div>
</div> </div>
<!-- This element is to trick the browser into centering the modal contents. --> <!-- 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> <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 <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" 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" role="dialog"
@ -126,10 +107,7 @@
v-if="clientCreateName.length" v-if="clientCreateName.length"
type="button" type="button"
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" 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"
@click=" @click="createClient()"
createClient();
clientCreate = null;
"
> >
Create Create
</button> </button>
@ -143,7 +121,7 @@
<button <button
type="button" type="button"
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" 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"
@click="clientCreate = null" @click="clientCreateShowModal = null"
> >
Cancel Cancel
</button> </button>
@ -153,7 +131,7 @@
</div> </div>
<!-- Delete Dialog --> <!-- Delete Dialog -->
<div v-if="clientDelete" class="fixed z-10 inset-0 overflow-y-auto"> <div v-if="clientToDelete" 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"> <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. Background overlay, show/hide based on modal state.
@ -202,7 +180,7 @@
</h3> </h3>
<div class="mt-2"> <div class="mt-2">
<p class="text-sm text-gray-500 dark:text-neutral-300"> <p class="text-sm text-gray-500 dark:text-neutral-300">
Are you sure you want to delete <strong>{{ clientDelete.name }}</strong Are you sure you want to delete <strong>{{ clientToDelete.name }}</strong
>? This action cannot be undone. >? This action cannot be undone.
</p> </p>
</div> </div>
@ -213,17 +191,14 @@
<button <button
type="button" type="button"
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" 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"
@click=" @click="deleteClient(clientToDelete)"
deleteClient(clientDelete);
clientDelete = null;
"
> >
Delete Delete
</button> </button>
<button <button
type="button" type="button"
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" 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"
@click="clientDelete = null" @click="clientToDelete = null"
> >
Cancel Cancel
</button> </button>
@ -247,7 +222,7 @@
</template> </template>
<script setup> <script setup>
import { ref, reactive } from 'vue'; import { ref, onMounted, onBeforeUnmount } from 'vue';
import sha256 from 'crypto-js/sha256'; import sha256 from 'crypto-js/sha256';
import Auth from '@/components/Auth.vue'; import Auth from '@/components/Auth.vue';
@ -267,58 +242,44 @@ import { useStore } from './store/store';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
const store = useStore(); const store = useStore();
const { authenticated } = storeToRefs(store); const { authenticated, requiresPassword } = storeToRefs(store);
// const authenticated = ref(null); const { clients, clientsPersist, clientCreateShowModal, clientToDelete, clientCreateName, qrcode } = storeToRefs(store);
// const authenticating = ref(false);
// const password = ref(null);
const requiresPassword = ref(null);
const clients = ref(null); const logout = store.logout;
const clientsPersist = reactive({}); const createClient = store.createClient;
const clientDelete = ref(null); const deleteClient = store.deleteClient;
const clientCreate = ref(null);
const clientCreateName = ref('');
const qrcode = ref(null);
const currentRelease = ref(null); const currentRelease = ref(null);
const latestRelease = ref(null); const latestRelease = ref(null);
const isDark = ref(null); const refreshInterval = ref(null);
const api = new API(); const api = new API();
isDark.value = false; onMounted(() => {
if (localStorage.theme === 'dark') { api
isDark.value = true; .getSession()
} .then((session) => {
authenticated.value = session.authenticated;
// authenticated.value = false; //debug
requiresPassword.value = session.requiresPassword;
api refreshInterval.value = setInterval(refresh, 1000);
.getSession() })
.then((session) => { .catch((err) => {
authenticated.value = session.authenticated;
// authenticated.value = false; //debug
requiresPassword.value = session.requiresPassword;
refresh({
updateCharts: true,
}).catch((err) => {
console.log(err.message || err.toString()); console.log(err.message || err.toString());
}); });
}) });
.catch((err) => {
console.log(err.message || err.toString());
});
setInterval(() => { onBeforeUnmount(() => {
refresh({ clearInterval(refreshInterval.value);
updateCharts: true, });
}).catch(console.error);
}, 1000);
getRelease(); getRelease();
function handleNewClient() { function handleNewClient() {
clientCreate.value = true; clientCreateShowModal.value = true;
clientCreateName.value = ''; clientCreateName.value = '';
} }
@ -354,118 +315,54 @@ async function getRelease() {
async function refresh({ updateCharts = false } = {}) { async function refresh({ updateCharts = false } = {}) {
if (!authenticated.value) return; if (!authenticated.value) return;
const clientsData = await api.getClients(); try {
clients.value = clientsData.map((client) => { const clientsData = await api.getClients();
if (client.name.includes('@') && client.name.includes('.')) { clients.value = clientsData.map((client) => {
client.avatar = `https://www.gravatar.com/avatar/${sha256(client.name)}?d=blank`; if (client.name.includes('@') && client.name.includes('.')) {
} client.avatar = `https://www.gravatar.com/avatar/${sha256(client.name)}?d=blank`;
}
if (!clientsPersist[client.id]) {
clientsPersist[client.id] = {}; if (!clientsPersist[client.id]) {
clientsPersist[client.id].transferRxHistory = Array(50).fill(0); clientsPersist[client.id] = {};
clientsPersist[client.id].transferRxPrevious = client.transferRx; clientsPersist[client.id].transferRxHistory = Array(50).fill(0);
clientsPersist[client.id].transferTxHistory = Array(50).fill(0); clientsPersist[client.id].transferRxPrevious = client.transferRx;
clientsPersist[client.id].transferTxPrevious = client.transferTx; clientsPersist[client.id].transferTxHistory = Array(50).fill(0);
} clientsPersist[client.id].transferTxPrevious = client.transferTx;
}
// Debug
client.transferRx = clientsPersist[client.id].transferRxPrevious + Math.random() * 1000; // Debug
client.transferTx = clientsPersist[client.id].transferTxPrevious + Math.random() * 1000; client.transferRx = clientsPersist[client.id].transferRxPrevious + Math.random() * 1000;
client.latestHandshakeAt = new Date('2024-03-20'); client.transferTx = clientsPersist[client.id].transferTxPrevious + Math.random() * 1000;
// updateCharts = false; client.latestHandshakeAt = new Date('2024-03-20');
updateCharts = true; // DEV TODO: Update. Get from settings
if (updateCharts) {
clientsPersist[client.id].transferRxCurrent = client.transferRx - clientsPersist[client.id].transferRxPrevious; if (updateCharts) {
clientsPersist[client.id].transferRxPrevious = client.transferRx; clientsPersist[client.id].transferRxCurrent = client.transferRx - clientsPersist[client.id].transferRxPrevious;
clientsPersist[client.id].transferTxCurrent = client.transferTx - clientsPersist[client.id].transferTxPrevious; clientsPersist[client.id].transferRxPrevious = client.transferRx;
clientsPersist[client.id].transferTxPrevious = client.transferTx; clientsPersist[client.id].transferTxCurrent = client.transferTx - clientsPersist[client.id].transferTxPrevious;
clientsPersist[client.id].transferTxPrevious = client.transferTx;
clientsPersist[client.id].transferRxHistory.push(clientsPersist[client.id].transferRxCurrent);
clientsPersist[client.id].transferRxHistory.shift(); clientsPersist[client.id].transferRxHistory.push(clientsPersist[client.id].transferRxCurrent);
clientsPersist[client.id].transferRxHistory.shift();
clientsPersist[client.id].transferTxHistory.push(clientsPersist[client.id].transferTxCurrent);
clientsPersist[client.id].transferTxHistory.shift(); clientsPersist[client.id].transferTxHistory.push(clientsPersist[client.id].transferTxCurrent);
} clientsPersist[client.id].transferTxHistory.shift();
}
client.transferTxCurrent = clientsPersist[client.id].transferTxCurrent;
client.transferRxCurrent = clientsPersist[client.id].transferRxCurrent; client.transferTxCurrent = clientsPersist[client.id].transferTxCurrent;
client.transferRxCurrent = clientsPersist[client.id].transferRxCurrent;
client.transferTxHistory = clientsPersist[client.id].transferTxHistory;
client.transferRxHistory = clientsPersist[client.id].transferRxHistory; client.transferTxHistory = clientsPersist[client.id].transferTxHistory;
client.transferMax = Math.max(...client.transferTxHistory, ...client.transferRxHistory); client.transferRxHistory = clientsPersist[client.id].transferRxHistory;
client.transferMax = Math.max(...client.transferTxHistory, ...client.transferRxHistory);
client.hoverTx = clientsPersist[client.id].hoverTx;
client.hoverRx = clientsPersist[client.id].hoverRx; client.hoverTx = clientsPersist[client.id].hoverTx;
client.hoverRx = clientsPersist[client.id].hoverRx;
return client;
}); return client;
}
// function login(e) {
// e.preventDefault();
// if (!password.value) return;
// if (authenticating.value) return;
// authenticating.value = true;
// api
// .createSession({
// password: password,
// })
// .then(async () => {
// const session = await api.getSession();
// authenticated.value = session.authenticated;
// requiresPassword.value = session.requiresPassword;
// return this.refresh();
// })
// .catch((err) => {
// alert(err.message || err.toString());
// })
// .finally(() => {
// this.authenticating = false;
// this.password = null;
// });
// }
function logout(e) {
e.preventDefault();
api
.deleteSession()
.then(() => {
authenticated.value = false;
clients.value = null;
})
.catch((err) => {
alert(err.message || err.toString());
}); });
} } catch (error) {
console.log('Something went wrong', error);
function createClient() {
const name = clientCreateName;
if (!name.value) return;
api
.createClient({ name })
.catch((err) => alert(err.message || err.toString()))
.finally(() => refresh().catch(console.error));
}
function deleteClient(client) {
api
.deleteClient({ clientId: client.id })
.catch((err) => alert(err.message || err.toString()))
.finally(() => refresh().catch(console.error));
}
function toggleTheme() {
if (isDark.value) {
localStorage.theme = 'light';
document.documentElement.classList.remove('dark');
} else {
localStorage.theme = 'dark';
document.documentElement.classList.add('dark');
} }
isDark.value = !isDark.value;
} }
</script> </script>

7
webui/src/components/Auth.vue

@ -2,7 +2,10 @@
<div> <div>
<h1 class="text-4xl font-medium my-16 text-gray-700 dark:text-neutral-200 text-center">WireGuard</h1> <h1 class="text-4xl font-medium my-16 text-gray-700 dark:text-neutral-200 text-center">WireGuard</h1>
<form class="shadow rounded-md bg-white dark:bg-neutral-700 mx-auto w-64 p-5 overflow-hidden mt-10" @submit="login"> <form
class="shadow rounded-md bg-white dark:bg-neutral-700 mx-auto w-64 p-5 overflow-hidden mt-10"
@submit="store.login"
>
<!-- Avatar --> <!-- 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"> <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" /> <IconAvatarDefault class="w-10 h-10 m-5 text-white dark:text-white" />
@ -46,5 +49,5 @@ import { useStore } from '@/store/store';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
const store = useStore(); const store = useStore();
const { password, authenticating, login } = storeToRefs(store); const { password, authenticating } = storeToRefs(store);
</script> </script>

24
webui/src/components/ClientControls.vue

@ -2,18 +2,18 @@
<div class="text-gray-400 dark:text-neutral-400 flex gap-1 items-center justify-between"> <div class="text-gray-400 dark:text-neutral-400 flex gap-1 items-center justify-between">
<!-- Enable/Disable --> <!-- Enable/Disable -->
<div <div
@click="disableClient(client)"
v-if="client.enabled === true" v-if="client.enabled === true"
title="Disable Client" 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" class="inline-block align-middle rounded-full w-10 h-6 mr-1 bg-red-800 cursor-pointer hover:bg-red-700 transition-all"
@click="disableClient(client)"
> >
<div class="rounded-full w-4 h-4 m-1 ml-5 bg-white"></div> <div class="rounded-full w-4 h-4 m-1 ml-5 bg-white"></div>
</div> </div>
<div <div
@click="enableClient(client)"
v-if="client.enabled === false" v-if="client.enabled === false"
title="Enable Client" 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" 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"
@click="enableClient(client)"
> >
<div class="rounded-full w-4 h-4 m-1 bg-white"></div> <div class="rounded-full w-4 h-4 m-1 bg-white"></div>
</div> </div>
@ -22,7 +22,7 @@
<button <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" 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" title="Show QR Code"
@click="qrcode = `./api/wireguard/client/${client.id}/qrcode.svg`" @click="getQrCode(client)"
> >
<IconQRCode /> <IconQRCode />
</button> </button>
@ -41,7 +41,7 @@
<button <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" 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" title="Delete Client"
@click="clientDelete = client" @click="handleDeleteClick()"
> >
<IconDelete /> <IconDelete />
</button> </button>
@ -49,15 +49,20 @@
</template> </template>
<script setup> <script setup>
import { storeToRefs } from 'pinia';
import IconQRCode from '@/components/icons/IconQRCode.vue'; import IconQRCode from '@/components/icons/IconQRCode.vue';
import IconDownload from '@/components/icons/IconDownload.vue'; import IconDownload from '@/components/icons/IconDownload.vue';
import IconDelete from '@/components/icons/IconDelete.vue'; import IconDelete from '@/components/icons/IconDelete.vue';
import API from '@/services/api'; import API from '@/services/api';
const api = new API(); const api = new API();
defineProps({ import { useStore } from '@/store/store';
const store = useStore();
const { qrcode, clientToDelete } = storeToRefs(store);
const props = defineProps({
client: {}, client: {},
}); });
@ -70,4 +75,11 @@ function disableClient(client) {
api.disableClient({ clientId: client.id }).catch((err) => alert(err.message || err.toString())); api.disableClient({ clientId: client.id }).catch((err) => alert(err.message || err.toString()));
// .finally(() => refresh().catch(console.error)); // .finally(() => refresh().catch(console.error));
} }
async function getQrCode(client) {
qrcode.value = `${api.SERVER}/api/wireguard/client/${client.id}/qrcode.svg`;
}
function handleDeleteClick() {
clientToDelete.value = props.client;
}
</script> </script>

15
webui/src/services/api.js

@ -1,12 +1,14 @@
const SERVER = 'http://127.0.0.1:51821';
export default class API { export default class API {
constructor() {
this.SERVER = 'http://localhost:51821'; //! DEV
}
async call({ method, path, body }) { async call({ method, path, body }) {
const res = await fetch(`${SERVER}/api${path}`, { const res = await fetch(`${this.SERVER}/api${path}`, {
method, method,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
credentials: 'include', //! DEV
body: body ? JSON.stringify(body) : undefined, body: body ? JSON.stringify(body) : undefined,
}); });
@ -110,4 +112,11 @@ export default class API {
body: { address }, body: { address },
}); });
} }
async getQrCode({ clientId }) {
return this.call({
method: 'get',
path: `/wireguard/client/${clientId}/qrcode.svg`,
});
}
} }

70
webui/src/store/store.js

@ -1,5 +1,5 @@
import API from '@/services/api'; import API from '@/services/api';
import { ref } from 'vue'; import { ref, reactive } from 'vue';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
@ -9,6 +9,14 @@ export const useStore = defineStore('store', () => {
const authenticated = ref(null); const authenticated = ref(null);
const authenticating = ref(false); const authenticating = ref(false);
const password = ref(null); const password = ref(null);
const requiresPassword = ref(null);
const clients = ref(null);
const clientsPersist = reactive({});
const clientToDelete = ref(null);
const clientCreateShowModal = ref(null);
const clientCreateName = ref('');
const qrcode = ref(null);
function login(e) { function login(e) {
e.preventDefault(); e.preventDefault();
@ -19,27 +27,75 @@ export const useStore = defineStore('store', () => {
authenticating.value = true; authenticating.value = true;
api api
.createSession({ .createSession({
password: password, password: password.value,
}) })
.then(async () => { .then(async () => {
const session = await api.getSession(); const session = await api.getSession();
authenticated.value = session.authenticated; authenticated.value = session.authenticated;
requiresPassword.value = session.requiresPassword; requiresPassword.value = session.requiresPassword;
return this.refresh(); // return this.refresh();
}) })
.catch((err) => { .catch((err) => {
alert(err.message || err.toString()); console.log(err.message || err.toString());
}) })
.finally(() => { .finally(() => {
this.authenticating = false; authenticating.value = false;
this.password = null; password.value = null;
});
}
function logout(e) {
e.preventDefault();
api
.deleteSession()
.then(() => {
authenticated.value = false;
clients.value = null;
})
.catch((err) => {
alert(err.message || err.toString());
}); });
} }
function createClient() {
if (!clientCreateName.value) return;
api
.createClient({ name: clientCreateName.value })
.catch((err) => alert(err.message || err.toString()))
.finally
// () => refresh().catch(console.error)
();
clientCreateShowModal.value = null;
}
function deleteClient(client) {
api
.deleteClient({ clientId: client.id })
.catch((err) => alert(err.message || err.toString()))
.finally
// () => refresh().catch(console.error)
();
clientToDelete.value = null;
}
return { return {
authenticated, authenticated,
authenticating, authenticating,
login,
password, password,
requiresPassword,
clients,
clientsPersist,
clientToDelete,
clientCreateShowModal,
clientCreateName,
qrcode,
login,
logout,
createClient,
deleteClient,
}; };
}); });

Loading…
Cancel
Save