mirror of https://github.com/wg-easy/wg-easy
28 changed files with 199 additions and 828 deletions
@ -1,7 +1,7 @@ |
|||
import { sql, relations } from 'drizzle-orm'; |
|||
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'; |
|||
|
|||
import { oneTimeLinks } from './oneTimeLinks'; |
|||
import { oneTimeLinks } from '../../schema'; |
|||
|
|||
export const clients = sqliteTable('clients_table', { |
|||
id: int().primaryKey({ autoIncrement: true }), |
@ -0,0 +1,39 @@ |
|||
import type { DBType } from '#db/sqlite'; |
|||
import { eq, sql } from 'drizzle-orm'; |
|||
import { clients } from './schema'; |
|||
|
|||
function createPreparedStatement(db: DBType) { |
|||
return { |
|||
findAll: db.query.clients |
|||
.findMany({ |
|||
with: { |
|||
oneTimeLink: true, |
|||
}, |
|||
}) |
|||
.prepare(), |
|||
findById: db.query.clients |
|||
.findFirst({ where: eq(clients.id, sql.placeholder('id')) }) |
|||
.prepare(), |
|||
}; |
|||
} |
|||
|
|||
export class ClientsService { |
|||
#statements: ReturnType<typeof createPreparedStatement>; |
|||
|
|||
constructor(db: DBType) { |
|||
this.#statements = createPreparedStatement(db); |
|||
} |
|||
|
|||
async findAll() { |
|||
const result = await this.#statements.findAll.all(); |
|||
return result.map((row) => ({ |
|||
...row, |
|||
createdAt: new Date(row.createdAt), |
|||
updatedAt: new Date(row.updatedAt), |
|||
})); |
|||
} |
|||
|
|||
async findById(id: number) { |
|||
return this.#statements.findById.all({ id }); |
|||
} |
|||
} |
@ -0,0 +1,86 @@ |
|||
import type { InferSelectModel } from 'drizzle-orm'; |
|||
import { zod } from '#imports'; |
|||
|
|||
import type { clients } from './schema'; |
|||
|
|||
const schemaForType = |
|||
<T>() => |
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|||
<S extends zod.ZodType<T, any, any>>(arg: S) => { |
|||
return arg; |
|||
}; |
|||
|
|||
export type ClientsType = InferSelectModel<typeof clients>; |
|||
|
|||
export type CreateClientType = Omit< |
|||
ClientsType, |
|||
'createdAt' | 'updatedAt' | 'id' |
|||
>; |
|||
|
|||
export type UpdateClientType = Omit< |
|||
CreateClientType, |
|||
'privateKey' | 'publicKey' | 'preSharedKey' |
|||
>; |
|||
|
|||
const name = zod |
|||
.string({ message: 'zod.name' }) |
|||
.min(1, 'zod.nameMin') |
|||
.pipe(safeStringRefine); |
|||
|
|||
const expireDate = zod |
|||
.string({ message: 'zod.expireDate' }) |
|||
.min(1, 'zod.expireDateMin') |
|||
.pipe(safeStringRefine) |
|||
.nullable(); |
|||
|
|||
const address = zod |
|||
.string({ message: 'zod.address' }) |
|||
.min(1, { message: 'zod.addressMin' }) |
|||
.pipe(safeStringRefine); |
|||
|
|||
const address4 = zod |
|||
.string({ message: 'zod.address4' }) |
|||
.min(1, { message: 'zod.address4Min' }) |
|||
.pipe(safeStringRefine); |
|||
|
|||
const address6 = zod |
|||
.string({ message: 'zod.address6' }) |
|||
.min(1, { message: 'zod.address6Min' }) |
|||
.pipe(safeStringRefine); |
|||
|
|||
const allowedIps = zod |
|||
.array(address, { message: 'zod.allowedIps' }) |
|||
.min(1, { message: 'zod.allowedIpsMin' }); |
|||
|
|||
const serverAllowedIps = zod.array(address, { |
|||
message: 'zod.serverAllowedIps', |
|||
}); |
|||
|
|||
const mtu = zod |
|||
.number({ message: 'zod.mtu' }) |
|||
.min(1280, { message: 'zod.mtuMin' }) |
|||
.max(9000, { message: 'zod.mtuMax' }); |
|||
|
|||
const persistentKeepalive = zod |
|||
.number({ message: 'zod.persistentKeepalive' }) |
|||
.min(0, 'zod.persistentKeepaliveMin') |
|||
.max(65535, 'zod.persistentKeepaliveMax'); |
|||
|
|||
const enabled = zod.boolean({ message: 'zod.enabled' }); |
|||
|
|||
const dns = zod.array(address, { message: 'zod.dns' }).min(1, 'zod.dnsMin'); |
|||
|
|||
export const ClientUpdateSchema = schemaForType<UpdateClientType>()( |
|||
zod.object({ |
|||
name: name, |
|||
enabled: enabled, |
|||
expiresAt: expireDate, |
|||
ipv4Address: address4, |
|||
ipv6Address: address6, |
|||
allowedIps: allowedIps, |
|||
serverAllowedIps: serverAllowedIps, |
|||
mtu: mtu, |
|||
persistentKeepalive: persistentKeepalive, |
|||
dns: dns, |
|||
}) |
|||
); |
@ -1,7 +1,7 @@ |
|||
import { sql } from 'drizzle-orm'; |
|||
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'; |
|||
|
|||
import { wgInterface } from './interface'; |
|||
import { wgInterface } from '../../schema'; |
|||
|
|||
export const hooks = sqliteTable('hooks_table', { |
|||
id: int() |
@ -1,9 +1,7 @@ |
|||
import { sql, relations } from 'drizzle-orm'; |
|||
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'; |
|||
|
|||
import { userConfig } from './userConfig'; |
|||
import { hooks } from './hooks'; |
|||
import { prometheus } from './metrics'; |
|||
import { userConfig, hooks, prometheus } from '../../schema'; |
|||
|
|||
// maybe support multiple interfaces in the future
|
|||
export const wgInterface = sqliteTable('interface_table', { |
@ -1,7 +1,7 @@ |
|||
import { sql } from 'drizzle-orm'; |
|||
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'; |
|||
|
|||
import { wgInterface } from './interface'; |
|||
import { wgInterface } from '../../schema'; |
|||
|
|||
export const prometheus = sqliteTable('prometheus_table', { |
|||
id: int() |
@ -1,7 +1,7 @@ |
|||
import { sql, relations } from 'drizzle-orm'; |
|||
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'; |
|||
|
|||
import { clients } from './clients'; |
|||
import { clients } from '../../schema'; |
|||
|
|||
export const oneTimeLinks = sqliteTable('one_time_links_table', { |
|||
id: int().primaryKey({ autoIncrement: true }), |
@ -1,7 +1,7 @@ |
|||
import { sql } from 'drizzle-orm'; |
|||
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'; |
|||
|
|||
import { wgInterface } from './interface'; |
|||
import { wgInterface } from '../../schema'; |
|||
|
|||
// default* means clients store it themselves
|
|||
export const userConfig = sqliteTable('user_config_table', { |
@ -0,0 +1,9 @@ |
|||
// Make sure to not use any Path Aliases in these files
|
|||
export * from './repositories/clients/schema'; |
|||
export * from './repositories/general/schema'; |
|||
export * from './repositories/hooks/schema'; |
|||
export * from './repositories/interface/schema'; |
|||
export * from './repositories/metrics/schema'; |
|||
export * from './repositories/oneTimeLinks/schema'; |
|||
export * from './repositories/userConfig/schema'; |
|||
export * from './repositories/users/schema'; |
@ -0,0 +1,36 @@ |
|||
import { drizzle } from 'drizzle-orm/libsql'; |
|||
import { migrate as drizzleMigrate } from 'drizzle-orm/libsql/migrator'; |
|||
import { createClient } from '@libsql/client'; |
|||
|
|||
import * as schema from './schema'; |
|||
import { ClientsService } from './repositories/clients/service'; |
|||
|
|||
const client = createClient({ url: 'file:/etc/wireguard/wg0.db' }); |
|||
const db = drizzle({ client, schema }); |
|||
|
|||
export async function connect() { |
|||
await migrate(); |
|||
return new DBService(db); |
|||
} |
|||
|
|||
class DBService { |
|||
clients = new ClientsService(db); |
|||
constructor(private db: DBType) {} |
|||
} |
|||
|
|||
export type DBType = typeof db; |
|||
export type DBServiceType = DBService; |
|||
|
|||
async function migrate() { |
|||
try { |
|||
console.log('Migrating database...'); |
|||
await drizzleMigrate(db, { |
|||
migrationsFolder: './services/database/migrations', |
|||
}); |
|||
console.log('Migration complete'); |
|||
} catch (e) { |
|||
if (e instanceof Error) { |
|||
console.log('Failed to migrate database:', e.message); |
|||
} |
|||
} |
|||
} |
@ -1,356 +0,0 @@ |
|||
import crypto from 'node:crypto'; |
|||
import debug from 'debug'; |
|||
import { JSONFilePreset } from 'lowdb/node'; |
|||
import type { Low } from 'lowdb'; |
|||
import type { DeepReadonly } from 'vue'; |
|||
import { parseCidr } from 'cidr-tools'; |
|||
import { stringifyIp } from 'ip-bigint'; |
|||
|
|||
import { |
|||
DatabaseProvider, |
|||
DatabaseError, |
|||
DEFAULT_DATABASE, |
|||
} from './repositories/database'; |
|||
import { UserRepository, type User } from './repositories/user'; |
|||
import type { Database } from './repositories/database'; |
|||
import { migrationRunner } from './migrations'; |
|||
import { |
|||
ClientRepository, |
|||
type UpdateClient, |
|||
type CreateClient, |
|||
type OneTimeLink, |
|||
} from './repositories/client'; |
|||
import { |
|||
SystemRepository, |
|||
type General, |
|||
type UpdateWGConfig, |
|||
type UpdateWGInterface, |
|||
type WGHooks, |
|||
} from './repositories/system'; |
|||
import { SetupRepository, type Steps } from './repositories/setup'; |
|||
|
|||
const DEBUG = debug('LowDB'); |
|||
|
|||
export class LowDBSetup extends SetupRepository { |
|||
#db: Low<Database>; |
|||
constructor(db: Low<Database>) { |
|||
super(); |
|||
this.#db = db; |
|||
} |
|||
async done() { |
|||
if (this.#db.data.setup === 'success') { |
|||
return true; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
async get() { |
|||
return this.#db.data.setup; |
|||
} |
|||
|
|||
async set(step: Steps) { |
|||
this.#db.update((v) => { |
|||
v.setup = step; |
|||
}); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 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; |
|||
// system is only null if migration failed
|
|||
if (system === null) { |
|||
throw new DatabaseError(DatabaseError.ERROR_INIT); |
|||
} |
|||
return makeReadonly(system); |
|||
} |
|||
|
|||
async updateClientsHostPort(host: string, port: number): Promise<void> { |
|||
DEBUG('Update Clients Host and Port endpoint'); |
|||
this.#db.update((v) => { |
|||
v.system.userConfig.host = host; |
|||
v.system.userConfig.port = port; |
|||
}); |
|||
} |
|||
|
|||
async updateGeneral(general: General) { |
|||
DEBUG('Update General'); |
|||
this.#db.update((v) => { |
|||
v.system.general = general; |
|||
}); |
|||
} |
|||
|
|||
async updateInterface(wgInterface: UpdateWGInterface) { |
|||
DEBUG('Update Interface'); |
|||
this.#db.update((v) => { |
|||
const oldInterface = v.system.interface; |
|||
v.system.interface = { |
|||
...oldInterface, |
|||
...wgInterface, |
|||
}; |
|||
}); |
|||
} |
|||
|
|||
async updateUserConfig(userConfig: UpdateWGConfig) { |
|||
DEBUG('Update User Config'); |
|||
this.#db.update((v) => { |
|||
const oldUserConfig = v.system.userConfig; |
|||
v.system.userConfig = { |
|||
...oldUserConfig, |
|||
...userConfig, |
|||
}; |
|||
}); |
|||
} |
|||
|
|||
async updateHooks(hooks: WGHooks) { |
|||
DEBUG('Update Hooks'); |
|||
this.#db.update((v) => { |
|||
v.system.hooks = hooks; |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* updates the address range and the interface address |
|||
*/ |
|||
async updateAddressRange(address4Range: string, address6Range: string) { |
|||
DEBUG('Update Address Range'); |
|||
const cidr4 = parseCidr(address4Range); |
|||
const cidr6 = parseCidr(address6Range); |
|||
this.#db.update((v) => { |
|||
v.system.userConfig.address4Range = address4Range; |
|||
v.system.userConfig.address6Range = address6Range; |
|||
v.system.interface.address4 = stringifyIp({ |
|||
number: cidr4.start + 1n, |
|||
version: 4, |
|||
}); |
|||
v.system.interface.address6 = stringifyIp({ |
|||
number: cidr6.start + 1n, |
|||
version: 6, |
|||
}); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
class LowDBUser extends UserRepository { |
|||
#db: Low<Database>; |
|||
constructor(db: Low<Database>) { |
|||
super(); |
|||
this.#db = db; |
|||
} |
|||
|
|||
async findAll() { |
|||
return makeReadonly(this.#db.data.users); |
|||
} |
|||
|
|||
async findById(id: string) { |
|||
DEBUG('Get User'); |
|||
return makeReadonly(this.#db.data.users.find((user) => user.id === id)); |
|||
} |
|||
|
|||
async create(username: string, password: string) { |
|||
DEBUG('Create User'); |
|||
|
|||
const isUserExist = this.#db.data.users.find( |
|||
(user) => user.username === username |
|||
); |
|||
if (isUserExist) { |
|||
throw createError({ |
|||
statusCode: 409, |
|||
statusMessage: 'Username already taken', |
|||
}); |
|||
} |
|||
|
|||
const now = new Date().toISOString(); |
|||
const isUserEmpty = this.#db.data.users.length === 0; |
|||
|
|||
const hash = await hashPassword(password); |
|||
|
|||
const newUser: User = { |
|||
id: crypto.randomUUID(), |
|||
password: hash, |
|||
username, |
|||
email: null, |
|||
name: 'Administrator', |
|||
role: isUserEmpty ? 'ADMIN' : 'CLIENT', |
|||
enabled: true, |
|||
createdAt: now, |
|||
updatedAt: now, |
|||
}; |
|||
|
|||
await this.#db.update((data) => data.users.push(newUser)); |
|||
} |
|||
|
|||
async update(user: User) { |
|||
// TODO: avoid mutation, prefer .update, updatedAt
|
|||
let oldUser = await this.findById(user.id); |
|||
if (oldUser) { |
|||
DEBUG('Update User'); |
|||
oldUser = user; |
|||
await this.#db.write(); |
|||
} |
|||
} |
|||
|
|||
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)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
class LowDBClient extends ClientRepository { |
|||
#db: Low<Database>; |
|||
constructor(db: Low<Database>) { |
|||
super(); |
|||
this.#db = db; |
|||
} |
|||
async findAll() { |
|||
DEBUG('GET Clients'); |
|||
return makeReadonly(this.#db.data.clients); |
|||
} |
|||
|
|||
async findById(id: string) { |
|||
DEBUG('Get Client'); |
|||
return makeReadonly(this.#db.data.clients[id]); |
|||
} |
|||
|
|||
async create(client: CreateClient) { |
|||
DEBUG('Create Client'); |
|||
const id = crypto.randomUUID(); |
|||
const now = new Date().toISOString(); |
|||
const newClient = { ...client, createdAt: now, updatedAt: now, id }; |
|||
await this.#db.update((data) => { |
|||
data.clients[id] = newClient; |
|||
}); |
|||
} |
|||
|
|||
async delete(id: string) { |
|||
DEBUG('Delete Client'); |
|||
await this.#db.update((data) => { |
|||
// TODO: find something better than delete
|
|||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|||
delete data.clients[id]; |
|||
}); |
|||
} |
|||
|
|||
async toggle(id: string, enable: boolean) { |
|||
DEBUG('Toggle Client'); |
|||
await this.#db.update((data) => { |
|||
if (data.clients[id]) { |
|||
data.clients[id].enabled = enable; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
async updateExpirationDate(id: string, expirationDate: string | null) { |
|||
DEBUG('Update Client Expiration Date'); |
|||
await this.#db.update((data) => { |
|||
if (data.clients[id]) { |
|||
data.clients[id].expiresAt = expirationDate; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
async deleteOneTimeLink(id: string) { |
|||
DEBUG('Delete Client One Time Link'); |
|||
await this.#db.update((data) => { |
|||
if (data.clients[id]) { |
|||
if (data.clients[id].oneTimeLink) { |
|||
// Bug where Client makes 2 requests
|
|||
data.clients[id].oneTimeLink.expiresAt = new Date( |
|||
Date.now() + 10 * 1000 |
|||
).toISOString(); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
|
|||
async createOneTimeLink(id: string, oneTimeLink: OneTimeLink) { |
|||
DEBUG('Create Client One Time Link'); |
|||
await this.#db.update((data) => { |
|||
if (data.clients[id]) { |
|||
data.clients[id].oneTimeLink = oneTimeLink; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
async update(id: string, client: UpdateClient) { |
|||
DEBUG('Create Client'); |
|||
const now = new Date().toISOString(); |
|||
await this.#db.update((data) => { |
|||
const oldClient = data.clients[id]; |
|||
if (!oldClient) { |
|||
return; |
|||
} |
|||
data.clients[id] = { |
|||
...oldClient, |
|||
...client, |
|||
updatedAt: now, |
|||
}; |
|||
}); |
|||
} |
|||
} |
|||
|
|||
export default class LowDB extends DatabaseProvider { |
|||
#db: Low<Database>; |
|||
|
|||
setup: LowDBSetup; |
|||
system: LowDBSystem; |
|||
user: LowDBUser; |
|||
client: LowDBClient; |
|||
|
|||
private constructor(db: Low<Database>) { |
|||
super(); |
|||
this.#db = db; |
|||
this.setup = new LowDBSetup(this.#db); |
|||
this.system = new LowDBSystem(this.#db); |
|||
this.user = new LowDBUser(this.#db); |
|||
this.client = new LowDBClient(this.#db); |
|||
} |
|||
|
|||
async runMigrations() { |
|||
await migrationRunner(this.#db); |
|||
} |
|||
|
|||
/** |
|||
* @throws |
|||
*/ |
|||
static override async connect() { |
|||
try { |
|||
DEBUG('Connecting...'); |
|||
const db = await JSONFilePreset( |
|||
'/etc/wireguard/db.json', |
|||
DEFAULT_DATABASE |
|||
); |
|||
const inst = new LowDB(db); |
|||
DEBUG('Running Migrations...'); |
|||
await inst.runMigrations(); |
|||
DEBUG('Migrations ran successfully.'); |
|||
DEBUG('Connected successfully.'); |
|||
return inst; |
|||
} catch (e) { |
|||
DEBUG(e); |
|||
throw new Error('Failed to initialize Database'); |
|||
} |
|||
} |
|||
|
|||
async disconnect() { |
|||
DEBUG('Disconnected successfully'); |
|||
} |
|||
} |
@ -1,87 +0,0 @@ |
|||
import type { Low } from 'lowdb'; |
|||
import type { Database } from '../repositories/database'; |
|||
import { parseCidr } from 'cidr-tools'; |
|||
import { stringifyIp } from 'ip-bigint'; |
|||
|
|||
export async function run1(db: Low<Database>) { |
|||
const privateKey = await wg.generatePrivateKey(); |
|||
const publicKey = await wg.getPublicKey(privateKey); |
|||
|
|||
const address4Range = '10.8.0.0/24'; |
|||
const address6Range = 'fdcc:ad94:bacf:61a4::cafe:0/112'; |
|||
const cidr4 = parseCidr(address4Range); |
|||
const cidr6 = parseCidr(address6Range); |
|||
|
|||
const database: Database = { |
|||
migrations: [], |
|||
setup: 1, |
|||
system: { |
|||
general: { |
|||
sessionTimeout: 3600, // 1 hour
|
|||
}, |
|||
// Config to configure Server
|
|||
interface: { |
|||
privateKey: privateKey, |
|||
publicKey: publicKey, |
|||
address4: stringifyIp({ number: cidr4.start + 1n, version: 4 }), |
|||
address6: stringifyIp({ number: cidr6.start + 1n, version: 6 }), |
|||
mtu: 1420, |
|||
port: 51820, |
|||
device: 'eth0', |
|||
}, |
|||
// Config to configure Peer & Client Config
|
|||
userConfig: { |
|||
mtu: 1420, |
|||
persistentKeepalive: 0, |
|||
address4Range: address4Range, |
|||
address6Range: address6Range, |
|||
defaultDns: ['1.1.1.1', '2606:4700:4700::1111'], |
|||
allowedIps: ['0.0.0.0/0', '::/0'], |
|||
host: '', |
|||
port: 51820, |
|||
}, |
|||
// Config to configure Firewall or general hooks
|
|||
hooks: { |
|||
PreUp: '', |
|||
PostUp: [ |
|||
'iptables -t nat -A POSTROUTING -s {{address4}} -o {{device}} -j MASQUERADE;', |
|||
'iptables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT;', |
|||
'iptables -A FORWARD -i wg0 -j ACCEPT;', |
|||
'iptables -A FORWARD -o wg0 -j ACCEPT;', |
|||
'ip6tables -t nat -A POSTROUTING -s {{address6}} -o {{device}} -j MASQUERADE;', |
|||
'ip6tables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT;', |
|||
'ip6tables -A FORWARD -i wg0 -j ACCEPT;', |
|||
'ip6tables -A FORWARD -o wg0 -j ACCEPT;', |
|||
].join(' '), |
|||
PreDown: '', |
|||
PostDown: [ |
|||
'iptables -t nat -D POSTROUTING -s {{address4}} -o {{device}} -j MASQUERADE;', |
|||
'iptables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT;', |
|||
'iptables -D FORWARD -i wg0 -j ACCEPT;', |
|||
'iptables -D FORWARD -o wg0 -j ACCEPT;', |
|||
'ip6tables -t nat -D POSTROUTING -s {{address6}} -o {{device}} -j MASQUERADE;', |
|||
'ip6tables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT;', |
|||
'ip6tables -D FORWARD -i wg0 -j ACCEPT;', |
|||
'ip6tables -D FORWARD -o wg0 -j ACCEPT;', |
|||
].join(' '), |
|||
}, |
|||
metrics: { |
|||
prometheus: { |
|||
enabled: false, |
|||
password: null, |
|||
}, |
|||
}, |
|||
sessionConfig: { |
|||
// TODO: be able to invalidate all sessions
|
|||
password: getRandomHex(256), |
|||
name: 'wg-easy', |
|||
cookie: {}, |
|||
}, |
|||
}, |
|||
users: [], |
|||
clients: {}, |
|||
}; |
|||
|
|||
db.data = database; |
|||
db.write(); |
|||
} |
@ -1,30 +0,0 @@ |
|||
import type { Low } from 'lowdb'; |
|||
import type { Database } from '../repositories/database'; |
|||
import { run1 } from './1'; |
|||
|
|||
export type MIGRATION_FN = (db: Low<Database>) => Promise<void>; |
|||
|
|||
const MIGRATION_LIST = [ |
|||
// Adds Initial Database Structure
|
|||
{ id: '1', fn: run1 }, |
|||
] satisfies { id: string; fn: MIGRATION_FN }[]; |
|||
|
|||
/** |
|||
* Runs all migrations |
|||
* @throws |
|||
*/ |
|||
export async function migrationRunner(db: Low<Database>) { |
|||
const ranMigrations = db.data.migrations; |
|||
for (const migration of MIGRATION_LIST) { |
|||
if (ranMigrations.includes(migration.id)) { |
|||
continue; |
|||
} |
|||
try { |
|||
await migration.fn(db); |
|||
db.data.migrations.push(migration.id); |
|||
} catch (e) { |
|||
throw new Error(`Failed to run Migration ${migration.id}: ${e}`); |
|||
} |
|||
} |
|||
await db.write(); |
|||
} |
@ -1,62 +0,0 @@ |
|||
import type { DeepReadonly } from 'vue'; |
|||
|
|||
export type OneTimeLink = { |
|||
oneTimeLink: string; |
|||
/** ISO String */ |
|||
expiresAt: string; |
|||
}; |
|||
|
|||
export type Client = { |
|||
id: string; |
|||
name: string; |
|||
address4: string; |
|||
address6: string; |
|||
privateKey: string; |
|||
publicKey: string; |
|||
preSharedKey: string; |
|||
/** ISO String */ |
|||
expiresAt: string | null; |
|||
allowedIps: string[]; |
|||
serverAllowedIPs: string[]; |
|||
oneTimeLink: OneTimeLink | null; |
|||
/** ISO String */ |
|||
createdAt: string; |
|||
/** ISO String */ |
|||
updatedAt: string; |
|||
enabled: boolean; |
|||
persistentKeepalive: number; |
|||
mtu: number; |
|||
}; |
|||
|
|||
export type CreateClient = Omit<Client, 'createdAt' | 'updatedAt' | 'id'>; |
|||
|
|||
export type UpdateClient = Omit< |
|||
Client, |
|||
| 'createdAt' |
|||
| 'updatedAt' |
|||
| 'id' |
|||
| 'oneTimeLink' |
|||
| 'privateKey' |
|||
| 'publicKey' |
|||
| 'preSharedKey' |
|||
>; |
|||
|
|||
/** |
|||
* Interface for client-related database operations. |
|||
* This interface provides methods for managing client data. |
|||
*/ |
|||
export abstract class ClientRepository { |
|||
abstract findAll(): Promise<DeepReadonly<Record<string, Client>>>; |
|||
abstract findById(id: string): Promise<DeepReadonly<Client | undefined>>; |
|||
|
|||
abstract create(client: CreateClient): Promise<void>; |
|||
abstract delete(id: string): Promise<void>; |
|||
abstract toggle(id: string, enable: boolean): Promise<void>; |
|||
abstract deleteOneTimeLink(id: string): Promise<void>; |
|||
abstract createOneTimeLink( |
|||
id: string, |
|||
oneTimeLink: OneTimeLink |
|||
): Promise<void>; |
|||
|
|||
abstract update(id: string, client: UpdateClient): Promise<void>; |
|||
} |
@ -1,81 +0,0 @@ |
|||
import type { ClientRepository, Client } from './client'; |
|||
import type { SetupRepository, Steps } from './setup'; |
|||
import type { System, SystemRepository } from './system'; |
|||
import type { User, UserRepository } from './user'; |
|||
|
|||
// Represent data structure
|
|||
export type Database = { |
|||
migrations: string[]; |
|||
setup: Steps; |
|||
system: System; |
|||
users: User[]; |
|||
clients: Record<string, Client>; |
|||
}; |
|||
|
|||
export const DEFAULT_DATABASE: Database = { |
|||
migrations: [], |
|||
setup: 1, |
|||
system: null as never, |
|||
users: [], |
|||
clients: {}, |
|||
}; |
|||
|
|||
/** |
|||
* Abstract class for database operations. |
|||
* Provides methods to connect, disconnect, and interact with system and user data. |
|||
*/ |
|||
export abstract class DatabaseProvider { |
|||
/** |
|||
* Connects to the database. |
|||
*/ |
|||
static connect(): Promise<DatabaseProvider> { |
|||
throw new Error('Not implemented'); |
|||
} |
|||
|
|||
/** |
|||
* Disconnects from the database. |
|||
*/ |
|||
abstract disconnect(): Promise<void>; |
|||
|
|||
abstract setup: SetupRepository; |
|||
abstract system: SystemRepository; |
|||
abstract user: UserRepository; |
|||
abstract client: ClientRepository; |
|||
} |
|||
|
|||
/** |
|||
* Represents a specialized error class for database-related operations. |
|||
* This class is designed to work with internationalization (i18n) by using message keys. |
|||
* The actual error messages are expected to be retrieved using these keys from an i18n system. |
|||
* |
|||
* ### Usage: |
|||
* When throwing a `DatabaseError`, you provide an i18n key as the message. |
|||
* The key will be used by the i18n system to retrieve the corresponding localized error message. |
|||
* |
|||
* Example: |
|||
* ```typescript
|
|||
* throw new DatabaseError(DatabaseError.ERROR_INIT); |
|||
* ... |
|||
* // event handler routes
|
|||
* if (error instanceof DatabaseError) { |
|||
* const t = await useTranslation(event); |
|||
* throw createError({ |
|||
* statusCode: 400, |
|||
* statusMessage: t(error.message), |
|||
* message: error.message, |
|||
* }); |
|||
* } else { |
|||
* throw createError('Something happened !'); |
|||
* } |
|||
* ``` |
|||
* |
|||
* @extends {Error} |
|||
*/ |
|||
export class DatabaseError extends Error { |
|||
static readonly ERROR_INIT = 'errorInit'; |
|||
|
|||
constructor(message: string) { |
|||
super(message); |
|||
this.name = 'DatabaseError'; |
|||
} |
|||
} |
@ -1,11 +0,0 @@ |
|||
export type Steps = 1 | 2 | 3 | 4 | 5 | 'success'; |
|||
|
|||
/** |
|||
* Interface for setup-related database operations. |
|||
* This interface provides methods for managing setup data. |
|||
*/ |
|||
export abstract class SetupRepository { |
|||
abstract done(): Promise<boolean>; |
|||
abstract get(): Promise<Steps>; |
|||
abstract set(step: Steps): Promise<void>; |
|||
} |
@ -1,90 +0,0 @@ |
|||
import type { SessionConfig } from 'h3'; |
|||
import type { DeepReadonly } from 'vue'; |
|||
|
|||
export type WGHooks = { |
|||
PreUp: string; |
|||
PostUp: string; |
|||
PreDown: string; |
|||
PostDown: string; |
|||
}; |
|||
|
|||
export type WGInterface = { |
|||
privateKey: string; |
|||
publicKey: string; |
|||
address4: string; |
|||
address6: string; |
|||
mtu: number; |
|||
port: number; |
|||
device: string; |
|||
}; |
|||
|
|||
export type WGConfig = { |
|||
mtu: number; |
|||
persistentKeepalive: number; |
|||
address4Range: string; |
|||
address6Range: string; |
|||
defaultDns: string[]; |
|||
allowedIps: string[]; |
|||
host: string; |
|||
port: number; |
|||
}; |
|||
|
|||
export enum ChartType { |
|||
None = 0, |
|||
Line = 1, |
|||
Area = 2, |
|||
Bar = 3, |
|||
} |
|||
|
|||
export type Prometheus = { |
|||
enabled: boolean; |
|||
password: string | null; |
|||
}; |
|||
|
|||
export type Metrics = { |
|||
prometheus: Prometheus; |
|||
}; |
|||
|
|||
export type General = { |
|||
sessionTimeout: number; |
|||
}; |
|||
|
|||
export type System = { |
|||
general: General; |
|||
|
|||
interface: WGInterface; |
|||
|
|||
userConfig: WGConfig; |
|||
|
|||
hooks: WGHooks; |
|||
|
|||
metrics: Metrics; |
|||
|
|||
sessionConfig: SessionConfig; |
|||
}; |
|||
|
|||
export type UpdateWGInterface = Omit< |
|||
WGInterface, |
|||
'privateKey' | 'publicKey' | 'address4' | 'address6' |
|||
>; |
|||
|
|||
export type UpdateWGConfig = Omit<WGConfig, 'address4Range' | 'address6Range'>; |
|||
|
|||
/** |
|||
* 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 abstract class SystemRepository { |
|||
abstract get(): Promise<DeepReadonly<System>>; |
|||
|
|||
abstract updateClientsHostPort(host: string, port: number): Promise<void>; |
|||
|
|||
abstract updateGeneral(general: General): Promise<void>; |
|||
|
|||
abstract updateInterface(wgInterface: UpdateWGInterface): Promise<void>; |
|||
|
|||
abstract updateUserConfig(userConfig: UpdateWGConfig): Promise<void>; |
|||
|
|||
abstract updateHooks(hooks: WGHooks): Promise<void>; |
|||
} |
@ -1,41 +0,0 @@ |
|||
import type { DeepReadonly } from 'vue'; |
|||
|
|||
/** |
|||
* 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; |
|||
email: string | null; |
|||
/** ISO String */ |
|||
createdAt: string; |
|||
/** ISO String */ |
|||
updatedAt: string; |
|||
enabled: boolean; |
|||
}; |
|||
|
|||
/** |
|||
* Interface for user-related database operations. |
|||
* This interface provides methods for managing user data. |
|||
*/ |
|||
export abstract class UserRepository { |
|||
abstract findAll(): Promise<DeepReadonly<User[]>>; |
|||
abstract findById(id: string): Promise<DeepReadonly<User | undefined>>; |
|||
|
|||
abstract create(username: string, password: string): Promise<void>; |
|||
abstract update(user: User): Promise<void>; |
|||
abstract delete(id: string): Promise<void>; |
|||
} |
@ -1,8 +0,0 @@ |
|||
export * from './schema/clients'; |
|||
export * from './schema/general'; |
|||
export * from './schema/hooks'; |
|||
export * from './schema/interface'; |
|||
export * from './schema/metrics'; |
|||
export * from './schema/oneTimeLinks'; |
|||
export * from './schema/userConfig'; |
|||
export * from './schema/users'; |
@ -1,8 +0,0 @@ |
|||
import { drizzle } from 'drizzle-orm/libsql'; |
|||
import { createClient } from '@libsql/client'; |
|||
import * as schema from './schema'; |
|||
|
|||
const client = createClient({ url: 'file:/etc/wireguard/wg0.db' }); |
|||
const db = drizzle({ client, schema }); |
|||
|
|||
export default db; |
Loading…
Reference in new issue