Browse Source

migrate to sqlite

pull/1619/head
Bernd Storath 3 months ago
parent
commit
40bbc27331
  1. 8
      src/app/pages/admin/hooks.vue
  2. 12
      src/app/pages/clients/[id].vue
  3. 6
      src/server/api/wireguard/backup.get.ts
  4. 6
      src/server/api/wireguard/restore.put.ts
  5. 36
      src/server/database/repositories/client/types.ts
  6. 31
      src/server/database/repositories/metrics/service.ts
  7. 10
      src/server/database/repositories/oneTimeLink/service.ts
  8. 13
      src/server/database/repositories/oneTimeLink/types.ts
  9. 13
      src/server/database/repositories/user/types.ts
  10. 7
      src/server/database/repositories/userConfig/types.ts
  11. 4
      src/server/database/sqlite.ts
  12. 6
      src/server/routes/cnf/[oneTimeLink].ts
  13. 6
      src/server/routes/metrics/index.get.ts
  14. 6
      src/server/routes/metrics/json.get.ts
  15. 73
      src/server/utils/WireGuard.ts
  16. 74
      src/server/utils/metrics.ts
  17. 4
      src/server/utils/types.ts

8
src/app/pages/admin/hooks.vue

@ -2,10 +2,10 @@
<main v-if="data">
<FormElement @submit.prevent="submit">
<FormGroup>
<FormTextField id="PreUp" v-model="data.PreUp" label="PreUp" />
<FormTextField id="PostUp" v-model="data.PostUp" label="PostUp" />
<FormTextField id="PreDown" v-model="data.PreDown" label="PreDown" />
<FormTextField id="PostDown" v-model="data.PostDown" label="PostDown" />
<FormTextField id="PreUp" v-model="data.preUp" label="PreUp" />
<FormTextField id="PostUp" v-model="data.postUp" label="PostUp" />
<FormTextField id="PreDown" v-model="data.preDown" label="PreDown" />
<FormTextField id="PostDown" v-model="data.postDown" label="PostDown" />
</FormGroup>
<FormGroup>
<FormHeading>Actions</FormHeading>

12
src/app/pages/clients/[id].vue

@ -25,13 +25,13 @@
<FormGroup>
<FormHeading>Address</FormHeading>
<FormTextField
id="address4"
v-model.trim="data.address4"
id="ipv4Address"
v-model.trim="data.ipv4Address"
label="IPv4"
/>
<FormTextField
id="address6"
v-model.trim="data.address6"
id="ipv6Address"
v-model.trim="data.ipv6Address"
label="IPv6"
/>
</FormGroup>
@ -42,8 +42,8 @@
<FormGroup>
<FormHeading>Server Allowed IPs</FormHeading>
<FormArrayField
v-model="data.serverAllowedIPs"
name="serverAllowedIPs"
v-model="data.serverAllowedIps"
name="serverAllowedIps"
/>
</FormGroup>
<FormGroup></FormGroup>

6
src/server/api/wireguard/backup.get.ts

