Browse Source

wip: frontend

pull/1244/head
Bernd Storath 11 months ago
parent
commit
009cbaf8a7
  1. 3
      src/components/Client/Client.vue
  2. 79
      src/components/Client/ExpireDate.vue
  3. 2
      src/components/Client/Name.vue
  4. 21
      src/components/Client/OneTimeLink.vue
  5. 46
      src/components/Client/OneTimeLinkBtn.vue
  6. 1
      src/components/Clients/Empty.vue
  7. 1
      src/components/Clients/New.vue
  8. 49
      src/components/Clients/Sort.vue
  9. 1
      src/pages/index.vue
  10. 24
      src/pages/login.vue
  11. 12
      src/stores/clients.ts
  12. 7
      src/stores/global.ts
  13. 5
      src/stores/modal.ts
  14. 77
      src/utils/api.ts
  15. 27
      src/utils/math.ts

3
src/components/Client/Client.vue

@ -19,6 +19,8 @@
/>
<ClientLastSeen :client="client" />
</div>
<ClientOneTimeLink :client="client" />
<ClientExpireDate :client="client" />
</div>
<!-- Info -->
@ -40,6 +42,7 @@
<ClientSwitch :client="client" />
<ClientQRCode :client="client" />
<ClientConfig :client="client" />
<ClientOneTimeLinkBtn :client="client" />
<ClientDelete :client="client" />
</div>
</div>

79
src/components/Client/ExpireDate.vue

@ -0,0 +1,79 @@
<template>
<div
v-show="globalStore.enableExpireTime"
class="block md:inline-block pb-1 md:pb-0 text-gray-500 dark:text-neutral-400 text-xs"
>
<span class="group">
<!-- Show -->
<input
v-show="clientEditExpireDateId === client.id"
v-model="clientEditExpireDate"
v-on:keyup.enter="
updateClientExpireDate(client, clientEditExpireDate);
clientEditExpireDate = null;
clientEditExpireDateId = null;
"
v-on:keyup.escape="
clientEditExpireDate = null;
clientEditExpireDateId = null;
"
:ref="'client-' + client.id + '-expire'"
type="text"
class="rounded border-2 dark:bg-neutral-700 border-gray-100 dark:border-neutral-600 focus:border-gray-200 dark:focus:border-neutral-500 outline-none w-70 text-black dark:text-neutral-300 dark:placeholder:text-neutral-500 text-xs p-0"
/>
<span
v-show="clientEditExpireDateId !== client.id"
class="inline-block"
>{{ client.expiredAt | expiredDateFormat }}</span
>
<!-- Edit -->
<span
v-show="clientEditExpireDateId !== client.id"
@click="
clientEditExpireDate = client.expiredAt
? client.expiredAt.toISOString().slice(0, 10)
: 'yyyy-mm-dd';
clientEditExpireDateId = client.id;
setTimeout(
() => $refs['client-' + client.id + '-expire'][0].select(),
1
);
"
class="cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 inline align-middle opacity-25 hover:opacity-100"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
</span>
</span>
</div>
</template>
<script setup lang="ts">
defineProps<{ client: LocalClient }>();
const globalStore = useGlobalStore();
const clientsStore = useClientsStore();
function updateClientExpireDate(
client: LocalClient,
expireDate: string | null
) {
api
.updateClientExpireDate({ clientId: client.id, expireDate })
.catch((err) => alert(err.message || err.toString()))
.finally(() => clientsStore.refresh().catch(console.error));
}
</script>

2
src/components/Client/Name.vue

@ -53,7 +53,7 @@ const clientNameInput = ref<HTMLInputElement | null>(null);
const clientEditName = ref<null | string>(null);
const clientEditNameId = ref<null | string>(null);
function updateClientName(client: WGClient, name: string | null) {
function updateClientName(client: LocalClient, name: string | null) {
if (name === null) {
return;
}

21
src/components/Client/OneTimeLink.vue

@ -0,0 +1,21 @@
<template>
<div
v-if="
enableOneTimeLinks &&
client.oneTimeLink !== null &&
client.oneTimeLink !== ''
"
:ref="'client-' + client.id + '-link'"
class="text-gray-400 text-xs"
>
<a :href="'./cnf/' + client.oneTimeLink + ''"
>{{ document.location.protocol }}//{{ document.location.host }}/cnf/{{
client.oneTimeLink
}}</a
>
</div>
</template>
<script setup lang="ts">
defineProps<{ client: LocalClient }>();
</script>

