Browse Source

Migrate to sqlite

pull/1619/head
Bernd Storath 5 months ago
parent
commit
22ccd4d51b
No known key found for this signature in database GPG Key ID: D6C85685A555540F
  1. 14
      src/app/components/admin/CidrDialog.vue
  2. 49
      src/app/pages/admin/config.vue
  3. 34
      src/app/pages/admin/interface.vue
  4. 2
      src/eslint.config.mjs
  5. 42
      src/i18n/locales/en.json
  6. 2
      src/server/api/admin/general.get.ts
  7. 7
      src/server/api/admin/general.post.ts
  8. 2
      src/server/api/admin/hooks.get.ts
  9. 13
      src/server/api/admin/hooks.post.ts
  10. 7
      src/server/api/admin/interface.get.ts
  11. 9
      src/server/api/admin/interface.post.ts
  12. 15
      src/server/api/admin/interface/cidr.post.ts
  13. 12
      src/server/api/admin/interface/index.get.ts
  14. 14
      src/server/api/admin/interface/index.post.ts
  15. 7
      src/server/api/admin/userconfig.get.ts
  16. 14
      src/server/api/admin/userconfig.post.ts
  17. 9
      src/server/api/admin/userconfig/cidr.post.ts
  18. 4
      src/server/api/admin/userconfig/index.get.ts
  19. 9
      src/server/api/admin/userconfig/index.post.ts
  20. 10
      src/server/api/setup/4.post.ts
  21. 10
      src/server/api/setup/5.post.ts
  22. 9
      src/server/api/setup/migrate.post.ts
  23. 4
      src/server/database/repositories/client/service.ts
  24. 44
      src/server/database/repositories/client/types.ts
  25. 2
      src/server/database/repositories/general/types.ts
  26. 15
      src/server/database/repositories/hooks/service.ts
  27. 14
      src/server/database/repositories/hooks/types.ts
  28. 46
      src/server/database/repositories/interface/service.ts
  29. 45
      src/server/database/repositories/interface/types.ts
  30. 21
      src/server/database/repositories/userConfig/service.ts
  31. 23
      src/server/database/repositories/userConfig/types.ts
  32. 1
      src/server/database/schema.ts
  33. 32
      src/server/utils/handler.ts
  34. 37
      src/server/utils/types.ts
  35. 2
      src/shared/utils/permissions.ts

14
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" class="mb-5 mt-2 text-sm leading-normal text-gray-500 dark:text-neutral-300"
> >
<FormGroup> <FormGroup>
<FormTextField id="address4" v-model="address4" label="IPv4" /> <FormTextField id="ipv4Cidr" v-model="ipv4Cidr" label="IPv4" />
<FormTextField id="address6" v-model="address6" label="IPv6" /> <FormTextField id="ipv6Cidr" v-model="ipv6Cidr" label="IPv6" />
</FormGroup> </FormGroup>
</DialogDescription> </DialogDescription>
<div class="mt-6 flex justify-end gap-2"> <div class="mt-6 flex justify-end gap-2">
@ -26,7 +26,7 @@
<BaseButton>{{ $t('cancel') }}</BaseButton> <BaseButton>{{ $t('cancel') }}</BaseButton>
</DialogClose> </DialogClose>
<DialogClose as-child> <DialogClose as-child>
<BaseButton @click="$emit('change', address4, address6)" <BaseButton @click="$emit('change', ipv4Cidr, ipv6Cidr)"
>Change</BaseButton >Change</BaseButton
> >
</DialogClose> </DialogClose>
@ -40,10 +40,10 @@
defineEmits(['change']); defineEmits(['change']);
const props = defineProps<{ const props = defineProps<{
triggerClass?: string; triggerClass?: string;
address4: string; ipv4Cidr: string;
address6: string; ipv6Cidr: string;
}>(); }>();
const address4 = ref(props.address4); const ipv4Cidr = ref(props.ipv4Cidr);
const address6 = ref(props.address6); const ipv6Cidr = ref(props.ipv6Cidr);
</script> </script>

49
src/app/pages/admin/config.vue