@ -1,9 +1,9 @@
export default definePermissionEventHandler(
actions.ADMIN,
async ({ event }) => {
const config = await WireGuard.backupConfiguration();
async (/*{ event }*/) => {
/*const config = await WireGuard.backupConfiguration();
setHeader(event, 'Content-Disposition', 'attachment; filename="wg0.json"');
setHeader(event, 'Content-Type', 'text/json');
return config;
return config;*/
}
);

6
src/server/api/wireguard/restore.put.ts

@ -1,8 +1,8 @@
export default definePermissionEventHandler(
actions.ADMIN,
async ({ event }) => {
const { file } = await readValidatedBody(event, validateZod(fileType));
async (/*{ event }*/) => {
/*const { file } = await readValidatedBody(event, validateZod(fileType));
await WireGuard.restoreConfiguration(file);
return { success: true };
return { success: true };*/
}
);

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

@ -1,12 +1,12 @@
import type { InferSelectModel } from 'drizzle-orm';
import { zod } from '#imports';
import z from 'zod';
import type { client } from './schema';
const schemaForType =
<T>() =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
<S extends zod.ZodType<T, any, any>>(arg: S) => {
<S extends z.ZodType<T, any, any>>(arg: S) => {
return arg;
};
@ -24,65 +24,65 @@ export type UpdateClientType = Omit<
'privateKey' | 'publicKey' | 'preSharedKey'
>;
const name = zod
const name = z
.string({ message: 'zod.client.name' })
.min(1, 'zod.client.nameMin')
.pipe(safeStringRefine);
const expiresAt = zod
const expiresAt = z
.string({ message: 'zod.client.expireDate' })
.min(1, 'zod.client.expireDateMin')
.pipe(safeStringRefine)
.nullable();
const address = zod
const address = z
.string({ message: 'zod.client.address' })
.min(1, { message: 'zod.client.addressMin' })
.pipe(safeStringRefine);
const address4 = zod
const address4 = z
.string({ message: 'zod.client.address4' })
.min(1, { message: 'zod.client.address4Min' })
.pipe(safeStringRefine);
const address6 = zod
const address6 = z
.string({ message: 'zod.client.address6' })
.min(1, { message: 'zod.client.address6Min' })
.pipe(safeStringRefine);
const allowedIps = zod
const allowedIps = z
.array(address, { message: 'zod.client.allowedIps' })
.min(1, { message: 'zod.client.allowedIpsMin' });
const serverAllowedIps = zod.array(address, {
const serverAllowedIps = z.array(address, {
message: 'zod.serverAllowedIps',
});
const mtu = zod
const mtu = z
.number({ message: 'zod.client.mtu' })
.min(1280, { message: 'zod.client.mtuMin' })
.max(9000, { message: 'zod.client.mtuMax' });
const persistentKeepalive = zod
const persistentKeepalive = z
.number({ message: 'zod.client.persistentKeepalive' })
.min(0, 'zod.client.persistentKeepaliveMin')
.max(65535, 'zod.client.persistentKeepaliveMax');
const enabled = zod.boolean({ message: 'zod.enabled' });
const enabled = z.boolean({ message: 'zod.enabled' });
const dns = zod
const dns = z
.array(address, { message: 'zod.client.dns' })
.min(1, 'zod.client.dnsMin');
export const ClientCreateSchema = zod.object({
export const ClientCreateSchema = z.object({
name: name,
expiresAt: expiresAt,
});
export type ClientCreateType = zod.infer<typeof ClientCreateSchema>;
export type ClientCreateType = z.infer<typeof ClientCreateSchema>;
export const ClientUpdateSchema = schemaForType<UpdateClientType>()(
zod.object({
z.object({
name: name,
enabled: enabled,
expiresAt: expiresAt,
@ -96,8 +96,8 @@ export const ClientUpdateSchema = schemaForType<UpdateClientType>()(
})
);
const clientId = zod.number({ message: 'zod.client.id' });
const clientId = z.number({ message: 'zod.client.id' });
export const ClientGetSchema = zod.object({
export const ClientGetSchema = z.object({
clientId: clientId,
});

31
src/server/database/repositories/metrics/service.ts

@ -0,0 +1,31 @@
import type { DBType } from '#db/sqlite';
import { eq, sql } from 'drizzle-orm';
import { prometheus } from './schema';
function createPreparedStatement(db: DBType) {
return {
get: db.query.prometheus
.findFirst({ where: eq(prometheus.id, sql.placeholder('interface')) })
.prepare(),
};
}
export class PrometheusService {
#statements: ReturnType<typeof createPreparedStatement>;
constructor(db: DBType) {
this.#statements = createPreparedStatement(db);
}
get(infName: string) {
return this.#statements.get.execute({ interface: infName });
}
}
export class MetricsService {
prometheus: PrometheusService;
constructor(db: DBType) {
this.prometheus = new PrometheusService(db);
}
}

10
src/server/database/repositories/oneTimeLink/service.ts

@ -18,6 +18,11 @@ function createPreparedStatement(db: DBType) {
expiresAt: sql.placeholder('expiresAt'),
})
.prepare(),
erase: db
.update(oneTimeLink)
.set({ expiresAt: sql.placeholder('expiresAt') as never as string })
.where(eq(oneTimeLink.clientId, sql.placeholder('id')))
.prepare(),
};
}
@ -39,4 +44,9 @@ export class OneTimeLinkService {
return this.#statements.create.execute({ id, oneTimeLink, expiresAt });
}
erase(id: ID) {
const expiresAt = Date.now() + 10 * 1000;
return this.#statements.erase.execute({ id, expiresAt });
}
}

13
src/server/database/repositories/oneTimeLink/types.ts

@ -1,4 +1,17 @@
import type { InferSelectModel } from 'drizzle-orm';
import type { oneTimeLink } from './schema';
import { z } from 'zod';
export type OneTimeLinkType = InferSelectModel<typeof oneTimeLink>;
const oneTimeLinkType = z
.string({ message: 'zod.otl' })
.min(1, 'zod.otlMin')
.pipe(safeStringRefine);
export const OneTimeLinkGetSchema = z.object(
{
oneTimeLink: oneTimeLinkType,
},
{ message: objectMessage }
);

13
src/server/database/repositories/user/types.ts

@ -1,14 +1,15 @@
import type { InferSelectModel } from 'drizzle-orm';
import type { user } from './schema';
import z from 'zod';
export type UserType = InferSelectModel<typeof user>;
const username = zod
const username = z
.string({ message: 'zod.user.username' })
.min(8, 'zod.user.usernameMin')
.pipe(safeStringRefine);
const password = zod
const password = z
.string({ message: 'zod.user.password' })
.min(12, 'zod.user.passwordMin')
.regex(/[A-Z]/, 'zod.user.passwordUppercase')
@ -17,9 +18,9 @@ const password = zod
.regex(/[!@#$%^&*(),.?":{}|<>]/, 'zod.user.passwordSpecial')
.pipe(safeStringRefine);
const remember = zod.boolean({ message: 'zod.user.remember' });
const remember = z.boolean({ message: 'zod.user.remember' });
export const UserLoginSchema = zod.object(
export const UserLoginSchema = z.object(
{
username: username,
password: password,
@ -28,11 +29,11 @@ export const UserLoginSchema = zod.object(
{ message: objectMessage }
);
const accept = zod.boolean().refine((val) => val === true, {
const accept = z.boolean().refine((val) => val === true, {
message: 'zod.user.accept',
});
export const UserSetupType = zod.object(
export const UserSetupType = z.object(
{
username: username,
password: password,

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

@ -1,19 +1,20 @@
import type { InferSelectModel } from 'drizzle-orm';
import type { userConfig } from './schema';
import z from 'zod';
export type UserConfigType = InferSelectModel<typeof userConfig>;
const host = zod
const host = z
.string({ message: 'zod.userConfig.host' })
.min(1, 'zod.userConfig.hostMin')
.pipe(safeStringRefine);
const port = zod
const port = z
.number({ message: 'zod.userConfig.port' })
.min(1, 'zod.userConfig.portMin')
.max(65535, 'zod.userConfig.portMax');
export const UserConfigSetupType = zod.object({
export const UserConfigSetupType = z.object({
host: host,
port: port,
});

4
src/server/database/sqlite.ts

@ -11,6 +11,7 @@ import { UserConfigService } from './repositories/userConfig/service';
import { InterfaceService } from './repositories/interface/service';
import { HooksService } from './repositories/hooks/service';
import { OneTimeLinkService } from './repositories/oneTimeLink/service';
import { MetricsService } from './repositories/metrics/service';
const DB_DEBUG = debug('Database');
@ -30,6 +31,8 @@ class DBService {
interfaces: InterfaceService;
hooks: HooksService;
oneTimeLinks: OneTimeLinkService;
metrics: MetricsService;
constructor(db: DBType) {
this.clients = new ClientService(db);
this.general = new GeneralService(db);
@ -38,6 +41,7 @@ class DBService {
this.interfaces = new InterfaceService(db);
this.hooks = new HooksService(db);
this.oneTimeLinks = new OneTimeLinkService(db);
this.metrics = new MetricsService(db);
}
}

6
src/server/routes/cnf/[oneTimeLink].ts

@ -1,7 +1,9 @@
import { OneTimeLinkGetSchema } from '#db/repositories/oneTimeLink/types';
export default defineEventHandler(async (event) => {
const { oneTimeLink } = await getValidatedRouterParams(
event,
validateZod(oneTimeLinkType)
validateZod(OneTimeLinkGetSchema)
);
const clients = await WireGuard.getClients();
const client = clients.find(
@ -15,7 +17,7 @@ export default defineEventHandler(async (event) => {
}
const clientId = client.id;
const config = await WireGuard.getClientConfiguration({ clientId });
await WireGuard.eraseOneTimeLink({ clientId });
await Database.oneTimeLinks.erase(clientId);
setHeader(
event,
'Content-Disposition',

6
src/server/routes/metrics/index.get.ts

@ -1,8 +1,8 @@
export default defineEventHandler(async (event) => {
// TODO: check password
const system = await Database.system.get();
if (!system.metrics.prometheus.enabled) {
const prometheus = await Database.metrics.prometheus.get('wg0');
if (!prometheus) {
throw createError({
statusCode: 400,
message: 'Prometheus metrics are not enabled',
@ -10,5 +10,5 @@ export default defineEventHandler(async (event) => {
}
setHeader(event, 'Content-Type', 'text/plain');
return WireGuard.getMetrics();
return getPrometheusResponse();
});

6
src/server/routes/metrics/json.get.ts

@ -1,13 +1,13 @@
export default defineEventHandler(async () => {
// TODO: check password
const system = await Database.system.get();
if (!system.metrics.prometheus.enabled) {
const prometheus = await Database.metrics.prometheus.get('wg0');
if (!prometheus) {
throw createError({
statusCode: 400,
message: 'Prometheus metrics are not enabled',
});
}
return WireGuard.getMetricsJSON();
return getMetricsJSON();
});

73
src/server/utils/WireGuard.ts

@ -217,79 +217,6 @@ class WireGuard {
await this.saveConfig();
}
async getMetrics() {
const clients = await this.getClients();
let wireguardPeerCount = 0;
let wireguardEnabledPeersCount = 0;
let wireguardConnectedPeersCount = 0;
let wireguardSentBytes = '';
let wireguardReceivedBytes = '';
let wireguardLatestHandshakeSeconds = '';
for (const client of clients) {
wireguardPeerCount++;
if (client.enabled === true) {
wireguardEnabledPeersCount++;
}
if (client.endpoint !== null) {
wireguardConnectedPeersCount++;
}
wireguardSentBytes += `wireguard_sent_bytes{interface="wg0",enabled="${client.enabled}",ipv4Address="${client.ipv4Address}",ipv6Address="${client.ipv6Address}",name="${client.name}"} ${Number(client.transferTx)}\n`;
wireguardReceivedBytes += `wireguard_received_bytes{interface="wg0",enabled="${client.enabled}",ipv4Address="${client.ipv4Address}",ipv6Address="${client.ipv6Address}",name="${client.name}"} ${Number(client.transferRx)}\n`;
wireguardLatestHandshakeSeconds += `wireguard_latest_handshake_seconds{interface="wg0",enabled="${client.enabled}",ipv4Address="${client.ipv4Address}",ipv6Address="${client.ipv6Address}",name="${client.name}"} ${client.latestHandshakeAt ? (new Date().getTime() - new Date(client.latestHandshakeAt).getTime()) / 1000 : 0}\n`;
}
let returnText = '# HELP wg-easy and wireguard metrics\n';
returnText += '\n# HELP wireguard_configured_peers\n';
returnText += '# TYPE wireguard_configured_peers gauge\n';
returnText += `wireguard_configured_peers{interface="wg0"} ${wireguardPeerCount}\n`;
returnText += '\n# HELP wireguard_enabled_peers\n';
returnText += '# TYPE wireguard_enabled_peers gauge\n';
returnText += `wireguard_enabled_peers{interface="wg0"} ${wireguardEnabledPeersCount}\n`;
returnText += '\n# HELP wireguard_connected_peers\n';
returnText += '# TYPE wireguard_connected_peers gauge\n';
returnText += `wireguard_connected_peers{interface="wg0"} ${wireguardConnectedPeersCount}\n`;
returnText += '\n# HELP wireguard_sent_bytes Bytes sent to the peer\n';
returnText += '# TYPE wireguard_sent_bytes counter\n';
returnText += `${wireguardSentBytes}`;
returnText +=
'\n# HELP wireguard_received_bytes Bytes received from the peer\n';
returnText += '# TYPE wireguard_received_bytes counter\n';
returnText += `${wireguardReceivedBytes}`;
returnText +=
'\n# HELP wireguard_latest_handshake_seconds UNIX timestamp seconds of the last handshake\n';
returnText += '# TYPE wireguard_latest_handshake_seconds gauge\n';
returnText += `${wireguardLatestHandshakeSeconds}`;
return returnText;
}
async getMetricsJSON() {
const clients = await this.getClients();
let wireguardPeerCount = 0;
let wireguardEnabledPeersCount = 0;
let wireguardConnectedPeersCount = 0;
for (const client of clients) {
wireguardPeerCount++;
if (client.enabled === true) {
wireguardEnabledPeersCount++;
}
if (client.endpoint !== null) {
wireguardConnectedPeersCount++;
}
}
return {
wireguard_configured_peers: wireguardPeerCount,
wireguard_enabled_peers: wireguardEnabledPeersCount,
wireguard_connected_peers: wireguardConnectedPeersCount,
};
}
}
export default new WireGuard();

74
src/server/utils/metrics.ts

@ -0,0 +1,74 @@
// TODO: rewrite
export async function getPrometheusResponse() {
const clients = await WireGuard.getClients();
let wireguardPeerCount = 0;
let wireguardEnabledPeersCount = 0;
let wireguardConnectedPeersCount = 0;
let wireguardSentBytes = '';
let wireguardReceivedBytes = '';
let wireguardLatestHandshakeSeconds = '';
for (const client of clients) {
wireguardPeerCount++;
if (client.enabled === true) {
wireguardEnabledPeersCount++;
}
if (client.endpoint !== null) {
wireguardConnectedPeersCount++;
}
wireguardSentBytes += `wireguard_sent_bytes{interface="wg0",enabled="${client.enabled}",ipv4Address="${client.ipv4Address}",ipv6Address="${client.ipv6Address}",name="${client.name}"} ${Number(client.transferTx)}\n`;
wireguardReceivedBytes += `wireguard_received_bytes{interface="wg0",enabled="${client.enabled}",ipv4Address="${client.ipv4Address}",ipv6Address="${client.ipv6Address}",name="${client.name}"} ${Number(client.transferRx)}\n`;
wireguardLatestHandshakeSeconds += `wireguard_latest_handshake_seconds{interface="wg0",enabled="${client.enabled}",ipv4Address="${client.ipv4Address}",ipv6Address="${client.ipv6Address}",name="${client.name}"} ${client.latestHandshakeAt ? (new Date().getTime() - new Date(client.latestHandshakeAt).getTime()) / 1000 : 0}\n`;
}
let returnText = '# HELP wg-easy and wireguard metrics\n';
returnText += '\n# HELP wireguard_configured_peers\n';
returnText += '# TYPE wireguard_configured_peers gauge\n';
returnText += `wireguard_configured_peers{interface="wg0"} ${wireguardPeerCount}\n`;
returnText += '\n# HELP wireguard_enabled_peers\n';
returnText += '# TYPE wireguard_enabled_peers gauge\n';
returnText += `wireguard_enabled_peers{interface="wg0"} ${wireguardEnabledPeersCount}\n`;
returnText += '\n# HELP wireguard_connected_peers\n';
returnText += '# TYPE wireguard_connected_peers gauge\n';
returnText += `wireguard_connected_peers{interface="wg0"} ${wireguardConnectedPeersCount}\n`;
returnText += '\n# HELP wireguard_sent_bytes Bytes sent to the peer\n';
returnText += '# TYPE wireguard_sent_bytes counter\n';
returnText += `${wireguardSentBytes}`;
returnText +=
'\n# HELP wireguard_received_bytes Bytes received from the peer\n';
returnText += '# TYPE wireguard_received_bytes counter\n';
returnText += `${wireguardReceivedBytes}`;
returnText +=
'\n# HELP wireguard_latest_handshake_seconds UNIX timestamp seconds of the last handshake\n';
returnText += '# TYPE wireguard_latest_handshake_seconds gauge\n';
returnText += `${wireguardLatestHandshakeSeconds}`;
return returnText;
}
export async function getMetricsJSON() {
const clients = await WireGuard.getClients();
let wireguardPeerCount = 0;
let wireguardEnabledPeersCount = 0;
let wireguardConnectedPeersCount = 0;
for (const client of clients) {
wireguardPeerCount++;
if (client.enabled === true) {
wireguardEnabledPeersCount++;
}
if (client.endpoint !== null) {
wireguardConnectedPeersCount++;
}
}
return {
wireguard_configured_peers: wireguardPeerCount,
wireguard_enabled_peers: wireguardEnabledPeersCount,
wireguard_connected_peers: wireguardConnectedPeersCount,
};
}

4
src/server/utils/types.ts

@ -2,8 +2,6 @@ import type { ZodSchema } from 'zod';
import z from 'zod';
import type { H3Event, EventHandlerRequest } from 'h3';
export { default as zod } from 'zod';
export const objectMessage = 'zod.body';
export const safeStringRefine = z
@ -28,7 +26,7 @@ export function validateZod<T>(
return await schema.parseAsync(data);
} catch (error) {
let message = 'Unexpected Error';
if (error instanceof zod.ZodError) {
if (error instanceof z.ZodError) {
message = error.issues
.map((v) => {
let m = v.message;

Loading…
Cancel
Save