Browse Source

improve structure, start migration

pull/1619/head
Bernd Storath 3 months ago
parent
commit
0cb6eddf1e
  1. 4
      src/drizzle.config.ts
  2. 5
      src/nuxt.config.ts
  3. 2
      src/server/database/repositories/clients/schema.ts
  4. 39
      src/server/database/repositories/clients/service.ts
  5. 86
      src/server/database/repositories/clients/types.ts
  6. 1
      src/server/database/repositories/general/schema.ts
  7. 2
      src/server/database/repositories/hooks/schema.ts
  8. 4
      src/server/database/repositories/interface/schema.ts
  9. 2
      src/server/database/repositories/metrics/schema.ts
  10. 2
      src/server/database/repositories/oneTimeLinks/schema.ts
  11. 2
      src/server/database/repositories/userConfig/schema.ts
  12. 0
      src/server/database/repositories/users/schema.ts
  13. 9
      src/server/database/schema.ts
  14. 36
      src/server/database/sqlite.ts
  15. 10
      src/server/utils/Database.ts
  16. 39
      src/server/utils/WireGuard.ts
  17. 2
      src/server/utils/cmd.ts
  18. 8
      src/server/utils/types.ts
  19. 356
      src/services/database/lowdb.ts
  20. 87
      src/services/database/migrations/1.ts
  21. 30
      src/services/database/migrations/index.ts
  22. 62
      src/services/database/repositories/client.ts
  23. 81
      src/services/database/repositories/database.ts
  24. 11
      src/services/database/repositories/setup.ts
  25. 90
      src/services/database/repositories/system.ts
  26. 41
      src/services/database/repositories/user.ts
  27. 8
      src/services/database/schema.ts
  28. 8
      src/services/database/sqlite.ts

4
src/drizzle.config.ts

@ -1,8 +1,8 @@
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
out: './migrations',
schema: './services/database/schema.ts',
out: './server/database/migrations',
schema: './server/database/schema.ts',
dialect: 'sqlite',
dbCredentials: {
url: 'file:./wg0.db',

5
src/nuxt.config.ts

@ -1,3 +1,5 @@
import { fileURLToPath } from 'node:url';
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
future: {
@ -44,5 +46,8 @@ export default defineNuxtConfig({
target: 'es2020',
},
},
alias: {
'#db': fileURLToPath(new URL('./server/database/', import.meta.url)),
},
},
});

2
src/services/database/schema/clients.ts → src/server/database/repositories/clients/schema.ts

