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
.DS_Store
*.swp
# lowdb data file
db.json

1
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.',

1
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",

17
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/[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]:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
[email protected]:
resolution: {integrity: sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==}
engines: {node: '>=18'}
[email protected]:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
@ -3841,6 +3848,10 @@ packages:
[email protected]:
resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==}
[email protected]:
resolution: {integrity: sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==}
engines: {node: '>=18'}
[email protected]:
resolution: {integrity: sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==}
@ -7582,6 +7593,10 @@ snapshots:
[email protected]: {}
[email protected]:
dependencies:
steno: 4.0.2
[email protected]: {}
[email protected]:
@ -8659,6 +8674,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
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) => {
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 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() {

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

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 {
Address,
Url,
IpTables,
Lang,
Port,
@ -29,7 +29,7 @@ export type System = {
wgPath: string;
wgDevice: string;
wgHost: Address;
wgHost: Url;
wgPort: Port;
wgConfigPort: Port;

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

Loading…
Cancel
Save