diff --git a/src/app/pages/admin/hooks.vue b/src/app/pages/admin/hooks.vue index 1566e8a6..a613b0f2 100644 --- a/src/app/pages/admin/hooks.vue +++ b/src/app/pages/admin/hooks.vue @@ -2,10 +2,10 @@
- - - - + + + + Actions diff --git a/src/app/pages/clients/[id].vue b/src/app/pages/clients/[id].vue index c7935b19..1f2fcabb 100644 --- a/src/app/pages/clients/[id].vue +++ b/src/app/pages/clients/[id].vue @@ -25,13 +25,13 @@ Address @@ -42,8 +42,8 @@ Server Allowed IPs diff --git a/src/server/api/wireguard/backup.get.ts b/src/server/api/wireguard/backup.get.ts index 9132c0e9..648b95e9 100644 --- a/src/server/api/wireguard/backup.get.ts +++ b/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;*/ } ); diff --git a/src/server/api/wireguard/restore.put.ts b/src/server/api/wireguard/restore.put.ts index 3b246d00..c7a95ba5 100644 --- a/src/server/api/wireguard/restore.put.ts +++ b/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 };*/ } ); diff --git a/src/server/database/repositories/client/types.ts b/src/server/database/repositories/client/types.ts index 45a5bd69..d6a1b820 100644 --- a/src/server/database/repositories/client/types.ts +++ b/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 = () => // eslint-disable-next-line @typescript-eslint/no-explicit-any - >(arg: S) => { + >(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; +export type ClientCreateType = z.infer; export const ClientUpdateSchema = schemaForType()( - zod.object({ + z.object({ name: name, enabled: enabled, expiresAt: expiresAt, @@ -96,8 +96,8 @@ export const ClientUpdateSchema = schemaForType()( }) ); -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, }); diff --git a/src/server/database/repositories/metrics/service.ts b/src/server/database/repositories/metrics/service.ts index e69de29b..38685749 100644 --- a/src/server/database/repositories/metrics/service.ts +++ b/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; + + 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); + } +} diff --git a/src/server/database/repositories/oneTimeLink/service.ts b/src/server/database/repositories/oneTimeLink/service.ts index 48b8d8d1..2854d67a 100644 --- a/src/server/database/repositories/oneTimeLink/service.ts +++ b/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 }); + } } diff --git a/src/server/database/repositories/oneTimeLink/types.ts b/src/server/database/repositories/oneTimeLink/types.ts index ea0ac729..0114deea 100644 --- a/src/server/database/repositories/oneTimeLink/types.ts +++ b/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; + +const oneTimeLinkType = z + .string({ message: 'zod.otl' }) + .min(1, 'zod.otlMin') + .pipe(safeStringRefine); + +export const OneTimeLinkGetSchema = z.object( + { + oneTimeLink: oneTimeLinkType, + }, + { message: objectMessage } +); diff --git a/src/server/database/repositories/user/types.ts b/src/server/database/repositories/user/types.ts index 2a1017e4..7b629365 100644 --- a/src/server/database/repositories/user/types.ts +++ b/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; -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, diff --git a/src/server/database/repositories/userConfig/types.ts b/src/server/database/repositories/userConfig/types.ts index 91a4721b..1358bf8c 100644 --- a/src/server/database/repositories/userConfig/types.ts +++ b/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; -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, }); diff --git a/src/server/database/sqlite.ts b/src/server/database/sqlite.ts index b6848214..738e1192 100644 --- a/src/server/database/sqlite.ts +++ b/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); } } diff --git a/src/server/routes/cnf/[oneTimeLink].ts b/src/server/routes/cnf/[oneTimeLink].ts index 9afbeed3..1b7c695b 100644 --- a/src/server/routes/cnf/[oneTimeLink].ts +++ b/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', diff --git a/src/server/routes/metrics/index.get.ts b/src/server/routes/metrics/index.get.ts index 6cd0d46d..427e84ec 100644 --- a/src/server/routes/metrics/index.get.ts +++ b/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(); }); diff --git a/src/server/routes/metrics/json.get.ts b/src/server/routes/metrics/json.get.ts index eaef7024..d9211241 100644 --- a/src/server/routes/metrics/json.get.ts +++ b/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(); }); diff --git a/src/server/utils/WireGuard.ts b/src/server/utils/WireGuard.ts index 8a56a158..86cd338e 100644 --- a/src/server/utils/WireGuard.ts +++ b/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(); diff --git a/src/server/utils/metrics.ts b/src/server/utils/metrics.ts new file mode 100644 index 00000000..9c007c66 --- /dev/null +++ b/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, + }; +} diff --git a/src/server/utils/types.ts b/src/server/utils/types.ts index b25a2e5c..ba3d365d 100644 --- a/src/server/utils/types.ts +++ b/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( 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;