46
src/components/Client/OneTimeLinkBtn.vue

@ -0,0 +1,46 @@
<template>
<button
v-if="enableOneTimeLinks"
:disabled="!client.downloadableConfig"
class="align-middle inline-block bg-gray-100 dark:bg-neutral-600 dark:text-neutral-300 p-2 rounded transition"
:class="{
'hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white':
client.downloadableConfig,
'is-disabled': !client.downloadableConfig,
}"
:title="!client.downloadableConfig ? $t('noPrivKey') : $t('OneTimeLink')"
@click="
if (client.downloadableConfig) {
showOneTimeLink(client);
}
"
>
<svg
class="w-5"
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="M13.213 9.787a3.391 3.391 0 0 0-4.795 0l-3.425 3.426a3.39 3.39 0 0 0 4.795 4.794l.321-.304m-.321-4.49a3.39 3.39 0 0 0 4.795 0l3.424-3.426a3.39 3.39 0 0 0-4.794-4.795l-1.028.961"
/>
</svg>
</button>
</template>
<script setup lang="ts">
defineProps<{ client: LocalClient }>();
const clientsStore = useClientsStore();
function showOneTimeLink(client: LocalClient) {
api
.showOneTimeLink({ clientId: client.id })
.catch((err) => alert(err.message || err.toString()))
.finally(() => clientsStore.refresh().catch(console.error));
}
</script>

1
src/components/Clients/Empty.vue

@ -6,6 +6,7 @@
@click="
modalStore.clientCreate = true;
modalStore.clientCreateName = '';
modalStore.clientExpireDate = '';
"
>
<IconsPlus class="w-4 mr-2" />

1
src/components/Clients/New.vue

@ -3,6 +3,7 @@
@click="
modalStore.clientCreate = true;
modalStore.clientCreateName = '';
modalStore.clientExpireDate = '';
"
>
<IconsPlus class="w-4 md:mr-2" />

49
src/components/Clients/Sort.vue

@ -0,0 +1,49 @@
<template>
<button
v-if="enableSortClient"
@click="sortClient = !sortClient"
class="hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 max-md:border-x-0 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 md:rounded inline-flex items-center transition"
>
<svg
v-if="sortClient === true"
inline
class="w-4 md:mr-2"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
d="M12 19.75C11.9015 19.7504 11.8038 19.7312 11.7128 19.6934C11.6218 19.6557 11.5392 19.6001 11.47 19.53L5.47 13.53C5.33752 13.3878 5.2654 13.1997 5.26882 13.0054C5.27225 12.8111 5.35096 12.6258 5.48838 12.4883C5.62579 12.3509 5.81118 12.2722 6.00548 12.2688C6.19978 12.2654 6.38782 12.3375 6.53 12.47L12 17.94L17.47 12.47C17.6122 12.3375 17.8002 12.2654 17.9945 12.2688C18.1888 12.2722 18.3742 12.3509 18.5116 12.4883C18.649 12.6258 18.7277 12.8111 18.7312 13.0054C18.7346 13.1997 18.6625 13.3878 18.53 13.53L12.53 19.53C12.4608 19.6001 12.3782 19.6557 12.2872 19.6934C12.1962 19.7312 12.0985 19.7504 12 19.75Z"
fill="#000000"
/>
<path
d="M12 19.75C11.8019 19.7474 11.6126 19.6676 11.4725 19.5275C11.3324 19.3874 11.2526 19.1981 11.25 19V5C11.25 4.80109 11.329 4.61032 11.4697 4.46967C11.6103 4.32902 11.8011 4.25 12 4.25C12.1989 4.25 12.3897 4.32902 12.5303 4.46967C12.671 4.61032 12.75 4.80109 12.75 5V19C12.7474 19.1981 12.6676 19.3874 12.5275 19.5275C12.3874 19.6676 12.1981 19.7474 12 19.75Z"
fill="#000000"
/>
</svg>
<svg
v-if="sortClient === false"
inline
class="w-4 md:mr-2"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
d="M18 11.75C17.9015 11.7505 17.8038 11.7313 17.7128 11.6935C17.6218 11.6557 17.5392 11.6001 17.47 11.53L12 6.06001L6.53 11.53C6.38782 11.6625 6.19978 11.7346 6.00548 11.7312C5.81118 11.7278 5.62579 11.649 5.48838 11.5116C5.35096 11.3742 5.27225 11.1888 5.26882 10.9945C5.2654 10.8002 5.33752 10.6122 5.47 10.47L11.47 4.47001C11.6106 4.32956 11.8012 4.25067 12 4.25067C12.1987 4.25067 12.3894 4.32956 12.53 4.47001L18.53 10.47C18.6705 10.6106 18.7493 10.8013 18.7493 11C18.7493 11.1988 18.6705 11.3894 18.53 11.53C18.4608 11.6001 18.3782 11.6557 18.2872 11.6935C18.1962 11.7313 18.0985 11.7505 18 11.75Z"
fill="#000000"
/>
<path
d="M12 19.75C11.8019 19.7474 11.6126 19.6676 11.4725 19.5275C11.3324 19.3874 11.2526 19.1981 11.25 19V5C11.25 4.80109 11.329 4.61032 11.4697 4.46967C11.6103 4.32902 11.8011 4.25 12 4.25C12.1989 4.25 12.3897 4.32902 12.5303 4.46967C12.671 4.61032 12.75 4.80109 12.75 5V19C12.7474 19.1981 12.6676 19.3874 12.5275 19.5275C12.3874 19.6676 12.1981 19.7474 12 19.75Z"
fill="#000000"
/>
</svg>
<span class="max-md:hidden text-sm">{{ $t('sort') }}</span>
</button>
</template>
<script setup lang="ts"></script>

