import { eq, sql } from 'drizzle-orm'; import { containsCidr, parseCidr } from 'cidr-tools'; import { client } from './schema'; import type { ClientCreateFromExistingType, ClientCreateType, UpdateClientType, } from './types'; import type { DBType } from '#db/sqlite'; import { wgInterface, userConfig } from '#db/schema'; function createPreparedStatement(db: DBType) { return { findAll: db.query.client .findMany({ with: { oneTimeLink: true, }, }) .prepare(), findAllPublic: db.query.client .findMany({ with: { oneTimeLink: true, }, columns: { privateKey: false, preSharedKey: false, }, }) .prepare(), findById: db.query.client .findFirst({ where: eq(client.id, sql.placeholder('id')) }) .prepare(), findByUserId: db.query.client .findMany({ where: eq(client.userId, sql.placeholder('userId')), with: { oneTimeLink: true }, columns: { privateKey: false, preSharedKey: false, }, }) .prepare(), toggle: db .update(client) .set({ enabled: sql.placeholder('enabled') as never as boolean }) .where(eq(client.id, sql.placeholder('id'))) .prepare(), delete: db .delete(client) .where(eq(client.id, sql.placeholder('id'))) .prepare(), }; } export class ClientService { #db: DBType; #statements: ReturnType; constructor(db: DBType) { this.#db = db; this.#statements = createPreparedStatement(db); } async getForUser(userId: ID) { const result = await this.#statements.findByUserId.execute({ userId }); return result.map((row) => ({ ...row, createdAt: new Date(row.createdAt), updatedAt: new Date(row.updatedAt), })); } /** * Never return values directly from this function. Use {@link getAllPublic} instead. */ async getAll() { const result = await this.#statements.findAll.execute(); return result.map((row) => ({ ...row, createdAt: new Date(row.createdAt), updatedAt: new Date(row.updatedAt), })); } /** * Returns all clients without sensitive data */ async getAllPublic() { const result = await this.#statements.findAllPublic.execute(); return result.map((row) => ({ ...row, createdAt: new Date(row.createdAt), updatedAt: new Date(row.updatedAt), })); } get(id: ID) { return this.#statements.findById.execute({ id }); } async create({ name, expiresAt }: ClientCreateType) { const privateKey = await wg.generatePrivateKey(); const publicKey = await wg.getPublicKey(privateKey); const preSharedKey = await wg.generatePreSharedKey(); return this.#db.transaction(async (tx) => { const clients = await tx.query.client.findMany().execute(); const clientInterface = await tx.query.wgInterface .findFirst({ where: eq(wgInterface.name, 'wg0'), }) .execute(); if (!clientInterface) { throw new Error('WireGuard interface not found'); } const clientConfig = await tx.query.userConfig .findFirst({ where: eq(userConfig.id, clientInterface.name), }) .execute(); if (!clientConfig) { throw new Error('WireGuard interface configuration not found'); } const ipv4Cidr = parseCidr(clientInterface.ipv4Cidr); const ipv4Address = nextIP(4, ipv4Cidr, clients); const ipv6Cidr = parseCidr(clientInterface.ipv6Cidr); const ipv6Address = nextIP(6, ipv6Cidr, clients); await tx .insert(client) .values({ name, // TODO: properly assign user id userId: 1, interfaceId: 'wg0', expiresAt, privateKey, publicKey, preSharedKey, ipv4Address, ipv6Address, mtu: clientConfig.defaultMtu, persistentKeepalive: clientConfig.defaultPersistentKeepalive, serverAllowedIps: [], enabled: true, }) .execute(); }); } toggle(id: ID, enabled: boolean) { return this.#statements.toggle.execute({ id, enabled }); } delete(id: ID) { return this.#statements.delete.execute({ id }); } update(id: ID, data: UpdateClientType) { return this.#db.transaction(async (tx) => { const clientInterface = await tx.query.wgInterface .findFirst({ where: eq(wgInterface.name, 'wg0'), }) .execute(); if (!clientInterface) { throw new Error('WireGuard interface not found'); } if (!containsCidr(clientInterface.ipv4Cidr, data.ipv4Address)) { throw new Error('IPv4 address is not within the CIDR range'); } if (!containsCidr(clientInterface.ipv6Cidr, data.ipv6Address)) { throw new Error('IPv6 address is not within the CIDR range'); } await tx.update(client).set(data).where(eq(client.id, id)).execute(); }); } async createFromExisting({ name, enabled, ipv4Address, ipv6Address, preSharedKey, privateKey, publicKey, }: ClientCreateFromExistingType) { const clientConfig = await Database.userConfigs.get(); return this.#db .insert(client) .values({ name, userId: 1, interfaceId: 'wg0', privateKey, publicKey, preSharedKey, ipv4Address, ipv6Address, mtu: clientConfig.defaultMtu, allowedIps: clientConfig.defaultAllowedIps, dns: clientConfig.defaultDns, persistentKeepalive: clientConfig.defaultPersistentKeepalive, serverAllowedIps: [], enabled, }) .execute(); } }