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) => {
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<WGSession>(event, {
...system.sessionConfig,
});
const session = await useSession<WGSession>(event, conf);
const data = await session.update({
userId: user.id,

4
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,

2
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,
};

13
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<Database['system']>,
clients: DeepReadonly<Database['clients']>
) {
return nextIP(4, system, clients);
}
export function nextIPv6(
system: Database['system'],
clients: Database['clients']
system: DeepReadonly<Database['system']>,
clients: DeepReadonly<Database['clients']>
) {
return nextIP(6, system, clients);
}
function nextIP(
version: 4 | 6,
system: Database['system'],
clients: Database['clients']
system: DeepReadonly<Database['system']>,
clients: DeepReadonly<Database['clients']>
) {
const cidr = parseCidr(system.userConfig[`address${version}Range`]);
let address;

10
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<Client>) => {
const allowedIps = [
`${client.address4}/32`,
`${client.address6}/128`,
@ -17,7 +18,7 @@ PresharedKey = ${client.preSharedKey}
AllowedIPs = ${allowedIps.join(', ')}`;
},
generateServerInterface: (system: System) => {
generateServerInterface: (system: DeepReadonly<System>) => {
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<System>,
client: DeepReadonly<Client>
) => {
const cidr4Block = parseCidr(system.userConfig.address4Range).prefix;
const cidr6Block = parseCidr(system.userConfig.address6Range).prefix;

22
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<T>(a: T): DeepReadonly<T> {
return structuredClone(a) as DeepReadonly<T>;
}
class LowDBSystem extends SystemRepository {
#db: Low<Database>;
constructor(db: Low<Database>) {
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<string, Feature>) {
@ -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) {

7
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<Client, 'createdAt' | 'updatedAt'>;
* This interface provides methods for managing client data.
*/
export abstract class ClientRepository {
abstract findAll(): Promise<Record<string, Client>>;
abstract findById(id: string): Promise<Client | undefined>;
abstract findAll(): Promise<DeepReadonly<Record<string, Client>>>;
abstract findById(id: string): Promise<DeepReadonly<Client | undefined>>;
abstract create(client: NewClient): Promise<void>;
abstract delete(id: string): 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.
* 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 {
/**

9
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<System>;
abstract get(): Promise<DeepReadonly<System>>;
abstract updateFeatures(features: Record<string, Feature>): 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 :
*
@ -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<User[]>;
/**
* Retrieves a user by their ID or User object from the database.
*/
abstract findById(id: string): Promise<User | undefined>;
abstract findAll(): Promise<DeepReadonly<User[]>>;
abstract findById(id: string): Promise<DeepReadonly<User | undefined>>;
abstract create(username: string, password: string): Promise<void>;
/**
* Updates a user in the database.
*/
abstract update(user: User): Promise<void>;
/**
* Deletes a user from the database.
*/
abstract delete(id: string): Promise<void>;
}

Loading…
Cancel
Save