From aa55223273ceba7dd2a178f53be661e2621d8b05 Mon Sep 17 00:00:00 2001 From: tetuaoro <65575727+tetuaoro@users.noreply.github.com> Date: Sat, 31 Aug 2024 13:27:07 +0200 Subject: [PATCH] rebase & add: persistent lowdb provider --- .gitignore | 2 + src/i18n.config.ts | 1 + src/package.json | 1 + src/pnpm-lock.yaml | 17 +++ src/server/utils/Database.ts | 5 +- src/services/database/inmemory.ts | 70 +--------- src/services/database/lowdb.ts | 121 ++++++++++++++++++ .../database/repositories/database.ts | 11 ++ .../database/repositories/system/index.ts | 55 ++++++++ .../database/repositories/system/model.ts | 4 +- src/services/database/repositories/types.ts | 14 +- 11 files changed, 233 insertions(+), 68 deletions(-) create mode 100644 src/services/database/lowdb.ts create mode 100644 src/services/database/repositories/system/index.ts diff --git a/.gitignore b/.gitignore index e6fce2a6..ed408701 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ /src/node_modules .DS_Store *.swp +# lowdb data file +db.json \ No newline at end of file diff --git a/src/i18n.config.ts b/src/i18n.config.ts index 4b247f89..a86c66e9 100644 --- a/src/i18n.config.ts +++ b/src/i18n.config.ts @@ -46,6 +46,7 @@ export default defineI18nConfig(() => ({ ExpireDate: 'Expire Date', Permanent: 'Permanent', OneTimeLink: 'Generate short one time link', + errorInit: 'Initialization failed.', errorDatabaseConn: 'Failed to connect to the database.', errorPasswordReq: 'Password does not meet the strength requirements. It must be at least 12 characters long, with at least one uppercase letter, one lowercase letter, one number, and one special character.', diff --git a/src/package.json b/src/package.json index 85b2482d..43e83f86 100644 --- a/src/package.json +++ b/src/package.json @@ -30,6 +30,7 @@ "crc-32": "^1.2.2", "debug": "^4.3.6", "js-sha256": "^0.11.0", + "lowdb": "^7.0.1", "nuxt": "^3.12.4", "pinia": "^2.2.1", "qrcode": "^1.5.4", diff --git a/src/pnpm-lock.yaml b/src/pnpm-lock.yaml index a55a8750..4d3699c3 100644 --- a/src/pnpm-lock.yaml +++ b/src/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: js-sha256: specifier: ^0.11.0 version: 0.11.0 + lowdb: + specifier: ^7.0.1 + version: 7.0.1 nuxt: specifier: ^3.12.4 version: 3.12.4(@parcel/watcher@2.4.1)(@types/node@22.0.2)(eslint@9.8.0)(ioredis@5.4.1)(magicast@0.3.4)(optionator@0.9.4)(rollup@4.19.2)(terser@5.31.3)(typescript@5.5.4)(vite@5.3.5(@types/node@22.0.2)(terser@5.31.3))(vue-tsc@2.0.29(typescript@5.5.4)) @@ -2868,6 +2871,10 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lowdb@7.0.1: + resolution: {integrity: sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==} + engines: {node: '>=18'} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -3841,6 +3848,10 @@ packages: std-env@3.7.0: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + steno@4.0.2: + resolution: {integrity: sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==} + engines: {node: '>=18'} + streamx@2.18.0: resolution: {integrity: sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==} @@ -7582,6 +7593,10 @@ snapshots: lodash@4.17.21: {} + lowdb@7.0.1: + dependencies: + steno: 4.0.2 + lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -8659,6 +8674,8 @@ snapshots: std-env@3.7.0: {} + steno@4.0.2: {} + streamx@2.18.0: dependencies: fast-fifo: 1.3.2 diff --git a/src/server/utils/Database.ts b/src/server/utils/Database.ts index 409d2314..eecc80e6 100644 --- a/src/server/utils/Database.ts +++ b/src/server/utils/Database.ts @@ -4,9 +4,10 @@ * */ -import InMemory from '~/services/database/inmemory'; +// import InMemory from '~/services/database/inmemory'; +import LowDb from '~/services/database/lowdb'; -const provider = new InMemory(); +const provider = new LowDb(); provider.connect().catch((err) => { console.error(err); diff --git a/src/services/database/inmemory.ts b/src/services/database/inmemory.ts index 183ae214..d415b92f 100644 --- a/src/services/database/inmemory.ts +++ b/src/services/database/inmemory.ts @@ -1,82 +1,26 @@ -import debug from 'debug'; -import packageJson from '@/package.json'; import crypto from 'node:crypto'; +import debug from 'debug'; import DatabaseProvider, { DatabaseError } from './repositories/database'; -import { ChartType, Lang } from './repositories/types'; +import { hashPassword, isPasswordStrong } from '~/server/utils/password'; +import { Lang } from './repositories/types'; +import SYSTEM from './repositories/system'; -import type { ID } from './repositories/types'; -import type { System } from './repositories/system/model'; import type { User } from './repositories/user/model'; -import { hashPassword, isPasswordStrong } from '~/server/utils/password'; +import type { ID } from './repositories/types'; const DEBUG = debug('InMemoryDB'); -// Represent in-memory data structure -type InMemoryData = { - system: System | null; - users: User[]; -}; - // In-Memory Database Provider export default class InMemory extends DatabaseProvider { - protected data: InMemoryData = { system: null, users: [] }; - async connect() { - DEBUG('Connection...'); - const system: System = { - release: packageJson.release.version, - interface: { - privateKey: '', - publicKey: '', - address: '10.8.0.1', - }, - port: 51821, - webuiHost: '0.0.0.0', - sessionTimeout: 3600, // 1 hour - lang: Lang.EN, - userConfig: { - mtu: 1420, - persistentKeepalive: 0, - rangeAddress: '10.8.0.x', - defaultDns: ['1.1.1.1'], - allowedIps: ['0.0.0.0/0', '::/0'], - }, - wgPath: '/etc/wireguard/', - wgDevice: 'wg0', - wgHost: '', - wgPort: 51820, - wgConfigPort: 51820, - iptables: { - wgPreUp: '', - wgPostUp: '', - wgPreDown: '', - wgPostDown: '', - }, - trafficStats: { - enabled: false, - type: ChartType.None, - }, - wgEnableExpiresTime: false, - wgEnableOneTimeLinks: false, - wgEnableSortClients: false, - prometheus: { - enabled: false, - password: null, - }, - sessionConfig: { - password: '', - name: 'wg-easy', - cookie: undefined, - }, - }; - - this.data.system = system; + this.data.system = SYSTEM; DEBUG('Connection done'); } async disconnect() { this.data = { system: null, users: [] }; + DEBUG('Diconnect done'); } async getSystem() { diff --git a/src/services/database/lowdb.ts b/src/services/database/lowdb.ts new file mode 100644 index 00000000..72cd435e --- /dev/null +++ b/src/services/database/lowdb.ts @@ -0,0 +1,121 @@ +import crypto from 'node:crypto'; +import debug from 'debug'; +import { join } from 'path'; + +import DatabaseProvider, { DatabaseError } from './repositories/database'; +import { hashPassword, isPasswordStrong } from '~/server/utils/password'; +import { JSONFilePreset } from 'lowdb/node'; +import { Lang } from './repositories/types'; +import SYSTEM from './repositories/system'; + +import type { User } from './repositories/user/model'; +import type { DBData } from './repositories/database'; +import type { ID } from './repositories/types'; +import type { Low } from 'lowdb'; + +const DEBUG = debug('LowDB'); + +export default class LowDB extends DatabaseProvider { + private _db!: Low; + + private async __init() { + // TODO: assume path to db file + const dbFilePath = join(WG_PATH, 'db.json'); + this._db = await JSONFilePreset(dbFilePath, this.data); + } + + async connect() { + try { + // load file db + await this._db.read(); + DEBUG('Connection done'); + return; + } catch (error) { + DEBUG('Database does not exist : ', error); + } + + try { + await this.__init(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + throw new DatabaseError(DatabaseError.ERROR_INIT); + } + + this._db.update((data) => (data.system = SYSTEM)); + + DEBUG('Connection done'); + } + + async disconnect() { + DEBUG('Diconnect done'); + } + + async getSystem() { + DEBUG('Get System'); + return this._db.data.system; + } + + async getLang() { + return this._db.data.system?.lang || Lang.EN; + } + + async getUsers() { + return this._db.data.users; + } + + async getUser(id: ID) { + DEBUG('Get User'); + return this._db.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._db.data.users.find( + (user) => user.username === username + ); + if (isUserExist) { + throw new DatabaseError(DatabaseError.ERROR_USER_EXIST); + } + + const now = new Date(); + const isUserEmpty = this._db.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._db.update((data) => data.users.push(newUser)); + } + + async updateUser(user: User) { + let _user = await this.getUser(user.id); + if (_user) { + DEBUG('Update User'); + _user = user; + this._db.write(); + } + } + + async deleteUser(id: ID) { + DEBUG('Delete User'); + const idx = this._db.data.users.findIndex((user) => user.id === id); + if (idx !== -1) { + this._db.update((data) => data.users.splice(idx, 1)); + } + } +} diff --git a/src/services/database/repositories/database.ts b/src/services/database/repositories/database.ts index 12aa4102..4ad511f9 100644 --- a/src/services/database/repositories/database.ts +++ b/src/services/database/repositories/database.ts @@ -4,6 +4,14 @@ import type { Lang, ID } from './types'; import type { User } from './user/model'; import type { System } from './system/model'; +// TODO: re-export type from /user & /system + +// Represent data structure +export type DBData = { + system: System | null; + users: User[]; +}; + /** * Abstract class for database operations. * Provides methods to connect, disconnect, and interact with system and user data. @@ -14,6 +22,8 @@ import type { System } from './system/model'; export default abstract class DatabaseProvider implements SystemRepository, UserRepository { + protected data: DBData = { system: null, users: [] }; + /** * Connects to the database. */ @@ -68,6 +78,7 @@ export default abstract class DatabaseProvider * @extends {Error} */ export class DatabaseError extends Error { + static readonly ERROR_INIT = 'errorInit'; static readonly ERROR_PASSWORD_REQ = 'errorPasswordReq'; static readonly ERROR_USER_EXIST = 'errorUserExist'; static readonly ERROR_DATABASE_CONNECTION = 'errorDatabaseConn'; diff --git a/src/services/database/repositories/system/index.ts b/src/services/database/repositories/system/index.ts new file mode 100644 index 00000000..d08dc8b9 --- /dev/null +++ b/src/services/database/repositories/system/index.ts @@ -0,0 +1,55 @@ +import packageJson from '@/package.json'; + +import { ChartType, Lang } from '../types'; + +import type { System } from './model'; + +const DEFAULT_SYSTEM_MODEL: System = { + release: packageJson.release.version, + interface: { + privateKey: '', + publicKey: '', + address: '10.8.0.1', + }, + port: PORT ? Number(PORT) : 51821, + webuiHost: '0.0.0.0', + sessionTimeout: 3600, // 1 hour + lang: 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: { + wgPreUp: '', + wgPostUp: '', + wgPreDown: '', + wgPostDown: '', + }, + trafficStats: { + enabled: false, + type: ChartType.None, + }, + wgEnableExpiresTime: false, + wgEnableOneTimeLinks: false, + wgEnableSortClients: false, + prometheus: { + enabled: false, + password: null, + }, + sessionConfig: { + password: getRandomHex(256), + name: 'wg-easy', + cookie: undefined, + }, +}; + +export default DEFAULT_SYSTEM_MODEL; diff --git a/src/services/database/repositories/system/model.ts b/src/services/database/repositories/system/model.ts index 6d19c13d..aadcb741 100644 --- a/src/services/database/repositories/system/model.ts +++ b/src/services/database/repositories/system/model.ts @@ -1,6 +1,6 @@ import type { SessionConfig } from 'h3'; import type { - Address, + Url, IpTables, Lang, Port, @@ -29,7 +29,7 @@ export type System = { wgPath: string; wgDevice: string; - wgHost: Address; + wgHost: Url; wgPort: Port; wgConfigPort: Port; diff --git a/src/services/database/repositories/types.ts b/src/services/database/repositories/types.ts index 7bec4bf3..07e9785f 100644 --- a/src/services/database/repositories/types.ts +++ b/src/services/database/repositories/types.ts @@ -7,11 +7,23 @@ export enum Lang { FR = 'fr', } +export type Ipv4 = `${number}.${number}.${number}.${number}`; +export type Ipv4CIDR = `${number}.${number}.${number}.${number}/${number}`; +export type Ipv6 = + `${string}:${string}:${string}:${string}:${string}:${string}:${string}:${string}`; +export type Ipv6CIDR = + `${string}:${string}:${string}:${string}:${string}:${string}:${string}:${string}/${number}`; + +export type Address = Ipv4 | Ipv4CIDR | Ipv6 | Ipv6CIDR | '::/0'; + +export type UrlHttp = `http://${string}`; +export type UrlHttps = `https://${string}`; +export type Url = string | UrlHttp | UrlHttps | Address; + export type ID = crypto.UUID; export type Version = string; export type SessionTimeOut = number; export type Port = number; -export type Address = string; export type HashPassword = string; export type Command = string; export type Key = string;