From d0de97b59fe7aeb31646752d0723f1d87470bf0a Mon Sep 17 00:00:00 2001 From: Bernd Storath <32197462+kaaax0815@users.noreply.github.com> Date: Tue, 3 Sep 2024 10:56:06 +0200 Subject: [PATCH] Feat: Migrations (#1344) * add migrations * improve migration runner * improve migration runner * document what each migration does --- Dockerfile | 2 +- Dockerfile.dev | 4 +- src/server/api/lang.get.ts | 5 +- src/server/utils/Database.ts | 1 - src/services/database/inmemory.ts | 95 ------------------- src/services/database/lowdb.ts | 35 +++---- src/services/database/migrations/1.ts | 90 ++++++++++++++++++ src/services/database/migrations/index.ts | 33 +++++++ .../database/repositories/database.ts | 12 +-- src/services/database/repositories/system.ts | 65 +------------ src/services/database/repositories/types.ts | 1 - 11 files changed, 152 insertions(+), 191 deletions(-) delete mode 100644 src/services/database/inmemory.ts create mode 100644 src/services/database/migrations/1.ts create mode 100644 src/services/database/migrations/index.ts delete mode 100644 src/services/database/repositories/types.ts diff --git a/Dockerfile b/Dockerfile index 16276e92..e17dff7e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,7 +34,7 @@ RUN apk add --no-cache \ RUN update-alternatives --install /sbin/iptables iptables /sbin/iptables-legacy 10 --slave /sbin/iptables-restore iptables-restore /sbin/iptables-legacy-restore --slave /sbin/iptables-save iptables-save /sbin/iptables-legacy-save # Set Environment -ENV DEBUG=Server,WireGuard +ENV DEBUG=Server,WireGuard,LowDB ENV PORT=51821 # Run Web UI diff --git a/Dockerfile.dev b/Dockerfile.dev index 74383667..d464e989 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -24,5 +24,5 @@ RUN apk add --no-cache \ RUN update-alternatives --install /sbin/iptables iptables /sbin/iptables-legacy 10 --slave /sbin/iptables-restore iptables-restore /sbin/iptables-legacy-restore --slave /sbin/iptables-save iptables-save /sbin/iptables-legacy-save # Set Environment -ENV DEBUG=Server,WireGuard -ENV PORT=51821 \ No newline at end of file +ENV DEBUG=Server,WireGuard,LowDB +ENV PORT=51821 diff --git a/src/server/api/lang.get.ts b/src/server/api/lang.get.ts index 4a926a7a..2c31214b 100644 --- a/src/server/api/lang.get.ts +++ b/src/server/api/lang.get.ts @@ -1,4 +1,5 @@ -export default defineEventHandler((event) => { +export default defineEventHandler(async (event) => { setHeader(event, 'Content-Type', 'application/json'); - return Database.getLang(); + const system = await Database.getSystem(); + return system.lang; }); diff --git a/src/server/utils/Database.ts b/src/server/utils/Database.ts index fc9194d7..b240f6f8 100644 --- a/src/server/utils/Database.ts +++ b/src/server/utils/Database.ts @@ -3,7 +3,6 @@ * This design allows for easy swapping of different database implementations. */ -// import InMemory from '~/services/database/inmemory'; import LowDb from '~~/services/database/lowdb'; const provider = new LowDb(); diff --git a/src/services/database/inmemory.ts b/src/services/database/inmemory.ts deleted file mode 100644 index 93c7831e..00000000 --- a/src/services/database/inmemory.ts +++ /dev/null @@ -1,95 +0,0 @@ -import crypto from 'node:crypto'; -import debug from 'debug'; - -import { - DatabaseProvider, - DatabaseError, - DEFAULT_DATABASE, -} from './repositories/database'; -import { DEFAULT_SYSTEM } from './repositories/system'; - -import type { User } from './repositories/user'; - -const DEBUG = debug('InMemoryDB'); - -export default class InMemory extends DatabaseProvider { - #data = DEFAULT_DATABASE; - - async connect() { - this.#data.system = DEFAULT_SYSTEM; - DEBUG('Connected successfully'); - } - - async disconnect() { - this.#data = { system: null, users: [] }; - DEBUG('Disconnected successfully'); - } - - async getSystem() { - DEBUG('Get System'); - return this.#data.system; - } - - async getLang() { - return this.#data.system?.lang || 'en'; - } - - async getUsers() { - return this.#data.users; - } - - async getUser(id: string) { - DEBUG('Get User'); - return this.#data.users.find((user) => user.id === id); - } - - async newUserWithPassword(username: string, password: string) { - DEBUG('New User'); - - if (username.length < 8) { - throw new DatabaseError(DatabaseError.ERROR_USERNAME_REQ); - } - - if (!isPasswordStrong(password)) { - throw new DatabaseError(DatabaseError.ERROR_PASSWORD_REQ); - } - - const isUserExist = this.#data.users.find( - (user) => user.username === username - ); - if (isUserExist) { - throw new DatabaseError(DatabaseError.ERROR_USER_EXIST); - } - - const now = new Date(); - const isUserEmpty = this.#data.users.length === 0; - - const newUser: User = { - id: crypto.randomUUID(), - password: hashPassword(password), - username, - role: isUserEmpty ? 'ADMIN' : 'CLIENT', - enabled: true, - createdAt: now, - updatedAt: now, - }; - - this.#data.users.push(newUser); - } - - async updateUser(user: User) { - let oldUser = await this.getUser(user.id); - if (oldUser) { - DEBUG('Update User'); - oldUser = user; - } - } - - async deleteUser(id: string) { - DEBUG('Delete User'); - const idx = this.#data.users.findIndex((user) => user.id === id); - if (idx !== -1) { - this.#data.users.splice(idx, 1); - } - } -} diff --git a/src/services/database/lowdb.ts b/src/services/database/lowdb.ts index 4605a622..7ed7fbbb 100644 --- a/src/services/database/lowdb.ts +++ b/src/services/database/lowdb.ts @@ -8,11 +8,11 @@ import { DEFAULT_DATABASE, } from './repositories/database'; import { JSONFilePreset } from 'lowdb/node'; -import { DEFAULT_SYSTEM } from './repositories/system'; import type { Low } from 'lowdb'; import type { User } from './repositories/user'; import type { Database } from './repositories/database'; +import { migrationRunner } from './migrations'; const DEBUG = debug('LowDB'); @@ -26,26 +26,20 @@ export default class LowDB extends DatabaseProvider { this.#db = await JSONFilePreset(dbFilePath, DEFAULT_DATABASE); } + /** + * @throws + */ async connect() { - try { - // load file db - await this.#db.read(); - DEBUG('Connected successfully'); - return; - } catch (error) { - DEBUG('Database does not exist : ', error); - } - try { await this.__init(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error) { + DEBUG('Running Migrations'); + await migrationRunner(this.#db); + DEBUG('Migrations ran successfully'); + } catch (e) { + DEBUG(e); throw new DatabaseError(DatabaseError.ERROR_INIT); } - // TODO: move to DEFAULT_DATABASE - this.#db.update((data) => (data.system = DEFAULT_SYSTEM)); - DEBUG('Connected successfully'); } @@ -55,11 +49,12 @@ export default class LowDB extends DatabaseProvider { async getSystem() { DEBUG('Get System'); - return this.#db.data.system; - } - - async getLang() { - return this.#db.data.system?.lang || 'en'; + const system = this.#db.data.system; + // system is only null if migration failed + if (system === null) { + throw new DatabaseError(DatabaseError.ERROR_INIT); + } + return system; } async getUsers() { diff --git a/src/services/database/migrations/1.ts b/src/services/database/migrations/1.ts new file mode 100644 index 00000000..7da1a511 --- /dev/null +++ b/src/services/database/migrations/1.ts @@ -0,0 +1,90 @@ +import type { Low } from 'lowdb'; +import type { Database } from '../repositories/database'; +import packageJson from '@@/package.json'; +import { ChartType } from '../repositories/system'; + +// TODO: use variables inside up/down script +const DEFAULT_ADDRESS = '10.8.0.x'; +const DEFAULT_DEVICE = 'eth0'; +const DEFAULT_WG_PORT = 51820; +const DEFAULT_POST_UP = ` +iptables -t nat -A POSTROUTING -s ${DEFAULT_ADDRESS.replace('x', '0')}/24 -o ${DEFAULT_DEVICE} -j MASQUERADE; +iptables -A INPUT -p udp -m udp --dport ${DEFAULT_WG_PORT} -j ACCEPT; +iptables -A FORWARD -i wg0 -j ACCEPT; +iptables -A FORWARD -o wg0 -j ACCEPT; +` + .split('\n') + .join(' '); +const DEFAULT_POST_DOWN = ` +iptables -t nat -D POSTROUTING -s ${DEFAULT_ADDRESS.replace('x', '0')}/24 -o ${DEFAULT_DEVICE} -j MASQUERADE; +iptables -D INPUT -p udp -m udp --dport ${DEFAULT_WG_PORT} -j ACCEPT; +iptables -D FORWARD -i wg0 -j ACCEPT; +iptables -D FORWARD -o wg0 -j ACCEPT; +` + .split('\n') + .join(' '); + +export async function run1(db: Low) { + const privateKey = await exec('wg genkey'); + const publicKey = await exec(`echo ${privateKey} | wg pubkey`, { + log: 'echo ***hidden*** | wg pubkey', + }); + const database: Database = { + migrations: [], + system: { + release: packageJson.release.version, + interface: { + privateKey: privateKey, + publicKey: publicKey, + address: DEFAULT_ADDRESS.replace('x', '1'), + }, + sessionTimeout: 3600, // 1 hour + lang: 'en', + userConfig: { + mtu: 1420, + persistentKeepalive: 0, + // TODO: assume handle CIDR to compute next ip in WireGuard + rangeAddress: '10.8.0.0/24', + defaultDns: ['1.1.1.1'], + allowedIps: ['0.0.0.0/0', '::/0'], + }, + wgPath: WG_PATH, + wgDevice: DEFAULT_DEVICE, + wgHost: WG_HOST || '', + wgPort: DEFAULT_WG_PORT, + wgConfigPort: 51820, + iptables: { + PreUp: '', + PostUp: DEFAULT_POST_UP, + PreDown: '', + PostDown: DEFAULT_POST_DOWN, + }, + trafficStats: { + enabled: false, + type: ChartType.None, + }, + clientExpiration: { + enabled: false, + }, + oneTimeLinks: { + enabled: false, + }, + sortClients: { + enabled: false, + }, + prometheus: { + enabled: false, + password: null, + }, + sessionConfig: { + password: getRandomHex(256), + name: 'wg-easy', + cookie: undefined, + }, + }, + users: [], + }; + + db.data = database; + db.write(); +} diff --git a/src/services/database/migrations/index.ts b/src/services/database/migrations/index.ts new file mode 100644 index 00000000..18406292 --- /dev/null +++ b/src/services/database/migrations/index.ts @@ -0,0 +1,33 @@ +import type { Low } from 'lowdb'; +import type { Database } from '../repositories/database'; +import { run1 } from './1'; + +export type MIGRATION_FN = (db: Low) => Promise; + +const MIGRATION_LIST = { + // Adds Initial Database Structure + '1': run1, +} satisfies Record; + +/** + * Runs all migrations + * @throws + */ +export async function migrationRunner(db: Low) { + const ranMigrations = db.data.migrations; + const runMigrations = Object.keys( + MIGRATION_LIST + ) as (keyof typeof MIGRATION_LIST)[]; + for (const migrationId of runMigrations) { + if (ranMigrations.includes(migrationId)) { + continue; + } + try { + await MIGRATION_LIST[migrationId](db); + db.data.migrations.push(migrationId); + } catch (e) { + throw new Error(`Failed to run Migration ${migrationId}: ${e}`); + } + } + await db.write(); +} diff --git a/src/services/database/repositories/database.ts b/src/services/database/repositories/database.ts index a27a3e45..ce1c6505 100644 --- a/src/services/database/repositories/database.ts +++ b/src/services/database/repositories/database.ts @@ -1,16 +1,16 @@ import type { System, SystemRepository } from './system'; import type { User, UserRepository } from './user'; -import type { Lang } from './types'; // Represent data structure export type Database = { - // TODO: always return correct value, greatly improves code - system: System | null; + migrations: string[]; + system: System; users: User[]; }; export const DEFAULT_DATABASE: Database = { - system: null, + migrations: [], + system: null as never, users: [], }; @@ -34,9 +34,7 @@ export abstract class DatabaseProvider */ abstract disconnect(): Promise; - abstract getSystem(): Promise; - // TODO: remove - abstract getLang(): Promise; + abstract getSystem(): Promise; abstract getUsers(): Promise; abstract getUser(id: string): Promise; diff --git a/src/services/database/repositories/system.ts b/src/services/database/repositories/system.ts index ca623443..06538600 100644 --- a/src/services/database/repositories/system.ts +++ b/src/services/database/repositories/system.ts @@ -1,7 +1,6 @@ -import packageJson from '@@/package.json'; - import type { SessionConfig } from 'h3'; -import type { Lang } from './types'; + +export type Lang = 'en' | 'fr'; export type IpTables = { PreUp: string; @@ -84,63 +83,5 @@ export interface SystemRepository { /** * Retrieves the system configuration data from the database. */ - getSystem(): Promise; - - /** - * Retrieves the system's language setting. - */ - getLang(): Promise; + getSystem(): Promise; } - -// TODO: move to migration -export const DEFAULT_SYSTEM: System = { - release: packageJson.release.version, - interface: { - privateKey: '', - publicKey: '', - address: '10.8.0.1', - }, - sessionTimeout: 3600, // 1 hour - lang: 'en', - userConfig: { - mtu: 1420, - persistentKeepalive: 0, - // TODO: assume handle CIDR to compute next ip in WireGuard - rangeAddress: '10.8.0.0/24', - defaultDns: ['1.1.1.1'], - allowedIps: ['0.0.0.0/0', '::/0'], - }, - wgPath: WG_PATH, - wgDevice: 'wg0', - wgHost: WG_HOST || '', - wgPort: 51820, - wgConfigPort: 51820, - iptables: { - PreUp: '', - PostUp: '', - PreDown: '', - PostDown: '', - }, - trafficStats: { - enabled: false, - type: ChartType.None, - }, - clientExpiration: { - enabled: false, - }, - oneTimeLinks: { - enabled: false, - }, - sortClients: { - enabled: false, - }, - prometheus: { - enabled: false, - password: null, - }, - sessionConfig: { - password: getRandomHex(256), - name: 'wg-easy', - cookie: undefined, - }, -}; diff --git a/src/services/database/repositories/types.ts b/src/services/database/repositories/types.ts deleted file mode 100644 index 312539dc..00000000 --- a/src/services/database/repositories/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type Lang = 'en' | 'fr';