diff --git a/src/app/components/ClientCard/ClientCard.vue b/src/app/components/ClientCard/ClientCard.vue index b60f44d6..514a851c 100644 --- a/src/app/components/ClientCard/ClientCard.vue +++ b/src/app/components/ClientCard/ClientCard.vue @@ -39,6 +39,7 @@ + diff --git a/src/app/components/ClientCard/DuplicateBtn.vue b/src/app/components/ClientCard/DuplicateBtn.vue new file mode 100644 index 00000000..5d47971a --- /dev/null +++ b/src/app/components/ClientCard/DuplicateBtn.vue @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/src/app/components/ClientCard/DuplicateDialog.vue b/src/app/components/ClientCard/DuplicateDialog.vue new file mode 100644 index 00000000..33bf9849 --- /dev/null +++ b/src/app/components/ClientCard/DuplicateDialog.vue @@ -0,0 +1,51 @@ + + + diff --git a/src/app/components/Icons/Duplicate.vue b/src/app/components/Icons/Duplicate.vue new file mode 100644 index 00000000..cf509ab8 --- /dev/null +++ b/src/app/components/Icons/Duplicate.vue @@ -0,0 +1,8 @@ + + + + diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 4714d839..9986c2cd 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -84,6 +84,7 @@ "sort": "Sortieren", "create": "Client erstellen", "created": "Client wurde erstellt", + "duplicate": "Client duplizieren", "new": "Neuer Client", "name": "Name", "expireDate": "Ablaufdatum", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 86120f19..1c16dafb 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -84,6 +84,7 @@ "sort": "Sort", "create": "Create Client", "created": "Client created", + "duplicate": "Duplicate Client", "new": "New Client", "name": "Name", "expireDate": "Expire Date", diff --git a/src/server/api/client/[clientId]/duplicate.post.ts b/src/server/api/client/[clientId]/duplicate.post.ts new file mode 100644 index 00000000..4e112907 --- /dev/null +++ b/src/server/api/client/[clientId]/duplicate.post.ts @@ -0,0 +1,26 @@ +import { ClientDuplicateSchema, ClientGetSchema } from '#db/repositories/client/types'; + +export default definePermissionEventHandler( + 'clients', + 'create', + async ({ event, checkPermissions }) => { + const { clientId } = await getValidatedRouterParams( + event, + validateZod(ClientGetSchema, event) + ); + + const { name } = await readValidatedBody( + event, + validateZod(ClientDuplicateSchema, event) + ); + + const client = await Database.clients.get(clientId); + checkPermissions(client); + + const result = await Database.clients.createFromExistingId({ name, clientId }); + await WireGuard.saveConfig(); + + const newClientId = result[0]!.clientId; + return { success: true, newClientId }; + } +); diff --git a/src/server/database/repositories/client/service.ts b/src/server/database/repositories/client/service.ts index 97953cda..fb71c509 100644 --- a/src/server/database/repositories/client/service.ts +++ b/src/server/database/repositories/client/service.ts @@ -3,6 +3,7 @@ import { containsCidr, parseCidr } from 'cidr-tools'; import { client } from './schema'; import type { ClientCreateFromExistingType, + ClientCreateFromExistingIdType, ClientCreateType, UpdateClientType, } from './types'; @@ -301,4 +302,75 @@ export class ClientService { }) .execute(); } + + async createFromExistingId({ name, clientId }: ClientCreateFromExistingIdType) { + const privateKey = await wg.generatePrivateKey(); + const publicKey = await wg.getPublicKey(privateKey); + const preSharedKey = await wg.generatePreSharedKey(); + + return this.#db.transaction(async (tx) => { + const clients = await tx.query.client.findMany().execute(); + + const sourceClient = await tx.query.client.findFirst({ + where: eq(client.id, clientId), + }) + .execute(); + + if (!sourceClient) { + throw new Error('No Client with provided Id found'); + } + + const clientInterface = await tx.query.wgInterface + .findFirst({ + where: eq(wgInterface.name, sourceClient.interfaceId), + }) + .execute(); + + if (!clientInterface) { + throw new Error('WireGuard interface not found'); + } + + const ipv4Cidr = parseCidr(clientInterface.ipv4Cidr); + const ipv4Address = nextIP(4, ipv4Cidr, clients); + const ipv6Cidr = parseCidr(clientInterface.ipv6Cidr); + const ipv6Address = nextIP(6, ipv6Cidr, clients); + + return await tx + .insert(client) + .values({ + userId: sourceClient.userId, + interfaceId: sourceClient.interfaceId, + name: name, + ipv4Address: ipv4Address, + ipv6Address: ipv6Address, + preUp: sourceClient.preUp, + postUp: sourceClient.postUp, + preDown: sourceClient.preDown, + postDown: sourceClient.postDown, + privateKey: privateKey, + publicKey: publicKey, + preSharedKey: preSharedKey, + expiresAt: sourceClient.expiresAt, + allowedIps: sourceClient.allowedIps, + serverAllowedIps: sourceClient.serverAllowedIps, + firewallIps: sourceClient.firewallIps, + persistentKeepalive: sourceClient.persistentKeepalive, + mtu: sourceClient.mtu, + jC: sourceClient.jC, + jMin: sourceClient.jMin, + jMax: sourceClient.jMax, + i1: sourceClient.i1, + i2: sourceClient.i2, + i3: sourceClient.i3, + i4: sourceClient.i4, + i5: sourceClient.i5, + dns: sourceClient.dns, + serverEndpoint: sourceClient.serverEndpoint, + enabled: sourceClient.enabled, + }) + .returning({ clientId: client.id }) + .execute(); + + }); + } } diff --git a/src/server/database/repositories/client/types.ts b/src/server/database/repositories/client/types.ts index b0aa7fa3..cd470ff3 100644 --- a/src/server/database/repositories/client/types.ts +++ b/src/server/database/repositories/client/types.ts @@ -94,6 +94,43 @@ export const ClientGetSchema = z.object({ clientId: clientId, }); +export const ClientDuplicateSchema = z.object({ + name: name, +}); + +export type ClientCreateFromExistingIdType = Pick< + ClientType, + | 'userId' + | 'interfaceId' + | 'name' + | 'ipv4Address' + | 'ipv6Address' + | 'preUp' + | 'postUp' + | 'preDown' + | 'postDown' + | 'privateKey' + | 'publicKey' + | 'preSharedKey' + | 'expiresAt' + | 'allowedIps' + | 'serverAllowedIps' + | 'firewallIps' + | 'persistentKeepalive' + | 'mtu' + | 'jC' + | 'jMin' + | 'jMax' + | 'i1' + | 'i2' + | 'i3' + | 'i4' + | 'i5' + | 'dns' + | 'serverEndpoint' + | 'enabled' +>; + export type ClientCreateFromExistingType = Pick< ClientType, | 'name'