Browse Source

Migrate to sqlite

pull/1619/head
Bernd Storath 3 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. 19
      src/server/api/admin/general.post.ts
  8. 2
      src/server/api/admin/hooks.get.ts
  9. 23
      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"
>
<FormGroup>
<FormTextField id="address4" v-model="address4" label="IPv4" />
<FormTextField id="address6" v-model="address6" label="IPv6" />
<FormTextField id="ipv4Cidr" v-model="ipv4Cidr" label="IPv4" />
<FormTextField id="ipv6Cidr" v-model="ipv6Cidr" label="IPv6" />
</FormGroup>
</DialogDescription>
<div class="mt-6 flex justify-end gap-2">
@ -26,7 +26,7 @@
<BaseButton>{{ $t('cancel') }}</BaseButton>
</DialogClose>
<DialogClose as-child>
<BaseButton @click="$emit('change', address4, address6)"
<BaseButton @click="$emit('change', ipv4Cidr, ipv6Cidr)"
>Change</BaseButton
>
</DialogClose>
@ -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);
</script>

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

@ -8,7 +8,10 @@
</FormGroup>
<FormGroup>
<FormHeading>Allowed IPs</FormHeading>
<FormArrayField v-model="data.allowedIps" name="allowedIps" />
<FormArrayField
v-model="data.defaultAllowedIps"
name="defaultAllowedIps"
/>
</FormGroup>
<FormGroup>
<FormHeading>DNS</FormHeading>
@ -16,10 +19,14 @@
</FormGroup>
<FormGroup>
<FormHeading>Advanced</FormHeading>
<FormNumberField id="mtu" v-model="data.mtu" label="MTU" />
<FormNumberField
id="keepalive"
v-model="data.persistentKeepalive"
id="defaultMtu"
v-model="data.defaultMtu"
label="MTU"
/>
<FormNumberField
id="defaultPersistentKeepalive"
v-model="data.defaultPersistentKeepalive"
label="Persistent Keepalive"
/>
</FormGroup>
@ -27,14 +34,6 @@
<FormHeading>Actions</FormHeading>
<FormActionField type="submit" label="Save" />
<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>
</FormElement>
</main>
@ -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,
});
}
}
}
</script>

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

@ -11,6 +11,14 @@
<FormHeading>Actions</FormHeading>
<FormActionField type="submit" label="Save" />
<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>
</FormElement>
</main>
@ -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,
});
}
}
}
</script>

2
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

42
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",

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();
return {
sessionTimeout: sessionConfig.sessionTimeout,

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

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');
if (!hooks) {
throw new Error('Hooks not found');

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

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

10
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)

9
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
/*

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 { 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) {

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

@ -3,13 +3,6 @@ import z from 'zod';
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 ClientType = InferSelectModel<typeof client>;
@ -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<typeof ClientCreateSchema>;
export const ClientUpdateSchema = schemaForType<UpdateClientType>()(
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,
})
);

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 SetupStepType = { step: number; done: boolean };

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

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

45
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<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 { 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<typeof createPreparedStatement>;
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();
}
}

23
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<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/userConfig/schema';
// TODO: move to types
export type ID = number;

32
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<TReq>; 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<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
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>(
schema: ZodSchema<T>,
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 = {
ADMIN: 'ADMIN',

Loading…
Cancel
Save