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 { sql, relations } from 'drizzle-orm'; |
||||
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'; |
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'; |
||||
|
|
||||
import { oneTimeLinks } from './oneTimeLinks'; |
import { oneTimeLinks } from '../../schema'; |
||||
|
|
||||
export const clients = sqliteTable('clients_table', { |
export const clients = sqliteTable('clients_table', { |
||||
id: int().primaryKey({ autoIncrement: true }), |
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 { sql } from 'drizzle-orm'; |
||||
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'; |
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'; |
||||
|
|
||||
import { wgInterface } from './interface'; |
import { wgInterface } from '../../schema'; |
||||
|
|
||||
export const hooks = sqliteTable('hooks_table', { |
export const hooks = sqliteTable('hooks_table', { |
||||
id: int() |
id: int() |
@ -1,9 +1,7 @@ |
|||||
import { sql, relations } from 'drizzle-orm'; |
import { sql, relations } from 'drizzle-orm'; |
||||
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'; |
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'; |
||||
|
|
||||
import { userConfig } from './userConfig'; |
import { userConfig, hooks, prometheus } from '../../schema'; |
||||
import { hooks } from './hooks'; |
|
||||
import { prometheus } from './metrics'; |
|
||||
|
|
||||
// maybe support multiple interfaces in the future
|
// maybe support multiple interfaces in the future
|
||||
export const wgInterface = sqliteTable('interface_table', { |
export const wgInterface = sqliteTable('interface_table', { |
@ -1,7 +1,7 @@ |
|||||
import { sql } from 'drizzle-orm'; |
import { sql } from 'drizzle-orm'; |
||||
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'; |
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'; |
||||
|
|
||||
import { wgInterface } from './interface'; |
import { wgInterface } from '../../schema'; |
||||
|
|
||||
export const prometheus = sqliteTable('prometheus_table', { |
export const prometheus = sqliteTable('prometheus_table', { |
||||
id: int() |
id: int() |
@ -1,7 +1,7 @@ |
|||||
import { sql, relations } from 'drizzle-orm'; |
import { sql, relations } from 'drizzle-orm'; |
||||
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'; |
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', { |
export const oneTimeLinks = sqliteTable('one_time_links_table', { |
||||
id: int().primaryKey({ autoIncrement: true }), |
id: int().primaryKey({ autoIncrement: true }), |
@ -1,7 +1,7 @@ |
|||||
import { sql } from 'drizzle-orm'; |
import { sql } from 'drizzle-orm'; |
||||
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'; |
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'; |
||||
|
|
||||
import { wgInterface } from './interface'; |
import { wgInterface } from '../../schema'; |
||||
|
|
||||
// default* means clients store it themselves
|
// default* means clients store it themselves
|
||||
export const userConfig = sqliteTable('user_config_table', { |
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