@ -8,7 +8,10 @@
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormHeading>Allowed IPs</FormHeading> <FormHeading>Allowed IPs</FormHeading>
<FormArrayField v-model="data.allowedIps" name="allowedIps" /> <FormArrayField
v-model="data.defaultAllowedIps"
name="defaultAllowedIps"
/>
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormHeading>DNS</FormHeading> <FormHeading>DNS</FormHeading>
@ -16,10 +19,14 @@
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormHeading>Advanced</FormHeading> <FormHeading>Advanced</FormHeading>
<FormNumberField id="mtu" v-model="data.mtu" label="MTU" />
<FormNumberField <FormNumberField
id="keepalive" id="defaultMtu"
v-model="data.persistentKeepalive" v-model="data.defaultMtu"
label="MTU"
/>
<FormNumberField
id="defaultPersistentKeepalive"
v-model="data.defaultPersistentKeepalive"
label="Persistent Keepalive" label="Persistent Keepalive"
/> />
</FormGroup> </FormGroup>
@ -27,14 +34,6 @@
<FormHeading>Actions</FormHeading> <FormHeading>Actions</FormHeading>
<FormActionField type="submit" label="Save" /> <FormActionField type="submit" label="Save" />
<FormActionField label="Revert" @click="revert" /> <FormActionField label="Revert" @click="revert" />
<AdminCidrDialog
trigger-class="col-span-2"
:address6="data.address6Range"
:address4="data.address4Range"
@change="changeCidr"
>
<FormActionField label="Change CIDR" class="w-full" />
</AdminCidrDialog>
</FormGroup> </FormGroup>
</FormElement> </FormElement>
</main> </main>
@ -79,30 +78,4 @@ async function revert() {
await refresh(); await refresh();
data.value = toRef(_data.value).value; 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,
});
}
}
}
</script> </script>

34
src/app/pages/admin/interface.vue

@ -11,6 +11,14 @@
<FormHeading>Actions</FormHeading> <FormHeading>Actions</FormHeading>
<FormActionField type="submit" label="Save" /> <FormActionField type="submit" label="Save" />
<FormActionField label="Revert" @click="revert" /> <FormActionField label="Revert" @click="revert" />
<AdminCidrDialog
trigger-class="col-span-2"
:ipv4-cidr="data.ipv4Cidr"
:ipv6-cidr="data.ipv6Cidr"
@change="changeCidr"
>
<FormActionField label="Change CIDR" class="w-full" />
</AdminCidrDialog>
</FormGroup> </FormGroup>
</FormElement> </FormElement>
</main> </main>
@ -55,4 +63,30 @@ async function revert() {
await refresh(); await refresh();
data.value = toRef(_data.value).value; 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,
});
}
}
}
</script> </script>

2
src/eslint.config.mjs

@ -2,3 +2,5 @@ import { createConfigForNuxt } from '@nuxt/eslint-config/flat';
import eslintConfigPrettier from 'eslint-config-prettier'; import eslintConfigPrettier from 'eslint-config-prettier';
export default createConfigForNuxt().append(eslintConfigPrettier); export default createConfigForNuxt().append(eslintConfigPrettier);
// TODO: add typescript-eslint, import/order, ban raw defineEventHandler

42
src/i18n/locales/en.json

