Browse Source

refactor code

pull/1342/head
Bernd Storath 2 years ago
parent
commit
0a5b0acd06
  1. 2
      README.md
  2. 2
      src/server/api/session.post.ts
  3. 1
      src/server/utils/Database.ts
  4. 51
      src/services/database/inmemory.ts
  5. 60
      src/services/database/lowdb.ts
  6. 30
      src/services/database/repositories/database.ts
  7. 146
      src/services/database/repositories/system.ts
  8. 55
      src/services/database/repositories/system/index.ts
  9. 45
      src/services/database/repositories/system/model.ts
  10. 22
      src/services/database/repositories/system/repository.ts
  11. 62
      src/services/database/repositories/types.ts
  12. 55
      src/services/database/repositories/user.ts
  13. 29
      src/services/database/repositories/user/model.ts
  14. 39
      src/services/database/repositories/user/repository.ts.ts

2
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

2
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)

1
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';

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

60
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<DBData>;
#db!: Low<Database>;
// 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));
}
}
}

30
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<System | null>;
abstract getLang(): Promise<Lang>;
abstract getUsers(): Promise<Array<User>>;
abstract getUser(id: ID): Promise<User | undefined>;
abstract getUsers(): Promise<User[]>;
abstract getUser(id: string): Promise<User | undefined>;
abstract newUserWithPassword(
username: string,
password: string
): Promise<void>;
abstract updateUser(_user: User): Promise<void>;
abstract deleteUser(id: ID): Promise<void>;
abstract updateUser(user: User): Promise<void>;
abstract deleteUser(id: string): Promise<void>;
}
/**

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

55
src/services/database/repositories/system/index.ts

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

45
src/services/database/repositories/system/model.ts

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

22
src/services/database/repositories/system/repository.ts

@ -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<System | null>} A promise that resolves to the system data
* if found, or `undefined` if the system data is not available.
*/
getSystem(): Promise<System | null>;
/**
* Retrieves the system's language setting.
* @returns {Promise<Lang>} The current language setting of the system.
*/
getLang(): Promise<Lang>;
}

62
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<Address>;
allowedIps: Array<Address>;
};
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';

55
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<User[]>;
/**
* Retrieves a user by their ID or User object from the database.
*/
getUser(id: string): Promise<User | undefined>;
newUserWithPassword(username: string, password: string): Promise<void>;
/**
* Updates a user in the database.
*/
updateUser(user: User): Promise<void>;
/**
* Deletes a user from the database.
*/
deleteUser(id: string): Promise<void>;
}

29
src/services/database/repositories/user/model.ts

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

39
src/services/database/repositories/user/repository.ts.ts

@ -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<Array<User>>} A array of users data.
*/
getUsers(): Promise<Array<User>>;
/**
* 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<User | undefined>} A promise that resolves to the user data
* if found, or `undefined` if the user is not available.
*/
getUser(id: ID): Promise<User | undefined>;
newUserWithPassword(username: string, password: string): Promise<void>;
/**
* Updates a user in the database.
* @param {User} user - The user to be saved.
*
* @returns {Promise<void>} A promise that resolves when the operation is complete.
*/
updateUser(user: User): Promise<void>;
/**
* Deletes a user from the database.
* @param {ID} id - The ID of the user or a User object to be deleted.
* @returns {Promise<void>} A promise that resolves when the user has been deleted.
*/
deleteUser(id: ID): Promise<void>;
}
Loading…
Cancel
Save