Browse Source

Refactor New UI (#1342)

* refactor code

* refactor code

* add some todos

* update pnpm, start migrating to database

* add missing i18n key

* add todo

* basic setup styling
pull/1648/head
Bernd Storath 9 months ago
committed by Bernd Storath
parent
commit
2e6190eb0d
  1. 2
      README.md
  2. 2
      package.json
  3. 1
      src/i18n.config.ts
  4. 5241
      src/package-lock.json
  5. 2
      src/package.json
  6. 30
      src/pages/setup.vue
  7. 3
      src/server/api/cnf/[clientOneTimeLink].ts
  8. 1
      src/server/api/remember-me.get.ts
  9. 2
      src/server/api/session.post.ts
  10. 6
      src/server/api/ui-chart-type.get.ts
  11. 12
      src/server/api/ui-sort-clients.get.ts
  12. 2
      src/server/api/wg-enable-expire-time.get.ts
  13. 2
      src/server/api/wg-enable-one-time-links.get.ts
  14. 1
      src/server/middleware/setup.ts
  15. 3
      src/server/utils/Database.ts
  16. 8
      src/server/utils/WireGuard.ts
  17. 2
      src/server/utils/config.ts
  18. 51
      src/services/database/inmemory.ts
  19. 61
      src/services/database/lowdb.ts
  20. 31
      src/services/database/repositories/database.ts
  21. 146
      src/services/database/repositories/system.ts
  22. 55
      src/services/database/repositories/system/index.ts
  23. 45
      src/services/database/repositories/system/model.ts
  24. 22
      src/services/database/repositories/system/repository.ts
  25. 62
      src/services/database/repositories/types.ts
  26. 55
      src/services/database/repositories/user.ts
  27. 29
      src/services/database/repositories/user/model.ts
  28. 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 - Multilanguage Support
- Traffic Stats (default off) - Traffic Stats (default off)
- One Time Links (default off) - One Time Links (default off)
- Client Expiry (default off) - Client Expiration (default off)
- Prometheus metrics support - Prometheus metrics support
## Requirements ## Requirements

2
package.json

@ -7,5 +7,5 @@
"sudostart": "sudo docker run --env WG_HOST=0.0.0.0 --name wg-easy --cap-add=NET_ADMIN --cap-add=SYS_MODULE --sysctl=\"net.ipv4.conf.all.src_valid_mark=1\" --mount type=bind,source=\"$(pwd)\"/config,target=/etc/wireguard -p 51820:51820/udp -p 51821:51821/tcp wg-easy", "sudostart": "sudo docker run --env WG_HOST=0.0.0.0 --name wg-easy --cap-add=NET_ADMIN --cap-add=SYS_MODULE --sysctl=\"net.ipv4.conf.all.src_valid_mark=1\" --mount type=bind,source=\"$(pwd)\"/config,target=/etc/wireguard -p 51820:51820/udp -p 51821:51821/tcp wg-easy",
"start": "docker run --env WG_HOST=0.0.0.0 --name wg-easy --cap-add=NET_ADMIN --cap-add=SYS_MODULE --sysctl=\"net.ipv4.conf.all.src_valid_mark=1\" --mount type=bind,source=\"$(pwd)\"/config,target=/etc/wireguard -p 51820:51820/udp -p 51821:51821/tcp wg-easy" "start": "docker run --env WG_HOST=0.0.0.0 --name wg-easy --cap-add=NET_ADMIN --cap-add=SYS_MODULE --sysctl=\"net.ipv4.conf.all.src_valid_mark=1\" --mount type=bind,source=\"$(pwd)\"/config,target=/etc/wireguard -p 51820:51820/udp -p 51821:51821/tcp wg-easy"
}, },
"packageManager": "pnpm@9.7.1" "packageManager": "pnpm@9.9.0"
} }

1
src/i18n.config.ts