@ -54,30 +54,17 @@
"body": "Body must be a valid object", "body": "Body must be a valid object",
"device": "Device must be a valid string", "device": "Device must be a valid string",
"deviceMin": "Device must be at least 1 Character", "deviceMin": "Device must be at least 1 Character",
"hook": "Hook must be a valid string",
"client": { "client": {
"id": "Client ID must be a valid number", "id": "Client ID must be a valid number",
"name": "Name must be a valid string", "name": "Name must be a valid string",
"nameMin": "Name must be at least 1 Character", "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", "expireDate": "expiredDate must be a valid string",
"expireDateMin": "expiredDate must be at least 1 Character", "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", "address4": "IPv4 Address must be a valid string",
"address4Min": "IPv4 Address must be a be at least 1 Character", "address4Min": "IPv4 Address must be a be at least 1 Character",
"address6": "IPv6 Address must be a valid string", "address6": "IPv6 Address must be a valid string",
"address6Min": "IPv6 Address must be a be at least 1 Character", "address6Min": "IPv6 Address must be a be at least 1 Character",
"allowedIps": "Allowed IPs must be a valid array of strings", "serverAllowedIps": "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"
}, },
"user": { "user": {
"username": "Username must be a valid string", "username": "Username must be a valid string",
@ -93,14 +80,31 @@
}, },
"userConfig": { "userConfig": {
"host": "Host must be a valid string", "host": "Host must be a valid string",
"hostMin": "Host must contain at least 1 character", "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"
}, },
"general": { "general": {
"sessionTimeout": "Session Timeout must be a valid number" "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", "name": "Name",
"username": "Username", "username": "Username",

2
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(); const sessionConfig = await Database.general.getSessionConfig();
return { return {
sessionTimeout: sessionConfig.sessionTimeout, sessionTimeout: sessionConfig.sessionTimeout,

7
src/server/api/admin/general.post.ts

@ -1,10 +1,13 @@
import { GeneralUpdateSchema } from '#db/repositories/general/types'; import { GeneralUpdateSchema } from '#db/repositories/general/types';
export default defineEventHandler(async (event) => { export default definePermissionEventHandler(
actions.ADMIN,
async ({ event }) => {
const data = await readValidatedBody( const data = await readValidatedBody(
event, event,
validateZod(GeneralUpdateSchema, event) validateZod(GeneralUpdateSchema, event)
); );
await Database.general.update(data); await Database.general.update(data);
return { success: true }; return { success: true };
}); }
);

2
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'); const hooks = await Database.hooks.get('wg0');
if (!hooks) { if (!hooks) {
throw new Error('Hooks not found'); throw new Error('Hooks not found');

13
src/server/api/admin/hooks.post.ts

@ -1,9 +1,14 @@
export default defineEventHandler(async (event) => { import { HooksUpdateSchema } from '#db/repositories/hooks/types';
export default definePermissionEventHandler(
actions.ADMIN,
async ({ event }) => {
const data = await readValidatedBody( const data = await readValidatedBody(
event, event,
validateZod(hooksUpdateType, event) validateZod(HooksUpdateSchema, event)
); );
await Database.hooks.update(data); await Database.hooks.update('wg0', data);
await WireGuard.saveConfig(); await WireGuard.saveConfig();
return { success: true }; return { success: true };
}); }
);

7
src/server/api/admin/interface.get.ts

@ -1,7 +0,0 @@
export default defineEventHandler(async () => {
const wgInterface = await Database.interfaces.get('wg0');
return {
...wgInterface,
privateKey: undefined,
};
});

9
src/server/api/admin/interface.post.ts

@ -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 };
});

15
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 };
}
);

12
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,
};
});

14
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 };
}
);

7
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;
});

14
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 };
}
);

9
src/server/api/admin/userconfig/cidr.post.ts

@ -1,9 +0,0 @@
export default defineEventHandler(async (event) => {
const data = await readValidatedBody(
event,
validateZod(cidrUpdateType, event)
);
await WireGuard.updateAddressRange(data);
return { success: true };
});

4
src/server/api/admin/userconfig/index.get.ts

@ -1,4 +0,0 @@
export default defineEventHandler(async () => {
const system = await Database.system.get();
return system.userConfig;
});

9
src/server/api/admin/userconfig/index.post.ts

@ -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 };
});

10
src/server/api/setup/4.post.ts

