Browse Source

Feat: Improve Repository pattern (#1380)

* improve repository pattern

* fix errors
pull/1648/head
Bernd Storath 7 months ago
committed by Bernd Storath
parent
commit
9338f29290
  1. 2
      src/server/api/account/create.post.ts
  2. 4
      src/server/api/account/setup.post.ts
  3. 2
      src/server/api/cnf/[oneTimeLink].ts
  4. 2
      src/server/api/features.get.ts
  5. 2
      src/server/api/lang.get.ts
  6. 4
      src/server/api/session.post.ts
  7. 2
      src/server/api/wireguard/client/[clientId]/generateOneTimeLink.post.ts
  8. 4
      src/server/middleware/session.ts
  9. 2
      src/server/middleware/setup.ts
  10. 40
      src/server/utils/WireGuard.ts
  11. 2
      src/server/utils/config.ts
  12. 2
      src/server/utils/session.ts
  13. 142
      src/services/database/lowdb.ts
  14. 22
      src/services/database/migrations/1.ts
  15. 25
      src/services/database/repositories/client.ts
  16. 38
      src/services/database/repositories/database.ts
  17. 4
      src/services/database/repositories/system.ts
  18. 12
      src/services/database/repositories/user.ts

2
src/server/api/account/create.post.ts

@ -3,6 +3,6 @@ export default defineEventHandler(async (event) => {
event,
validateZod(passwordType)
);
await Database.createUser(username, password);
await Database.user.create(username, password);
return { success: true };
});

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

@ -3,13 +3,13 @@ export default defineEventHandler(async (event) => {
event,
validateZod(passwordType)
);
const users = await Database.getUsers();
const users = await Database.user.findAll();
if (users.length !== 0) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid state',
});
}
await Database.createUser(username, password);
await Database.user.create(username, password);
return { success: true };
});

2
src/server/api/cnf/[oneTimeLink].ts