1
src/pages/index.vue

@ -15,6 +15,7 @@
<div class="flex md:block md:flex-shrink-0 space-x-1">
<ClientsRestoreConfig />
<ClientsBackupConfig />
<ClientsSort />
<ClientsNew />
</div>
</div>

24
src/pages/login.vue

@ -27,6 +27,30 @@
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"
/>
<label
v-if="rememberMeEnabled"
class="inline-block mb-5 cursor-pointer whitespace-nowrap"
:title="$t('titleRememberMe')"
>
<input type="checkbox" class="sr-only" v-model="remember" />
<div
v-if="remember"
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
v-if="!remember"
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>
<span class="text-sm">{{ $t('rememberMe') }}</span>
</label>
<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"

12
src/stores/clients.ts

@ -20,12 +20,13 @@ export type ClientPersist = {
};
export const useClientsStore = defineStore('Clients', () => {
const globalStore = useGlobalStore();
const clients = ref<null | LocalClient[]>(null);
const clientsPersist = ref<Record<string, ClientPersist>>({});
async function refresh({ updateCharts = false } = {}) {
const { data: _clients } = await api.getClients();
const transformedClients = _clients.value?.map((client) => {
let transformedClients = _clients.value?.map((client) => {
let avatar = undefined;
if (client.name.includes('@') && client.name.includes('.')) {
avatar = `https://gravatar.com/avatar/${sha256(client.name.toLowerCase().trim())}.jpg`;
@ -106,6 +107,15 @@ export const useClientsStore = defineStore('Clients', () => {
hoverRx: clientPersist.hoverRx,
};
});
if (globalStore.enableSortClient) {
transformedClients = sortByProperty(
transformedClients,
'name',
globalStore.sortClient
);
}
clients.value = transformedClients ?? null;
}
return { clients, clientsPersist, refresh };

7
src/stores/global.ts

@ -8,6 +8,10 @@ export const useGlobalStore = defineStore('Global', () => {
null
);
const uiTrafficStats = ref(false);
const rememberMeEnabled = ref(false);
const enableSortClient = ref(false);
const sortClient = ref(true); // Sort clients by name, true = asc, false = desc
const { availableLocales, locale } = useI18n();
@ -51,6 +55,9 @@ export const useGlobalStore = defineStore('Global', () => {
uiShowCharts,
uiTrafficStats,
updateCharts,
rememberMeEnabled,
enableSortClient,
sortClient,
fetchRelease,
fetchChartType,
fetchTrafficStats,

5
src/stores/modal.ts

@ -5,14 +5,16 @@ export const useModalStore = defineStore('Modal', () => {
const clientDelete = ref<null | WGClient>(null);
const clientCreate = ref<null | boolean>(null);
const clientCreateName = ref<string>('');
const clientExpireDate = ref(null);
const qrcode = ref<null | string>(null);
function createClient() {
const name = clientCreateName.value;
const expireDate = clientExpireDate.value;
if (!name) return;
api
.createClient({ name })
.createClient({ name, expireDate })
.catch((err) => alert(err.message || err.toString()))
.finally(() => clientsStore.refresh().catch(console.error));
}
@ -31,6 +33,7 @@ export const useModalStore = defineStore('Modal', () => {
clientDelete,
clientCreate,
clientCreateName,
clientExpireDate,
qrcode,
createClient,
deleteClient,

77
src/utils/api.ts

@ -11,6 +11,12 @@ class API {
});
}
async getRememberMeEnabled() {
return useFetch('/api/remember-me', {
method: 'get',
});
}
async getTrafficStats() {
return useFetch('/api/ui-traffic-stats', {
method: 'get',
@ -23,6 +29,18 @@ class API {
});
}
async getEnableOneTimeLinks() {
return useFetch('/api/wg-enable-one-time-links', {
method: 'get',
});
}
async getEnableExpireTime() {
return useFetch('/api/wg-enable-expire-time', {
method: 'get',
});
}
async getSession() {
// TODO?: use useFetch
return $fetch('/api/session', {
@ -30,10 +48,16 @@ class API {
});
}
async createSession({ password }: { password: string | null }) {
async createSession({
password,
remember,
}: {
password: string | null;
remember: boolean;
}) {
return $fetch('/api/session', {
method: 'post',
body: { password },
body: { password, remember },
});
}
@ -49,28 +73,40 @@ class API {
});
}
async createClient({ name }: { name: string }) {
async createClient({
name,
expireDate,
}: {
name: string;
expireDate: string;
}) {
return $fetch('/api/wireguard/client', {
method: 'POST',
body: { name },
method: 'post',
body: { name, expireDate },
});
}
async deleteClient({ clientId }: { clientId: string }) {
return $fetch(`/api/wireguard/client/${clientId}`, {
method: 'DELETE',
method: 'delete',
});
}
async showOneTimeLink({ clientId }: { clientId: string }) {
return $fetch(`/api/wireguard/${clientId}/:clientId/generateOneTimeLink`, {
method: 'post',
});
}
async enableClient({ clientId }: { clientId: string }) {
return $fetch(`/api/wireguard/client/${clientId}/enable`, {
method: 'POST',
method: 'post',
});
}
async disableClient({ clientId }: { clientId: string }) {
return $fetch(`/api/wireguard/client/${clientId}/disable`, {
method: 'POST',
method: 'post',
});
}
@ -82,7 +118,7 @@ class API {
name: string;
}) {
return $fetch(`/api/wireguard/client/${clientId}/name`, {
method: 'PUT',
method: 'put',
body: { name },
});
}
@ -95,17 +131,36 @@ class API {
address: string;
}) {
return $fetch(`/api/wireguard/client/${clientId}/address`, {
method: 'PUT',
method: 'put',
body: { address },
});
}
async updateClientExpireDate({
clientId,
expireDate,
}: {
clientId: string;
expireDate: string | null;
}) {
return $fetch(`/api/wireguard/client/${clientId}/expireDate`, {
method: 'put',
body: { expireDate },
});
}
async restoreConfiguration(file: string) {
return $fetch('/api/wireguard/restore', {
method: 'PUT',
method: 'put',
body: { file },
});
}
async getSortClients() {
return useFetch('/api/ui-sort-clients', {
method: 'get',
});
}
}
type WGClientReturn = Awaited<

27
src/utils/math.ts

@ -29,3 +29,30 @@ export function dateTime(value: Date) {
minute: 'numeric',
}).format(value);
}
/**
* Sorts an array of objects by a specified property in ascending or descending order.
*
* @param array The array of objects to be sorted.
* @param property The property to sort the array by.
* @param sort Whether to sort the array in ascending (default, true) or descending order (false).
*/
export function sortByProperty<T>(
array: T[],
property: keyof T,
sort: boolean = true
): T[] {
if (sort) {
return array.sort((a, b) =>
typeof a[property] === 'string'
? (a[property] as string).localeCompare(b[property] as string)
: (a[property] as number) - (b[property] as number)
);
}
return array.sort((a, b) =>
typeof a[property] === 'string'
? (b[property] as string).localeCompare(a[property] as string)
: (b[property] as number) - (a[property] as number)
);
}

Loading…
Cancel
Save