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 @@
+
+
+
+
+
+
+ {{ $t('client.duplicate') }}
+
+
+
+
+
+
+
+
+ {{ $t('dialog.cancel') }}
+
+
+
+ {{ $t('client.create') }}
+
+
+
+
+
+
+
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'