@ -4,6 +4,7 @@ export default defineI18nConfig(() => ({
messages: { messages: {
en: { en: {
name: 'Name', name: 'Name',
username: 'Username',
password: 'Password', password: 'Password',
signIn: 'Sign In', signIn: 'Sign In',
logout: 'Logout', logout: 'Logout',

5241
src/package-lock.json

File diff suppressed because it is too large

2
src/package.json

@ -51,5 +51,5 @@
"typescript": "^5.5.4", "typescript": "^5.5.4",
"vue-tsc": "^2.0.29" "vue-tsc": "^2.0.29"
}, },
"packageManager": "pnpm@9.7.1" "packageManager": "pnpm@9.9.0"
} }

30
src/pages/setup.vue

@ -1,7 +1,15 @@
<template> <template>
<main> <main>
<div> <h1
<h1>Welcome to your first setup of wg-easy !</h1> class="text-4xl font-medium my-16 text-gray-700 dark:text-neutral-200 text-center"
>
<img src="/logo.png" width="32" class="inline align-middle dark:bg" />
<span class="align-middle">WireGuard</span>
</h1>
<div
class="shadow rounded-md bg-white dark:bg-neutral-700 mx-auto w-96 p-5 overflow-hidden mt-10 text-gray-700 dark:text-neutral-200"
>
<h2>Welcome to your first setup of wg-easy !</h2>
<p>Please first enter an admin username and a strong password.</p> <p>Please first enter an admin username and a strong password.</p>
<form @submit="newAccount"> <form @submit="newAccount">
<div> <div>
@ -12,6 +20,7 @@
type="text" type="text"
name="username" name="username"
autocomplete="username" autocomplete="username"
class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 outline-none"
/> />
</div> </div>
<div> <div>
@ -22,13 +31,26 @@
type="password" type="password"
name="password" name="password"
autocomplete="new-password" autocomplete="new-password"
class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 outline-none"
/> />
</div> </div>
<div> <div>
<label for="accept">I accept the condition.</label> <label for="accept">I accept the condition</label>
<input id="accept" type="checkbox" name="accept" /> <input id="accept" type="checkbox" name="accept" />
</div> </div>
<button type="submit">Save</button> <input
type="submit"
value="Save"
:class="[
{
'bg-red-800 dark:bg-red-800 hover:bg-red-700 dark:hover:bg-red-700 transition cursor-pointer':
password && username,
'bg-gray-200 dark:bg-neutral-800 cursor-not-allowed':
!password && !username,
},
'w-full rounded shadow py-2 text-sm text-white dark:text-white',
]"
/>
</form> </form>
</div> </div>
</main> </main>

3
src/server/api/cnf/[clientOneTimeLink].ts

@ -5,12 +5,13 @@ export default defineEventHandler(async (event) => {
statusCode: 500, statusCode: 500,
statusMessage: 'Invalid', statusMessage: 'Invalid',
}); });
if (!system.wgEnableOneTimeLinks) { if (!system.oneTimeLinks.enabled) {
throw createError({ throw createError({
status: 404, status: 404,
message: 'Invalid state', message: 'Invalid state',
}); });
} }
// TODO: validate with zod
const clientOneTimeLink = getRouterParam(event, 'clientOneTimeLink'); const clientOneTimeLink = getRouterParam(event, 'clientOneTimeLink');
const clients = await WireGuard.getClients(); const clients = await WireGuard.getClients();
const client = clients.find( const client = clients.find(

1
src/server/api/remember-me.get.ts

@ -1,4 +1,5 @@
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
setHeader(event, 'Content-Type', 'application/json'); setHeader(event, 'Content-Type', 'application/json');
// TODO: enable by default
return MAX_AGE > 0; return MAX_AGE > 0;
}); });

2
src/server/api/session.post.ts

@ -22,8 +22,6 @@ export default defineEventHandler(async (event) => {
}); });
} }
// TODO: timing againts timing attack
const system = await Database.getSystem(); const system = await Database.getSystem();
if (!system) if (!system)
throw createError({ throw createError({

6
src/server/api/ui-chart-type.get.ts

@ -7,9 +7,5 @@ export default defineEventHandler(async (event) => {
statusMessage: 'Invalid', statusMessage: 'Invalid',
}); });
const number = system.trafficStats.type; return system.trafficStats.type;
if (Number.isNaN(number)) {
return 0;
}
return number;
}); });

12
src/server/api/ui-sort-clients.get.ts

@ -1,5 +1,11 @@
export default defineEventHandler((event) => { export default defineEventHandler(async (event) => {
setHeader(event, 'Content-Type', 'application/json'); setHeader(event, 'Content-Type', 'application/json');
const sort = UI_ENABLE_SORT_CLIENTS; const system = await Database.getSystem();
return sort === 'true' ? true : false; if (!system)
throw createError({
statusCode: 500,
statusMessage: 'Invalid',
});
return system.sortClients.enabled;
}); });

