mirror of https://github.com/wg-easy/wg-easy
37 changed files with 295 additions and 383 deletions
@ -1,60 +0,0 @@ |
|||
<template> |
|||
<span class="group"> |
|||
<!-- Show --> |
|||
<input |
|||
v-show="clientEditAddress4Id === client.id" |
|||
ref="clientAddress4Input" |
|||
v-model="clientEditAddress4" |
|||
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-20 text-black dark:text-neutral-300 dark:placeholder:text-neutral-500" |
|||
@keyup.enter=" |
|||
updateClientAddress4(client, clientEditAddress4); |
|||
clientEditAddress4 = null; |
|||
clientEditAddress4Id = null; |
|||
" |
|||
@keyup.escape=" |
|||
clientEditAddress4 = null; |
|||
clientEditAddress4Id = null; |
|||
" |
|||
/> |
|||
<span v-show="clientEditAddress4Id !== client.id" class="inline-block">{{ |
|||
client.address4 |
|||
}}</span> |
|||
|
|||
<!-- Edit --> |
|||
<span |
|||
v-show="clientEditAddress4Id !== client.id" |
|||
class="cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" |
|||
@click=" |
|||
clientEditAddress4 = client.address4; |
|||
clientEditAddress4Id = client.id; |
|||
nextTick(() => clientAddress4Input?.select()); |
|||
" |
|||
> |
|||
<IconsEdit |
|||
class="h-4 w-4 inline align-middle opacity-25 hover:opacity-100" |
|||
/> |
|||
</span> |
|||
</span> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
defineProps<{ |
|||
client: LocalClient; |
|||
}>(); |
|||
|
|||
const clientsStore = useClientsStore(); |
|||
|
|||
const clientAddress4Input = ref<HTMLInputElement | null>(null); |
|||
const clientEditAddress4 = ref<null | string>(null); |
|||
const clientEditAddress4Id = ref<null | string>(null); |
|||
|
|||
function updateClientAddress4(client: WGClient, address4: string | null) { |
|||
if (address4 === null) { |
|||
return; |
|||
} |
|||
api |
|||
.updateClientAddress4({ clientId: client.id, address4 }) |
|||
.catch((err) => alert(err.message || err.toString())) |
|||
.finally(() => clientsStore.refresh().catch(console.error)); |
|||
} |
|||
</script> |
@ -1,17 +0,0 @@ |
|||
<template> |
|||
<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="$t('deleteClient')" |
|||
@click="modalStore.clientDelete = client" |
|||
> |
|||
<IconsDelete class="w-5" /> |
|||
</button> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
defineProps<{ |
|||
client: LocalClient; |
|||
}>(); |
|||
|
|||
const modalStore = useModalStore(); |
|||
</script> |
@ -1,91 +0,0 @@ |
|||
<template> |
|||
<div |
|||
v-show="globalStore.features.clientExpiration.enabled" |
|||
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" |
|||
ref="clientExpireDateInput" |
|||
v-model="clientEditExpireDate" |
|||
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" |
|||
@keyup.enter=" |
|||
updateClientExpireDate(client, clientEditExpireDate); |
|||
clientEditExpireDate = null; |
|||
clientEditExpireDateId = null; |
|||
" |
|||
@keyup.escape=" |
|||
clientEditExpireDate = null; |
|||
clientEditExpireDateId = null; |
|||
" |
|||
/> |
|||
<span |
|||
v-show="clientEditExpireDateId !== client.id" |
|||
class="inline-block" |
|||
>{{ expiredDateFormat(client.expiresAt) }}</span |
|||
> |
|||
|
|||
<!-- Edit --> |
|||
<span |
|||
v-show="clientEditExpireDateId !== client.id" |
|||
class="cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" |
|||
@click=" |
|||
clientEditExpireDate = client.expiresAt |
|||
? client.expiresAt.slice(0, 10) |
|||
: 'yyyy-mm-dd'; |
|||
clientEditExpireDateId = client.id; |
|||
nextTick(() => clientExpireDateInput?.select()); |
|||
" |
|||
> |
|||
<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(); |
|||
const clientEditExpireDate = ref<string | null>(null); |
|||
const clientEditExpireDateId = ref<string | null>(null); |
|||
const { t, locale } = useI18n(); |
|||
|
|||
const clientExpireDateInput = ref<HTMLInputElement | null>(null); |
|||
|
|||
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)); |
|||
} |
|||
|
|||
function expiredDateFormat(value: string | null) { |
|||
if (value === null) return t('Permanent'); |
|||
const dateTime = new Date(value); |
|||
return dateTime.toLocaleDateString(locale.value, { |
|||
year: 'numeric', |
|||
month: 'long', |
|||
day: 'numeric', |
|||
}); |
|||
} |
|||
</script> |
@ -1,65 +0,0 @@ |
|||
<template> |
|||
<div |
|||
class="text-gray-700 dark:text-neutral-200 group text-sm md:text-base" |
|||
:title="$t('createdOn') + dateTime(new Date(client.createdAt))" |
|||
> |
|||
<!-- Show --> |
|||
<input |
|||
v-show="clientEditNameId === client.id" |
|||
ref="clientNameInput" |
|||
v-model="clientEditName" |
|||
class="rounded px-1 border-2 dark:bg-neutral-700 border-gray-100 dark:border-neutral-600 focus:border-gray-200 dark:focus:border-neutral-500 dark:placeholder:text-neutral-500 outline-none w-30" |
|||
@keyup.enter=" |
|||
updateClientName(client, clientEditName); |
|||
clientEditName = null; |
|||
clientEditNameId = null; |
|||
" |
|||
@keyup.escape=" |
|||
clientEditName = null; |
|||
clientEditNameId = null; |
|||
" |
|||
/> |
|||
<span |
|||
v-show="clientEditNameId !== client.id" |
|||
class="border-t-2 border-b-2 border-transparent" |
|||
>{{ client.name }}</span |
|||
> |
|||
|
|||
<!-- Edit --> |
|||
<span |
|||
v-show="clientEditNameId !== client.id" |
|||
class="cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" |
|||
@click=" |
|||
clientEditName = client.name; |
|||
clientEditNameId = client.id; |
|||
nextTick(() => clientNameInput?.select()); |
|||
" |
|||
> |
|||
<IconsEdit |
|||
class="h-4 w-4 inline align-middle opacity-25 hover:opacity-100" |
|||
/> |
|||
</span> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
defineProps<{ |
|||
client: LocalClient; |
|||
}>(); |
|||
|
|||
const clientsStore = useClientsStore(); |
|||
|
|||
const clientNameInput = ref<HTMLInputElement | null>(null); |
|||
const clientEditName = ref<null | string>(null); |
|||
const clientEditNameId = ref<null | string>(null); |
|||
|
|||
function updateClientName(client: LocalClient, name: string | null) { |
|||
if (name === null) { |
|||
return; |
|||
} |
|||
api |
|||
.updateClientName({ clientId: client.id, name }) |
|||
.catch((err) => alert(err.message || err.toString())) |
|||
.finally(() => clientsStore.refresh().catch(console.error)); |
|||
} |
|||
</script> |
@ -0,0 +1,11 @@ |
|||
<template> |
|||
<span class="inline-block"> |
|||
{{ client.address4 }} |
|||
</span> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
defineProps<{ |
|||
client: LocalClient; |
|||
}>(); |
|||
</script> |
@ -1,7 +1,7 @@ |
|||
<template> |
|||
<a |
|||
:disabled="!client.downloadableConfig" |
|||
:href="'./api/wireguard/client/' + client.id + '/configuration'" |
|||
:href="'./api/client/' + client.id + '/configuration'" |
|||
:download="client.downloadableConfig ? 'configuration' : null" |
|||
class="align-middle inline-block bg-gray-100 dark:bg-neutral-600 dark:text-neutral-300 p-2 rounded transition" |
|||
:class="{ |
@ -0,0 +1,14 @@ |
|||
<template> |
|||
<NuxtLink |
|||
class="align-middle bg-gray-100 dark:bg-neutral-600 dark:text-neutral-300 p-2 rounded transition hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white" |
|||
:to="`/clients/${client.id}`" |
|||
> |
|||
<IconsEdit class="w-5" /> |
|||
</NuxtLink> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
defineProps<{ |
|||
client: LocalClient; |
|||
}>(); |
|||
</script> |
@ -0,0 +1,25 @@ |
|||
<template> |
|||
<div |
|||
v-show="globalStore.features.clientExpiration.enabled" |
|||
class="block md:inline-block pb-1 md:pb-0 text-gray-500 dark:text-neutral-400 text-xs" |
|||
> |
|||
<span class="inline-block">{{ expiredDateFormat(client.expiresAt) }}</span> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
defineProps<{ client: LocalClient }>(); |
|||
|
|||
const globalStore = useGlobalStore(); |
|||
const { t, locale } = useI18n(); |
|||
|
|||
function expiredDateFormat(value: string | null) { |
|||
if (value === null) return t('Permanent'); |
|||
const dateTime = new Date(value); |
|||
return dateTime.toLocaleDateString(locale.value, { |
|||
year: 'numeric', |
|||
month: 'long', |
|||
day: 'numeric', |
|||
}); |
|||
} |
|||
</script> |
@ -0,0 +1,16 @@ |
|||
<template> |
|||
<div |
|||
class="text-gray-700 dark:text-neutral-200 group text-sm md:text-base" |
|||
:title="$t('createdOn') + dateTime(new Date(client.createdAt))" |
|||
> |
|||
<span class="border-t-2 border-b-2 border-transparent"> |
|||
{{ client.name }} |
|||
</span> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
defineProps<{ |
|||
client: LocalClient; |
|||
}>(); |
|||
</script> |
@ -0,0 +1,112 @@ |
|||
<template> |
|||
<main v-if="data"> |
|||
<Panel> |
|||
<PanelHead> |
|||
<PanelHeadTitle :text="data.name" /> |
|||
</PanelHead> |
|||
<PanelBody> |
|||
<section class="grid grid-cols-1 gap-4 md:grid-cols-2"> |
|||
<h4 class="text-2xl col-span-full py-6"> |
|||
{{ $t('me.sectionGeneral') }} |
|||
</h4> |
|||
<Label for="name" class="font-semibold md:align-middle md:leading-10"> |
|||
{{ 'Name' }} |
|||
</Label> |
|||
<input |
|||
id="name" |
|||
v-model.trim="data.name" |
|||
type="text" |
|||
class="dark:bg-neutral-700 text-gray-500 dark:text-neutral-200 border-2 border-gray-100 dark:border-neutral-800 rounded-lg focus:border-red-800 dark:placeholder:text-neutral-400 focus:outline-0 focus:ring-0" |
|||
/> |
|||
<Label |
|||
for="enabled" |
|||
class="font-semibold md:align-middle md:leading-10" |
|||
> |
|||
{{ 'Enabled' }} |
|||
</Label> |
|||
<input |
|||
id="enabled" |
|||
v-model.trim="data.enabled" |
|||
type="checkbox" |
|||
class="dark:bg-neutral-700 text-gray-500 dark:text-neutral-200 border-2 border-gray-100 dark:border-neutral-800 rounded-lg focus:border-red-800 dark:placeholder:text-neutral-400 focus:outline-0 focus:ring-0" |
|||
/> |
|||
</section> |
|||
<section class="grid grid-cols-1 gap-4 md:grid-cols-2"> |
|||
<h4 class="text-2xl col-span-full py-6"> |
|||
{{ 'Address' }} |
|||
</h4> |
|||
<Label for="ipv4" class="font-semibold md:align-middle md:leading-10"> |
|||
{{ 'IPv4' }} |
|||
</Label> |
|||
<input |
|||
id="ipv4" |
|||
v-model.trim="data.address4" |
|||
type="text" |
|||
class="dark:bg-neutral-700 text-gray-500 dark:text-neutral-200 border-2 border-gray-100 dark:border-neutral-800 rounded-lg focus:border-red-800 dark:placeholder:text-neutral-400 focus:outline-0 focus:ring-0" |
|||
/> |
|||
<Label for="ipv6" class="font-semibold md:align-middle md:leading-10"> |
|||
{{ 'IPv6' }} |
|||
</Label> |
|||
<input |
|||
id="ipv6" |
|||
v-model.trim="data.address6" |
|||
type="text" |
|||
class="dark:bg-neutral-700 text-gray-500 dark:text-neutral-200 border-2 border-gray-100 dark:border-neutral-800 rounded-lg focus:border-red-800 dark:placeholder:text-neutral-400 focus:outline-0 focus:ring-0" |
|||
/> |
|||
</section> |
|||
<section class="grid grid-cols-1 gap-4 md:grid-cols-2"> |
|||
<h4 class="text-2xl col-span-full py-6"> |
|||
{{ 'Advanced' }} |
|||
</h4> |
|||
<Label |
|||
for="keepalive" |
|||
class="font-semibold md:align-middle md:leading-10" |
|||
> |
|||
{{ 'Persistent Keepalive' }} |
|||
</Label> |
|||
<input |
|||
id="keepalive" |
|||
v-model.number="data.persistentKeepalive" |
|||
type="number" |
|||
class="dark:bg-neutral-700 text-gray-500 dark:text-neutral-200 border-2 border-gray-100 dark:border-neutral-800 rounded-lg focus:border-red-800 dark:placeholder:text-neutral-400 focus:outline-0 focus:ring-0" |
|||
/> |
|||
</section> |
|||
<section class="grid grid-cols-1 gap-4 md:grid-cols-2"> |
|||
<h4 class="text-2xl col-span-full py-6"> |
|||
{{ 'Action' }} |
|||
</h4> |
|||
<Label |
|||
for="rotateprivkey" |
|||
class="font-semibold md:align-middle md:leading-10" |
|||
> |
|||
{{ 'Rotate Private Key' }} |
|||
</Label> |
|||
<input |
|||
id="rotateprivkey" |
|||
value="Do !" |
|||
type="button" |
|||
class="dark:bg-neutral-700 text-gray-500 dark:text-neutral-200 border-2 border-gray-100 dark:border-neutral-800 rounded-lg focus:border-red-800 dark:placeholder:text-neutral-400 focus:outline-0 focus:ring-0" |
|||
/> |
|||
<Label |
|||
for="delete" |
|||
class="font-semibold md:align-middle md:leading-10" |
|||
> |
|||
{{ 'Delete' }} |
|||
</Label> |
|||
<input |
|||
id="delete" |
|||
value="Do !" |
|||
type="button" |
|||
class="dark:bg-neutral-700 text-gray-500 dark:text-neutral-200 border-2 border-gray-100 dark:border-neutral-800 rounded-lg focus:border-red-800 dark:placeholder:text-neutral-400 focus:outline-0 focus:ring-0" |
|||
/> |
|||
</section> |
|||
</PanelBody> |
|||
</Panel> |
|||
</main> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
const route = useRoute(); |
|||
const id = route.params.id as string; |
|||
const { data } = await useFetch(`/api/client/${id}`, { method: 'get' }); |
|||
</script> |
@ -1,50 +0,0 @@ |
|||
import { parseCidr } from 'cidr-tools'; |
|||
import { stringifyIp } from 'ip-bigint'; |
|||
import { z } from 'zod'; |
|||
|
|||
// TODO: check what are missing
|
|||
const clientSchema = z.object({ |
|||
id: z.string(), |
|||
name: z.string(), |
|||
address: z.string(), |
|||
privateKey: z.string(), |
|||
publicKey: z.string(), |
|||
preSharedKey: z.string(), |
|||
createdAt: z.string(), |
|||
updatedAt: z.string(), |
|||
enabled: z.boolean(), |
|||
}); |
|||
|
|||
const oldConfigSchema = z.object({ |
|||
server: z.object({ |
|||
privateKey: z.string(), |
|||
publicKey: z.string(), |
|||
address: z.string(), |
|||
}), |
|||
clients: z.record(z.string(), clientSchema), |
|||
}); |
|||
|
|||
export default defineEventHandler(async (event) => { |
|||
const { file } = await readValidatedBody(event, validateZod(fileType, event)); |
|||
const file_ = await oldConfigSchema.parseAsync(JSON.parse(file)); |
|||
|
|||
for (const [_, value] of Object.entries(file_.clients)) { |
|||
// remove the unused field
|
|||
const { address: _, ...filterValue } = value; |
|||
const cidr4 = parseCidr(value.address); |
|||
const address4 = stringifyIp({ number: cidr4.start + 0n, version: 4 }); |
|||
|
|||
await Database.client.create({ |
|||
...filterValue, |
|||
address4, |
|||
address6: '', |
|||
expiresAt: null, |
|||
allowedIPs: ['0.0.0.0/0', '::/0'], |
|||
oneTimeLink: null, |
|||
serverAllowedIPs: [], |
|||
persistentKeepalive: 0, |
|||
}); |
|||
} |
|||
|
|||
return { success: true }; |
|||
}); |
@ -0,0 +1,7 @@ |
|||
export default defineEventHandler(async (event) => { |
|||
const { clientId } = await getValidatedRouterParams( |
|||
event, |
|||
validateZod(clientIdType) |
|||
); |
|||
return WireGuard.getClient({ clientId }); |
|||
}); |
@ -0,0 +1,83 @@ |
|||
import { parseCidr } from 'cidr-tools'; |
|||
import { stringifyIp } from 'ip-bigint'; |
|||
import { z } from 'zod'; |
|||
import type { Database } from '~~/services/database/repositories/database'; |
|||
|
|||
export default defineEventHandler(async (event) => { |
|||
const { file } = await readValidatedBody(event, validateZod(fileType, event)); |
|||
const setupDone = await Database.setup.done(); |
|||
if (setupDone) { |
|||
throw createError({ |
|||
statusCode: 400, |
|||
statusMessage: 'Invalid state', |
|||
}); |
|||
} |
|||
|
|||
const schema = z.object({ |
|||
server: z.object({ |
|||
privateKey: z.string(), |
|||
publicKey: z.string(), |
|||
address: z.string(), |
|||
}), |
|||
clients: z.record( |
|||
z.string(), |
|||
z.object({ |
|||
name: z.string(), |
|||
address: z.string(), |
|||
privateKey: z.string(), |
|||
publicKey: z.string(), |
|||
preSharedKey: z.string(), |
|||
createdAt: z.string(), |
|||
updatedAt: z.string(), |
|||
enabled: z.boolean(), |
|||
}) |
|||
), |
|||
}); |
|||
const res = await schema.safeParseAsync(JSON.parse(file)); |
|||
if (!res.success) { |
|||
throw new Error('Invalid Config'); |
|||
} |
|||
const system = await Database.system.get(); |
|||
const oldConfig = res.data; |
|||
const oldCidr = parseCidr(oldConfig.server.address + '/24'); |
|||
const db = { |
|||
system: { |
|||
...system, |
|||
// TODO: migrate to db calls
|
|||
interface: { |
|||
...system.interface, |
|||
address4: oldConfig.server.address, |
|||
privateKey: oldConfig.server.privateKey, |
|||
publicKey: oldConfig.server.publicKey, |
|||
}, |
|||
userConfig: { |
|||
...system.userConfig, |
|||
address4Range: |
|||
stringifyIp({ number: oldCidr.start, version: 4 }) + '/24', |
|||
}, |
|||
} satisfies Partial<Database['system']>, |
|||
clients: {} as Database['clients'], |
|||
}; |
|||
|
|||
for (const [oldId, oldClient] of Object.entries(oldConfig.clients)) { |
|||
const address6 = nextIPv6(db.system, db.clients); |
|||
|
|||
await Database.client.create({ |
|||
id: oldId, |
|||
address4: oldClient.address, |
|||
enabled: oldClient.enabled, |
|||
name: oldClient.name, |
|||
preSharedKey: oldClient.preSharedKey, |
|||
privateKey: oldClient.privateKey, |
|||
publicKey: oldClient.publicKey, |
|||
expiresAt: null, |
|||
oneTimeLink: null, |
|||
allowedIPs: db.system.userConfig.allowedIps, |
|||
serverAllowedIPs: [], |
|||
persistentKeepalive: 0, |
|||
address6: address6, |
|||
}); |
|||
} |
|||
|
|||
return { success: true }; |
|||
}); |
Loading…
Reference in new issue