@ -1,5 +1,5 @@
export default defineEventHandler(async (event) => {
const system = await Database.getSystem();
const system = await Database.system.get();
if (!system.oneTimeLinks.enabled) {
throw createError({
statusCode: 404,

2
src/server/api/features.get.ts

@ -1,5 +1,5 @@
export default defineEventHandler(async () => {
const system = await Database.getSystem();
const system = await Database.system.get();
return {
trafficStats: system.trafficStats,
sortClients: system.sortClients,

2
src/server/api/lang.get.ts

@ -1,5 +1,5 @@
export default defineEventHandler(async (event) => {
setHeader(event, 'Content-Type', 'application/json');
const system = await Database.getSystem();
const system = await Database.system.get();
return system.lang;
});

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

@ -6,7 +6,7 @@ export default defineEventHandler(async (event) => {
validateZod(credentialsType)
);
const users = await Database.getUsers();
const users = await Database.user.findAll();
const user = users.find((user) => user.username == username);
if (!user)
throw createError({
@ -23,7 +23,7 @@ export default defineEventHandler(async (event) => {
});
}
const system = await Database.getSystem();
const system = await Database.system.get();
const conf: SessionConfig = system.sessionConfig;

2
src/server/api/wireguard/client/[clientId]/generateOneTimeLink.post.ts

@ -1,5 +1,5 @@
export default defineEventHandler(async (event) => {
const system = await Database.getSystem();
const system = await Database.system.get();
if (!system.oneTimeLinks.enabled) {
throw createError({
status: 404,

4
src/server/middleware/session.ts

@ -10,7 +10,7 @@ export default defineEventHandler(async (event) => {
) {
return;
}
const system = await Database.getSystem();
const system = await Database.system.get();
if (!system)
throw createError({
statusCode: 500,
@ -24,7 +24,7 @@ export default defineEventHandler(async (event) => {
const authorization = getHeader(event, 'Authorization');
if (url.pathname.startsWith('/api/') && authorization) {
const users = await Database.getUsers();
const users = await Database.user.findAll();
const user = users.find((user) => user.id == session.data.userId);
if (!user)
throw createError({

2
src/server/middleware/setup.ts

@ -10,7 +10,7 @@ export default defineEventHandler(async (event) => {
return;
}
const users = await Database.getUsers();
const users = await Database.user.findAll();
if (users.length === 0) {
if (url.pathname.startsWith('/api/')) {
throw createError({

40
src/server/utils/WireGuard.ts

@ -16,8 +16,8 @@ class WireGuard {
}
async #saveWireguardConfig() {
const system = await Database.getSystem();
const clients = await Database.getClients();
const system = await Database.system.get();
const clients = await Database.client.findAll();
const result = [];
result.push(wg.generateServerInterface(system));
@ -42,7 +42,7 @@ class WireGuard {
}
async getClients() {
const dbClients = await Database.getClients();
const dbClients = await Database.client.findAll();
const clients = Object.entries(dbClients).map(([clientId, client]) => ({
id: clientId,
name: client.name,
@ -91,7 +91,7 @@ class WireGuard {
}
async getClient({ clientId }: { clientId: string }) {
const client = await Database.getClient(clientId);
const client = await Database.client.findById(clientId);
if (!client) {
throw createError({
statusCode: 404,
@ -103,7 +103,7 @@ class WireGuard {
}
async getClientConfiguration({ clientId }: { clientId: string }) {
const system = await Database.getSystem();
const system = await Database.system.get();
const client = await this.getClient({ clientId });
return wg.generateClientConfig(system, client);
@ -124,8 +124,8 @@ class WireGuard {
name: string;
expireDate: string | null;
}) {
const system = await Database.getSystem();
const clients = await Database.getClients();
const system = await Database.system.get();
const clients = await Database.client.findAll();
const privateKey = await wg.generatePrivateKey();
const publicKey = await wg.getPublicKey(privateKey);
@ -162,7 +162,7 @@ class WireGuard {
client.expiresAt = date.toISOString();
}
await Database.createClient(client);
await Database.client.create(client);
await this.saveConfig();
@ -170,12 +170,12 @@ class WireGuard {
}
async deleteClient({ clientId }: { clientId: string }) {
await Database.deleteClient(clientId);
await Database.client.delete(clientId);
await this.saveConfig();
}
async enableClient({ clientId }: { clientId: string }) {
await Database.toggleClient(clientId, true);
await Database.client.toggle(clientId, true);
await this.saveConfig();
}
@ -184,7 +184,7 @@ class WireGuard {
const key = `${clientId}-${Math.floor(Math.random() * 1000)}`;
const oneTimeLink = Math.abs(CRC32.str(key)).toString(16);
const expiresAt = new Date(Date.now() + 5 * 60 * 1000).toISOString();
await Database.createOneTimeLink(clientId, {
await Database.client.createOneTimeLink(clientId, {
oneTimeLink,
expiresAt,
});
@ -192,12 +192,12 @@ class WireGuard {
}
async eraseOneTimeLink({ clientId }: { clientId: string }) {
await Database.deleteOneTimeLink(clientId);
await Database.client.deleteOneTimeLink(clientId);
await this.saveConfig();
}
async disableClient({ clientId }: { clientId: string }) {
await Database.toggleClient(clientId, false);
await Database.client.toggle(clientId, false);
await this.saveConfig();
}
@ -209,7 +209,7 @@ class WireGuard {
clientId: string;
name: string;
}) {
await Database.updateClientName(clientId, name);
await Database.client.updateName(clientId, name);
await this.saveConfig();
}
@ -228,7 +228,7 @@ class WireGuard {
});
}
await Database.updateClientAddress4(clientId, address4);
await Database.client.updateAddress4(clientId, address4);
await this.saveConfig();
}
@ -250,7 +250,7 @@ class WireGuard {
updatedDate = date.toISOString();
}
await Database.updateClientExpirationDate(clientId, updatedDate);
await Database.client.updateExpirationDate(clientId, updatedDate);
await this.saveConfig();
}
@ -323,8 +323,8 @@ class WireGuard {
}
async cronJob() {
const clients = await Database.getClients();
const system = await Database.getSystem();
const clients = await Database.client.findAll();
const system = await Database.system.get();
// Expires Feature
if (system.clientExpiration.enabled) {
for (const client of Object.values(clients)) {
@ -334,7 +334,7 @@ class WireGuard {
new Date() > new Date(client.expiresAt)
) {
DEBUG(`Client ${client.id} expired.`);
await Database.toggleClient(client.id, false);
await Database.client.toggle(client.id, false);
}
}
}
@ -346,7 +346,7 @@ class WireGuard {
new Date() > new Date(client.oneTimeLink.expiresAt)
) {
DEBUG(`Client ${client.id} One Time Link expired.`);
await Database.deleteOneTimeLink(client.id);
await Database.client.deleteOneTimeLink(client.id);
}
}
}

2
src/server/utils/config.ts

@ -34,7 +34,7 @@ export async function migrateConfig(input: unknown) {
if (!res.success) {
throw new Error('Invalid Config');
}
const system = await Database.getSystem();
const system = await Database.system.get();
const oldConfig = res.data;
const oldCidr = parseCidr(oldConfig.server.address + '/24');
const db = {

2
src/server/utils/session.ts

@ -5,7 +5,7 @@ export type WGSession = {
};
export async function useWGSession(event: H3Event) {
const system = await Database.getSystem();
const system = await Database.system.get();
if (!system) throw new Error('Invalid');
return useSession<Partial<WGSession>>(event, system.sessionConfig);
}

142
src/services/database/lowdb.ts

@ -9,52 +9,26 @@ import {
import { JSONFilePreset } from 'lowdb/node';
import type { Low } from 'lowdb';
import type { User } from './repositories/user';
import { UserRepository, type User } from './repositories/user';
import type { Database } from './repositories/database';
import { migrationRunner } from './migrations';
import type { Client, NewClient, OneTimeLink } from './repositories/client';
import {
ClientRepository,
type Client,
type NewClient,
type OneTimeLink,
} from './repositories/client';
import { SystemRepository } from './repositories/system';
const DEBUG = debug('LowDB');
export default class LowDB extends DatabaseProvider {
#db!: Low<Database>;
#connected = false;
private async __init() {
const dbFilePath = '/etc/wireguard/db.json';
this.#db = await JSONFilePreset(dbFilePath, DEFAULT_DATABASE);
export class LowDBSystem extends SystemRepository {
#db: Low<Database>;
constructor(db: Low<Database>) {
super();
this.#db = db;
}
/**
* @throws
*/
async connect() {
if (this.#connected) {
return;
}
try {
await this.__init();
DEBUG('Running Migrations');
await migrationRunner(this.#db);
DEBUG('Migrations ran successfully');
} catch (e) {
DEBUG(e);
throw new Error('Failed to initialize Database');
}
this.#connected = true;
DEBUG('Connected successfully');
}
get connected() {
return this.#connected;
}
async disconnect() {
this.#connected = false;
DEBUG('Disconnected successfully');
}
async getSystem() {
async get() {
DEBUG('Get System');
const system = this.#db.data.system;
// system is only null if migration failed
@ -63,18 +37,25 @@ export default class LowDB extends DatabaseProvider {
}
return system;
}
}
export class LowDBUser extends UserRepository {
#db: Low<Database>;
constructor(db: Low<Database>) {
super();
this.#db = db;
}
// TODO: return copy to avoid mutation (everywhere)
async getUsers() {
async findAll() {
return this.#db.data.users;
}
async getUser(id: string) {
async findById(id: string) {
DEBUG('Get User');
return this.#db.data.users.find((user) => user.id === id);
}
async createUser(username: string, password: string) {
async create(username: string, password: string) {
DEBUG('Create User');
const isUserExist = this.#db.data.users.find(
@ -106,9 +87,9 @@ export default class LowDB extends DatabaseProvider {
await this.#db.update((data) => data.users.push(newUser));
}
async updateUser(user: User) {
async update(user: User) {
// TODO: avoid mutation, prefer .update, updatedAt
let oldUser = await this.getUser(user.id);
let oldUser = await this.findById(user.id);
if (oldUser) {
DEBUG('Update User');
oldUser = user;
@ -116,25 +97,32 @@ export default class LowDB extends DatabaseProvider {
}
}
async deleteUser(id: string) {
async delete(id: string) {
DEBUG('Delete User');
const idx = this.#db.data.users.findIndex((user) => user.id === id);
if (idx !== -1) {
await this.#db.update((data) => data.users.splice(idx, 1));
}
}
}
async getClients() {
export class LowDBClient extends ClientRepository {
#db: Low<Database>;
constructor(db: Low<Database>) {
super();
this.#db = db;
}
async findAll() {
DEBUG('GET Clients');
return this.#db.data.clients;
}
async getClient(id: string) {
async findById(id: string) {
DEBUG('Get Client');
return this.#db.data.clients[id];
}
async createClient(client: NewClient) {
async create(client: NewClient) {
DEBUG('Create Client');
const now = new Date().toISOString();
const newClient: Client = { ...client, createdAt: now, updatedAt: now };
@ -143,7 +131,7 @@ export default class LowDB extends DatabaseProvider {
});
}
async deleteClient(id: string) {
async delete(id: string) {
DEBUG('Delete Client');
await this.#db.update((data) => {
// TODO: find something better than delete
@ -152,7 +140,7 @@ export default class LowDB extends DatabaseProvider {
});
}
async toggleClient(id: string, enable: boolean) {
async toggle(id: string, enable: boolean) {
DEBUG('Toggle Client');
await this.#db.update((data) => {
if (data.clients[id]) {
@ -161,7 +149,7 @@ export default class LowDB extends DatabaseProvider {
});
}
async updateClientName(id: string, name: string) {
async updateName(id: string, name: string) {
DEBUG('Update Client Name');
await this.#db.update((data) => {
if (data.clients[id]) {
@ -170,7 +158,7 @@ export default class LowDB extends DatabaseProvider {
});
}
async updateClientAddress4(id: string, address4: string) {
async updateAddress4(id: string, address4: string) {
DEBUG('Update Client Address4');
await this.#db.update((data) => {
if (data.clients[id]) {
@ -179,7 +167,7 @@ export default class LowDB extends DatabaseProvider {
});
}
async updateClientExpirationDate(id: string, expirationDate: string | null) {
async updateExpirationDate(id: string, expirationDate: string | null) {
DEBUG('Update Client Expiration Date');
await this.#db.update((data) => {
if (data.clients[id]) {
@ -211,3 +199,49 @@ export default class LowDB extends DatabaseProvider {
});
}
}
export default class LowDB extends DatabaseProvider {
#db!: Low<Database>;
#connected = false;
system!: LowDBSystem;
user!: LowDBUser;
client!: LowDBClient;
/**
* @throws
*/
async connect() {
if (this.#connected) {
return;
}
try {
DEBUG('Connecting');
this.#db = await JSONFilePreset(
'/etc/wireguard/db.json',
DEFAULT_DATABASE
);
DEBUG('Running Migrations');
await migrationRunner(this.#db);
DEBUG('Migrations ran successfully');
} catch (e) {
DEBUG(e);
throw new Error('Failed to initialize Database');
}
this.system = new LowDBSystem(this.#db);
this.user = new LowDBUser(this.#db);
this.client = new LowDBClient(this.#db);
this.#connected = true;
DEBUG('Connected successfully');
}
get connected() {
return this.#connected;
}
async disconnect() {
this.#connected = false;
DEBUG('Disconnected successfully');
}
}

22
src/services/database/migrations/1.ts

@ -72,31 +72,29 @@ export async function run1(db: Low<Database>) {
};
// TODO: properly check if ipv6 support
database.system.iptables.PostUp = `
iptables -t nat -A POSTROUTING -s ${database.system.userConfig.address4Range} -o ${database.system.wgDevice} -j MASQUERADE;
database.system.iptables.PostUp =
`iptables -t nat -A POSTROUTING -s ${database.system.userConfig.address4Range} -o ${database.system.wgDevice} -j MASQUERADE;
iptables -A INPUT -p udp -m udp --dport ${database.system.wgPort} -j ACCEPT;
iptables -A FORWARD -i wg0 -j ACCEPT;
iptables -A FORWARD -o wg0 -j ACCEPT;
ip6tables -t nat -A POSTROUTING -s ${database.system.userConfig.address6Range} -o ${database.system.wgDevice} -j MASQUERADE;
ip6tables -A INPUT -p udp -m udp --dport ${database.system.wgPort} -j ACCEPT;
ip6tables -A FORWARD -i wg0 -j ACCEPT;
ip6tables -A FORWARD -o wg0 -j ACCEPT;
`
.split('\n')
.join(' ');
ip6tables -A FORWARD -o wg0 -j ACCEPT;`
.split('\n')
.join(' ');
database.system.iptables.PostDown = `
iptables -t nat -D POSTROUTING -s ${database.system.userConfig.address4Range} -o ${database.system.wgDevice} -j MASQUERADE;
database.system.iptables.PostDown =
`iptables -t nat -D POSTROUTING -s ${database.system.userConfig.address4Range} -o ${database.system.wgDevice} -j MASQUERADE;
iptables -D INPUT -p udp -m udp --dport ${database.system.wgPort} -j ACCEPT;
iptables -D FORWARD -i wg0 -j ACCEPT;
iptables -D FORWARD -o wg0 -j ACCEPT;
ip6tables -t nat -D POSTROUTING -s ${database.system.userConfig.address6Range} -o ${database.system.wgDevice} -j MASQUERADE;
ip6tables -D INPUT -p udp -m udp --dport ${database.system.wgPort} -j ACCEPT;
ip6tables -D FORWARD -i wg0 -j ACCEPT;
ip6tables -D FORWARD -o wg0 -j ACCEPT;
`
.split('\n')
.join(' ');
ip6tables -D FORWARD -o wg0 -j ACCEPT;`
.split('\n')
.join(' ');
db.data = database;
db.write();

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

@ -31,18 +31,21 @@ export type NewClient = Omit<Client, 'createdAt' | 'updatedAt'>;
* Interface for client-related database operations.
* This interface provides methods for managing client data.
*/
export interface ClientRepository {
getClients(): Promise<Record<string, Client>>;
getClient(id: string): Promise<Client | undefined>;
createClient(client: NewClient): Promise<void>;
deleteClient(id: string): Promise<void>;
toggleClient(id: string, enable: boolean): Promise<void>;
updateClientName(id: string, name: string): Promise<void>;
updateClientAddress4(id: string, address4: string): Promise<void>;
updateClientExpirationDate(
export abstract class ClientRepository {
abstract findAll(): Promise<Record<string, Client>>;
abstract findById(id: string): Promise<Client | undefined>;
abstract create(client: NewClient): Promise<void>;
abstract delete(id: string): Promise<void>;
abstract toggle(id: string, enable: boolean): Promise<void>;
abstract updateName(id: string, name: string): Promise<void>;
abstract updateAddress4(id: string, address4: string): Promise<void>;
abstract updateExpirationDate(
id: string,
expirationDate: string | null
): Promise<void>;
deleteOneTimeLink(id: string): Promise<void>;
createOneTimeLink(id: string, oneTimeLink: OneTimeLink): Promise<void>;
abstract deleteOneTimeLink(id: string): Promise<void>;
abstract createOneTimeLink(
id: string,
oneTimeLink: OneTimeLink
): Promise<void>;
}

38
src/services/database/repositories/database.ts

@ -1,9 +1,4 @@
import type {
ClientRepository,
Client,
NewClient,
OneTimeLink,
} from './client';
import type { ClientRepository, Client } from './client';
import type { System, SystemRepository } from './system';
import type { User, UserRepository } from './user';
@ -29,9 +24,7 @@ export const DEFAULT_DATABASE: Database = {
* **Note :** Always throw `DatabaseError` to ensure proper API error handling.
*
*/
export abstract class DatabaseProvider
implements SystemRepository, UserRepository, ClientRepository
{
export abstract class DatabaseProvider {
/**
* Connects to the database.
*/
@ -42,30 +35,9 @@ export abstract class DatabaseProvider
*/
abstract disconnect(): Promise<void>;
abstract getSystem(): Promise<System>;
abstract getUsers(): Promise<User[]>;
abstract getUser(id: string): Promise<User | undefined>;
abstract createUser(username: string, password: string): Promise<void>;
abstract updateUser(user: User): Promise<void>;
abstract deleteUser(id: string): Promise<void>;
abstract getClients(): Promise<Record<string, Client>>;
abstract getClient(id: string): Promise<Client | undefined>;
abstract createClient(client: NewClient): Promise<void>;
abstract deleteClient(id: string): Promise<void>;
abstract toggleClient(id: string, enable: boolean): Promise<void>;
abstract updateClientName(id: string, name: string): Promise<void>;
abstract updateClientAddress4(id: string, address4: string): Promise<void>;
abstract updateClientExpirationDate(
id: string,
expirationDate: string | null
): Promise<void>;
abstract deleteOneTimeLink(id: string): Promise<void>;
abstract createOneTimeLink(
id: string,
oneTimeLink: OneTimeLink
): Promise<void>;
abstract system: SystemRepository;
abstract user: UserRepository;
abstract client: ClientRepository;
}
/**

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

@ -80,9 +80,9 @@ export type System = {
* This interface provides methods for retrieving system configuration data
* and specific system properties, such as the language setting, from the database.
*/
export interface SystemRepository {
export abstract class SystemRepository {
/**
* Retrieves the system configuration data from the database.
*/
getSystem(): Promise<System>;
abstract get(): Promise<System>;
}

12
src/services/database/repositories/user.ts

@ -28,26 +28,26 @@ export type User = {
* Interface for user-related database operations.
* This interface provides methods for managing user data.
*/
export interface UserRepository {
export abstract class UserRepository {
/**
* Retrieves all users from the database.
*/
getUsers(): Promise<User[]>;
abstract findAll(): Promise<User[]>;
/**
* Retrieves a user by their ID or User object from the database.
*/
getUser(id: string): Promise<User | undefined>;
abstract findById(id: string): Promise<User | undefined>;
createUser(username: string, password: string): Promise<void>;
abstract create(username: string, password: string): Promise<void>;
/**
* Updates a user in the database.
*/
updateUser(user: User): Promise<void>;
abstract update(user: User): Promise<void>;
/**
* Deletes a user from the database.
*/
deleteUser(id: string): Promise<void>;
abstract delete(id: string): Promise<void>;
}

Loading…
Cancel
Save