2
src/server/api/wg-enable-expire-time.get.ts

@ -7,5 +7,5 @@ export default defineEventHandler(async (event) => {
statusMessage: 'Invalid', statusMessage: 'Invalid',
}); });
return system.wgEnableExpiresTime; return system.clientExpiration.enabled;
}); });

2
src/server/api/wg-enable-one-time-links.get.ts

@ -7,5 +7,5 @@ export default defineEventHandler(async (event) => {
statusMessage: 'Invalid', statusMessage: 'Invalid',
}); });
return system.wgEnableOneTimeLinks; return system.oneTimeLinks.enabled;
}); });

1
src/server/middleware/setup.ts

@ -10,6 +10,7 @@ export default defineEventHandler(async (event) => {
} }
const users = await Database.getUsers(); const users = await Database.getUsers();
// TODO: better error messages for api requests
if (users.length === 0) { if (users.length === 0) {
return sendRedirect(event, '/setup', 302); return sendRedirect(event, '/setup', 302);
} }

3
src/server/utils/Database.ts

@ -1,7 +1,6 @@
/** /**
* Changing the Database Provider * Changing the Database Provider
* This design allows for easy swapping of different database implementations. * This design allows for easy swapping of different database implementations.
*
*/ */
// import InMemory from '~/services/database/inmemory'; // import InMemory from '~/services/database/inmemory';
@ -14,4 +13,6 @@ provider.connect().catch((err) => {
process.exit(1); process.exit(1);
}); });
// TODO: check if old config exists and tell user about migration path
export default provider; export default provider;

8
src/server/utils/WireGuard.ts

