Browse Source

Feat: Migrations (#1344)

* add migrations

* improve migration runner

* improve migration runner

* document what each migration does
pull/1648/head
Bernd Storath 7 months ago
committed by Bernd Storath
parent
commit
785ab025db
  1. 2
      Dockerfile
  2. 4
      Dockerfile.dev
  3. 5
      src/server/api/lang.get.ts
  4. 1
      src/server/utils/Database.ts
  5. 95
      src/services/database/inmemory.ts
  6. 35
      src/services/database/lowdb.ts
  7. 90
      src/services/database/migrations/1.ts
  8. 33
      src/services/database/migrations/index.ts
  9. 12
      src/services/database/repositories/database.ts
  10. 65
      src/services/database/repositories/system.ts
  11. 1
      src/services/database/repositories/types.ts

2
Dockerfile

@ -34,7 +34,7 @@ RUN apk add --no-cache \
RUN update-alternatives --install /usr/sbin/iptables iptables /usr/sbin/iptables-legacy 10 --slave /usr/sbin/iptables-restore iptables-restore /usr/sbin/iptables-legacy-restore --slave /usr/sbin/iptables-save iptables-save /usr/sbin/iptables-legacy-save
# Set Environment
ENV DEBUG=Server,WireGuard
ENV DEBUG=Server,WireGuard,LowDB
ENV PORT=51821
# Run Web UI

4
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
ENV DEBUG=Server,WireGuard,LowDB
ENV PORT=51821

5
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;
});

1
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();

95
src/services/database/inmemory.ts

@ -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);
}
}
}

35
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() {

90
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<Database>) {
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();
}

33
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<Database>) => Promise<void>;
const MIGRATION_LIST = {
// Adds Initial Database Structure
'1': run1,
} satisfies Record<string, MIGRATION_FN>;
/**
* Runs all migrations
* @throws
*/
export async function migrationRunner(db: Low<Database>) {
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();
}

12
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<void>;
abstract getSystem(): Promise<System | null>;
// TODO: remove
abstract getLang(): Promise<Lang>;
abstract getSystem(): Promise<System>;
abstract getUsers(): Promise<User[]>;
abstract getUser(id: string): Promise<User | undefined>;

65
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<System | null>;
/**
* Retrieves the system's language setting.
*/
getLang(): Promise<Lang>;
getSystem(): Promise<System>;
}
// 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,
},
};

1
src/services/database/repositories/types.ts

@ -1 +0,0 @@
export type Lang = 'en' | 'fr';
Loading…
Cancel
Save