@ -1,14 +1,6 @@
import { UserSetupType } from '#db/repositories/user/types'; import { UserSetupType } from '#db/repositories/user/types';
export default defineEventHandler(async (event) => { export default defineSetupEventHandler(async ({ event }) => {
const { done } = await Database.general.getSetupStep();
if (done) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid state',
});
}
const { username, password } = await readValidatedBody( const { username, password } = await readValidatedBody(
event, event,
validateZod(UserSetupType, event) validateZod(UserSetupType, event)

10
src/server/api/setup/5.post.ts

@ -1,14 +1,6 @@
import { UserConfigSetupType } from '#db/repositories/userConfig/types'; import { UserConfigSetupType } from '#db/repositories/userConfig/types';
export default defineEventHandler(async (event) => { export default defineSetupEventHandler(async ({ event }) => {
const { done } = await Database.general.getSetupStep();
if (done) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid state',
});
}
const { host, port } = await readValidatedBody( const { host, port } = await readValidatedBody(
event, event,
validateZod(UserConfigSetupType, event) validateZod(UserConfigSetupType, event)

9
src/server/api/setup/migrate.post.ts

@ -2,14 +2,7 @@
import { stringifyIp } from 'ip-bigint'; import { stringifyIp } from 'ip-bigint';
import { z } from 'zod';*/ import { z } from 'zod';*/
export default defineEventHandler(async (/*event*/) => { export default defineSetupEventHandler(async (/*{ event }*/) => {
const { done } = await Database.general.getSetupStep();
if (done) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid state',
});
}
// TODO: Implement // TODO: Implement
/* /*

4
src/server/database/repositories/client/service.ts

@ -2,8 +2,8 @@ import type { DBType } from '#db/sqlite';
import { eq, sql } from 'drizzle-orm'; import { eq, sql } from 'drizzle-orm';
import { client } from './schema'; import { client } from './schema';
import type { ClientCreateType, UpdateClientType } from './types'; import type { ClientCreateType, UpdateClientType } from './types';
import type { ID } from '../../schema'; import type { ID } from '#db/schema';
import { wgInterface, userConfig } from '../../schema'; import { wgInterface, userConfig } from '#db/schema';
import { parseCidr } from 'cidr-tools'; import { parseCidr } from 'cidr-tools';
function createPreparedStatement(db: DBType) { function createPreparedStatement(db: DBType) {

44
src/server/database/repositories/client/types.ts

@ -3,13 +3,6 @@ import z from 'zod';
import type { client } from './schema'; import type { client } from './schema';
const schemaForType =
<T>() =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
<S extends z.ZodType<T, any, any>>(arg: S) => {
return arg;
};
export type ID = string; export type ID = string;
export type ClientType = InferSelectModel<typeof client>; export type ClientType = InferSelectModel<typeof client>;
@ -35,11 +28,6 @@ const expiresAt = z
.pipe(safeStringRefine) .pipe(safeStringRefine)
.nullable(); .nullable();
const address = z
.string({ message: 'zod.client.address' })
.min(1, { message: 'zod.client.addressMin' })
.pipe(safeStringRefine);
const address4 = z const address4 = z
.string({ message: 'zod.client.address4' }) .string({ message: 'zod.client.address4' })
.min(1, { message: 'zod.client.address4Min' }) .min(1, { message: 'zod.client.address4Min' })
@ -50,30 +38,10 @@ const address6 = z
.min(1, { message: 'zod.client.address6Min' }) .min(1, { message: 'zod.client.address6Min' })
.pipe(safeStringRefine); .pipe(safeStringRefine);
const allowedIps = z const serverAllowedIps = z.array(AddressSchema, {
.array(address, { message: 'zod.client.allowedIps' })
.min(1, { message: 'zod.client.allowedIpsMin' });
const serverAllowedIps = z.array(address, {
message: 'zod.serverAllowedIps', 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({ export const ClientCreateSchema = z.object({
name: name, name: name,
expiresAt: expiresAt, expiresAt: expiresAt,
@ -84,15 +52,15 @@ export type ClientCreateType = z.infer<typeof ClientCreateSchema>;
export const ClientUpdateSchema = schemaForType<UpdateClientType>()( export const ClientUpdateSchema = schemaForType<UpdateClientType>()(
z.object({ z.object({
name: name, name: name,
enabled: enabled, enabled: EnabledSchema,
expiresAt: expiresAt, expiresAt: expiresAt,
ipv4Address: address4, ipv4Address: address4,
ipv6Address: address6, ipv6Address: address6,
allowedIps: allowedIps, allowedIps: AllowedIpsSchema,
serverAllowedIps: serverAllowedIps, serverAllowedIps: serverAllowedIps,
mtu: mtu, mtu: MtuSchema,
persistentKeepalive: persistentKeepalive, persistentKeepalive: PersistentKeepaliveSchema,
dns: dns, dns: DnsSchema,
}) })
); );

2
src/server/database/repositories/general/types.ts

@ -11,3 +11,5 @@ export const GeneralUpdateSchema = z.object({
}); });
export type GeneralUpdateType = z.infer<typeof GeneralUpdateSchema>; export type GeneralUpdateType = z.infer<typeof GeneralUpdateSchema>;
export type SetupStepType = { step: number; done: boolean };

15
src/server/database/repositories/hooks/service.ts

@ -1,6 +1,7 @@
import type { DBType } from '#db/sqlite'; import type { DBType } from '#db/sqlite';
import { eq, sql } from 'drizzle-orm'; import { eq, sql } from 'drizzle-orm';
import { hooks } from './schema'; import { hooks } from './schema';
import type { HooksUpdateType } from './types';
function createPreparedStatement(db: DBType) { function createPreparedStatement(db: DBType) {
return { return {
@ -11,13 +12,23 @@ function createPreparedStatement(db: DBType) {
} }
export class HooksService { export class HooksService {
#db: DBType;
#statements: ReturnType<typeof createPreparedStatement>; #statements: ReturnType<typeof createPreparedStatement>;
constructor(db: DBType) { constructor(db: DBType) {
this.#db = db;
this.#statements = createPreparedStatement(db); this.#statements = createPreparedStatement(db);
} }
get(wgInterface: string) { get(infName: string) {
return this.#statements.get.execute({ interface: wgInterface }); 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();
} }
} }

14
src/server/database/repositories/hooks/types.ts

@ -1,4 +1,18 @@
import type { InferSelectModel } from 'drizzle-orm'; import type { InferSelectModel } from 'drizzle-orm';
import type { hooks } from './schema'; import type { hooks } from './schema';
import z from 'zod';
export type HooksType = InferSelectModel<typeof hooks>; export type HooksType = InferSelectModel<typeof hooks>;
export type HooksUpdateType = Omit<HooksType, 'id' | 'createdAt' | 'updatedAt'>;
const hook = z.string({ message: 'zod.hook' }).pipe(safeStringRefine);
export const HooksUpdateSchema = schemaForType<HooksUpdateType>()(
z.object({
preUp: hook,
postUp: hook,
preDown: hook,
postDown: hook,
})
);

46
src/server/database/repositories/interface/service.ts

@ -1,6 +1,10 @@
import type { DBType } from '#db/sqlite'; import type { DBType } from '#db/sqlite';
import isCidr from 'is-cidr';
import { eq, sql } from 'drizzle-orm'; import { eq, sql } from 'drizzle-orm';
import { wgInterface } from './schema'; 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) { function createPreparedStatement(db: DBType) {
return { return {
@ -20,9 +24,11 @@ function createPreparedStatement(db: DBType) {
} }
export class InterfaceService { export class InterfaceService {
#db: DBType;
#statements: ReturnType<typeof createPreparedStatement>; #statements: ReturnType<typeof createPreparedStatement>;
constructor(db: DBType) { constructor(db: DBType) {
this.#db = db;
this.#statements = createPreparedStatement(db); this.#statements = createPreparedStatement(db);
} }
@ -41,4 +47,44 @@ export class InterfaceService {
publicKey, 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();
}
});
}
} }

45
src/server/database/repositories/interface/types.ts

@ -1,4 +1,49 @@
import type { InferSelectModel } from 'drizzle-orm'; import type { InferSelectModel } from 'drizzle-orm';
import type { wgInterface } from './schema'; import type { wgInterface } from './schema';
import z from 'zod';
export type InterfaceType = InferSelectModel<typeof wgInterface>; export type InterfaceType = InferSelectModel<typeof wgInterface>;
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<InterfaceUpdateType>()(
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<InterfaceCidrUpdateType>()(
z.object({
ipv4Cidr: cidr,
ipv6Cidr: cidr,
})
);

21
src/server/database/repositories/userConfig/service.ts

@ -1,6 +1,7 @@
import type { DBType } from '#db/sqlite'; import type { DBType } from '#db/sqlite';
import { eq, sql } from 'drizzle-orm'; import { eq, sql } from 'drizzle-orm';
import { userConfig } from './schema'; import { userConfig } from './schema';
import type { UserConfigUpdateType } from './types';
function createPreparedStatement(db: DBType) { function createPreparedStatement(db: DBType) {
return { return {
@ -19,21 +20,31 @@ function createPreparedStatement(db: DBType) {
} }
export class UserConfigService { export class UserConfigService {
#db: DBType;
#statements: ReturnType<typeof createPreparedStatement>; #statements: ReturnType<typeof createPreparedStatement>;
constructor(db: DBType) { constructor(db: DBType) {
this.#db = db;
this.#statements = createPreparedStatement(db); this.#statements = createPreparedStatement(db);
} }
async get(wgInterface: string) { get(infName: string) {
return await this.#statements.get.execute({ interface: wgInterface }); return this.#statements.get.execute({ interface: infName });
} }
async updateHostPort(wgInterface: string, host: string, port: number) { updateHostPort(infName: string, host: string, port: number) {
return await this.#statements.updateHostPort.execute({ return this.#statements.updateHostPort.execute({
interface: wgInterface, interface: infName,
host, host,
port, port,
}); });
} }
update(infName: string, data: UserConfigUpdateType) {
return this.#db
.update(userConfig)
.set(data)
.where(eq(userConfig.id, infName))
.execute();
}
} }

23
src/server/database/repositories/userConfig/types.ts

@ -9,12 +9,23 @@ const host = z
.min(1, 'zod.userConfig.hostMin') .min(1, 'zod.userConfig.hostMin')
.pipe(safeStringRefine); .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({ export const UserConfigSetupType = z.object({
host: host, host: host,
port: port, port: PortSchema,
}); });
export type UserConfigUpdateType = Omit<
UserConfigType,
'id' | 'createdAt' | 'updatedAt'
>;
export const UserConfigUpdateSchema = schemaForType<UserConfigUpdateType>()(
z.object({
port: PortSchema,
defaultMtu: MtuSchema,
defaultPersistentKeepalive: PersistentKeepaliveSchema,
defaultDns: DnsSchema,
defaultAllowedIps: AllowedIpsSchema,
host: host,
})
);

1
src/server/database/schema.ts

@ -8,4 +8,5 @@ export * from './repositories/oneTimeLink/schema';
export * from './repositories/user/schema'; export * from './repositories/user/schema';
export * from './repositories/userConfig/schema'; export * from './repositories/userConfig/schema';
// TODO: move to types
export type ID = number; export type ID = number;

32
src/server/utils/handler.ts

@ -1,11 +1,15 @@
import type { EventHandlerRequest, EventHandlerResponse, H3Event } from 'h3'; import type { EventHandlerRequest, EventHandlerResponse, H3Event } from 'h3';
import type { UserType } from '#db/repositories/user/types'; import type { UserType } from '#db/repositories/user/types';
import type { SetupStepType } from '../database/repositories/general/types';
type PermissionHandler< type PermissionHandler<
TReq extends EventHandlerRequest, TReq extends EventHandlerRequest,
TRes extends EventHandlerResponse, TRes extends EventHandlerResponse,
> = { (params: { event: H3Event<TReq>; user: UserType }): TRes }; > = { (params: { event: H3Event<TReq>; user: UserType }): TRes };
/**
* check if the user has the permission to perform the action
*/
export const definePermissionEventHandler = < export const definePermissionEventHandler = <
TReq extends EventHandlerRequest, TReq extends EventHandlerRequest,
TRes extends EventHandlerResponse, TRes extends EventHandlerResponse,
@ -25,3 +29,31 @@ export const definePermissionEventHandler = <
return await handler({ event, user }); return await handler({ event, user });
}); });
}; };
type SetupHandler<
TReq extends EventHandlerRequest,
TRes extends EventHandlerResponse,
> = { (params: { event: H3Event<TReq>; 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<TReq, TRes>
) => {
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 });
});
};

37
src/server/utils/types.ts

@ -13,6 +13,43 @@ export const safeStringRefine = z
// TODO: create custom getValidatedRouterParams and readValidatedBody wrapper // 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 =
<T>() =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
<S extends z.ZodType<T, any, any>>(arg: S) => {
return arg;
};
export function validateZod<T>( export function validateZod<T>(
schema: ZodSchema<T>, schema: ZodSchema<T>,
event?: H3Event<EventHandlerRequest> event?: H3Event<EventHandlerRequest>

2
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 = { export const actions = {
ADMIN: 'ADMIN', ADMIN: 'ADMIN',

Loading…
Cancel
Save