@ -464,9 +464,13 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
async cronJobEveryMinute() { async cronJobEveryMinute() {
const config = await this.getConfig(); const config = await this.getConfig();
const system = await Database.getSystem();
if (!system) {
throw new Error('Invalid Database');
}
let needSaveConfig = false; let needSaveConfig = false;
// Expires Feature // Expires Feature
if (WG_ENABLE_EXPIRES_TIME === 'true') { if (system.clientExpiration.enabled) {
for (const client of Object.values(config.clients)) { for (const client of Object.values(config.clients)) {
if (client.enabled !== true) continue; if (client.enabled !== true) continue;
if ( if (
@ -481,7 +485,7 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
} }
} }
// One Time Link Feature // One Time Link Feature
if (WG_ENABLE_ONE_TIME_LINKS === 'true') { if (system.oneTimeLinks.enabled) {
for (const client of Object.values(config.clients)) { for (const client of Object.values(config.clients)) {
if ( if (
client.oneTimeLink !== null && client.oneTimeLink !== null &&

2
src/server/utils/config.ts

@ -2,8 +2,6 @@ import type { SessionConfig } from 'h3';
import debug from 'debug'; import debug from 'debug';
export const PORT = process.env.PORT || '51821';
export const WEBUI_HOST = process.env.WEBUI_HOST || '0.0.0.0';
export const MAX_AGE = process.env.MAX_AGE export const MAX_AGE = process.env.MAX_AGE
? parseInt(process.env.MAX_AGE, 10) * 60 ? parseInt(process.env.MAX_AGE, 10) * 60
: 0; : 0;

51
src/services/database/inmemory.ts

@ -1,44 +1,47 @@
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import debug from 'debug'; 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 { hashPassword, isPasswordStrong } from '~/server/utils/password';
import { Lang } from './repositories/types'; import { DEFAULT_SYSTEM } from './repositories/system';
import SYSTEM from './repositories/system';
import type { User } from './repositories/user/model'; import type { User } from './repositories/user';
import type { ID } from './repositories/types';
const DEBUG = debug('InMemoryDB'); const DEBUG = debug('InMemoryDB');
// In-Memory Database Provider
export default class InMemory extends DatabaseProvider { export default class InMemory extends DatabaseProvider {
#data = DEFAULT_DATABASE;
async connect() { async connect() {
this.data.system = SYSTEM; this.#data.system = DEFAULT_SYSTEM;
DEBUG('Connection done'); DEBUG('Connected successfully');
} }
async disconnect() { async disconnect() {
this.data = { system: null, users: [] }; this.#data = { system: null, users: [] };
DEBUG('Diconnect done'); DEBUG('Disconnected successfully');
} }
async getSystem() { async getSystem() {
DEBUG('Get System'); DEBUG('Get System');
return this.data.system; return this.#data.system;
} }
async getLang() { async getLang() {
return this.data.system?.lang || Lang.EN; return this.#data.system?.lang || 'en';
} }
async getUsers() { async getUsers() {
return this.data.users; return this.#data.users;
} }
async getUser(id: ID) { async getUser(id: string) {
DEBUG('Get User'); 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) { async newUserWithPassword(username: string, password: string) {
@ -52,7 +55,7 @@ export default class InMemory extends DatabaseProvider {
throw new DatabaseError(DatabaseError.ERROR_PASSWORD_REQ); throw new DatabaseError(DatabaseError.ERROR_PASSWORD_REQ);
} }
const isUserExist = this.data.users.find( const isUserExist = this.#data.users.find(
(user) => user.username === username (user) => user.username === username
); );
if (isUserExist) { if (isUserExist) {
@ -60,7 +63,7 @@ export default class InMemory extends DatabaseProvider {
} }
const now = new Date(); const now = new Date();
const isUserEmpty = this.data.users.length === 0; const isUserEmpty = this.#data.users.length === 0;
const newUser: User = { const newUser: User = {
id: crypto.randomUUID(), id: crypto.randomUUID(),
@ -72,22 +75,22 @@ export default class InMemory extends DatabaseProvider {
updatedAt: now, updatedAt: now,
}; };
this.data.users.push(newUser); this.#data.users.push(newUser);
} }
async updateUser(user: User) { async updateUser(user: User) {
let _user = await this.getUser(user.id); let oldUser = await this.getUser(user.id);
if (_user) { if (oldUser) {
DEBUG('Update User'); DEBUG('Update User');
_user = user; oldUser = user;
} }
} }
async deleteUser(id: ID) { async deleteUser(id: string) {
DEBUG('Delete User'); 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) { if (idx !== -1) {
this.data.users.splice(idx, 1); this.#data.users.splice(idx, 1);
} }
} }
} }

61
src/services/database/lowdb.ts

@ -2,33 +2,36 @@ import crypto from 'node:crypto';
import debug from 'debug'; import debug from 'debug';
import { join } from 'path'; 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 { hashPassword, isPasswordStrong } from '~/server/utils/password';
import { JSONFilePreset } from 'lowdb/node'; import { JSONFilePreset } from 'lowdb/node';
import { Lang } from './repositories/types'; import { DEFAULT_SYSTEM } from './repositories/system';
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'; import type { Low } from 'lowdb';
import type { User } from './repositories/user';
import type { Database } from './repositories/database';
const DEBUG = debug('LowDB'); const DEBUG = debug('LowDB');
export default class LowDB extends DatabaseProvider { export default class LowDB extends DatabaseProvider {
private _db!: Low<DBData>; #db!: Low<Database>;
// is this really needed?
private async __init() { private async __init() {
// TODO: assume path to db file // TODO: assume path to db file
const dbFilePath = join(WG_PATH, 'db.json'); const dbFilePath = join(WG_PATH, 'db.json');
this._db = await JSONFilePreset(dbFilePath, this.data); this.#db = await JSONFilePreset(dbFilePath, DEFAULT_DATABASE);
} }
async connect() { async connect() {
try { try {
// load file db // load file db
await this._db.read(); await this.#db.read();
DEBUG('Connection done'); DEBUG('Connected successfully');
return; return;
} catch (error) { } catch (error) {
DEBUG('Database does not exist : ', error); DEBUG('Database does not exist : ', error);
@ -41,36 +44,38 @@ export default class LowDB extends DatabaseProvider {
throw new DatabaseError(DatabaseError.ERROR_INIT); 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() { async disconnect() {
DEBUG('Diconnect done'); DEBUG('Disconnected successfully');
} }
async getSystem() { async getSystem() {
DEBUG('Get System'); DEBUG('Get System');
return this._db.data.system; return this.#db.data.system;
} }
async getLang() { async getLang() {
return this._db.data.system?.lang || Lang.EN; return this.#db.data.system?.lang || 'en';
} }
async getUsers() { async getUsers() {
return this._db.data.users; return this.#db.data.users;
} }
async getUser(id: ID) { async getUser(id: string) {
DEBUG('Get User'); 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) { async newUserWithPassword(username: string, password: string) {
DEBUG('New User'); DEBUG('New User');
// TODO: should be handled by zod. completely remove database error
if (username.length < 8) { if (username.length < 8) {
throw new DatabaseError(DatabaseError.ERROR_USERNAME_REQ); throw new DatabaseError(DatabaseError.ERROR_USERNAME_REQ);
} }
@ -79,7 +84,7 @@ export default class LowDB extends DatabaseProvider {
throw new DatabaseError(DatabaseError.ERROR_PASSWORD_REQ); 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 (user) => user.username === username
); );
if (isUserExist) { if (isUserExist) {
@ -87,7 +92,7 @@ export default class LowDB extends DatabaseProvider {
} }
const now = new Date(); const now = new Date();
const isUserEmpty = this._db.data.users.length === 0; const isUserEmpty = this.#db.data.users.length === 0;
const newUser: User = { const newUser: User = {
id: crypto.randomUUID(), id: crypto.randomUUID(),
@ -99,23 +104,23 @@ export default class LowDB extends DatabaseProvider {
updatedAt: now, updatedAt: now,
}; };
this._db.update((data) => data.users.push(newUser)); this.#db.update((data) => data.users.push(newUser));
} }
async updateUser(user: User) { async updateUser(user: User) {
let _user = await this.getUser(user.id); let oldUser = await this.getUser(user.id);
if (_user) { if (oldUser) {
DEBUG('Update User'); DEBUG('Update User');
_user = user; oldUser = user;
this._db.write(); this.#db.write();
} }
} }
async deleteUser(id: ID) { async deleteUser(id: string) {
DEBUG('Delete User'); 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) { if (idx !== -1) {
this._db.update((data) => data.users.splice(idx, 1)); this.#db.update((data) => data.users.splice(idx, 1));
} }
} }
} }

31
src/services/database/repositories/database.ts

@ -1,17 +1,19 @@
import type SystemRepository from './system/repository'; import type { System, SystemRepository } from './system';
import type UserRepository from './user/repository.ts'; import type { User, UserRepository } from './user';
import type { Lang, ID } from './types'; import type { Lang } from './types';
import type { User } from './user/model';
import type { System } from './system/model';
// TODO: re-export type from /user & /system
// Represent data structure // Represent data structure
export type DBData = { export type Database = {
// TODO: always return correct value, greatly improves code
system: System | null; system: System | null;
users: User[]; users: User[];
}; };
export const DEFAULT_DATABASE: Database = {
system: null,
users: [],
};
/** /**
* 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.
@ -19,11 +21,9 @@ export type DBData = {
* **Note :** Always throw `DatabaseError` to ensure proper API error handling. * **Note :** Always throw `DatabaseError` to ensure proper API error handling.
* *
*/ */
export default abstract class DatabaseProvider export abstract class DatabaseProvider
implements SystemRepository, UserRepository implements SystemRepository, UserRepository
{ {
protected data: DBData = { system: null, users: [] };
/** /**
* Connects to the database. * Connects to the database.
*/ */
@ -35,16 +35,17 @@ export default abstract class DatabaseProvider
abstract disconnect(): Promise<void>; abstract disconnect(): Promise<void>;
abstract getSystem(): Promise<System | null>; abstract getSystem(): Promise<System | null>;
// TODO: remove
abstract getLang(): Promise<Lang>; abstract getLang(): Promise<Lang>;
abstract getUsers(): Promise<Array<User>>; abstract getUsers(): Promise<User[]>;
abstract getUser(id: ID): Promise<User | undefined>; abstract getUser(id: string): Promise<User | undefined>;
abstract newUserWithPassword( abstract newUserWithPassword(
username: string, username: string,
password: string password: string
): Promise<void>; ): Promise<void>;
abstract updateUser(_user: User): Promise<void>; abstract updateUser(user: User): Promise<void>;
abstract deleteUser(id: ID): 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 type Lang = 'en' | 'fr';
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;
};

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