Browse Source

rebase & add: persistent lowdb provider

pull/1330/head
tetuaoro 2 years ago
parent
commit
aa55223273
  1. 2
      .gitignore
  2. 1
      src/i18n.config.ts
  3. 1
      src/package.json
  4. 17
      src/pnpm-lock.yaml
  5. 5
      src/server/utils/Database.ts
  6. 70
      src/services/database/inmemory.ts
  7. 121
      src/services/database/lowdb.ts
  8. 11
      src/services/database/repositories/database.ts
  9. 55
      src/services/database/repositories/system/index.ts
  10. 4
      src/services/database/repositories/system/model.ts
  11. 14
      src/services/database/repositories/types.ts

2
.gitignore

@ -4,3 +4,5 @@
/src/node_modules /src/node_modules
.DS_Store .DS_Store
*.swp *.swp
# lowdb data file
db.json

1
src/i18n.config.ts

@ -46,6 +46,7 @@ export default defineI18nConfig(() => ({
ExpireDate: 'Expire Date', ExpireDate: 'Expire Date',
Permanent: 'Permanent', Permanent: 'Permanent',
OneTimeLink: 'Generate short one time link', OneTimeLink: 'Generate short one time link',
errorInit: 'Initialization failed.',
errorDatabaseConn: 'Failed to connect to the database.', errorDatabaseConn: 'Failed to connect to the database.',
errorPasswordReq: 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.', '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.',

1
src/package.json

@ -30,6 +30,7 @@
"crc-32": "^1.2.2", "crc-32": "^1.2.2",
"debug": "^4.3.6", "debug": "^4.3.6",
"js-sha256": "^0.11.0", "js-sha256": "^0.11.0",
"lowdb": "^7.0.1",
"nuxt": "^3.12.4", "nuxt": "^3.12.4",
"pinia": "^2.2.1", "pinia": "^2.2.1",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",

17
src/pnpm-lock.yaml

@ -41,6 +41,9 @@ importers:
js-sha256: js-sha256:
specifier: ^0.11.0 specifier: ^0.11.0
version: 0.11.0 version: 0.11.0
lowdb:
specifier: ^7.0.1
version: 7.0.1
nuxt: nuxt:
specifier: ^3.12.4 specifier: ^3.12.4
version: 3.12.4(@parcel/[email protected])(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected](@types/[email protected])([email protected]))([email protected]([email protected])) version: 3.12.4(@parcel/[email protected])(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected](@types/[email protected])([email protected]))([email protected]([email protected]))
@ -2868,6 +2871,10 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
[email protected]:
resolution: {integrity: sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==}
engines: {node: '>=18'}
[email protected]: [email protected]:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
@ -3841,6 +3848,10 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==}
[email protected]:
resolution: {integrity: sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==}
engines: {node: '>=18'}
[email protected]: [email protected]:
resolution: {integrity: sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==} resolution: {integrity: sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==}
@ -7582,6 +7593,10 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]:
dependencies:
steno: 4.0.2
[email protected]: {} [email protected]: {}
[email protected]: [email protected]:
@ -8659,6 +8674,8 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: [email protected]:
dependencies: dependencies:
fast-fifo: 1.3.2 fast-fifo: 1.3.2

5
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) => { provider.connect().catch((err) => {
console.error(err); console.error(err);

70
src/services/database/inmemory.ts

@ -1,82 +1,26 @@
import debug from 'debug';
import packageJson from '@/package.json';
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import debug from 'debug';
import DatabaseProvider, { DatabaseError } from './repositories/database'; 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 type { User } from './repositories/user/model';
import { hashPassword, isPasswordStrong } from '~/server/utils/password'; import type { ID } from './repositories/types';
const DEBUG = debug('InMemoryDB'); const DEBUG = debug('InMemoryDB');
// Represent in-memory data structure
type InMemoryData = {
system: System | null;
users: User[];
};
// In-Memory Database Provider // In-Memory Database Provider
export default class InMemory extends DatabaseProvider { export default class InMemory extends DatabaseProvider {
protected data: InMemoryData = { system: null, users: [] };
async connect() { async connect() {
DEBUG('Connection...'); this.data.system = SYSTEM;
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;
DEBUG('Connection done'); DEBUG('Connection done');
} }
async disconnect() { async disconnect() {
this.data = { system: null, users: [] }; this.data = { system: null, users: [] };
DEBUG('Diconnect done');
} }
async getSystem() { async getSystem() {

121
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<DBData>;
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));
}
}
}

11
src/services/database/repositories/database.ts

@ -4,6 +4,14 @@ import type { Lang, ID } from './types';
import type { User } from './user/model'; import type { User } from './user/model';
import type { System } from './system/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. * Abstract class for database operations.
* Provides methods to connect, disconnect, and interact with system and user data. * 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 export default abstract class DatabaseProvider
implements SystemRepository, UserRepository implements SystemRepository, UserRepository
{ {
protected data: DBData = { system: null, users: [] };
/** /**
* Connects to the database. * Connects to the database.
*/ */
@ -68,6 +78,7 @@ export default abstract class DatabaseProvider
* @extends {Error} * @extends {Error}
*/ */
export class DatabaseError extends Error { export class DatabaseError extends Error {
static readonly ERROR_INIT = 'errorInit';
static readonly ERROR_PASSWORD_REQ = 'errorPasswordReq'; static readonly ERROR_PASSWORD_REQ = 'errorPasswordReq';
static readonly ERROR_USER_EXIST = 'errorUserExist'; static readonly ERROR_USER_EXIST = 'errorUserExist';
static readonly ERROR_DATABASE_CONNECTION = 'errorDatabaseConn'; static readonly ERROR_DATABASE_CONNECTION = 'errorDatabaseConn';

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

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

@ -1,6 +1,6 @@
import type { SessionConfig } from 'h3'; import type { SessionConfig } from 'h3';
import type { import type {
Address, Url,
IpTables, IpTables,
Lang, Lang,
Port, Port,
@ -29,7 +29,7 @@ export type System = {
wgPath: string; wgPath: string;
wgDevice: string; wgDevice: string;
wgHost: Address; wgHost: Url;
wgPort: Port; wgPort: Port;
wgConfigPort: Port; wgConfigPort: Port;

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

@ -7,11 +7,23 @@ export enum Lang {
FR = 'fr', 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 ID = crypto.UUID;
export type Version = string; export type Version = string;
export type SessionTimeOut = number; export type SessionTimeOut = number;
export type Port = number; export type Port = number;
export type Address = string;
export type HashPassword = string; export type HashPassword = string;
export type Command = string; export type Command = string;
export type Key = string; export type Key = string;

Loading…
Cancel
Save