@ -1,7 +1,7 @@
import { sql, relations } from 'drizzle-orm';
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { oneTimeLinks } from './oneTimeLinks';
import { oneTimeLinks } from '../../schema';
export const clients = sqliteTable('clients_table', {
id: int().primaryKey({ autoIncrement: true }),

39
src/server/database/repositories/clients/service.ts

@ -0,0 +1,39 @@
import type { DBType } from '#db/sqlite';
import { eq, sql } from 'drizzle-orm';
import { clients } from './schema';
function createPreparedStatement(db: DBType) {
return {
findAll: db.query.clients
.findMany({
with: {
oneTimeLink: true,
},
})
.prepare(),
findById: db.query.clients
.findFirst({ where: eq(clients.id, sql.placeholder('id')) })
.prepare(),
};
}
export class ClientsService {
#statements: ReturnType<typeof createPreparedStatement>;
constructor(db: DBType) {
this.#statements = createPreparedStatement(db);
}
async findAll() {
const result = await this.#statements.findAll.all();
return result.map((row) => ({
...row,
createdAt: new Date(row.createdAt),
updatedAt: new Date(row.updatedAt),
}));
}
async findById(id: number) {
return this.#statements.findById.all({ id });
}
}

86
src/server/database/repositories/clients/types.ts

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

1
src/services/database/schema/general.ts → src/server/database/repositories/general/schema.ts

@ -4,6 +4,7 @@ import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core';
export const general = sqliteTable('general_table', {
// limit to one entry
id: int().primaryKey({ autoIncrement: false }).default(1),
//test: text().notNull(),
sessionTimeout: int('session_timeout').notNull(),
createdAt: text('created_at')
.notNull()

2
src/services/database/schema/hooks.ts → src/server/database/repositories/hooks/schema.ts

@ -1,7 +1,7 @@
import { sql } from 'drizzle-orm';
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { wgInterface } from './interface';
import { wgInterface } from '../../schema';
export const hooks = sqliteTable('hooks_table', {
id: int()

4
src/services/database/schema/interface.ts → src/server/database/repositories/interface/schema.ts

@ -1,9 +1,7 @@
import { sql, relations } from 'drizzle-orm';
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { userConfig } from './userConfig';
import { hooks } from './hooks';
import { prometheus } from './metrics';
import { userConfig, hooks, prometheus } from '../../schema';
// maybe support multiple interfaces in the future
export const wgInterface = sqliteTable('interface_table', {

2
src/services/database/schema/metrics.ts → src/server/database/repositories/metrics/schema.ts

@ -1,7 +1,7 @@
import { sql } from 'drizzle-orm';
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { wgInterface } from './interface';
import { wgInterface } from '../../schema';
export const prometheus = sqliteTable('prometheus_table', {
id: int()

2
src/services/database/schema/oneTimeLinks.ts → src/server/database/repositories/oneTimeLinks/schema.ts

@ -1,7 +1,7 @@
import { sql, relations } from 'drizzle-orm';
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { clients } from './clients';
import { clients } from '../../schema';
export const oneTimeLinks = sqliteTable('one_time_links_table', {
id: int().primaryKey({ autoIncrement: true }),

2
src/services/database/schema/userConfig.ts → src/server/database/repositories/userConfig/schema.ts

@ -1,7 +1,7 @@
import { sql } from 'drizzle-orm';
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { wgInterface } from './interface';
import { wgInterface } from '../../schema';
// default* means clients store it themselves
export const userConfig = sqliteTable('user_config_table', {

0
src/services/database/schema/users.ts → src/server/database/repositories/users/schema.ts

9
src/server/database/schema.ts

@ -0,0 +1,9 @@
// Make sure to not use any Path Aliases in these files
export * from './repositories/clients/schema';
export * from './repositories/general/schema';
export * from './repositories/hooks/schema';
export * from './repositories/interface/schema';
export * from './repositories/metrics/schema';
export * from './repositories/oneTimeLinks/schema';
export * from './repositories/userConfig/schema';
export * from './repositories/users/schema';

36
src/server/database/sqlite.ts

@ -0,0 +1,36 @@
import { drizzle } from 'drizzle-orm/libsql';
import { migrate as drizzleMigrate } from 'drizzle-orm/libsql/migrator';
import { createClient } from '@libsql/client';
import * as schema from './schema';
import { ClientsService } from './repositories/clients/service';
const client = createClient({ url: 'file:/etc/wireguard/wg0.db' });
const db = drizzle({ client, schema });
export async function connect() {
await migrate();
return new DBService(db);
}
class DBService {
clients = new ClientsService(db);
constructor(private db: DBType) {}
}
export type DBType = typeof db;
export type DBServiceType = DBService;
async function migrate() {
try {
console.log('Migrating database...');
await drizzleMigrate(db, {
migrationsFolder: './services/database/migrations',
});
console.log('Migration complete');
} catch (e) {
if (e instanceof Error) {
console.log('Failed to migrate database:', e.message);
}
}
}

10
src/server/utils/Database.ts

@ -2,8 +2,7 @@
* Changing the Database Provider
* This design allows for easy swapping of different database implementations.
*/
import LowDb from '~~/services/database/lowdb';
import { connect, type DBServiceType } from '#db/sqlite';
const nullObject = new Proxy(
{},
@ -15,11 +14,10 @@ const nullObject = new Proxy(
);
// eslint-disable-next-line import/no-mutable-exports
let provider = nullObject as never as LowDb;
let provider = nullObject as never as DBServiceType;
LowDb.connect().then((v) => {
provider = v;
WireGuard.Startup();
connect().then((db) => {
provider = db;
});
// TODO: check if old config exists and tell user about migration path

39
src/server/utils/WireGuard.ts

@ -50,20 +50,9 @@ class WireGuard {
}
async getClients() {
const dbClients = await Database.client.findAll();
const clients = Object.entries(dbClients).map(([clientId, client]) => ({
id: clientId,
name: client.name,
enabled: client.enabled,
address4: client.address4,
address6: client.address6,
publicKey: client.publicKey,
createdAt: new Date(client.createdAt),
updatedAt: new Date(client.updatedAt),
expiresAt: client.expiresAt,
allowedIps: client.allowedIps,
oneTimeLink: client.oneTimeLink,
persistentKeepalive: null as string | null,
const dbClients = await Database.clients.findAll();
const clients = dbClients.map((client) => ({
...client,
latestHandshakeAt: null as Date | null,
endpoint: null as string | null,
transferRx: null as number | null,
@ -73,14 +62,7 @@ class WireGuard {
// Loop WireGuard status
const dump = await wg.dump();
dump.forEach(
({
publicKey,
latestHandshakeAt,
endpoint,
transferRx,
transferTx,
persistentKeepalive,
}) => {
({ publicKey, latestHandshakeAt, endpoint, transferRx, transferTx }) => {
const client = clients.find((client) => client.publicKey === publicKey);
if (!client) {
return;
@ -90,25 +72,12 @@ class WireGuard {
client.endpoint = endpoint;
client.transferRx = transferRx;
client.transferTx = transferTx;
client.persistentKeepalive = persistentKeepalive;
}
);
return clients;
}
async getClient({ clientId }: { clientId: string }) {
const client = await Database.client.findById(clientId);
if (!client) {
throw createError({
statusCode: 404,
statusMessage: `Client Not Found: ${clientId}`,
});
}
return client;
}
async getClientConfiguration({ clientId }: { clientId: string }) {
const system = await Database.system.get();
const client = await this.getClient({ clientId });

2
src/server/utils/cmd.ts

@ -1,5 +1,7 @@
import childProcess from 'child_process';
import {} from '~/';
export function exec(
cmd: string,
{ log }: { log: boolean | string } = { log: true }

8
src/server/utils/types.ts

@ -1,10 +1,12 @@
import type { ZodSchema, ZodTypeDef } from 'zod';
import { z, ZodError } from 'zod';
import z from 'zod';
import type { H3Event, EventHandlerRequest } from 'h3';
export { default as zod } from 'zod';
const objectMessage = 'zod.body';
const safeStringRefine = z
export const safeStringRefine = z
.string()
.refine(
(v) => v !== '__proto__' && v !== 'constructor' && v !== 'prototype',
@ -228,7 +230,7 @@ export function validateZod<T>(
return await schema.parseAsync(data);
} catch (error) {
let message = 'Unexpected Error';
if (error instanceof ZodError) {
if (error instanceof zod.ZodError) {
message = error.issues
.map((v) => {
let m = v.message;

356
src/services/database/lowdb.ts

@ -1,356 +0,0 @@
import crypto from 'node:crypto';
import debug from 'debug';
import { JSONFilePreset } from 'lowdb/node';
import type { Low } from 'lowdb';
import type { DeepReadonly } from 'vue';
import { parseCidr } from 'cidr-tools';
import { stringifyIp } from 'ip-bigint';
import {
DatabaseProvider,
DatabaseError,
DEFAULT_DATABASE,
} from './repositories/database';
import { UserRepository, type User } from './repositories/user';
import type { Database } from './repositories/database';
import { migrationRunner } from './migrations';
import {
ClientRepository,
type UpdateClient,
type CreateClient,
type OneTimeLink,
} from './repositories/client';
import {
SystemRepository,
type General,
type UpdateWGConfig,
type UpdateWGInterface,
type WGHooks,
} from './repositories/system';
import { SetupRepository, type Steps } from './repositories/setup';
const DEBUG = debug('LowDB');
export class LowDBSetup extends SetupRepository {
#db: Low<Database>;
constructor(db: Low<Database>) {
super();
this.#db = db;
}
async done() {
if (this.#db.data.setup === 'success') {
return true;
}
return false;
}
async get() {
return this.#db.data.setup;
}
async set(step: Steps) {
this.#db.update((v) => {
v.setup = step;
});
}
}
/**
* deep copies object and
* makes readonly on type level
*/
function makeReadonly<T>(a: T): DeepReadonly<T> {
return structuredClone(a) as DeepReadonly<T>;
}
class LowDBSystem extends SystemRepository {
#db: Low<Database>;
constructor(db: Low<Database>) {
super();
this.#db = db;
}
async get() {
DEBUG('Get System');
const system = this.#db.data.system;
// system is only null if migration failed
if (system === null) {
throw new DatabaseError(DatabaseError.ERROR_INIT);
}
return makeReadonly(system);
}
async updateClientsHostPort(host: string, port: number): Promise<void> {
DEBUG('Update Clients Host and Port endpoint');
this.#db.update((v) => {
v.system.userConfig.host = host;
v.system.userConfig.port = port;
});
}
async updateGeneral(general: General) {
DEBUG('Update General');
this.#db.update((v) => {
v.system.general = general;
});
}
async updateInterface(wgInterface: UpdateWGInterface) {
DEBUG('Update Interface');
this.#db.update((v) => {
const oldInterface = v.system.interface;
v.system.interface = {
...oldInterface,
...wgInterface,
};
});
}
async updateUserConfig(userConfig: UpdateWGConfig) {
DEBUG('Update User Config');
this.#db.update((v) => {
const oldUserConfig = v.system.userConfig;
v.system.userConfig = {
...oldUserConfig,
...userConfig,
};
});
}
async updateHooks(hooks: WGHooks) {
DEBUG('Update Hooks');
this.#db.update((v) => {
v.system.hooks = hooks;
});
}
/**
* updates the address range and the interface address
*/
async updateAddressRange(address4Range: string, address6Range: string) {
DEBUG('Update Address Range');
const cidr4 = parseCidr(address4Range);
const cidr6 = parseCidr(address6Range);
this.#db.update((v) => {
v.system.userConfig.address4Range = address4Range;
v.system.userConfig.address6Range = address6Range;
v.system.interface.address4 = stringifyIp({
number: cidr4.start + 1n,
version: 4,
});
v.system.interface.address6 = stringifyIp({
number: cidr6.start + 1n,
version: 6,
});
});
}
}
class LowDBUser extends UserRepository {
#db: Low<Database>;
constructor(db: Low<Database>) {
super();
this.#db = db;
}
async findAll() {
return makeReadonly(this.#db.data.users);
}
async findById(id: string) {
DEBUG('Get User');
return makeReadonly(this.#db.data.users.find((user) => user.id === id));
}
async create(username: string, password: string) {
DEBUG('Create User');
const isUserExist = this.#db.data.users.find(
(user) => user.username === username
);
if (isUserExist) {
throw createError({
statusCode: 409,
statusMessage: 'Username already taken',
});
}
const now = new Date().toISOString();
const isUserEmpty = this.#db.data.users.length === 0;
const hash = await hashPassword(password);
const newUser: User = {
id: crypto.randomUUID(),
password: hash,
username,
email: null,
name: 'Administrator',
role: isUserEmpty ? 'ADMIN' : 'CLIENT',
enabled: true,
createdAt: now,
updatedAt: now,
};
await this.#db.update((data) => data.users.push(newUser));
}
async update(user: User) {
// TODO: avoid mutation, prefer .update, updatedAt
let oldUser = await this.findById(user.id);
if (oldUser) {
DEBUG('Update User');
oldUser = user;
await this.#db.write();
}
}
async delete(id: string) {
DEBUG('Delete User');
const idx = this.#db.data.users.findIndex((user) => user.id === id);
if (idx !== -1) {
await this.#db.update((data) => data.users.splice(idx, 1));
}
}
}
class LowDBClient extends ClientRepository {
#db: Low<Database>;
constructor(db: Low<Database>) {
super();
this.#db = db;
}
async findAll() {
DEBUG('GET Clients');
return makeReadonly(this.#db.data.clients);
}
async findById(id: string) {
DEBUG('Get Client');
return makeReadonly(this.#db.data.clients[id]);
}
async create(client: CreateClient) {
DEBUG('Create Client');
const id = crypto.randomUUID();
const now = new Date().toISOString();
const newClient = { ...client, createdAt: now, updatedAt: now, id };
await this.#db.update((data) => {
data.clients[id] = newClient;
});
}
async delete(id: string) {
DEBUG('Delete Client');
await this.#db.update((data) => {
// TODO: find something better than delete
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete data.clients[id];
});
}
async toggle(id: string, enable: boolean) {
DEBUG('Toggle Client');
await this.#db.update((data) => {
if (data.clients[id]) {
data.clients[id].enabled = enable;
}
});
}
async updateExpirationDate(id: string, expirationDate: string | null) {
DEBUG('Update Client Expiration Date');
await this.#db.update((data) => {
if (data.clients[id]) {
data.clients[id].expiresAt = expirationDate;
}
});
}
async deleteOneTimeLink(id: string) {
DEBUG('Delete Client One Time Link');
await this.#db.update((data) => {
if (data.clients[id]) {
if (data.clients[id].oneTimeLink) {
// Bug where Client makes 2 requests
data.clients[id].oneTimeLink.expiresAt = new Date(
Date.now() + 10 * 1000
).toISOString();
}
}
});
}
async createOneTimeLink(id: string, oneTimeLink: OneTimeLink) {
DEBUG('Create Client One Time Link');
await this.#db.update((data) => {
if (data.clients[id]) {
data.clients[id].oneTimeLink = oneTimeLink;
}
});
}
async update(id: string, client: UpdateClient) {
DEBUG('Create Client');
const now = new Date().toISOString();
await this.#db.update((data) => {
const oldClient = data.clients[id];
if (!oldClient) {
return;
}
data.clients[id] = {
...oldClient,
...client,
updatedAt: now,
};
});
}
}
export default class LowDB extends DatabaseProvider {
#db: Low<Database>;
setup: LowDBSetup;
system: LowDBSystem;
user: LowDBUser;
client: LowDBClient;
private constructor(db: Low<Database>) {
super();
this.#db = db;
this.setup = new LowDBSetup(this.#db);
this.system = new LowDBSystem(this.#db);
this.user = new LowDBUser(this.#db);
this.client = new LowDBClient(this.#db);
}
async runMigrations() {
await migrationRunner(this.#db);
}
/**
* @throws
*/
static override async connect() {
try {
DEBUG('Connecting...');
const db = await JSONFilePreset(
'/etc/wireguard/db.json',
DEFAULT_DATABASE
);
const inst = new LowDB(db);
DEBUG('Running Migrations...');
await inst.runMigrations();
DEBUG('Migrations ran successfully.');
DEBUG('Connected successfully.');
return inst;
} catch (e) {
DEBUG(e);
throw new Error('Failed to initialize Database');
}
}
async disconnect() {
DEBUG('Disconnected successfully');
}
}

87
src/services/database/migrations/1.ts

@ -1,87 +0,0 @@
import type { Low } from 'lowdb';
import type { Database } from '../repositories/database';
import { parseCidr } from 'cidr-tools';
import { stringifyIp } from 'ip-bigint';
export async function run1(db: Low<Database>) {
const privateKey = await wg.generatePrivateKey();
const publicKey = await wg.getPublicKey(privateKey);
const address4Range = '10.8.0.0/24';
const address6Range = 'fdcc:ad94:bacf:61a4::cafe:0/112';
const cidr4 = parseCidr(address4Range);
const cidr6 = parseCidr(address6Range);
const database: Database = {
migrations: [],
setup: 1,
system: {
general: {
sessionTimeout: 3600, // 1 hour
},
// Config to configure Server
interface: {
privateKey: privateKey,
publicKey: publicKey,
address4: stringifyIp({ number: cidr4.start + 1n, version: 4 }),
address6: stringifyIp({ number: cidr6.start + 1n, version: 6 }),
mtu: 1420,
port: 51820,
device: 'eth0',
},
// Config to configure Peer & Client Config
userConfig: {
mtu: 1420,
persistentKeepalive: 0,
address4Range: address4Range,
address6Range: address6Range,
defaultDns: ['1.1.1.1', '2606:4700:4700::1111'],
allowedIps: ['0.0.0.0/0', '::/0'],
host: '',
port: 51820,
},
// Config to configure Firewall or general hooks
hooks: {
PreUp: '',
PostUp: [
'iptables -t nat -A POSTROUTING -s {{address4}} -o {{device}} -j MASQUERADE;',
'iptables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT;',
'iptables -A FORWARD -i wg0 -j ACCEPT;',
'iptables -A FORWARD -o wg0 -j ACCEPT;',
'ip6tables -t nat -A POSTROUTING -s {{address6}} -o {{device}} -j MASQUERADE;',
'ip6tables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT;',
'ip6tables -A FORWARD -i wg0 -j ACCEPT;',
'ip6tables -A FORWARD -o wg0 -j ACCEPT;',
].join(' '),
PreDown: '',
PostDown: [
'iptables -t nat -D POSTROUTING -s {{address4}} -o {{device}} -j MASQUERADE;',
'iptables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT;',
'iptables -D FORWARD -i wg0 -j ACCEPT;',
'iptables -D FORWARD -o wg0 -j ACCEPT;',
'ip6tables -t nat -D POSTROUTING -s {{address6}} -o {{device}} -j MASQUERADE;',
'ip6tables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT;',
'ip6tables -D FORWARD -i wg0 -j ACCEPT;',
'ip6tables -D FORWARD -o wg0 -j ACCEPT;',
].join(' '),
},
metrics: {
prometheus: {
enabled: false,
password: null,
},
},
sessionConfig: {
// TODO: be able to invalidate all sessions
password: getRandomHex(256),
name: 'wg-easy',
cookie: {},
},
},
users: [],
clients: {},
};
db.data = database;
db.write();
}

30
src/services/database/migrations/index.ts

@ -1,30 +0,0 @@
import type { Low } from 'lowdb';
import type { Database } from '../repositories/database';
import { run1 } from './1';
export type MIGRATION_FN = (db: Low<Database>) => Promise<void>;
const MIGRATION_LIST = [
// Adds Initial Database Structure
{ id: '1', fn: run1 },
] satisfies { id: string; fn: MIGRATION_FN }[];
/**
* Runs all migrations
* @throws
*/
export async function migrationRunner(db: Low<Database>) {
const ranMigrations = db.data.migrations;
for (const migration of MIGRATION_LIST) {
if (ranMigrations.includes(migration.id)) {
continue;
}
try {
await migration.fn(db);
db.data.migrations.push(migration.id);
} catch (e) {
throw new Error(`Failed to run Migration ${migration.id}: ${e}`);
}
}
await db.write();
}

62
src/services/database/repositories/client.ts

@ -1,62 +0,0 @@
import type { DeepReadonly } from 'vue';
export type OneTimeLink = {
oneTimeLink: string;
/** ISO String */
expiresAt: string;
};
export type Client = {
id: string;
name: string;
address4: string;
address6: string;
privateKey: string;
publicKey: string;
preSharedKey: string;
/** ISO String */
expiresAt: string | null;
allowedIps: string[];
serverAllowedIPs: string[];
oneTimeLink: OneTimeLink | null;
/** ISO String */
createdAt: string;
/** ISO String */
updatedAt: string;
enabled: boolean;
persistentKeepalive: number;
mtu: number;
};
export type CreateClient = Omit<Client, 'createdAt' | 'updatedAt' | 'id'>;
export type UpdateClient = Omit<
Client,
| 'createdAt'
| 'updatedAt'
| 'id'
| 'oneTimeLink'
| 'privateKey'
| 'publicKey'
| 'preSharedKey'
>;
/**
* Interface for client-related database operations.
* This interface provides methods for managing client data.
*/
export abstract class ClientRepository {
abstract findAll(): Promise<DeepReadonly<Record<string, Client>>>;
abstract findById(id: string): Promise<DeepReadonly<Client | undefined>>;
abstract create(client: CreateClient): Promise<void>;
abstract delete(id: string): Promise<void>;
abstract toggle(id: string, enable: boolean): Promise<void>;
abstract deleteOneTimeLink(id: string): Promise<void>;
abstract createOneTimeLink(
id: string,
oneTimeLink: OneTimeLink
): Promise<void>;
abstract update(id: string, client: UpdateClient): Promise<void>;
}

81
src/services/database/repositories/database.ts

@ -1,81 +0,0 @@
import type { ClientRepository, Client } from './client';
import type { SetupRepository, Steps } from './setup';
import type { System, SystemRepository } from './system';
import type { User, UserRepository } from './user';
// Represent data structure
export type Database = {
migrations: string[];
setup: Steps;
system: System;
users: User[];
clients: Record<string, Client>;
};
export const DEFAULT_DATABASE: Database = {
migrations: [],
setup: 1,
system: null as never,
users: [],
clients: {},
};
/**
* Abstract class for database operations.
* Provides methods to connect, disconnect, and interact with system and user data.
*/
export abstract class DatabaseProvider {
/**
* Connects to the database.
*/
static connect(): Promise<DatabaseProvider> {
throw new Error('Not implemented');
}
/**
* Disconnects from the database.
*/
abstract disconnect(): Promise<void>;
abstract setup: SetupRepository;
abstract system: SystemRepository;
abstract user: UserRepository;
abstract client: ClientRepository;
}
/**
* Represents a specialized error class for database-related operations.
* This class is designed to work with internationalization (i18n) by using message keys.
* The actual error messages are expected to be retrieved using these keys from an i18n system.
*
* ### Usage:
* When throwing a `DatabaseError`, you provide an i18n key as the message.
* The key will be used by the i18n system to retrieve the corresponding localized error message.
*
* Example:
* ```typescript
* throw new DatabaseError(DatabaseError.ERROR_INIT);
* ...
* // event handler routes
* if (error instanceof DatabaseError) {
* const t = await useTranslation(event);
* throw createError({
* statusCode: 400,
* statusMessage: t(error.message),
* message: error.message,
* });
* } else {
* throw createError('Something happened !');
* }
* ```
*
* @extends {Error}
*/
export class DatabaseError extends Error {
static readonly ERROR_INIT = 'errorInit';
constructor(message: string) {
super(message);
this.name = 'DatabaseError';
}
}

11
src/services/database/repositories/setup.ts

@ -1,11 +0,0 @@
export type Steps = 1 | 2 | 3 | 4 | 5 | 'success';
/**
* Interface for setup-related database operations.
* This interface provides methods for managing setup data.
*/
export abstract class SetupRepository {
abstract done(): Promise<boolean>;
abstract get(): Promise<Steps>;
abstract set(step: Steps): Promise<void>;
}

90
src/services/database/repositories/system.ts

@ -1,90 +0,0 @@
import type { SessionConfig } from 'h3';
import type { DeepReadonly } from 'vue';
export type WGHooks = {
PreUp: string;
PostUp: string;
PreDown: string;
PostDown: string;
};
export type WGInterface = {
privateKey: string;
publicKey: string;
address4: string;
address6: string;
mtu: number;
port: number;
device: string;
};
export type WGConfig = {
mtu: number;
persistentKeepalive: number;
address4Range: string;
address6Range: string;
defaultDns: string[];
allowedIps: string[];
host: string;
port: number;
};
export enum ChartType {
None = 0,
Line = 1,
Area = 2,
Bar = 3,
}
export type Prometheus = {
enabled: boolean;
password: string | null;
};
export type Metrics = {
prometheus: Prometheus;
};
export type General = {
sessionTimeout: number;
};
export type System = {
general: General;
interface: WGInterface;
userConfig: WGConfig;
hooks: WGHooks;
metrics: Metrics;
sessionConfig: SessionConfig;
};
export type UpdateWGInterface = Omit<
WGInterface,
'privateKey' | 'publicKey' | 'address4' | 'address6'
>;
export type UpdateWGConfig = Omit<WGConfig, 'address4Range' | 'address6Range'>;
/**
* Interface for system-related database operations.
* This interface provides methods for retrieving system configuration data
* and specific system properties, such as the language setting, from the database.
*/
export abstract class SystemRepository {
abstract get(): Promise<DeepReadonly<System>>;
abstract updateClientsHostPort(host: string, port: number): Promise<void>;
abstract updateGeneral(general: General): Promise<void>;
abstract updateInterface(wgInterface: UpdateWGInterface): Promise<void>;
abstract updateUserConfig(userConfig: UpdateWGConfig): Promise<void>;
abstract updateHooks(hooks: WGHooks): Promise<void>;
}

41
src/services/database/repositories/user.ts

@ -1,41 +0,0 @@
import type { DeepReadonly } from 'vue';
/**
* Represents user roles within the application, each with specific permissions :
*
* - `ADMIN`: Full permissions to all resources, including the app, database, etc
* - `EDITOR`: Granted write and read permissions on their own resources as well as
* `CLIENT` resources, but without `ADMIN` privileges
* - `CLIENT`: Granted write and read permissions only on their own resources.
*/
export type ROLE = 'ADMIN' | 'EDITOR' | 'CLIENT';
/**
* Representing a user data structure.
*/
export type User = {
id: string;
role: ROLE;
username: string;
password: string;
name: string;
email: string | null;
/** ISO String */
createdAt: string;
/** ISO String */
updatedAt: string;
enabled: boolean;
};
/**
* Interface for user-related database operations.
* This interface provides methods for managing user data.
*/
export abstract class UserRepository {
abstract findAll(): Promise<DeepReadonly<User[]>>;
abstract findById(id: string): Promise<DeepReadonly<User | undefined>>;
abstract create(username: string, password: string): Promise<void>;
abstract update(user: User): Promise<void>;
abstract delete(id: string): Promise<void>;
}

8
src/services/database/schema.ts

@ -1,8 +0,0 @@
export * from './schema/clients';
export * from './schema/general';
export * from './schema/hooks';
export * from './schema/interface';
export * from './schema/metrics';
export * from './schema/oneTimeLinks';
export * from './schema/userConfig';
export * from './schema/users';

8
src/services/database/sqlite.ts

@ -1,8 +0,0 @@
import { drizzle } from 'drizzle-orm/libsql';
import { createClient } from '@libsql/client';
import * as schema from './schema';
const client = createClient({ url: 'file:/etc/wireguard/wg0.db' });
const db = drizzle({ client, schema });
export default db;
Loading…
Cancel
Save