From 0a5b0acd067c2327dc07dfa6e03c59ee4c499c6f Mon Sep 17 00:00:00 2001 From: Bernd Storath <999999bst@gmail.com> Date: Mon, 2 Sep 2024 14:01:03 +0200 Subject: [PATCH] refactor code --- README.md | 2 +- src/server/api/session.post.ts | 2 +- src/server/utils/Database.ts | 1 - src/services/database/inmemory.ts | 51 +++--- src/services/database/lowdb.ts | 60 +++---- .../database/repositories/database.ts | 30 ++-- src/services/database/repositories/system.ts | 146 ++++++++++++++++++ .../database/repositories/system/index.ts | 55 ------- .../database/repositories/system/model.ts | 45 ------ .../repositories/system/repository.ts | 22 --- src/services/database/repositories/types.ts | 62 +------- src/services/database/repositories/user.ts | 55 +++++++ .../database/repositories/user/model.ts | 29 ---- .../repositories/user/repository.ts.ts | 39 ----- 14 files changed, 278 insertions(+), 321 deletions(-) create mode 100644 src/services/database/repositories/system.ts delete mode 100644 src/services/database/repositories/system/index.ts delete mode 100644 src/services/database/repositories/system/model.ts delete mode 100644 src/services/database/repositories/system/repository.ts create mode 100644 src/services/database/repositories/user.ts delete mode 100644 src/services/database/repositories/user/model.ts delete mode 100644 src/services/database/repositories/user/repository.ts.ts diff --git a/README.md b/README.md index 2fa78f8f..78e263e5 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ You have found the easiest way to install & manage WireGuard on any Linux host! - Multilanguage Support - Traffic Stats (default off) - One Time Links (default off) -- Client Expiry (default off) +- Client Expiration (default off) - Prometheus metrics support ## Requirements diff --git a/src/server/api/session.post.ts b/src/server/api/session.post.ts index 625b9bd8..d6712475 100644 --- a/src/server/api/session.post.ts +++ b/src/server/api/session.post.ts @@ -22,7 +22,7 @@ export default defineEventHandler(async (event) => { }); } - // TODO: timing againts timing attack + // TODO: timing against timing attack const system = await Database.getSystem(); if (!system) diff --git a/src/server/utils/Database.ts b/src/server/utils/Database.ts index eecc80e6..be96c2d2 100644 --- a/src/server/utils/Database.ts +++ b/src/server/utils/Database.ts @@ -1,7 +1,6 @@ /** * Changing the Database Provider * This design allows for easy swapping of different database implementations. - * */ // import InMemory from '~/services/database/inmemory'; diff --git a/src/services/database/inmemory.ts b/src/services/database/inmemory.ts index d415b92f..c74c2551 100644 --- a/src/services/database/inmemory.ts +++ b/src/services/database/inmemory.ts @@ -1,44 +1,47 @@ import crypto from 'node:crypto'; import debug from 'debug'; -import DatabaseProvider, { DatabaseError } from './repositories/database'; +import { + DatabaseProvider, + DatabaseError, + DEFAULT_DATABASE, +} from './repositories/database'; import { hashPassword, isPasswordStrong } from '~/server/utils/password'; -import { Lang } from './repositories/types'; -import SYSTEM from './repositories/system'; +import { DEFAULT_SYSTEM } from './repositories/system'; -import type { User } from './repositories/user/model'; -import type { ID } from './repositories/types'; +import type { User } from './repositories/user'; const DEBUG = debug('InMemoryDB'); -// In-Memory Database Provider export default class InMemory extends DatabaseProvider { + #data = DEFAULT_DATABASE; + async connect() { - this.data.system = SYSTEM; - DEBUG('Connection done'); + this.#data.system = DEFAULT_SYSTEM; + DEBUG('Connected successfully'); } async disconnect() { - this.data = { system: null, users: [] }; - DEBUG('Diconnect done'); + this.#data = { system: null, users: [] }; + DEBUG('Disconnected successfully'); } async getSystem() { DEBUG('Get System'); - return this.data.system; + return this.#data.system; } async getLang() { - return this.data.system?.lang || Lang.EN; + return this.#data.system?.lang || 'en'; } async getUsers() { - return this.data.users; + return this.#data.users; } - async getUser(id: ID) { + async getUser(id: string) { DEBUG('Get User'); - return this.data.users.find((user) => user.id === id); + return this.#data.users.find((user) => user.id === id); } async newUserWithPassword(username: string, password: string) { @@ -52,7 +55,7 @@ export default class InMemory extends DatabaseProvider { throw new DatabaseError(DatabaseError.ERROR_PASSWORD_REQ); } - const isUserExist = this.data.users.find( + const isUserExist = this.#data.users.find( (user) => user.username === username ); if (isUserExist) { @@ -60,7 +63,7 @@ export default class InMemory extends DatabaseProvider { } const now = new Date(); - const isUserEmpty = this.data.users.length === 0; + const isUserEmpty = this.#data.users.length === 0; const newUser: User = { id: crypto.randomUUID(), @@ -72,22 +75,22 @@ export default class InMemory extends DatabaseProvider { updatedAt: now, }; - this.data.users.push(newUser); + this.#data.users.push(newUser); } async updateUser(user: User) { - let _user = await this.getUser(user.id); - if (_user) { + let oldUser = await this.getUser(user.id); + if (oldUser) { DEBUG('Update User'); - _user = user; + oldUser = user; } } - async deleteUser(id: ID) { + async deleteUser(id: string) { DEBUG('Delete User'); - const idx = this.data.users.findIndex((user) => user.id === id); + const idx = this.#data.users.findIndex((user) => user.id === id); if (idx !== -1) { - this.data.users.splice(idx, 1); + this.#data.users.splice(idx, 1); } } } diff --git a/src/services/database/lowdb.ts b/src/services/database/lowdb.ts index 72cd435e..48387138 100644 --- a/src/services/database/lowdb.ts +++ b/src/services/database/lowdb.ts @@ -2,33 +2,36 @@ import crypto from 'node:crypto'; import debug from 'debug'; import { join } from 'path'; -import DatabaseProvider, { DatabaseError } from './repositories/database'; +import { + DatabaseProvider, + DatabaseError, + DEFAULT_DATABASE, +} 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 { DEFAULT_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'; +import type { User } from './repositories/user'; +import type { Database } from './repositories/database'; const DEBUG = debug('LowDB'); export default class LowDB extends DatabaseProvider { - private _db!: Low; + #db!: Low; + // is this really needed? private async __init() { // TODO: assume path to db file const dbFilePath = join(WG_PATH, 'db.json'); - this._db = await JSONFilePreset(dbFilePath, this.data); + this.#db = await JSONFilePreset(dbFilePath, DEFAULT_DATABASE); } async connect() { try { // load file db - await this._db.read(); - DEBUG('Connection done'); + await this.#db.read(); + DEBUG('Connected successfully'); return; } catch (error) { DEBUG('Database does not exist : ', error); @@ -41,31 +44,32 @@ export default class LowDB extends DatabaseProvider { throw new DatabaseError(DatabaseError.ERROR_INIT); } - this._db.update((data) => (data.system = SYSTEM)); + // TODO: move to DEFAULT_DATABASE + this.#db.update((data) => (data.system = DEFAULT_SYSTEM)); - DEBUG('Connection done'); + DEBUG('Connected successfully'); } async disconnect() { - DEBUG('Diconnect done'); + DEBUG('Disconnected successfully'); } async getSystem() { DEBUG('Get System'); - return this._db.data.system; + return this.#db.data.system; } async getLang() { - return this._db.data.system?.lang || Lang.EN; + return this.#db.data.system?.lang || 'en'; } async getUsers() { - return this._db.data.users; + return this.#db.data.users; } - async getUser(id: ID) { + async getUser(id: string) { DEBUG('Get User'); - return this._db.data.users.find((user) => user.id === id); + return this.#db.data.users.find((user) => user.id === id); } async newUserWithPassword(username: string, password: string) { @@ -79,7 +83,7 @@ export default class LowDB extends DatabaseProvider { throw new DatabaseError(DatabaseError.ERROR_PASSWORD_REQ); } - const isUserExist = this._db.data.users.find( + const isUserExist = this.#db.data.users.find( (user) => user.username === username ); if (isUserExist) { @@ -87,7 +91,7 @@ export default class LowDB extends DatabaseProvider { } const now = new Date(); - const isUserEmpty = this._db.data.users.length === 0; + const isUserEmpty = this.#db.data.users.length === 0; const newUser: User = { id: crypto.randomUUID(), @@ -99,23 +103,23 @@ export default class LowDB extends DatabaseProvider { updatedAt: now, }; - this._db.update((data) => data.users.push(newUser)); + this.#db.update((data) => data.users.push(newUser)); } async updateUser(user: User) { - let _user = await this.getUser(user.id); - if (_user) { + let oldUser = await this.getUser(user.id); + if (oldUser) { DEBUG('Update User'); - _user = user; - this._db.write(); + oldUser = user; + this.#db.write(); } } - async deleteUser(id: ID) { + async deleteUser(id: string) { DEBUG('Delete User'); - const idx = this._db.data.users.findIndex((user) => user.id === id); + const idx = this.#db.data.users.findIndex((user) => user.id === id); if (idx !== -1) { - this._db.update((data) => data.users.splice(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 48e33af8..36d791d6 100644 --- a/src/services/database/repositories/database.ts +++ b/src/services/database/repositories/database.ts @@ -1,17 +1,19 @@ -import type SystemRepository from './system/repository'; -import type UserRepository from './user/repository.ts'; -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 +import type { System, SystemRepository } from './system'; +import type { User, UserRepository } from './user'; +import type { Lang } from './types'; // Represent data structure -export type DBData = { +export type Database = { + // TODO: always return correct value, greatly improves code system: System | null; users: User[]; }; +export const DEFAULT_DATABASE: Database = { + system: null, + users: [], +}; + /** * Abstract class for database operations. * Provides methods to connect, disconnect, and interact with system and user data. @@ -19,11 +21,9 @@ export type DBData = { * **Note :** Always throw `DatabaseError` to ensure proper API error handling. * */ -export default abstract class DatabaseProvider +export abstract class DatabaseProvider implements SystemRepository, UserRepository { - protected data: DBData = { system: null, users: [] }; - /** * Connects to the database. */ @@ -37,14 +37,14 @@ export default abstract class DatabaseProvider abstract getSystem(): Promise; abstract getLang(): Promise; - abstract getUsers(): Promise>; - abstract getUser(id: ID): Promise; + abstract getUsers(): Promise; + abstract getUser(id: string): Promise; abstract newUserWithPassword( username: string, password: string ): Promise; - abstract updateUser(_user: User): Promise; - abstract deleteUser(id: ID): Promise; + abstract updateUser(user: User): Promise; + abstract deleteUser(id: string): Promise; } /** diff --git a/src/services/database/repositories/system.ts b/src/services/database/repositories/system.ts new file mode 100644 index 00000000..eb763463 --- /dev/null +++ b/src/services/database/repositories/system.ts @@ -0,0 +1,146 @@ +import packageJson from '@/package.json'; + +import type { SessionConfig } from 'h3'; +import type { Lang } from './types'; + +export type IpTables = { + PreUp: string; + PostUp: string; + PreDown: string; + PostDown: string; +}; + +export type WGInterface = { + privateKey: string; + publicKey: string; + address: string; +}; + +export type WGConfig = { + mtu: number; + persistentKeepalive: number; + rangeAddress: string; + defaultDns: string[]; + allowedIps: string[]; +}; + +export enum ChartType { + None = 0, + Line = 1, + Area = 2, + Bar = 3, +} + +export type TrafficStats = { + enabled: boolean; + type: ChartType; +}; + +export type Prometheus = { + enabled: boolean; + password: string | null; +}; + +export type Feature = { + enabled: boolean; +}; + +/** + * Representing the WireGuard network configuration data structure of a computer interface system. + */ +export type System = { + interface: WGInterface; + + release: string; + // maxAge + sessionTimeout: number; + lang: Lang; + + userConfig: WGConfig; + + wgPath: string; + wgDevice: string; + wgHost: string; + wgPort: number; + wgConfigPort: number; + + iptables: IpTables; + trafficStats: TrafficStats; + + clientExpiration: Feature; + oneTimeLinks: Feature; + sortClients: Feature; + + prometheus: Prometheus; + sessionConfig: SessionConfig; +}; + +/** + * 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 interface SystemRepository { + /** + * Retrieves the system configuration data from the database. + */ + getSystem(): Promise; + + /** + * Retrieves the system's language setting. + */ + getLang(): 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/system/index.ts b/src/services/database/repositories/system/index.ts deleted file mode 100644 index d08dc8b9..00000000 --- a/src/services/database/repositories/system/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -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 deleted file mode 100644 index aadcb741..00000000 --- a/src/services/database/repositories/system/model.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { SessionConfig } from 'h3'; -import type { - Url, - IpTables, - Lang, - Port, - Prometheus, - SessionTimeOut, - TrafficStats, - Version, - WGConfig, - WGInterface, -} from '../types'; - -/** - * Representing the WireGuard network configuration data structure of a computer interface system. - */ -export type System = { - interface: WGInterface; - - release: Version; - port: number; - webuiHost: string; - // maxAge - sessionTimeout: SessionTimeOut; - lang: Lang; - - userConfig: WGConfig; - - wgPath: string; - wgDevice: string; - wgHost: Url; - wgPort: Port; - wgConfigPort: Port; - - iptables: IpTables; - trafficStats: TrafficStats; - - wgEnableExpiresTime: boolean; - wgEnableOneTimeLinks: boolean; - wgEnableSortClients: boolean; - - prometheus: Prometheus; - sessionConfig: SessionConfig; -}; diff --git a/src/services/database/repositories/system/repository.ts b/src/services/database/repositories/system/repository.ts deleted file mode 100644 index b1b05da0..00000000 --- a/src/services/database/repositories/system/repository.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Lang } from '../types'; -import type { System } from './model'; - -/** - * 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 default interface SystemRepository { - /** - * Retrieves the system configuration data from the database. - * @returns {Promise} A promise that resolves to the system data - * if found, or `undefined` if the system data is not available. - */ - getSystem(): Promise; - - /** - * Retrieves the system's language setting. - * @returns {Promise} The current language setting of the system. - */ - getLang(): Promise; -} diff --git a/src/services/database/repositories/types.ts b/src/services/database/repositories/types.ts index 07e9785f..312539dc 100644 --- a/src/services/database/repositories/types.ts +++ b/src/services/database/repositories/types.ts @@ -1,61 +1 @@ -import type * as crypto from 'node:crypto'; - -export enum Lang { - /* english */ - EN = 'en', - /* french */ - 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 HashPassword = string; -export type Command = string; -export type Key = string; -export type IpTables = { - wgPreUp: Command; - wgPostUp: Command; - wgPreDown: Command; - wgPostDown: Command; -}; -export type WGInterface = { - privateKey: Key; - publicKey: Key; - address: Address; -}; -export type WGConfig = { - mtu: number; - persistentKeepalive: number; - rangeAddress: Address; - defaultDns: Array
; - allowedIps: Array
; -}; -export enum ChartType { - None = 0, - Line = 1, - Area = 2, - Bar = 3, -} -export type TrafficStats = { - enabled: boolean; - type: ChartType; -}; -export type Prometheus = { - enabled: boolean; - password: HashPassword | null; -}; +export type Lang = 'en' | 'fr'; diff --git a/src/services/database/repositories/user.ts b/src/services/database/repositories/user.ts new file mode 100644 index 00000000..c93c64fc --- /dev/null +++ b/src/services/database/repositories/user.ts @@ -0,0 +1,55 @@ +/** + * 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; + address?: string; + privateKey?: string; + publicKey?: string; + preSharedKey?: string; + createdAt: Date; + updatedAt: Date; + enabled: boolean; +}; + +/** + * Interface for user-related database operations. + * This interface provides methods for managing user data. + */ +export interface UserRepository { + /** + * Retrieves all users from the database. + */ + getUsers(): Promise; + + /** + * Retrieves a user by their ID or User object from the database. + */ + getUser(id: string): Promise; + + newUserWithPassword(username: string, password: string): Promise; + + /** + * Updates a user in the database. + */ + updateUser(user: User): Promise; + + /** + * Deletes a user from the database. + */ + deleteUser(id: string): Promise; +} diff --git a/src/services/database/repositories/user/model.ts b/src/services/database/repositories/user/model.ts deleted file mode 100644 index bd1910e4..00000000 --- a/src/services/database/repositories/user/model.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { Address, ID, Key, HashPassword } from '../types'; - -/** - * 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: ID; - role: ROLE; - username: string; - password: HashPassword; - name?: string; - address?: Address; - privateKey?: Key; - publicKey?: Key; - preSharedKey?: string; - createdAt: Date; - updatedAt: Date; - enabled: boolean; -}; diff --git a/src/services/database/repositories/user/repository.ts.ts b/src/services/database/repositories/user/repository.ts.ts deleted file mode 100644 index e1532c3a..00000000 --- a/src/services/database/repositories/user/repository.ts.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { ID } from '../types'; -import type { User } from './model'; - -/** - * Interface for user-related database operations. - * This interface provides methods for managing user data. - */ -export default interface UserRepository { - /** - * Retrieves all users from the database. - * @returns {Promise>} A array of users data. - */ - getUsers(): Promise>; - - /** - * Retrieves a user by their ID or User object from the database. - * @param {ID} id - The ID of the user or a User object. - * @returns {Promise} A promise that resolves to the user data - * if found, or `undefined` if the user is not available. - */ - getUser(id: ID): Promise; - - newUserWithPassword(username: string, password: string): Promise; - - /** - * Updates a user in the database. - * @param {User} user - The user to be saved. - * - * @returns {Promise} A promise that resolves when the operation is complete. - */ - updateUser(user: User): Promise; - - /** - * Deletes a user from the database. - * @param {ID} id - The ID of the user or a User object to be deleted. - * @returns {Promise} A promise that resolves when the user has been deleted. - */ - deleteUser(id: ID): Promise; -}