From 22ccd4d51b1557d856c77a9805dd45ce7443a37e Mon Sep 17 00:00:00 2001 From: Bernd Storath <999999bst@gmail.com> Date: Sun, 19 Jan 2025 16:03:14 +0100 Subject: [PATCH] Migrate to sqlite --- src/app/components/admin/CidrDialog.vue | 14 +++--- src/app/pages/admin/config.vue | 49 +++++-------------- src/app/pages/admin/interface.vue | 34 +++++++++++++ src/eslint.config.mjs | 2 + src/i18n/locales/en.json | 42 +++++++++------- src/server/api/admin/general.get.ts | 2 +- src/server/api/admin/general.post.ts | 19 ++++--- src/server/api/admin/hooks.get.ts | 2 +- src/server/api/admin/hooks.post.ts | 23 +++++---- src/server/api/admin/interface.get.ts | 7 --- src/server/api/admin/interface.post.ts | 9 ---- src/server/api/admin/interface/cidr.post.ts | 15 ++++++ src/server/api/admin/interface/index.get.ts | 12 +++++ src/server/api/admin/interface/index.post.ts | 14 ++++++ src/server/api/admin/userconfig.get.ts | 7 +++ src/server/api/admin/userconfig.post.ts | 14 ++++++ src/server/api/admin/userconfig/cidr.post.ts | 9 ---- src/server/api/admin/userconfig/index.get.ts | 4 -- src/server/api/admin/userconfig/index.post.ts | 9 ---- src/server/api/setup/4.post.ts | 10 +--- src/server/api/setup/5.post.ts | 10 +--- src/server/api/setup/migrate.post.ts | 9 +--- .../database/repositories/client/service.ts | 4 +- .../database/repositories/client/types.ts | 44 +++-------------- .../database/repositories/general/types.ts | 2 + .../database/repositories/hooks/service.ts | 15 +++++- .../database/repositories/hooks/types.ts | 14 ++++++ .../repositories/interface/service.ts | 46 +++++++++++++++++ .../database/repositories/interface/types.ts | 45 +++++++++++++++++ .../repositories/userConfig/service.ts | 21 ++++++-- .../database/repositories/userConfig/types.ts | 23 ++++++--- src/server/database/schema.ts | 1 + src/server/utils/handler.ts | 32 ++++++++++++ src/server/utils/types.ts | 37 ++++++++++++++ src/shared/utils/permissions.ts | 2 +- 35 files changed, 401 insertions(+), 201 deletions(-) delete mode 100644 src/server/api/admin/interface.get.ts delete mode 100644 src/server/api/admin/interface.post.ts create mode 100644 src/server/api/admin/interface/cidr.post.ts create mode 100644 src/server/api/admin/interface/index.get.ts create mode 100644 src/server/api/admin/interface/index.post.ts create mode 100644 src/server/api/admin/userconfig.get.ts create mode 100644 src/server/api/admin/userconfig.post.ts delete mode 100644 src/server/api/admin/userconfig/cidr.post.ts delete mode 100644 src/server/api/admin/userconfig/index.get.ts delete mode 100644 src/server/api/admin/userconfig/index.post.ts diff --git a/src/app/components/admin/CidrDialog.vue b/src/app/components/admin/CidrDialog.vue index a78bf180..bcf32809 100644 --- a/src/app/components/admin/CidrDialog.vue +++ b/src/app/components/admin/CidrDialog.vue @@ -17,8 +17,8 @@ class="mb-5 mt-2 text-sm leading-normal text-gray-500 dark:text-neutral-300" > - - + +
@@ -26,7 +26,7 @@ {{ $t('cancel') }} - Change @@ -40,10 +40,10 @@ defineEmits(['change']); const props = defineProps<{ triggerClass?: string; - address4: string; - address6: string; + ipv4Cidr: string; + ipv6Cidr: string; }>(); -const address4 = ref(props.address4); -const address6 = ref(props.address6); +const ipv4Cidr = ref(props.ipv4Cidr); +const ipv6Cidr = ref(props.ipv6Cidr); diff --git a/src/app/pages/admin/config.vue b/src/app/pages/admin/config.vue index ca399d77..c66d2b5b 100644 --- a/src/app/pages/admin/config.vue +++ b/src/app/pages/admin/config.vue @@ -8,7 +8,10 @@ Allowed IPs - + DNS @@ -16,10 +19,14 @@ Advanced - + @@ -27,14 +34,6 @@ Actions - - - @@ -79,30 +78,4 @@ async function revert() { await refresh(); data.value = toRef(_data.value).value; } - -async function changeCidr(address4: string, address6: string) { - try { - const res = await $fetch(`/api/admin/userconfig/cidr`, { - method: 'post', - body: { address4, address6 }, - }); - toast.showToast({ - type: 'success', - title: 'Success', - message: 'Changed CIDR', - }); - if (!res.success) { - throw new Error('Failed to change CIDR'); - } - await refreshNuxtData(); - } catch (e) { - if (e instanceof Error) { - toast.showToast({ - type: 'error', - title: 'Error', - message: e.message, - }); - } - } -} diff --git a/src/app/pages/admin/interface.vue b/src/app/pages/admin/interface.vue index def6fc4a..7984c760 100644 --- a/src/app/pages/admin/interface.vue +++ b/src/app/pages/admin/interface.vue @@ -11,6 +11,14 @@ Actions + + + @@ -55,4 +63,30 @@ async function revert() { await refresh(); data.value = toRef(_data.value).value; } + +async function changeCidr(ipv4Cidr: string, ipv6Cidr: string) { + try { + const res = await $fetch(`/api/admin/interface/cidr`, { + method: 'post', + body: { ipv4Cidr, ipv6Cidr }, + }); + toast.showToast({ + type: 'success', + title: 'Success', + message: 'Changed CIDR', + }); + if (!res.success) { + throw new Error('Failed to change CIDR'); + } + await refreshNuxtData(); + } catch (e) { + if (e instanceof Error) { + toast.showToast({ + type: 'error', + title: 'Error', + message: e.message, + }); + } + } +} diff --git a/src/eslint.config.mjs b/src/eslint.config.mjs index ad6fe8e4..07172140 100644 --- a/src/eslint.config.mjs +++ b/src/eslint.config.mjs @@ -2,3 +2,5 @@ import { createConfigForNuxt } from '@nuxt/eslint-config/flat'; import eslintConfigPrettier from 'eslint-config-prettier'; export default createConfigForNuxt().append(eslintConfigPrettier); + +// TODO: add typescript-eslint, import/order, ban raw defineEventHandler diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 09d4e598..d0cd03df 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -54,30 +54,17 @@ "body": "Body must be a valid object", "device": "Device must be a valid string", "deviceMin": "Device must be at least 1 Character", - "hook": "Hook must be a valid string", "client": { "id": "Client ID must be a valid number", "name": "Name must be a valid string", "nameMin": "Name must be at least 1 Character", - "mtu": "MTU must be a valid number", - "mtuMin": "MTU must be at least 1280", - "mtuMax": "MTU must be at most 9000", - "persistentKeepalive": "Persistent Keepalive must be a valid number", - "persistentKeepaliveMin": "Persistent Keepalive must be at least 0", - "persistentKeepaliveMax": "Persistent Keepalive must be at most 65535", "expireDate": "expiredDate must be a valid string", "expireDateMin": "expiredDate must be at least 1 Character", - "address": "IP Address must be a valid string", - "addressMin": "IP Address must be a be at least 1 Character", "address4": "IPv4 Address must be a valid string", "address4Min": "IPv4 Address must be a be at least 1 Character", "address6": "IPv6 Address must be a valid string", "address6Min": "IPv6 Address must be a be at least 1 Character", - "allowedIps": "Allowed IPs must be a valid array of strings", - "allowedIpsMin": "Allowed IPs must have at least 1 item", - "serverAllowedIps": "Allowed IPs must be a valid array of strings", - "dns": "DNS must be a valid array of strings", - "dnsMin": "DNS must have at least 1 item" + "serverAllowedIps": "Allowed IPs must be a valid array of strings" }, "user": { "username": "Username must be a valid string", @@ -93,14 +80,31 @@ }, "userConfig": { "host": "Host must be a valid string", - "hostMin": "Host must contain at least 1 character", - "port": "Port must be a valid number", - "portMin": "Port must be at least 1", - "portMax": "Port must be at most 65535" + "hostMin": "Host must contain at least 1 character" }, "general": { "sessionTimeout": "Session Timeout must be a valid number" - } + }, + "interface": { + "cidr": "CIDR must be a valid string", + "cidrMin": "CIDR must be at least 1 Character" + }, + "hook": "Hook must be a valid string", + "mtu": "MTU must be a valid number", + "mtuMin": "MTU must be at least 1280", + "mtuMax": "MTU must be at most 9000", + "port": "Port must be a valid number", + "portMin": "Port must be at least 1", + "portMax": "Port must be at most 65535", + "persistentKeepalive": "Persistent Keepalive must be a valid number", + "persistentKeepaliveMin": "Persistent Keepalive must be at least 0", + "persistentKeepaliveMax": "Persistent Keepalive must be at most 65535", + "address": "IP Address must be a valid string", + "addressMin": "IP Address must be a be at least 1 Character", + "dns": "DNS must be a valid array of strings", + "dnsMin": "DNS must have at least 1 item", + "allowedIps": "Allowed IPs must be a valid array of strings", + "allowedIpsMin": "Allowed IPs must have at least 1 item" }, "name": "Name", "username": "Username", diff --git a/src/server/api/admin/general.get.ts b/src/server/api/admin/general.get.ts index 367c69c2..d27f39f9 100644 --- a/src/server/api/admin/general.get.ts +++ b/src/server/api/admin/general.get.ts @@ -1,4 +1,4 @@ -export default defineEventHandler(async () => { +export default definePermissionEventHandler(actions.ADMIN, async () => { const sessionConfig = await Database.general.getSessionConfig(); return { sessionTimeout: sessionConfig.sessionTimeout, diff --git a/src/server/api/admin/general.post.ts b/src/server/api/admin/general.post.ts index 8919d30a..4a28cf29 100644 --- a/src/server/api/admin/general.post.ts +++ b/src/server/api/admin/general.post.ts @@ -1,10 +1,13 @@ import { GeneralUpdateSchema } from '#db/repositories/general/types'; -export default defineEventHandler(async (event) => { - const data = await readValidatedBody( - event, - validateZod(GeneralUpdateSchema, event) - ); - await Database.general.update(data); - return { success: true }; -}); +export default definePermissionEventHandler( + actions.ADMIN, + async ({ event }) => { + const data = await readValidatedBody( + event, + validateZod(GeneralUpdateSchema, event) + ); + await Database.general.update(data); + return { success: true }; + } +); diff --git a/src/server/api/admin/hooks.get.ts b/src/server/api/admin/hooks.get.ts index f90913f3..470ba3c2 100644 --- a/src/server/api/admin/hooks.get.ts +++ b/src/server/api/admin/hooks.get.ts @@ -1,4 +1,4 @@ -export default defineEventHandler(async () => { +export default definePermissionEventHandler(actions.ADMIN, async () => { const hooks = await Database.hooks.get('wg0'); if (!hooks) { throw new Error('Hooks not found'); diff --git a/src/server/api/admin/hooks.post.ts b/src/server/api/admin/hooks.post.ts index 820c60f4..1a291438 100644 --- a/src/server/api/admin/hooks.post.ts +++ b/src/server/api/admin/hooks.post.ts @@ -1,9 +1,14 @@ -export default defineEventHandler(async (event) => { - const data = await readValidatedBody( - event, - validateZod(hooksUpdateType, event) - ); - await Database.hooks.update(data); - await WireGuard.saveConfig(); - return { success: true }; -}); +import { HooksUpdateSchema } from '#db/repositories/hooks/types'; + +export default definePermissionEventHandler( + actions.ADMIN, + async ({ event }) => { + const data = await readValidatedBody( + event, + validateZod(HooksUpdateSchema, event) + ); + await Database.hooks.update('wg0', data); + await WireGuard.saveConfig(); + return { success: true }; + } +); diff --git a/src/server/api/admin/interface.get.ts b/src/server/api/admin/interface.get.ts deleted file mode 100644 index 9ae225a3..00000000 --- a/src/server/api/admin/interface.get.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default defineEventHandler(async () => { - const wgInterface = await Database.interfaces.get('wg0'); - return { - ...wgInterface, - privateKey: undefined, - }; -}); diff --git a/src/server/api/admin/interface.post.ts b/src/server/api/admin/interface.post.ts deleted file mode 100644 index 92986561..00000000 --- a/src/server/api/admin/interface.post.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default defineEventHandler(async (event) => { - const data = await readValidatedBody( - event, - validateZod(interfaceUpdateType, event) - ); - await Database.system.updateInterface(data); - await WireGuard.saveConfig(); - return { success: true }; -}); diff --git a/src/server/api/admin/interface/cidr.post.ts b/src/server/api/admin/interface/cidr.post.ts new file mode 100644 index 00000000..7df34297 --- /dev/null +++ b/src/server/api/admin/interface/cidr.post.ts @@ -0,0 +1,15 @@ +import { InterfaceCidrUpdateSchema } from '#db/repositories/interface/types'; + +export default definePermissionEventHandler( + actions.ADMIN, + async ({ event }) => { + const data = await readValidatedBody( + event, + validateZod(InterfaceCidrUpdateSchema, event) + ); + + await Database.interfaces.updateCidr('wg0', data); + await WireGuard.saveConfig(); + return { success: true }; + } +); diff --git a/src/server/api/admin/interface/index.get.ts b/src/server/api/admin/interface/index.get.ts new file mode 100644 index 00000000..16536d6e --- /dev/null +++ b/src/server/api/admin/interface/index.get.ts @@ -0,0 +1,12 @@ +export default definePermissionEventHandler(actions.ADMIN, async () => { + const wgInterface = await Database.interfaces.get('wg0'); + + if (!wgInterface) { + throw new Error('Interface not found'); + } + + return { + ...wgInterface, + privateKey: undefined, + }; +}); diff --git a/src/server/api/admin/interface/index.post.ts b/src/server/api/admin/interface/index.post.ts new file mode 100644 index 00000000..11538d2e --- /dev/null +++ b/src/server/api/admin/interface/index.post.ts @@ -0,0 +1,14 @@ +import { InterfaceUpdateSchema } from '#db/repositories/interface/types'; + +export default definePermissionEventHandler( + actions.ADMIN, + async ({ event }) => { + const data = await readValidatedBody( + event, + validateZod(InterfaceUpdateSchema, event) + ); + await Database.interfaces.update('wg0', data); + await WireGuard.saveConfig(); + return { success: true }; + } +); diff --git a/src/server/api/admin/userconfig.get.ts b/src/server/api/admin/userconfig.get.ts new file mode 100644 index 00000000..d6315be3 --- /dev/null +++ b/src/server/api/admin/userconfig.get.ts @@ -0,0 +1,7 @@ +export default definePermissionEventHandler(actions.ADMIN, async () => { + const userConfig = await Database.userConfigs.get('wg0'); + if (!userConfig) { + throw new Error('User config not found'); + } + return userConfig; +}); diff --git a/src/server/api/admin/userconfig.post.ts b/src/server/api/admin/userconfig.post.ts new file mode 100644 index 00000000..afbc7499 --- /dev/null +++ b/src/server/api/admin/userconfig.post.ts @@ -0,0 +1,14 @@ +import { UserConfigUpdateSchema } from '#db/repositories/userConfig/types'; + +export default definePermissionEventHandler( + actions.ADMIN, + async ({ event }) => { + const data = await readValidatedBody( + event, + validateZod(UserConfigUpdateSchema, event) + ); + await Database.userConfigs.update('wg0', data); + await WireGuard.saveConfig(); + return { success: true }; + } +); diff --git a/src/server/api/admin/userconfig/cidr.post.ts b/src/server/api/admin/userconfig/cidr.post.ts deleted file mode 100644 index 5b845aa1..00000000 --- a/src/server/api/admin/userconfig/cidr.post.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default defineEventHandler(async (event) => { - const data = await readValidatedBody( - event, - validateZod(cidrUpdateType, event) - ); - - await WireGuard.updateAddressRange(data); - return { success: true }; -}); diff --git a/src/server/api/admin/userconfig/index.get.ts b/src/server/api/admin/userconfig/index.get.ts deleted file mode 100644 index b7683045..00000000 --- a/src/server/api/admin/userconfig/index.get.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default defineEventHandler(async () => { - const system = await Database.system.get(); - return system.userConfig; -}); diff --git a/src/server/api/admin/userconfig/index.post.ts b/src/server/api/admin/userconfig/index.post.ts deleted file mode 100644 index a6146db8..00000000 --- a/src/server/api/admin/userconfig/index.post.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default defineEventHandler(async (event) => { - const data = await readValidatedBody( - event, - validateZod(userConfigUpdateType, event) - ); - await Database.system.updateUserConfig(data); - await WireGuard.saveConfig(); - return { success: true }; -}); diff --git a/src/server/api/setup/4.post.ts b/src/server/api/setup/4.post.ts index fd7f29c0..0dd73670 100644 --- a/src/server/api/setup/4.post.ts +++ b/src/server/api/setup/4.post.ts @@ -1,14 +1,6 @@ import { UserSetupType } from '#db/repositories/user/types'; -export default defineEventHandler(async (event) => { - const { done } = await Database.general.getSetupStep(); - if (done) { - throw createError({ - statusCode: 400, - statusMessage: 'Invalid state', - }); - } - +export default defineSetupEventHandler(async ({ event }) => { const { username, password } = await readValidatedBody( event, validateZod(UserSetupType, event) diff --git a/src/server/api/setup/5.post.ts b/src/server/api/setup/5.post.ts index 514c4166..1e0b36d6 100644 --- a/src/server/api/setup/5.post.ts +++ b/src/server/api/setup/5.post.ts @@ -1,14 +1,6 @@ import { UserConfigSetupType } from '#db/repositories/userConfig/types'; -export default defineEventHandler(async (event) => { - const { done } = await Database.general.getSetupStep(); - if (done) { - throw createError({ - statusCode: 400, - statusMessage: 'Invalid state', - }); - } - +export default defineSetupEventHandler(async ({ event }) => { const { host, port } = await readValidatedBody( event, validateZod(UserConfigSetupType, event) diff --git a/src/server/api/setup/migrate.post.ts b/src/server/api/setup/migrate.post.ts index b4933de7..d5256f9c 100644 --- a/src/server/api/setup/migrate.post.ts +++ b/src/server/api/setup/migrate.post.ts @@ -2,14 +2,7 @@ import { stringifyIp } from 'ip-bigint'; import { z } from 'zod';*/ -export default defineEventHandler(async (/*event*/) => { - const { done } = await Database.general.getSetupStep(); - if (done) { - throw createError({ - statusCode: 400, - statusMessage: 'Invalid state', - }); - } +export default defineSetupEventHandler(async (/*{ event }*/) => { // TODO: Implement /* diff --git a/src/server/database/repositories/client/service.ts b/src/server/database/repositories/client/service.ts index 7c8971e1..942b220e 100644 --- a/src/server/database/repositories/client/service.ts +++ b/src/server/database/repositories/client/service.ts @@ -2,8 +2,8 @@ import type { DBType } from '#db/sqlite'; import { eq, sql } from 'drizzle-orm'; import { client } from './schema'; import type { ClientCreateType, UpdateClientType } from './types'; -import type { ID } from '../../schema'; -import { wgInterface, userConfig } from '../../schema'; +import type { ID } from '#db/schema'; +import { wgInterface, userConfig } from '#db/schema'; import { parseCidr } from 'cidr-tools'; function createPreparedStatement(db: DBType) { diff --git a/src/server/database/repositories/client/types.ts b/src/server/database/repositories/client/types.ts index 5b730d9f..68663209 100644 --- a/src/server/database/repositories/client/types.ts +++ b/src/server/database/repositories/client/types.ts @@ -3,13 +3,6 @@ import z from 'zod'; import type { client } from './schema'; -const schemaForType = - () => - // eslint-disable-next-line @typescript-eslint/no-explicit-any - >(arg: S) => { - return arg; - }; - export type ID = string; export type ClientType = InferSelectModel; @@ -35,11 +28,6 @@ const expiresAt = z .pipe(safeStringRefine) .nullable(); -const address = z - .string({ message: 'zod.client.address' }) - .min(1, { message: 'zod.client.addressMin' }) - .pipe(safeStringRefine); - const address4 = z .string({ message: 'zod.client.address4' }) .min(1, { message: 'zod.client.address4Min' }) @@ -50,30 +38,10 @@ const address6 = z .min(1, { message: 'zod.client.address6Min' }) .pipe(safeStringRefine); -const allowedIps = z - .array(address, { message: 'zod.client.allowedIps' }) - .min(1, { message: 'zod.client.allowedIpsMin' }); - -const serverAllowedIps = z.array(address, { +const serverAllowedIps = z.array(AddressSchema, { message: 'zod.serverAllowedIps', }); -const mtu = z - .number({ message: 'zod.client.mtu' }) - .min(1280, { message: 'zod.client.mtuMin' }) - .max(9000, { message: 'zod.client.mtuMax' }); - -const persistentKeepalive = z - .number({ message: 'zod.client.persistentKeepalive' }) - .min(0, 'zod.client.persistentKeepaliveMin') - .max(65535, 'zod.client.persistentKeepaliveMax'); - -const enabled = z.boolean({ message: 'zod.enabled' }); - -const dns = z - .array(address, { message: 'zod.client.dns' }) - .min(1, 'zod.client.dnsMin'); - export const ClientCreateSchema = z.object({ name: name, expiresAt: expiresAt, @@ -84,15 +52,15 @@ export type ClientCreateType = z.infer; export const ClientUpdateSchema = schemaForType()( z.object({ name: name, - enabled: enabled, + enabled: EnabledSchema, expiresAt: expiresAt, ipv4Address: address4, ipv6Address: address6, - allowedIps: allowedIps, + allowedIps: AllowedIpsSchema, serverAllowedIps: serverAllowedIps, - mtu: mtu, - persistentKeepalive: persistentKeepalive, - dns: dns, + mtu: MtuSchema, + persistentKeepalive: PersistentKeepaliveSchema, + dns: DnsSchema, }) ); diff --git a/src/server/database/repositories/general/types.ts b/src/server/database/repositories/general/types.ts index 9e4bb4a3..05944528 100644 --- a/src/server/database/repositories/general/types.ts +++ b/src/server/database/repositories/general/types.ts @@ -11,3 +11,5 @@ export const GeneralUpdateSchema = z.object({ }); export type GeneralUpdateType = z.infer; + +export type SetupStepType = { step: number; done: boolean }; diff --git a/src/server/database/repositories/hooks/service.ts b/src/server/database/repositories/hooks/service.ts index 1384070a..64d6c27c 100644 --- a/src/server/database/repositories/hooks/service.ts +++ b/src/server/database/repositories/hooks/service.ts @@ -1,6 +1,7 @@ import type { DBType } from '#db/sqlite'; import { eq, sql } from 'drizzle-orm'; import { hooks } from './schema'; +import type { HooksUpdateType } from './types'; function createPreparedStatement(db: DBType) { return { @@ -11,13 +12,23 @@ function createPreparedStatement(db: DBType) { } export class HooksService { + #db: DBType; #statements: ReturnType; constructor(db: DBType) { + this.#db = db; this.#statements = createPreparedStatement(db); } - get(wgInterface: string) { - return this.#statements.get.execute({ interface: wgInterface }); + get(infName: string) { + return this.#statements.get.execute({ interface: infName }); + } + + update(infName: string, data: HooksUpdateType) { + return this.#db + .update(hooks) + .set(data) + .where(eq(hooks.id, infName)) + .execute(); } } diff --git a/src/server/database/repositories/hooks/types.ts b/src/server/database/repositories/hooks/types.ts index 3372c504..3e76d045 100644 --- a/src/server/database/repositories/hooks/types.ts +++ b/src/server/database/repositories/hooks/types.ts @@ -1,4 +1,18 @@ import type { InferSelectModel } from 'drizzle-orm'; import type { hooks } from './schema'; +import z from 'zod'; export type HooksType = InferSelectModel; + +export type HooksUpdateType = Omit; + +const hook = z.string({ message: 'zod.hook' }).pipe(safeStringRefine); + +export const HooksUpdateSchema = schemaForType()( + z.object({ + preUp: hook, + postUp: hook, + preDown: hook, + postDown: hook, + }) +); diff --git a/src/server/database/repositories/interface/service.ts b/src/server/database/repositories/interface/service.ts index 7fa3e313..7361f617 100644 --- a/src/server/database/repositories/interface/service.ts +++ b/src/server/database/repositories/interface/service.ts @@ -1,6 +1,10 @@ import type { DBType } from '#db/sqlite'; +import isCidr from 'is-cidr'; import { eq, sql } from 'drizzle-orm'; import { wgInterface } from './schema'; +import type { InterfaceCidrUpdateType, InterfaceUpdateType } from './types'; +import { client as clientSchema } from '#db/schema'; +import { parseCidr } from 'cidr-tools'; function createPreparedStatement(db: DBType) { return { @@ -20,9 +24,11 @@ function createPreparedStatement(db: DBType) { } export class InterfaceService { + #db: DBType; #statements: ReturnType; constructor(db: DBType) { + this.#db = db; this.#statements = createPreparedStatement(db); } @@ -41,4 +47,44 @@ export class InterfaceService { publicKey, }); } + + update(infName: string, data: InterfaceUpdateType) { + return this.#db + .update(wgInterface) + .set(data) + .where(eq(wgInterface.name, infName)) + .execute(); + } + + updateCidr(infName: string, data: InterfaceCidrUpdateType) { + if (!isCidr(data.ipv4Cidr) || !isCidr(data.ipv6Cidr)) { + throw new Error('Invalid CIDR'); + } + return this.#db.transaction(async (tx) => { + await tx + .update(wgInterface) + .set(data) + .where(eq(wgInterface.name, infName)) + .execute(); + + const clients = await tx.query.client.findMany().execute(); + + for (const client of clients) { + // TODO: optimize + const clients = await tx.query.client.findMany().execute(); + + const nextIpv4 = nextIP(4, parseCidr(data.ipv4Cidr), clients); + const nextIpv6 = nextIP(6, parseCidr(data.ipv6Cidr), clients); + + await tx + .update(clientSchema) + .set({ + ipv4Address: nextIpv4, + ipv6Address: nextIpv6, + }) + .where(eq(clientSchema.id, client.id)) + .execute(); + } + }); + } } diff --git a/src/server/database/repositories/interface/types.ts b/src/server/database/repositories/interface/types.ts index ef6720d6..a003cc62 100644 --- a/src/server/database/repositories/interface/types.ts +++ b/src/server/database/repositories/interface/types.ts @@ -1,4 +1,49 @@ import type { InferSelectModel } from 'drizzle-orm'; import type { wgInterface } from './schema'; +import z from 'zod'; export type InterfaceType = InferSelectModel; + +export type InterfaceCreateType = Omit< + InterfaceType, + 'createdAt' | 'updatedAt' +>; + +export type InterfaceUpdateType = Omit< + InterfaceCreateType, + 'name' | 'createdAt' | 'updatedAt' | 'privateKey' | 'publicKey' +>; + +const device = z + .string({ message: 'zod.device' }) + .min(1, 'zod.deviceMin') + .pipe(safeStringRefine); + +const cidr = z + .string({ message: 'zod.interface.cidr' }) + .min(1, { message: 'zod.interface.cidrMin' }) + .pipe(safeStringRefine); + +export const InterfaceUpdateSchema = schemaForType()( + z.object({ + ipv4Cidr: cidr, + ipv6Cidr: cidr, + mtu: MtuSchema, + port: PortSchema, + device: device, + enabled: EnabledSchema, + }) +); + +export type InterfaceCidrUpdateType = { + ipv4Cidr: string; + ipv6Cidr: string; +}; + +export const InterfaceCidrUpdateSchema = + schemaForType()( + z.object({ + ipv4Cidr: cidr, + ipv6Cidr: cidr, + }) + ); diff --git a/src/server/database/repositories/userConfig/service.ts b/src/server/database/repositories/userConfig/service.ts index bbf9e5ec..42bdb99b 100644 --- a/src/server/database/repositories/userConfig/service.ts +++ b/src/server/database/repositories/userConfig/service.ts @@ -1,6 +1,7 @@ import type { DBType } from '#db/sqlite'; import { eq, sql } from 'drizzle-orm'; import { userConfig } from './schema'; +import type { UserConfigUpdateType } from './types'; function createPreparedStatement(db: DBType) { return { @@ -19,21 +20,31 @@ function createPreparedStatement(db: DBType) { } export class UserConfigService { + #db: DBType; #statements: ReturnType; constructor(db: DBType) { + this.#db = db; this.#statements = createPreparedStatement(db); } - async get(wgInterface: string) { - return await this.#statements.get.execute({ interface: wgInterface }); + get(infName: string) { + return this.#statements.get.execute({ interface: infName }); } - async updateHostPort(wgInterface: string, host: string, port: number) { - return await this.#statements.updateHostPort.execute({ - interface: wgInterface, + updateHostPort(infName: string, host: string, port: number) { + return this.#statements.updateHostPort.execute({ + interface: infName, host, port, }); } + + update(infName: string, data: UserConfigUpdateType) { + return this.#db + .update(userConfig) + .set(data) + .where(eq(userConfig.id, infName)) + .execute(); + } } diff --git a/src/server/database/repositories/userConfig/types.ts b/src/server/database/repositories/userConfig/types.ts index 1358bf8c..165f3f27 100644 --- a/src/server/database/repositories/userConfig/types.ts +++ b/src/server/database/repositories/userConfig/types.ts @@ -9,12 +9,23 @@ const host = z .min(1, 'zod.userConfig.hostMin') .pipe(safeStringRefine); -const port = z - .number({ message: 'zod.userConfig.port' }) - .min(1, 'zod.userConfig.portMin') - .max(65535, 'zod.userConfig.portMax'); - export const UserConfigSetupType = z.object({ host: host, - port: port, + port: PortSchema, }); + +export type UserConfigUpdateType = Omit< + UserConfigType, + 'id' | 'createdAt' | 'updatedAt' +>; + +export const UserConfigUpdateSchema = schemaForType()( + z.object({ + port: PortSchema, + defaultMtu: MtuSchema, + defaultPersistentKeepalive: PersistentKeepaliveSchema, + defaultDns: DnsSchema, + defaultAllowedIps: AllowedIpsSchema, + host: host, + }) +); diff --git a/src/server/database/schema.ts b/src/server/database/schema.ts index db3e45a7..73edce69 100644 --- a/src/server/database/schema.ts +++ b/src/server/database/schema.ts @@ -8,4 +8,5 @@ export * from './repositories/oneTimeLink/schema'; export * from './repositories/user/schema'; export * from './repositories/userConfig/schema'; +// TODO: move to types export type ID = number; diff --git a/src/server/utils/handler.ts b/src/server/utils/handler.ts index 738acc4a..754bb8d3 100644 --- a/src/server/utils/handler.ts +++ b/src/server/utils/handler.ts @@ -1,11 +1,15 @@ import type { EventHandlerRequest, EventHandlerResponse, H3Event } from 'h3'; import type { UserType } from '#db/repositories/user/types'; +import type { SetupStepType } from '../database/repositories/general/types'; type PermissionHandler< TReq extends EventHandlerRequest, TRes extends EventHandlerResponse, > = { (params: { event: H3Event; user: UserType }): TRes }; +/** + * check if the user has the permission to perform the action + */ export const definePermissionEventHandler = < TReq extends EventHandlerRequest, TRes extends EventHandlerResponse, @@ -25,3 +29,31 @@ export const definePermissionEventHandler = < return await handler({ event, user }); }); }; + +type SetupHandler< + TReq extends EventHandlerRequest, + TRes extends EventHandlerResponse, +> = { (params: { event: H3Event; setup: SetupStepType }): TRes }; + +/** + * check if the setup is done, if not, run the handler + */ +export const defineSetupEventHandler = < + TReq extends EventHandlerRequest, + TRes extends EventHandlerResponse, +>( + handler: SetupHandler +) => { + return defineEventHandler(async (event) => { + const setup = await Database.general.getSetupStep(); + + if (setup.done) { + throw createError({ + statusCode: 400, + statusMessage: 'Invalid state', + }); + } + + return await handler({ event, setup }); + }); +}; diff --git a/src/server/utils/types.ts b/src/server/utils/types.ts index 1515cfe1..41607b52 100644 --- a/src/server/utils/types.ts +++ b/src/server/utils/types.ts @@ -13,6 +13,43 @@ export const safeStringRefine = z // TODO: create custom getValidatedRouterParams and readValidatedBody wrapper +export const EnabledSchema = z.boolean({ message: 'zod.enabled' }); + +export const MtuSchema = z + .number({ message: 'zod.mtu' }) + .min(1280, { message: 'zod.mtuMin' }) + .max(9000, { message: 'zod.mtuMax' }); + +export const PortSchema = z + .number({ message: 'zod.port' }) + .min(1, { message: 'zod.portMin' }) + .max(65535, { message: 'zod.portMax' }); + +export const PersistentKeepaliveSchema = z + .number({ message: 'zod.persistentKeepalive' }) + .min(0, 'zod.persistentKeepaliveMin') + .max(65535, 'zod.persistentKeepaliveMax'); + +export const AddressSchema = z + .string({ message: 'zod.address' }) + .min(1, { message: 'zod.addressMin' }) + .pipe(safeStringRefine); + +export const DnsSchema = z + .array(AddressSchema, { message: 'zod.dns' }) + .min(1, 'zod.dnsMin'); + +export const AllowedIpsSchema = z + .array(AddressSchema, { message: 'zod.allowedIps' }) + .min(1, { message: 'zod.allowedIpsMin' }); + +export const schemaForType = + () => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + >(arg: S) => { + return arg; + }; + export function validateZod( schema: ZodSchema, event?: H3Event diff --git a/src/shared/utils/permissions.ts b/src/shared/utils/permissions.ts index f83ed6f6..3b7c5762 100644 --- a/src/shared/utils/permissions.ts +++ b/src/shared/utils/permissions.ts @@ -1,4 +1,4 @@ -// TODO: will need to be updated when we have more roles and actions +// TODO: implement ABAC export const actions = { ADMIN: 'ADMIN',