From 07cead5bf20c80e5cbf8bb0e58a97304442d82f7 Mon Sep 17 00:00:00 2001 From: tetuaoro <65575727+tetuaoro@users.noreply.github.com> Date: Thu, 29 Aug 2024 13:13:33 +0200 Subject: [PATCH] add: interfaces to connect a database provider - easy swapping between database provider --- .../database}/inmemory.ts | 68 ++++++++++----- src/composables/useDatabase.ts | 5 ++ src/plugins/database.server.ts | 15 ++++ src/ports/database.ts | 46 ++++++++++ src/ports/system/interface.ts | 22 +++++ src/ports/system/model.ts | 44 ++++++++++ src/ports/types.ts | 55 ++++++++++++ src/ports/user/interface.ts | 39 +++++++++ src/ports/user/model.ts | 28 ++++++ src/server/api/lang.get.ts | 8 +- src/server/databases/database.ts | 22 ----- src/server/databases/entities/system.ts | 86 ------------------- src/server/databases/entities/user.ts | 52 ----------- src/server/utils/databases/InMemory.ts | 12 --- .../utils/repositories/SystemRepository.ts | 14 --- .../utils/repositories/UserRepository.ts | 11 --- 16 files changed, 304 insertions(+), 223 deletions(-) rename src/{server/databases/providers => adapters/database}/inmemory.ts (61%) create mode 100644 src/composables/useDatabase.ts create mode 100644 src/plugins/database.server.ts create mode 100644 src/ports/database.ts create mode 100644 src/ports/system/interface.ts create mode 100644 src/ports/system/model.ts create mode 100644 src/ports/types.ts create mode 100644 src/ports/user/interface.ts create mode 100644 src/ports/user/model.ts delete mode 100644 src/server/databases/database.ts delete mode 100644 src/server/databases/entities/system.ts delete mode 100644 src/server/databases/entities/user.ts delete mode 100644 src/server/utils/databases/InMemory.ts delete mode 100644 src/server/utils/repositories/SystemRepository.ts delete mode 100644 src/server/utils/repositories/UserRepository.ts diff --git a/src/server/databases/providers/inmemory.ts b/src/adapters/database/inmemory.ts similarity index 61% rename from src/server/databases/providers/inmemory.ts rename to src/adapters/database/inmemory.ts index 9a20d505..2a0ee92b 100644 --- a/src/server/databases/providers/inmemory.ts +++ b/src/adapters/database/inmemory.ts @@ -1,16 +1,15 @@ -import type { SessionConfig } from 'h3'; -import type { Identity } from '../database'; -import type System from '../entities/system'; -import type User from '../entities/user'; -import type { UserProvider } from '../entities/user'; -import type { SystemProvider } from '../entities/system'; -import type DatabaseProvider from '../database'; - -import { ChartType } from '../entities/system'; import debug from 'debug'; - import packageJson from '@/package.json'; +import DatabaseProvider from '~/ports/database'; +import { ChartType, Lang } from '~/ports/types'; +import { ROLE } from '~/ports/user/model'; + +import type { SessionConfig } from 'h3'; +import type { System } from '~/ports/system/model'; +import type { User } from '~/ports/user/model'; +import type { Identity } from '~/ports/types'; + const INMDP_DEBUG = debug('InMemoryDP'); // Represent in-memory data structure @@ -20,12 +19,10 @@ type InMemoryData = { }; // In-Memory Database Provider -export default class InMemoryDP - implements DatabaseProvider, UserProvider, SystemProvider -{ - private data: InMemoryData = { users: [] }; +export class InMemory extends DatabaseProvider { + protected data: InMemoryData = { users: [] }; - async connect() { + override async connect() { INMDP_DEBUG('Connection...'); const system: System = { release: packageJson.release.version, @@ -37,7 +34,7 @@ export default class InMemoryDP port: 51821, webuiHost: '0.0.0.0', sessionTimeout: 3600, // 1 hour - lang: 'en', + lang: Lang.EN, userConfig: { mtu: 1420, persistentKeepalive: 0, @@ -76,13 +73,13 @@ export default class InMemoryDP INMDP_DEBUG('Connection done'); } - async disconnect() { + override async disconnect() { this.data = { users: [] }; } - async getSystem() { + override async getSystem() { INMDP_DEBUG('Get System'); - return this.data.system || null; + return this.data.system; } async saveSystem(system: System) { @@ -90,11 +87,15 @@ export default class InMemoryDP this.data.system = system; } - async getUsers() { + override async getLang() { + return this.data.system?.lang || Lang.EN; + } + + override async getUsers() { return this.data.users; } - async getUser(id: Identity) { + override async getUser(id: Identity) { INMDP_DEBUG('Get User'); if (typeof id === 'string') { return this.data.users.find((user) => user.id === id); @@ -102,14 +103,37 @@ export default class InMemoryDP return this.data.users.find((user) => user.id === id.id); } - async saveUser(user: User) { + override async saveUser(user: User) { let _user = await this.getUser(user); if (_user) { INMDP_DEBUG('Update User'); _user = user; } else { INMDP_DEBUG('New User'); + if (this.data.users.length == 0) { + // first user is admin + user.role = ROLE.ADMIN; + } this.data.users.push(user); } } + + override async deleteUser(id: Identity) { + const _id = typeof id === 'string' ? id : id.id; + const idx = this.data.users.findIndex((user) => user.id == _id); + if (idx !== -1) { + this.data.users.splice(idx, 1); + } + } +} + +export default function initInMemoryProvider() { + const provider = new InMemory(); + + provider.connect().catch((err) => { + console.error(err); + process.exit(1); + }); + + return provider; } diff --git a/src/composables/useDatabase.ts b/src/composables/useDatabase.ts new file mode 100644 index 00000000..a790a0c7 --- /dev/null +++ b/src/composables/useDatabase.ts @@ -0,0 +1,5 @@ +import type { InMemory } from '~/adapters/database/inmemory'; + +export default (): InMemory => { + return useNuxtApp().$database; +}; diff --git a/src/plugins/database.server.ts b/src/plugins/database.server.ts new file mode 100644 index 00000000..2848889c --- /dev/null +++ b/src/plugins/database.server.ts @@ -0,0 +1,15 @@ +/** + * Changing the Database Provider + * This design allows for easy swapping of different database implementations. + * + */ + +import initInMemoryProvider from '~/adapters/database/inmemory'; + +export default defineNuxtPlugin(() => { + return { + provide: { + database: initInMemoryProvider(), + }, + }; +}); diff --git a/src/ports/database.ts b/src/ports/database.ts new file mode 100644 index 00000000..fd0d8384 --- /dev/null +++ b/src/ports/database.ts @@ -0,0 +1,46 @@ +import type SystemRepository from './system/interface'; +import type UserRepository from './user/interface'; +import type { Identity, Undefined, Lang } from './types'; +import type { User } from './user/model'; +import type { System } from './system/model'; + +/** + * Abstract class for database operations. + * Provides methods to connect, disconnect, and interact with system and user data. + */ +export default abstract class DatabaseProvider + implements SystemRepository, UserRepository +{ + /** + * Connects to the database. + */ + connect(): Promise { + throw new Error('Method not implemented.'); + } + /** + * Disconnects from the database. + */ + disconnect(): Promise { + throw new Error('Method not implemented.'); + } + + getSystem(): Promise { + throw new Error('Method not implemented.'); + } + getLang(): Promise { + throw new Error('Method not implemented.'); + } + + getUsers(): Promise> { + throw new Error('Method not implemented.'); + } + getUser(_id: Identity): Promise { + throw new Error('Method not implemented.'); + } + saveUser(_user: User): Promise { + throw new Error('Method not implemented.'); + } + deleteUser(_id: Identity): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/src/ports/system/interface.ts b/src/ports/system/interface.ts new file mode 100644 index 00000000..7a82de5d --- /dev/null +++ b/src/ports/system/interface.ts @@ -0,0 +1,22 @@ +import type { Lang, Undefined } 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/ports/system/model.ts b/src/ports/system/model.ts new file mode 100644 index 00000000..c88c7ac1 --- /dev/null +++ b/src/ports/system/model.ts @@ -0,0 +1,44 @@ +import type { SessionConfig } from 'h3'; +import type { + Address, + IpTables, + Lang, + Port, + Prometheus, + SessionTimeOut, + String, + Boolean, + 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: Address; + wgPort: Port; + wgConfigPort: Port; + + iptables: IpTables; + trafficStats: TrafficStats; + + wgEnableExpiresTime: Boolean; + prometheus: Prometheus; + sessionConfig: SessionConfig; +}; diff --git a/src/ports/types.ts b/src/ports/types.ts new file mode 100644 index 00000000..539b5b70 --- /dev/null +++ b/src/ports/types.ts @@ -0,0 +1,55 @@ +export type Undefined = null | undefined | 0 | ''; +export enum Lang { + /* english */ + EN = 'en', + /* french */ + FR = 'fr', +} +export type String = string; +export type ID = String; +export type Boolean = boolean; +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; +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 | Undefined; +}; +/** + * `id` of T or T. + * + * @template T - The specific type that can be used in place of id String. + */ +export type Identity = ID | T; diff --git a/src/ports/user/interface.ts b/src/ports/user/interface.ts new file mode 100644 index 00000000..b4113ec3 --- /dev/null +++ b/src/ports/user/interface.ts @@ -0,0 +1,39 @@ +import type { Identity, Undefined } 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 {Identity} 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: Identity): Promise; + + /** + * Creates or updates a user in the database. + * @param {User} user - The user to be saved. + * + * **Note:** If the user already exists, this method will update their details. + * If the user does not exist, it will create a new user entry. + * @returns {Promise} A promise that resolves when the operation is complete. + */ + saveUser(user: User): Promise; + + /** + * Deletes a user from the database. + * @param {Identity} 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: Identity): Promise; +} diff --git a/src/ports/user/model.ts b/src/ports/user/model.ts new file mode 100644 index 00000000..791fcdde --- /dev/null +++ b/src/ports/user/model.ts @@ -0,0 +1,28 @@ +import type { Address, ID, Key, HashPassword, String } from '../types'; + +export enum ROLE { + /* Full permissions to any resources (app, database...) */ + ADMIN = 'ADMIN', + /* Grants write and read permissions on their own resources and `CLIENT` resources without `ADMIN` permissions */ + EDITOR = 'EDITOR', + /* Grants write and read permissions on their own resources */ + CLIENT = '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/server/api/lang.get.ts b/src/server/api/lang.get.ts index 0d768a70..3e2af09a 100644 --- a/src/server/api/lang.get.ts +++ b/src/server/api/lang.get.ts @@ -1,7 +1,7 @@ +import useDatabase from '~/composables/useDatabase'; + export default defineEventHandler(async (event) => { setHeader(event, 'Content-Type', 'application/json'); - - const provider = InMemory; // TODO multiple provider - const lang = await SystemRepository.getLang(provider); - return lang; + const db = useDatabase(); + return db.getLang(); }); diff --git a/src/server/databases/database.ts b/src/server/databases/database.ts deleted file mode 100644 index 60eb6c65..00000000 --- a/src/server/databases/database.ts +++ /dev/null @@ -1,22 +0,0 @@ -export type Undefined = null | undefined | 0 | ''; -/** - * `id` of T or T. - * - * @template T - The specific type that can be used in place of id string. - */ -export type Identity = string | T; - -/** - * Abstract class for database operations. - * Provides methods to connect, disconnect, and interact with system and user data. - */ -export default abstract class DatabaseProvider { - /** - * Connects to the database. - */ - abstract connect(): Promise; - /** - * Disconnects from the database. - */ - abstract disconnect(): Promise; -} diff --git a/src/server/databases/entities/system.ts b/src/server/databases/entities/system.ts deleted file mode 100644 index 4e1ae35e..00000000 --- a/src/server/databases/entities/system.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { SessionConfig } from 'h3'; -import type { Undefined } from '../database'; - -export type Lang = 'en' | 'ua' | 'ru' | 'tr' | 'no' | 'pl' | 'fr'; -export type Version = string; -export type SessionTimeOut = number; -export type Port = number; -export type Address = string; -export type PassordHash = 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?: PassordHash | Undefined; -}; - -/** - * Representing the WireGuard network configuration data structure of a computer interface system. - */ -type System = { - interface: WGInterface; - - release: Version; - port: number; - webuiHost: string; - // maxAge - sessionTimeout: SessionTimeOut; - lang: Lang; - - userConfig: WGConfig; - - wgPath: string; - wgDevice: string; - wgHost: Address; - wgPort: Port; - wgConfigPort: Port; - - iptables: IpTables; - trafficStats: TrafficStats; - - wgEnableExpiresTime: boolean; - prometheus: Prometheus; - sessionConfig: SessionConfig; -}; - -export default System; - -/** - * Abstract class for system-related database operations. - * This class provides a method for retrieving system configuration data from the database. - */ -export abstract class SystemProvider { - /** - * Retrieves system data from the database. - * @returns {Promise} The system data or null if not found. - */ - abstract getSystem(): Promise; -} diff --git a/src/server/databases/entities/user.ts b/src/server/databases/entities/user.ts deleted file mode 100644 index 1f2d635a..00000000 --- a/src/server/databases/entities/user.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { Identity, Undefined } from '../database'; -import type { Address, Key, PassordHash } from './system'; - -export type ROLE = 'ADMIN'; - -/** - * Representing a user data structure. - */ -type User = { - id: string; - roles: Array; - username: string; - password: PassordHash; - name: string; - address: Address; - privateKey: Key; - publicKey: Key; - preSharedKey: string; - createdAt: Date; - updatedAt: Date; - enabled: boolean; -}; - -export default User; - -/** - * Abstract class for user-related database operations. - * This class provides methods for retrieving and saving user data. - */ -export abstract class UserProvider { - /** - * Retrieves all users from the database. - * @returns {Promise>} The array of users data. - */ - abstract getUsers(): Promise>; - - /** - * Retrieves a user by their ID or by User structure from the database. - * @param {Identity} id - The ID of the user or a user. - * @returns {Promise} The user data or null if not found. - */ - abstract getUser(id: Identity): Promise; - - /** - * Creates or Updates a user in the database. - * @param {User} user - The user to be saved. - * - * **Note:** If the user already exists, this method will update their details. - * If the user does not exist, it will create a new user entry. - */ - abstract saveUser(user: User): Promise; -} diff --git a/src/server/utils/databases/InMemory.ts b/src/server/utils/databases/InMemory.ts deleted file mode 100644 index 3f9a6b28..00000000 --- a/src/server/utils/databases/InMemory.ts +++ /dev/null @@ -1,12 +0,0 @@ -import InMemoryDP from '@/server/databases/providers/inmemory'; - -const provider = new InMemoryDP(); // TODO multiple providers - -provider.connect().catch((err) => { - console.error(err); - provider.disconnect().catch((err) => { - console.error(err); - }); -}); - -export default provider; diff --git a/src/server/utils/repositories/SystemRepository.ts b/src/server/utils/repositories/SystemRepository.ts deleted file mode 100644 index 92ccac40..00000000 --- a/src/server/utils/repositories/SystemRepository.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Lang, SystemProvider } from '@/server/databases/entities/system'; - -class SystemRepository { - async getLang(provider: SystemProvider): Promise { - const _system = await provider.getSystem(); - if (_system) { - const { lang } = _system; - return lang; - } - return 'en'; - } -} - -export default new SystemRepository(); diff --git a/src/server/utils/repositories/UserRepository.ts b/src/server/utils/repositories/UserRepository.ts deleted file mode 100644 index 74acd14b..00000000 --- a/src/server/utils/repositories/UserRepository.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type User from '@/server/databases/entities/user'; -import type { UserProvider } from '@/server/databases/entities/user'; -import type { Undefined } from '@/server/databases/database'; - -class UserRepository { - newUser(db: UserProvider): Promise { - return db.getUser(''); - } -} - -export default new UserRepository();