Browse Source

make db results readonly

avoid weird side effects, when modifying the db object as its only allowed inside e.g. lowdb.ts
pull/1397/head
Bernd Storath 5 months ago
parent
commit
cdf159cba7
  1. 8
      src/server/api/session.post.ts
  2. 4
      src/server/api/setup/migrate.post.ts
  3. 2
      src/server/utils/WireGuard.ts
  4. 13
      src/server/utils/ip.ts
  5. 10
      src/server/utils/wgHelper.ts
  6. 22
      src/services/database/lowdb.ts
  7. 7
      src/services/database/repositories/client.ts
  8. 3
      src/services/database/repositories/database.ts
  9. 9
      src/services/database/repositories/system.ts
  10. 21
      src/services/database/repositories/user.ts

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

@ -1,5 +1,3 @@
import type { SessionConfig } from 'h3';
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const { username, password, remember } = await readValidatedBody( const { username, password, remember } = await readValidatedBody(
event, event,
@ -25,7 +23,7 @@ export default defineEventHandler(async (event) => {
const system = await Database.system.get(); const system = await Database.system.get();
const conf: SessionConfig = system.sessionConfig; const conf = { ...system.sessionConfig };
if (remember) { if (remember) {
conf.cookie = { conf.cookie = {
@ -34,9 +32,7 @@ export default defineEventHandler(async (event) => {
}; };
} }
const session = await useSession<WGSession>(event, { const session = await useSession<WGSession>(event, conf);
...system.sessionConfig,
});
const data = await session.update({ const data = await session.update({
userId: user.id, userId: user.id,

4
src/server/api/setup/migrate.post.ts

@ -52,6 +52,8 @@ export default defineEventHandler(async (event) => {
}, },
userConfig: { userConfig: {
...system.userConfig, ...system.userConfig,
defaultDns: [...system.userConfig.defaultDns],
allowedIps: [...system.userConfig.allowedIps],
address4Range: address4Range:
stringifyIp({ number: oldCidr.start, version: 4 }) + '/24', stringifyIp({ number: oldCidr.start, version: 4 }) + '/24',
}, },
@ -72,7 +74,7 @@ export default defineEventHandler(async (event) => {
publicKey: oldClient.publicKey, publicKey: oldClient.publicKey,
expiresAt: null, expiresAt: null,
oneTimeLink: null, oneTimeLink: null,
allowedIPs: db.system.userConfig.allowedIps, allowedIPs: [...db.system.userConfig.allowedIps],
serverAllowedIPs: [], serverAllowedIPs: [],
persistentKeepalive: 0, persistentKeepalive: 0,
address6: address6, address6: address6,

2
src/server/utils/WireGuard.ts

@ -149,7 +149,7 @@ class WireGuard {
oneTimeLink: null, oneTimeLink: null,
expiresAt: null, expiresAt: null,
enabled: true, enabled: true,
allowedIPs: system.userConfig.allowedIps, allowedIPs: [...system.userConfig.allowedIps],
serverAllowedIPs: null, serverAllowedIPs: null,
persistentKeepalive: system.userConfig.persistentKeepalive, persistentKeepalive: system.userConfig.persistentKeepalive,
}; };

13
src/server/utils/ip.ts

@ -1,25 +1,26 @@
import { parseCidr } from 'cidr-tools'; import { parseCidr } from 'cidr-tools';
import { stringifyIp } from 'ip-bigint'; import { stringifyIp } from 'ip-bigint';
import type { DeepReadonly } from 'vue';
import type { Database } from '~~/services/database/repositories/database'; import type { Database } from '~~/services/database/repositories/database';
export function nextIPv4( export function nextIPv4(
system: Database['system'], system: DeepReadonly<Database['system']>,
clients: Database['clients'] clients: DeepReadonly<Database['clients']>
) { ) {
return nextIP(4, system, clients); return nextIP(4, system, clients);
} }
export function nextIPv6( export function nextIPv6(
system: Database['system'], system: DeepReadonly<Database['system']>,
clients: Database['clients'] clients: DeepReadonly<Database['clients']>
) { ) {
return nextIP(6, system, clients); return nextIP(6, system, clients);
} }
function nextIP( function nextIP(
version: 4 | 6, version: 4 | 6,
system: Database['system'], system: DeepReadonly<Database['system']>,
clients: Database['clients'] clients: DeepReadonly<Database['clients']>
) { ) {
const cidr = parseCidr(system.userConfig[`address${version}Range`]); const cidr = parseCidr(system.userConfig[`address${version}Range`]);
let address; let address;

10
src/server/utils/wgHelper.ts

@ -1,9 +1,10 @@
import { parseCidr } from 'cidr-tools'; import { parseCidr } from 'cidr-tools';
import type { DeepReadonly } from 'vue';
import type { Client } from '~~/services/database/repositories/client'; import type { Client } from '~~/services/database/repositories/client';
import type { System } from '~~/services/database/repositories/system'; import type { System } from '~~/services/database/repositories/system';
export const wg = { export const wg = {
generateServerPeer: (client: Client) => { generateServerPeer: (client: DeepReadonly<Client>) => {
const allowedIps = [ const allowedIps = [
`${client.address4}/32`, `${client.address4}/32`,
`${client.address6}/128`, `${client.address6}/128`,
@ -17,7 +18,7 @@ PresharedKey = ${client.preSharedKey}
AllowedIPs = ${allowedIps.join(', ')}`; AllowedIPs = ${allowedIps.join(', ')}`;
}, },
generateServerInterface: (system: System) => { generateServerInterface: (system: DeepReadonly<System>) => {
const cidr4Block = parseCidr(system.userConfig.address4Range).prefix; const cidr4Block = parseCidr(system.userConfig.address4Range).prefix;
const cidr6Block = parseCidr(system.userConfig.address6Range).prefix; const cidr6Block = parseCidr(system.userConfig.address6Range).prefix;
@ -36,7 +37,10 @@ PreDown = ${system.iptables.PreDown}
PostDown = ${system.iptables.PostDown}`; PostDown = ${system.iptables.PostDown}`;
}, },
generateClientConfig: (system: System, client: Client) => { generateClientConfig: (
system: DeepReadonly<System>,
client: DeepReadonly<Client>
) => {
const cidr4Block = parseCidr(system.userConfig.address4Range).prefix; const cidr4Block = parseCidr(system.userConfig.address4Range).prefix;
const cidr6Block = parseCidr(system.userConfig.address6Range).prefix; const cidr6Block = parseCidr(system.userConfig.address6Range).prefix;

22
src/services/database/lowdb.ts

@ -28,6 +28,7 @@ import {
type Statistics, type Statistics,
} from './repositories/system'; } from './repositories/system';
import { SetupRepository, type Steps } from './repositories/setup'; import { SetupRepository, type Steps } from './repositories/setup';
import type { DeepReadonly } from 'vue';
const DEBUG = debug('LowDB'); const DEBUG = debug('LowDB');
@ -55,12 +56,21 @@ export class LowDBSetup extends SetupRepository {
} }
} }
/**
* deep copies object and
* makes readonly on type level
*/
function makeReadonly<T>(a: T): DeepReadonly<T> {
return structuredClone(a) as DeepReadonly<T>;
}
class LowDBSystem extends SystemRepository { class LowDBSystem extends SystemRepository {
#db: Low<Database>; #db: Low<Database>;
constructor(db: Low<Database>) { constructor(db: Low<Database>) {
super(); super();
this.#db = db; this.#db = db;
} }
async get() { async get() {
DEBUG('Get System'); DEBUG('Get System');
const system = this.#db.data.system; const system = this.#db.data.system;
@ -68,7 +78,7 @@ class LowDBSystem extends SystemRepository {
if (system === null) { if (system === null) {
throw new DatabaseError(DatabaseError.ERROR_INIT); throw new DatabaseError(DatabaseError.ERROR_INIT);
} }
return system; return makeReadonly(system);
} }
async updateFeatures(features: Record<string, Feature>) { async updateFeatures(features: Record<string, Feature>) {
@ -118,14 +128,14 @@ class LowDBUser extends UserRepository {
super(); super();
this.#db = db; this.#db = db;
} }
// TODO: return copy to avoid mutation (everywhere)
async findAll() { async findAll() {
return this.#db.data.users; return makeReadonly(this.#db.data.users);
} }
async findById(id: string) { async findById(id: string) {
DEBUG('Get User'); 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) { async create(username: string, password: string) {
@ -188,12 +198,12 @@ class LowDBClient extends ClientRepository {
} }
async findAll() { async findAll() {
DEBUG('GET Clients'); DEBUG('GET Clients');
return this.#db.data.clients; return makeReadonly(this.#db.data.clients);
} }
async findById(id: string) { async findById(id: string) {
DEBUG('Get Client'); DEBUG('Get Client');
return this.#db.data.clients[id]; return makeReadonly(this.#db.data.clients[id]);
} }
async create(client: NewClient) { async create(client: NewClient) {

7
src/services/database/repositories/client.ts

@ -1,3 +1,5 @@
import type { DeepReadonly } from 'vue';
export type OneTimeLink = { export type OneTimeLink = {
oneTimeLink: string; oneTimeLink: string;
/** ISO String */ /** ISO String */
@ -32,8 +34,9 @@ export type NewClient = Omit<Client, 'createdAt' | 'updatedAt'>;
* This interface provides methods for managing client data. * This interface provides methods for managing client data.
*/ */
export abstract class ClientRepository { export abstract class ClientRepository {
abstract findAll(): Promise<Record<string, Client>>; abstract findAll(): Promise<DeepReadonly<Record<string, Client>>>;
abstract findById(id: string): Promise<Client | undefined>; abstract findById(id: string): Promise<DeepReadonly<Client | undefined>>;
abstract create(client: NewClient): Promise<void>; abstract create(client: NewClient): Promise<void>;
abstract delete(id: string): Promise<void>; abstract delete(id: string): Promise<void>;
abstract toggle(id: string, enable: boolean): Promise<void>; abstract toggle(id: string, enable: boolean): Promise<void>;

3
src/services/database/repositories/database.ts

@ -23,9 +23,6 @@ export const DEFAULT_DATABASE: Database = {
/** /**
* 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.
*
* **Note :** Always throw `DatabaseError` to ensure proper API error handling.
*
*/ */
export abstract class DatabaseProvider { export abstract class DatabaseProvider {
/** /**

9
src/services/database/repositories/system.ts

@ -1,4 +1,5 @@
import type { SessionConfig } from 'h3'; import type { SessionConfig } from 'h3';
import type { DeepReadonly } from 'vue';
import type { LOCALES } from '~~/i18n.config'; import type { LOCALES } from '~~/i18n.config';
export type Lang = (typeof LOCALES)[number]['value']; export type Lang = (typeof LOCALES)[number]['value'];
@ -73,9 +74,6 @@ export const AvailableFeatures: (keyof Features)[] = [
'sortClients', 'sortClients',
] as const; ] as const;
/**
* Representing the WireGuard network configuration data structure of a computer interface system.
*/
export type System = { export type System = {
general: General; general: General;
@ -100,10 +98,7 @@ export type System = {
* and specific system properties, such as the language setting, from the database. * and specific system properties, such as the language setting, from the database.
*/ */
export abstract class SystemRepository { export abstract class SystemRepository {
/** abstract get(): Promise<DeepReadonly<System>>;
* Retrieves the system configuration data from the database.
*/
abstract get(): Promise<System>;
abstract updateFeatures(features: Record<string, Feature>): Promise<void>; abstract updateFeatures(features: Record<string, Feature>): Promise<void>;
abstract updateStatistics(statistics: Statistics): Promise<void>; abstract updateStatistics(statistics: Statistics): Promise<void>;

21
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 : * 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. * This interface provides methods for managing user data.
*/ */
export abstract class UserRepository { export abstract class UserRepository {
/** abstract findAll(): Promise<DeepReadonly<User[]>>;
* Retrieves all users from the database. abstract findById(id: string): Promise<DeepReadonly<User | undefined>>;
*/
abstract findAll(): Promise<User[]>;
/**
* Retrieves a user by their ID or User object from the database.
*/
abstract findById(id: string): Promise<User | undefined>;
abstract create(username: string, password: string): Promise<void>; abstract create(username: string, password: string): Promise<void>;
/**
* Updates a user in the database.
*/
abstract update(user: User): Promise<void>; abstract update(user: User): Promise<void>;
/**
* Deletes a user from the database.
*/
abstract delete(id: string): Promise<void>; abstract delete(id: string): Promise<void>;
} }

Loading…
Cancel
Save