From cdf159cba771b89a361273cdb61f4bc6dcf779b9 Mon Sep 17 00:00:00 2001 From: Bernd Storath <999999bst@gmail.com> Date: Tue, 12 Nov 2024 09:30:20 +0100 Subject: [PATCH] make db results readonly avoid weird side effects, when modifying the db object as its only allowed inside e.g. lowdb.ts --- src/server/api/session.post.ts | 8 ++----- src/server/api/setup/migrate.post.ts | 4 +++- src/server/utils/WireGuard.ts | 2 +- src/server/utils/ip.ts | 13 ++++++----- src/server/utils/wgHelper.ts | 10 ++++++--- src/services/database/lowdb.ts | 22 ++++++++++++++----- src/services/database/repositories/client.ts | 7 ++++-- .../database/repositories/database.ts | 3 --- src/services/database/repositories/system.ts | 9 ++------ src/services/database/repositories/user.ts | 21 ++++-------------- 10 files changed, 47 insertions(+), 52 deletions(-) diff --git a/src/server/api/session.post.ts b/src/server/api/session.post.ts index 364c7603..11a900ab 100644 --- a/src/server/api/session.post.ts +++ b/src/server/api/session.post.ts @@ -1,5 +1,3 @@ -import type { SessionConfig } from 'h3'; - export default defineEventHandler(async (event) => { const { username, password, remember } = await readValidatedBody( event, @@ -25,7 +23,7 @@ export default defineEventHandler(async (event) => { const system = await Database.system.get(); - const conf: SessionConfig = system.sessionConfig; + const conf = { ...system.sessionConfig }; if (remember) { conf.cookie = { @@ -34,9 +32,7 @@ export default defineEventHandler(async (event) => { }; } - const session = await useSession(event, { - ...system.sessionConfig, - }); + const session = await useSession(event, conf); const data = await session.update({ userId: user.id, diff --git a/src/server/api/setup/migrate.post.ts b/src/server/api/setup/migrate.post.ts index 6c92ccc7..be44d9bd 100644 --- a/src/server/api/setup/migrate.post.ts +++ b/src/server/api/setup/migrate.post.ts @@ -52,6 +52,8 @@ export default defineEventHandler(async (event) => { }, userConfig: { ...system.userConfig, + defaultDns: [...system.userConfig.defaultDns], + allowedIps: [...system.userConfig.allowedIps], address4Range: stringifyIp({ number: oldCidr.start, version: 4 }) + '/24', }, @@ -72,7 +74,7 @@ export default defineEventHandler(async (event) => { publicKey: oldClient.publicKey, expiresAt: null, oneTimeLink: null, - allowedIPs: db.system.userConfig.allowedIps, + allowedIPs: [...db.system.userConfig.allowedIps], serverAllowedIPs: [], persistentKeepalive: 0, address6: address6, diff --git a/src/server/utils/WireGuard.ts b/src/server/utils/WireGuard.ts index 02fc5f2d..86567309 100644 --- a/src/server/utils/WireGuard.ts +++ b/src/server/utils/WireGuard.ts @@ -149,7 +149,7 @@ class WireGuard { oneTimeLink: null, expiresAt: null, enabled: true, - allowedIPs: system.userConfig.allowedIps, + allowedIPs: [...system.userConfig.allowedIps], serverAllowedIPs: null, persistentKeepalive: system.userConfig.persistentKeepalive, }; diff --git a/src/server/utils/ip.ts b/src/server/utils/ip.ts index 3f2fb5e3..270bc1a7 100644 --- a/src/server/utils/ip.ts +++ b/src/server/utils/ip.ts @@ -1,25 +1,26 @@ import { parseCidr } from 'cidr-tools'; import { stringifyIp } from 'ip-bigint'; +import type { DeepReadonly } from 'vue'; import type { Database } from '~~/services/database/repositories/database'; export function nextIPv4( - system: Database['system'], - clients: Database['clients'] + system: DeepReadonly, + clients: DeepReadonly ) { return nextIP(4, system, clients); } export function nextIPv6( - system: Database['system'], - clients: Database['clients'] + system: DeepReadonly, + clients: DeepReadonly ) { return nextIP(6, system, clients); } function nextIP( version: 4 | 6, - system: Database['system'], - clients: Database['clients'] + system: DeepReadonly, + clients: DeepReadonly ) { const cidr = parseCidr(system.userConfig[`address${version}Range`]); let address; diff --git a/src/server/utils/wgHelper.ts b/src/server/utils/wgHelper.ts index 7c0c0ff5..f55c4d81 100644 --- a/src/server/utils/wgHelper.ts +++ b/src/server/utils/wgHelper.ts @@ -1,9 +1,10 @@ import { parseCidr } from 'cidr-tools'; +import type { DeepReadonly } from 'vue'; import type { Client } from '~~/services/database/repositories/client'; import type { System } from '~~/services/database/repositories/system'; export const wg = { - generateServerPeer: (client: Client) => { + generateServerPeer: (client: DeepReadonly) => { const allowedIps = [ `${client.address4}/32`, `${client.address6}/128`, @@ -17,7 +18,7 @@ PresharedKey = ${client.preSharedKey} AllowedIPs = ${allowedIps.join(', ')}`; }, - generateServerInterface: (system: System) => { + generateServerInterface: (system: DeepReadonly) => { const cidr4Block = parseCidr(system.userConfig.address4Range).prefix; const cidr6Block = parseCidr(system.userConfig.address6Range).prefix; @@ -36,7 +37,10 @@ PreDown = ${system.iptables.PreDown} PostDown = ${system.iptables.PostDown}`; }, - generateClientConfig: (system: System, client: Client) => { + generateClientConfig: ( + system: DeepReadonly, + client: DeepReadonly + ) => { const cidr4Block = parseCidr(system.userConfig.address4Range).prefix; const cidr6Block = parseCidr(system.userConfig.address6Range).prefix; diff --git a/src/services/database/lowdb.ts b/src/services/database/lowdb.ts index f6e8f7be..28e7927c 100644 --- a/src/services/database/lowdb.ts +++ b/src/services/database/lowdb.ts @@ -28,6 +28,7 @@ import { type Statistics, } from './repositories/system'; import { SetupRepository, type Steps } from './repositories/setup'; +import type { DeepReadonly } from 'vue'; const DEBUG = debug('LowDB'); @@ -55,12 +56,21 @@ export class LowDBSetup extends SetupRepository { } } +/** + * deep copies object and + * makes readonly on type level + */ +function makeReadonly(a: T): DeepReadonly { + return structuredClone(a) as DeepReadonly; +} + class LowDBSystem extends SystemRepository { #db: Low; constructor(db: Low) { super(); this.#db = db; } + async get() { DEBUG('Get System'); const system = this.#db.data.system; @@ -68,7 +78,7 @@ class LowDBSystem extends SystemRepository { if (system === null) { throw new DatabaseError(DatabaseError.ERROR_INIT); } - return system; + return makeReadonly(system); } async updateFeatures(features: Record) { @@ -118,14 +128,14 @@ class LowDBUser extends UserRepository { super(); this.#db = db; } - // TODO: return copy to avoid mutation (everywhere) + async findAll() { - return this.#db.data.users; + return makeReadonly(this.#db.data.users); } async findById(id: string) { DEBUG('Get User'); - return this.#db.data.users.find((user) => user.id === id); + return makeReadonly(this.#db.data.users.find((user) => user.id === id)); } async create(username: string, password: string) { @@ -188,12 +198,12 @@ class LowDBClient extends ClientRepository { } async findAll() { DEBUG('GET Clients'); - return this.#db.data.clients; + return makeReadonly(this.#db.data.clients); } async findById(id: string) { DEBUG('Get Client'); - return this.#db.data.clients[id]; + return makeReadonly(this.#db.data.clients[id]); } async create(client: NewClient) { diff --git a/src/services/database/repositories/client.ts b/src/services/database/repositories/client.ts index a5d2cc85..48e6adc7 100644 --- a/src/services/database/repositories/client.ts +++ b/src/services/database/repositories/client.ts @@ -1,3 +1,5 @@ +import type { DeepReadonly } from 'vue'; + export type OneTimeLink = { oneTimeLink: string; /** ISO String */ @@ -32,8 +34,9 @@ export type NewClient = Omit; * This interface provides methods for managing client data. */ export abstract class ClientRepository { - abstract findAll(): Promise>; - abstract findById(id: string): Promise; + abstract findAll(): Promise>>; + abstract findById(id: string): Promise>; + abstract create(client: NewClient): Promise; abstract delete(id: string): Promise; abstract toggle(id: string, enable: boolean): Promise; diff --git a/src/services/database/repositories/database.ts b/src/services/database/repositories/database.ts index adaaacd0..7ce50dca 100644 --- a/src/services/database/repositories/database.ts +++ b/src/services/database/repositories/database.ts @@ -23,9 +23,6 @@ export const DEFAULT_DATABASE: Database = { /** * Abstract class for database operations. * Provides methods to connect, disconnect, and interact with system and user data. - * - * **Note :** Always throw `DatabaseError` to ensure proper API error handling. - * */ export abstract class DatabaseProvider { /** diff --git a/src/services/database/repositories/system.ts b/src/services/database/repositories/system.ts index 38623b85..5285ca66 100644 --- a/src/services/database/repositories/system.ts +++ b/src/services/database/repositories/system.ts @@ -1,4 +1,5 @@ import type { SessionConfig } from 'h3'; +import type { DeepReadonly } from 'vue'; import type { LOCALES } from '~~/i18n.config'; export type Lang = (typeof LOCALES)[number]['value']; @@ -73,9 +74,6 @@ export const AvailableFeatures: (keyof Features)[] = [ 'sortClients', ] as const; -/** - * Representing the WireGuard network configuration data structure of a computer interface system. - */ export type System = { general: General; @@ -100,10 +98,7 @@ export type System = { * and specific system properties, such as the language setting, from the database. */ export abstract class SystemRepository { - /** - * Retrieves the system configuration data from the database. - */ - abstract get(): Promise; + abstract get(): Promise>; abstract updateFeatures(features: Record): Promise; abstract updateStatistics(statistics: Statistics): Promise; diff --git a/src/services/database/repositories/user.ts b/src/services/database/repositories/user.ts index 1bad8a28..995f826c 100644 --- a/src/services/database/repositories/user.ts +++ b/src/services/database/repositories/user.ts @@ -1,3 +1,5 @@ +import type { DeepReadonly } from 'vue'; + /** * Represents user roles within the application, each with specific permissions : * @@ -30,25 +32,10 @@ export type User = { * This interface provides methods for managing user data. */ export abstract class UserRepository { - /** - * Retrieves all users from the database. - */ - abstract findAll(): Promise; - - /** - * Retrieves a user by their ID or User object from the database. - */ - abstract findById(id: string): Promise; + abstract findAll(): Promise>; + abstract findById(id: string): Promise>; abstract create(username: string, password: string): Promise; - - /** - * Updates a user in the database. - */ abstract update(user: User): Promise; - - /** - * Deletes a user from the database. - */ abstract delete(id: string): Promise; }