Version 15.0.0-beta.1: Rewrite in Nuxt and Typescript, Move to UI (#1333)
* Add Nuxt, ESM, Typescript (#1244)
* wip: add nuxt
* basic implementation
* add changes from c9ff248
* update workflow, add eslint
* add types, fix wrong error message
* install correct bcrypt, move eslint to dev modules
* add docker dev script
* fix styling
* add wireguard routes
* typescript, vendors
* fix lint workflow
* lint fixes
* add prettier, format
* fix lint, add vscode settings
* better typescript
* use auto imports
* add prettier eslint config
* cache config
* fix styling issue, fix formatting
* fix tailwind problems
* fix logout not showing
* fix lint action
* Fix session middleware
* split files into correct methods
* use type safe api, fix typescript errors
* better return types
not tested
* change default working directory
* update workflows
* fix error
* correct session middleware, type safe session
* convert undefined to boolean
* correct key for api errors
* use zod to validate input
* add more jobs to check for good code
* add pinia
Co-authored-by: Sergei Birukov <suxscribe@gmail.com>
Co-authored-by: Bernd Storath <bernd.storath@offizium.de>
* use color mode plugin
* !! use better storage key name
Breaking as if old key exists it breaks as "auto" is not compatible with new "system"
* better local dev while dev container is running
use `docker compose -f docker-compose.dev.yml up`
or after changing dockerfile
`docker compose -f docker-compose.dev.yml up --build`
* update translation to match new theme mode
* improve dx
new devs get extensions recommended to catch errors, etc directly in vscode
* reduce errors, improve typing
* Split components (#1)
* update: introduce pages & components
fix lint
* update: starting split components
* use auto imports
* Improve workflows and docker
workflow fix step naming
simplify docker dev
simplify docker prod
revert to node 18
dockerfile naming scheme
* Split components (#2)
* update: starting split components
* upd: rebase & continue splitting components
- layouts: header & footer
- components: basic buttton
- pages: login page
* update: login page
* package.json: remove dev:pass script
* Split into Components, migrate to nuxt
fixup
shutdown wireguard properly
fix styling, fix store
split even more
clear interval
split even more
split even more
handle auth middleware on server
avoid flicker of login page
* fix: buttons spaces & move layouts to components (#3)
* update: icons into components
- fix: header login page
* fix: tailwind handle btn class
* Split into icons
fix avatar
move class to view not icon itself
fix icon
format
* invalidate cache to make restoreConfig work
* fix apexchart
* use different color mode module
other one resulted in hydration mismatch
* fix dialog
* fix bad i18n merge
* use nuxt 4
* fix typing, fix redirect, latest release on server
* start wireguard on start
* wait for shutdown
* improve zod errors, consistent server errors
* migrate to useFetch
this makes sure that there is no double fetching
* fix hydration issues, remove unnecessary state, rename function
* fetch globalstore globally
otherwise this will load on login to homepage
* migrate to useFetch
no javascript support
TODO: not properly tested
* update backend
* wip: frontend
* update frontend
* update pnpm lock
---------
Co-authored-by: Sergei Birukov <suxscribe@gmail.com>
Co-authored-by: Bernd Storath <bernd.storath@offizium.de>
Co-authored-by: tetuaoro <65575727+tetuaoro@users.noreply.github.com>
* Fix various issues
fix router param
fix max age
unit is seconds not ms
fix regressions
fix missing expire date in client create dialog
fix wrong type rules
fix wrong api endpoint
properly catch error running cron job
fix type issues
* add database (#1330)
* add: database abstraction
* update: get lang from database
* udpate: with repositories
* add: interfaces to connect a database provider
- easy swapping between database provider
* add: setup page
- add: in-memory database provider
- create a new account (signup)
- login with username and password
- first setup page to create an account
- PASSWORD_HASH was removed from environment and files was updated/removed due to that change
* update: Dockerfile
* fix: review done
- remove: REQUIRES_PASSWORD & RELEASE environment variables
* fix: i18n translation
- rename directories
* update: use database
* fix: typecheck
* fix: review
* rebase & add: persistent lowdb provider
* update: french translation
* revert: due to rebase
* remove & document
* Refactor New UI (#1342)
* refactor code
* refactor code
* add some todos
* update pnpm, start migrating to database
* add missing i18n key
* add todo
* basic setup styling
* nuxt 4 folder structure, update packages
* Feat: Migrations (#1344)
* add migrations
* improve migration runner
* improve migration runner
* document what each migration does
* Feat: Rewrite Wireguard to use Database (#1345)
* update wireguard
* update
* update
* remove all config
* move all features into one route
* improve code
* fix some issues
add wg_path, update documentation
* Feat: Cidr Support (#1347)
* cidr support
* add cidr
* fix some errors
fix server config
missing cidr block in server config
* Fix: Database Date type (#1349)
* Feat: IPv6 (#1354)
* start supporting ipv6
* add ipv6 support
* build server with es2020
es2019 doesn't support bigint
* fix issues, better naming
* Fix: Security (#1355)
* separate route for onboarding
* zse zod for validation
* use argon2id
* add build tools
* Feat: Server AllowedIPs, MTU (#1356)
* add wireguard helpers
* improve wireguard helpers
* add server mtu
* fix wg0.conf formatting
* add ipv6 support to docker compose and readme
* Feat: Docs (#1361)
* basic docs
* use semver versioning
* Feat: Migration (#1363)
* start migration
* improve migration
* remove endpoint from client
* improve docker
* Chore: Deprecate Dockerless (#1377)
* deprecate dockerless
* Feat: Improve Repository pattern (#1380)
* improve repository pattern
* fix errors
* Feat: Improve Database Handling (#1383)
* improve docker build
* build doc workflow
* Feat: Changelog, Release Notes (#1385)
* add changelog, use semver for update message
* use first line of release for short changelog
* load ipv6 iptables module
* Feat: Show Version in Footer (#1389)
update ui logic, always store release in global store.
new release logic uses rate limited github api, avoid using cache
* use i18n ally (#1391)
* improve gh actions
* Setup UI (#1392)
* update: setup ui page
* rebase
* remove script addition
* Fixed usage of Ukrainian instead of Russian in ru.json (#1414)
* Added translations for the Belarusian language (#1472)
* Install kmod from alpine repository (#1553)
Because the busybox modprobe utility is unable to load zstd compressed modules.
Co-authored-by: Matt <mmoore2012@users.noreply.github.com>
* WIP: Feat: UI, General Improvements (#1397)
* update: setup ui page
* remove script addition
* add admin panel
* basic user menu and admin page
* make usable admin panel
* add radix vue, improve ui
* fix features, add toast
* rewrite middleware logic, support basic auth
* add todo marker
* active tailwind forms
* remove some console.logs
* check if user is enabled
frontend doesn't handle this state yet, nothing will work as api routes will fail
* add email to user, basic account page
* better group database
* group even more
* basic statistics page
* update: admin ui
- add: common panel components to get same UI
- i18n: french
* update: setup page error handle
- use fetch error data to provide error message
- use translation to provider error message
* update: me page
* fix: :text props
* update: login page
* update: i18n french support
* fix: use radix toast duration
* update: reduce templates
- remake: setup page to add others step configuration (host/port/migration)
* udpate: setup page use wizard form step
* update: ui
* update: step page
- first step to choose a language
- use red color in light mode
- validate step before move toward
* update: setup page
- use radix select component to reduce boilerplate
* update: setup page
- add: database langugage method
- update: api lang & export supported languages
* update: setup page
- update ui select language
- change lang on selection
* fix: use global store
* fix: initial value
- update: sort langs by value
* fix: ui center paragraph
* fix: remove file extension & some revert
- add: script to run checks script
* update: setup page
- add: host/port section
- i18n: french
- fix: fallback translation
* refactor: split setup into files
* update: setup page
- redirect to login when the setup is done
- allow user to return to previous steps
- prompt error message
- i18n french
* add: migration UI step
- rename: components
- fix: label for & form child id
- i18n french sup
* add: migration server
* fix: use string instead of File
* improve: with zod validation
* restore: clients
* rework setup
* add client page, move api routes
* improve setup
* switch to agpl
* add step back
* update licensed under texts
cc -> agpl
* make db results readonly
avoid weird side effects, when modifying the db object as its only allowed inside e.g. lowdb.ts
* update footer links
* improve client edit page, add mtu
* reorder tailwind classes
* update packages
* update comments
* better toast, better avatar
* delete feature toggle
* remove chart, statistics from server
let user decide what he wants to display
* move into own components
* switch from AGPL-3.0-or-later to AGPL-3.0-only
AGPL-3.0-or-later is not OSI approved
* fix building source
fixes https://github.com/wg-easy/wg-easy/issues/1563
* update packages
---------
Co-authored-by: tetuaoro <65575727+tetuaoro@users.noreply.github.com>
* update readme
* Feat: Settings, UI, General Improvements (#1572)
* deprecate other languages
new ui has too many new strings
* fix wrong license in readme
* properly fetch release
* order safe data structure for migrations
* empty server allowed ips by default
* show userconfig in admin panel
* remove routes, fix config
* add ability to update clients
* handle form submit using js
avoid weird behavior with FormData
* global toast, be able to update client
* update packages
* fix date field
* delete client using radix dialog
* remove lang from backend, let users decide
* be able to change interface and general
* be able to update user config
* consistent allowedips
* fix array field
* improve avatar, code cleanup
* basic metrics support
* remove dateTime helper
* be able to change hooks
* start cidr update
* be able to update cidr
* Feat: SQLite (#1619)
* start drizzle migration
* split schema
* improve schema
* improve schema, cascade, unique
* improve structure, start migration
* migrate to sqlite
* work in prod docker
* start adding a better permission handler
* permission matrix, permission handler
* update packages
* move session timeout to session config, use new permission handler
* improve docker dev
only install dependencies if changed
* implement setup
* migrate to sqlite
* improve debug, fix custom migration
* migrate to sqlite
* regenerate migrations
* ignore autogenerated migrations from prettier
* migrate to sqlite
* migrate to sqlite
* Migrate to sqlite
* fix prod error
* move nuxt middleware from server to nuxt
* update corepack in prod dockerfile
* use correct branch for workflow
* make docker file build on armv6/v7
* fix client update
* update zod locales
* cancel pr workflow if new commit
* test concurrency
* Feat: Account (#1645)
* be able to change name, email
* be able to change password
* consistent naming
zod is a schema not a type
* use transaction instance
* add zod strings
* Feat: Prometheus (#1655)
* check metrics password
* rewrite prometheus and json metric endpoints
* move metrics to general
metrics is not per interface
* change metrics settings in admin panel
* add i18n keys
* Chore: Remove multi interface support (#1657)
* streamline references to wg0
database wg0 name makes no sense anymore
wg0 only in database, could be easily replaced, or support for custom name added
* fix default key gen
* Feat: Permission System (#1660)
* wip: add abac
* wip: add admin abac
* add me abac
* fix type issue
* move from role check
avoid authStore.userData?.role === roles.ADMIN
* Feat: Zod Generic String (#1661)
* start improving zod translations
* update zod translations
* Feat: Migration (#1663)
* show error for old env vars
* reorder setup, be able to migrate
* fix type issue
* footer and header in setup, remove lang setup step
* remove backup / restore
* refactor dialog (#1665)
* fixed Dockerfile HEALTHCHECK syntax (#1686)
HEALTHCHECK options should always come before the CMD instruction
* Feat: Info (#1666)
* add tooltip info, extract strings
* multi type toast
* improve useSubmit, i18n
* better login screen
* improve
* consistent folder casing
* consistent casing
* fix even more stuff
* temp
* fix type errors
* remove armv6/7 support for now
* add information to client page
* optimize dockerfile
* update base image in Dockerfile to use node:lts-alpine
* fix build stage
* Chore: TODOs (#1695)
* verify setup step
* improve readme
* format todos
* move id
* remove objectMessage
* style array field
* Chore: TODOs (#1696)
* fix chart
* replace localstorage with cookies
* Chore: Improvments (#1697)
* update packages
* fix tab issues
* consistent imports
* use eslint module
* update date
* improve docs
* update docs
* format
* fix docs, fix cookie
* recognize timing attack potential
* improve gh actions, issue templates (#1700)
* Feat improv (#1702)
* add insecure option, link readme to docs
* improve docs
* update version
* add warning to readme
---------
Co-authored-by: Sergei Birukov <suxscribe@gmail.com>
Co-authored-by: Bernd Storath <bernd.storath@offizium.de>
Co-authored-by: tetuaoro <65575727+tetuaoro@users.noreply.github.com>
Co-authored-by: laperuz92 <31198184+laperuz92@users.noreply.github.com>
Co-authored-by: Siomkin Alexander <siomkin.alexander@gmail.com>
Co-authored-by: Matt <102529127+mmoore2012@users.noreply.github.com>
Co-authored-by: Matt <mmoore2012@users.noreply.github.com>
Co-authored-by: Denis Kazimirov <rokiden@users.noreply.github.com>
This commit is contained in:
4
src/server/api/admin/general.get.ts
Normal file
4
src/server/api/admin/general.get.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePermissionEventHandler('admin', 'any', async () => {
|
||||
const generalConfig = await Database.general.getConfig();
|
||||
return generalConfig;
|
||||
});
|
||||
14
src/server/api/admin/general.post.ts
Normal file
14
src/server/api/admin/general.post.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { GeneralUpdateSchema } from '#db/repositories/general/types';
|
||||
|
||||
export default definePermissionEventHandler(
|
||||
'admin',
|
||||
'any',
|
||||
async ({ event }) => {
|
||||
const data = await readValidatedBody(
|
||||
event,
|
||||
validateZod(GeneralUpdateSchema, event)
|
||||
);
|
||||
await Database.general.update(data);
|
||||
return { success: true };
|
||||
}
|
||||
);
|
||||
4
src/server/api/admin/hooks.get.ts
Normal file
4
src/server/api/admin/hooks.get.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePermissionEventHandler('admin', 'any', async () => {
|
||||
const hooks = await Database.hooks.get();
|
||||
return hooks;
|
||||
});
|
||||
15
src/server/api/admin/hooks.post.ts
Normal file
15
src/server/api/admin/hooks.post.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { HooksUpdateSchema } from '#db/repositories/hooks/types';
|
||||
|
||||
export default definePermissionEventHandler(
|
||||
'admin',
|
||||
'any',
|
||||
async ({ event }) => {
|
||||
const data = await readValidatedBody(
|
||||
event,
|
||||
validateZod(HooksUpdateSchema, event)
|
||||
);
|
||||
await Database.hooks.update(data);
|
||||
await WireGuard.saveConfig();
|
||||
return { success: true };
|
||||
}
|
||||
);
|
||||
16
src/server/api/admin/interface/cidr.post.ts
Normal file
16
src/server/api/admin/interface/cidr.post.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { InterfaceCidrUpdateSchema } from '#db/repositories/interface/types';
|
||||
|
||||
export default definePermissionEventHandler(
|
||||
'admin',
|
||||
'any',
|
||||
async ({ event }) => {
|
||||
const data = await readValidatedBody(
|
||||
event,
|
||||
validateZod(InterfaceCidrUpdateSchema, event)
|
||||
);
|
||||
|
||||
await Database.interfaces.updateCidr(data);
|
||||
await WireGuard.saveConfig();
|
||||
return { success: true };
|
||||
}
|
||||
);
|
||||
8
src/server/api/admin/interface/index.get.ts
Normal file
8
src/server/api/admin/interface/index.get.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export default definePermissionEventHandler('admin', 'any', async () => {
|
||||
const wgInterface = await Database.interfaces.get();
|
||||
|
||||
return {
|
||||
...wgInterface,
|
||||
privateKey: undefined,
|
||||
};
|
||||
});
|
||||
15
src/server/api/admin/interface/index.post.ts
Normal file
15
src/server/api/admin/interface/index.post.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { InterfaceUpdateSchema } from '#db/repositories/interface/types';
|
||||
|
||||
export default definePermissionEventHandler(
|
||||
'admin',
|
||||
'any',
|
||||
async ({ event }) => {
|
||||
const data = await readValidatedBody(
|
||||
event,
|
||||
validateZod(InterfaceUpdateSchema, event)
|
||||
);
|
||||
await Database.interfaces.update(data);
|
||||
await WireGuard.saveConfig();
|
||||
return { success: true };
|
||||
}
|
||||
);
|
||||
4
src/server/api/admin/userconfig.get.ts
Normal file
4
src/server/api/admin/userconfig.get.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePermissionEventHandler('admin', 'any', async () => {
|
||||
const userConfig = await Database.userConfigs.get();
|
||||
return userConfig;
|
||||
});
|
||||
15
src/server/api/admin/userconfig.post.ts
Normal file
15
src/server/api/admin/userconfig.post.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { UserConfigUpdateSchema } from '#db/repositories/userConfig/types';
|
||||
|
||||
export default definePermissionEventHandler(
|
||||
'admin',
|
||||
'any',
|
||||
async ({ event }) => {
|
||||
const data = await readValidatedBody(
|
||||
event,
|
||||
validateZod(UserConfigUpdateSchema, event)
|
||||
);
|
||||
await Database.userConfigs.update(data);
|
||||
await WireGuard.saveConfig();
|
||||
return { success: true };
|
||||
}
|
||||
);
|
||||
37
src/server/api/client/[clientId]/configuration.get.ts
Normal file
37
src/server/api/client/[clientId]/configuration.get.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ClientGetSchema } from '#db/repositories/client/types';
|
||||
|
||||
export default definePermissionEventHandler(
|
||||
'clients',
|
||||
'view',
|
||||
async ({ event, checkPermissions }) => {
|
||||
const { clientId } = await getValidatedRouterParams(
|
||||
event,
|
||||
validateZod(ClientGetSchema, event)
|
||||
);
|
||||
const client = await Database.clients.get(clientId);
|
||||
checkPermissions(client);
|
||||
|
||||
if (!client) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Client not found',
|
||||
});
|
||||
}
|
||||
|
||||
const config = await WireGuard.getClientConfiguration({ clientId });
|
||||
const configName = client.name
|
||||
.replace(/[^a-zA-Z0-9_=+.-]/g, '-')
|
||||
.replace(/(-{2,}|-$)/g, '-')
|
||||
.replace(/-$/, '')
|
||||
.substring(0, 32);
|
||||
|
||||
setHeader(
|
||||
event,
|
||||
'Content-Disposition',
|
||||
`attachment; filename="${configName || clientId}.conf"`
|
||||
);
|
||||
|
||||
setHeader(event, 'Content-Type', 'text/plain');
|
||||
return config;
|
||||
}
|
||||
);
|
||||
19
src/server/api/client/[clientId]/disable.post.ts
Normal file
19
src/server/api/client/[clientId]/disable.post.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ClientGetSchema } from '#db/repositories/client/types';
|
||||
|
||||
export default definePermissionEventHandler(
|
||||
'clients',
|
||||
'update',
|
||||
async ({ event, checkPermissions }) => {
|
||||
const { clientId } = await getValidatedRouterParams(
|
||||
event,
|
||||
validateZod(ClientGetSchema, event)
|
||||
);
|
||||
|
||||
const client = await Database.clients.get(clientId);
|
||||
checkPermissions(client);
|
||||
|
||||
await Database.clients.toggle(clientId, false);
|
||||
await WireGuard.saveConfig();
|
||||
return { success: true };
|
||||
}
|
||||
);
|
||||
19
src/server/api/client/[clientId]/enable.post.ts
Normal file
19
src/server/api/client/[clientId]/enable.post.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ClientGetSchema } from '#db/repositories/client/types';
|
||||
|
||||
export default definePermissionEventHandler(
|
||||
'clients',
|
||||
'update',
|
||||
async ({ event, checkPermissions }) => {
|
||||
const { clientId } = await getValidatedRouterParams(
|
||||
event,
|
||||
validateZod(ClientGetSchema, event)
|
||||
);
|
||||
|
||||
const client = await Database.clients.get(clientId);
|
||||
checkPermissions(client);
|
||||
|
||||
await Database.clients.toggle(clientId, false);
|
||||
await WireGuard.saveConfig();
|
||||
return { success: true };
|
||||
}
|
||||
);
|
||||
18
src/server/api/client/[clientId]/generateOneTimeLink.post.ts
Normal file
18
src/server/api/client/[clientId]/generateOneTimeLink.post.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ClientGetSchema } from '#db/repositories/client/types';
|
||||
|
||||
export default definePermissionEventHandler(
|
||||
'clients',
|
||||
'update',
|
||||
async ({ event, checkPermissions }) => {
|
||||
const { clientId } = await getValidatedRouterParams(
|
||||
event,
|
||||
validateZod(ClientGetSchema, event)
|
||||
);
|
||||
|
||||
const client = await Database.clients.get(clientId);
|
||||
checkPermissions(client);
|
||||
|
||||
await Database.oneTimeLinks.generate(clientId);
|
||||
return { success: true };
|
||||
}
|
||||
);
|
||||
19
src/server/api/client/[clientId]/index.delete.ts
Normal file
19
src/server/api/client/[clientId]/index.delete.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ClientGetSchema } from '#db/repositories/client/types';
|
||||
|
||||
export default definePermissionEventHandler(
|
||||
'clients',
|
||||
'delete',
|
||||
async ({ event, checkPermissions }) => {
|
||||
const { clientId } = await getValidatedRouterParams(
|
||||
event,
|
||||
validateZod(ClientGetSchema, event)
|
||||
);
|
||||
|
||||
const client = await Database.clients.get(clientId);
|
||||
checkPermissions(client);
|
||||
|
||||
await Database.clients.delete(clientId);
|
||||
await WireGuard.saveConfig();
|
||||
return { success: true };
|
||||
}
|
||||
);
|
||||
23
src/server/api/client/[clientId]/index.get.ts
Normal file
23
src/server/api/client/[clientId]/index.get.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ClientGetSchema } from '#db/repositories/client/types';
|
||||
|
||||
export default definePermissionEventHandler(
|
||||
'clients',
|
||||
'view',
|
||||
async ({ event, checkPermissions }) => {
|
||||
const { clientId } = await getValidatedRouterParams(
|
||||
event,
|
||||
validateZod(ClientGetSchema, event)
|
||||
);
|
||||
|
||||
const result = await Database.clients.get(clientId);
|
||||
checkPermissions(result);
|
||||
|
||||
if (!result) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Client not found',
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
);
|
||||
28
src/server/api/client/[clientId]/index.post.ts
Normal file
28
src/server/api/client/[clientId]/index.post.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
ClientGetSchema,
|
||||
ClientUpdateSchema,
|
||||
} from '#db/repositories/client/types';
|
||||
|
||||
export default definePermissionEventHandler(
|
||||
'clients',
|
||||
'update',
|
||||
async ({ event, checkPermissions }) => {
|
||||
const { clientId } = await getValidatedRouterParams(
|
||||
event,
|
||||
validateZod(ClientGetSchema, event)
|
||||
);
|
||||
|
||||
const data = await readValidatedBody(
|
||||
event,
|
||||
validateZod(ClientUpdateSchema, event)
|
||||
);
|
||||
|
||||
const client = await Database.clients.get(clientId);
|
||||
checkPermissions(client);
|
||||
|
||||
await Database.clients.update(clientId, data);
|
||||
await WireGuard.saveConfig();
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
);
|
||||
19
src/server/api/client/[clientId]/qrcode.svg.get.ts
Normal file
19
src/server/api/client/[clientId]/qrcode.svg.get.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ClientGetSchema } from '#db/repositories/client/types';
|
||||
|
||||
export default definePermissionEventHandler(
|
||||
'clients',
|
||||
'view',
|
||||
async ({ event, checkPermissions }) => {
|
||||
const { clientId } = await getValidatedRouterParams(
|
||||
event,
|
||||
validateZod(ClientGetSchema, event)
|
||||
);
|
||||
|
||||
const client = await Database.clients.get(clientId);
|
||||
checkPermissions(client);
|
||||
|
||||
const svg = await WireGuard.getClientQRCodeSVG({ clientId });
|
||||
setHeader(event, 'Content-Type', 'image/svg+xml');
|
||||
return svg;
|
||||
}
|
||||
);
|
||||
6
src/server/api/client/index.get.ts
Normal file
6
src/server/api/client/index.get.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default definePermissionEventHandler('clients', 'custom', ({ user }) => {
|
||||
if (user.role === roles.ADMIN) {
|
||||
return WireGuard.getAllClients();
|
||||
}
|
||||
return WireGuard.getClientsForUser(user.id);
|
||||
});
|
||||
16
src/server/api/client/index.post.ts
Normal file
16
src/server/api/client/index.post.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ClientCreateSchema } from '#db/repositories/client/types';
|
||||
|
||||
export default definePermissionEventHandler(
|
||||
'clients',
|
||||
'create',
|
||||
async ({ event }) => {
|
||||
const { name, expiresAt } = await readValidatedBody(
|
||||
event,
|
||||
validateZod(ClientCreateSchema, event)
|
||||
);
|
||||
|
||||
await Database.clients.create({ name, expiresAt });
|
||||
await WireGuard.saveConfig();
|
||||
return { success: true };
|
||||
}
|
||||
);
|
||||
17
src/server/api/me/index.post.ts
Normal file
17
src/server/api/me/index.post.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { UserUpdateSchema } from '#db/repositories/user/types';
|
||||
|
||||
export default definePermissionEventHandler(
|
||||
'me',
|
||||
'update',
|
||||
async ({ event, user, checkPermissions }) => {
|
||||
const { name, email } = await readValidatedBody(
|
||||
event,
|
||||
validateZod(UserUpdateSchema, event)
|
||||
);
|
||||
|
||||
checkPermissions(user);
|
||||
|
||||
await Database.users.update(user.id, name, email);
|
||||
return { success: true };
|
||||
}
|
||||
);
|
||||
17
src/server/api/me/password.post.ts
Normal file
17
src/server/api/me/password.post.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { UserUpdatePasswordSchema } from '#db/repositories/user/types';
|
||||
|
||||
export default definePermissionEventHandler(
|
||||
'me',
|
||||
'update',
|
||||
async ({ event, user, checkPermissions }) => {
|
||||
const { newPassword, currentPassword } = await readValidatedBody(
|
||||
event,
|
||||
validateZod(UserUpdatePasswordSchema, event)
|
||||
);
|
||||
|
||||
checkPermissions(user);
|
||||
|
||||
await Database.users.updatePassword(user.id, currentPassword, newPassword);
|
||||
return { success: true };
|
||||
}
|
||||
);
|
||||
11
src/server/api/release.get.ts
Normal file
11
src/server/api/release.get.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { gt } from 'semver';
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
const latestRelease = await cachedFetchLatestRelease();
|
||||
const updateAvailable = gt(latestRelease.version, RELEASE);
|
||||
return {
|
||||
currentRelease: RELEASE,
|
||||
latestRelease: latestRelease,
|
||||
updateAvailable,
|
||||
};
|
||||
});
|
||||
16
src/server/api/session.delete.ts
Normal file
16
src/server/api/session.delete.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await useWGSession(event);
|
||||
const sessionId = session.id;
|
||||
|
||||
if (sessionId === undefined) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Not logged in',
|
||||
});
|
||||
}
|
||||
|
||||
await session.clear();
|
||||
|
||||
SERVER_DEBUG(`Deleted Session: ${sessionId}`);
|
||||
return { success: true };
|
||||
});
|
||||
25
src/server/api/session.get.ts
Normal file
25
src/server/api/session.get.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await useWGSession(event);
|
||||
|
||||
if (!session.data.userId) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Not logged in',
|
||||
});
|
||||
}
|
||||
const user = await Database.users.get(session.data.userId);
|
||||
if (!user) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Not found in Database',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
role: user.role,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
};
|
||||
});
|
||||
38
src/server/api/session.post.ts
Normal file
38
src/server/api/session.post.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { UserLoginSchema } from '#db/repositories/user/types';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { username, password, remember } = await readValidatedBody(
|
||||
event,
|
||||
validateZod(UserLoginSchema, event)
|
||||
);
|
||||
|
||||
// TODO: timing can be used to enumerate usernames
|
||||
|
||||
const user = await Database.users.getByUsername(username);
|
||||
if (!user)
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Incorrect credentials',
|
||||
});
|
||||
|
||||
const userHashPassword = user.password;
|
||||
const passwordValid = await isPasswordValid(password, userHashPassword);
|
||||
if (!passwordValid) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Incorrect credentials',
|
||||
});
|
||||
}
|
||||
|
||||
const session = await useWGSession(event, remember);
|
||||
|
||||
const data = await session.update({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
// TODO?: create audit log
|
||||
|
||||
SERVER_DEBUG(`New Session: ${data.id} for ${user.id} (${user.username})`);
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
13
src/server/api/setup/2.post.ts
Normal file
13
src/server/api/setup/2.post.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { UserSetupSchema } from '#db/repositories/user/types';
|
||||
|
||||
export default defineSetupEventHandler(2, async ({ event }) => {
|
||||
const { username, password } = await readValidatedBody(
|
||||
event,
|
||||
validateZod(UserSetupSchema, event)
|
||||
);
|
||||
|
||||
await Database.users.create(username, password);
|
||||
|
||||
await Database.general.setSetupStep(3);
|
||||
return { success: true };
|
||||
});
|
||||
13
src/server/api/setup/4.post.ts
Normal file
13
src/server/api/setup/4.post.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { UserConfigSetupSchema } from '#db/repositories/userConfig/types';
|
||||
|
||||
export default defineSetupEventHandler(4, async ({ event }) => {
|
||||
const { host, port } = await readValidatedBody(
|
||||
event,
|
||||
validateZod(UserConfigSetupSchema, event)
|
||||
);
|
||||
|
||||
await Database.userConfigs.updateHostPort(host, port);
|
||||
|
||||
await Database.general.setSetupStep(0);
|
||||
return { success: true };
|
||||
});
|
||||
76
src/server/api/setup/migrate.post.ts
Normal file
76
src/server/api/setup/migrate.post.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { parseCidr } from 'cidr-tools';
|
||||
import { stringifyIp } from 'ip-bigint';
|
||||
import { z } from 'zod';
|
||||
|
||||
export default defineSetupEventHandler('migrate', async ({ event }) => {
|
||||
const { file } = await readValidatedBody(
|
||||
event,
|
||||
validateZod(FileSchema, event)
|
||||
);
|
||||
|
||||
const schema = z.object({
|
||||
server: z.object({
|
||||
privateKey: z.string(),
|
||||
publicKey: z.string(),
|
||||
// only used for cidr
|
||||
address: z.string(),
|
||||
}),
|
||||
clients: z.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
// not used
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
address: z.string(),
|
||||
privateKey: z.string(),
|
||||
publicKey: z.string(),
|
||||
preSharedKey: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
enabled: z.boolean(),
|
||||
})
|
||||
),
|
||||
});
|
||||
const res = await schema.safeParseAsync(JSON.parse(file));
|
||||
if (!res.success) {
|
||||
throw new Error('Invalid Config');
|
||||
}
|
||||
|
||||
const oldConfig = res.data;
|
||||
|
||||
await Database.interfaces.updateKeyPair(
|
||||
oldConfig.server.privateKey,
|
||||
oldConfig.server.publicKey
|
||||
);
|
||||
|
||||
const ipv4Cidr = parseCidr(oldConfig.server.address + '/24');
|
||||
const ipv6Cidr = parseCidr('fdcc:ad94:bacf:61a4::cafe:0/112');
|
||||
|
||||
await Database.interfaces.updateCidr({
|
||||
ipv4Cidr:
|
||||
stringifyIp({ number: ipv4Cidr.start, version: 4 }) +
|
||||
`/${ipv4Cidr.prefix}`,
|
||||
ipv6Cidr: ipv6Cidr.cidr,
|
||||
});
|
||||
|
||||
for (const clientId in oldConfig.clients) {
|
||||
const clientConfig = oldConfig.clients[clientId];
|
||||
|
||||
if (!clientConfig) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const clients = await Database.clients.getAll();
|
||||
|
||||
const ipv6Address = nextIP(6, ipv6Cidr, clients);
|
||||
|
||||
await Database.clients.createFromExisting({
|
||||
...clientConfig,
|
||||
ipv4Address: clientConfig.address,
|
||||
ipv6Address,
|
||||
});
|
||||
}
|
||||
|
||||
await Database.general.setSetupStep(0);
|
||||
return { success: true };
|
||||
});
|
||||
97
src/server/database/migrations/0000_short_skin.sql
Normal file
97
src/server/database/migrations/0000_short_skin.sql
Normal file
@@ -0,0 +1,97 @@
|
||||
CREATE TABLE `clients_table` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`user_id` integer NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`ipv4_address` text NOT NULL,
|
||||
`ipv6_address` text NOT NULL,
|
||||
`private_key` text NOT NULL,
|
||||
`public_key` text NOT NULL,
|
||||
`pre_shared_key` text NOT NULL,
|
||||
`expires_at` text,
|
||||
`allowed_ips` text NOT NULL,
|
||||
`server_allowed_ips` text NOT NULL,
|
||||
`persistent_keepalive` integer NOT NULL,
|
||||
`mtu` integer NOT NULL,
|
||||
`dns` text NOT NULL,
|
||||
`enabled` integer NOT NULL,
|
||||
`created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
|
||||
`updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users_table`(`id`) ON UPDATE cascade ON DELETE restrict
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `clients_table_ipv4_address_unique` ON `clients_table` (`ipv4_address`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `clients_table_ipv6_address_unique` ON `clients_table` (`ipv6_address`);--> statement-breakpoint
|
||||
CREATE TABLE `general_table` (
|
||||
`id` integer PRIMARY KEY DEFAULT 1 NOT NULL,
|
||||
`setup_step` integer NOT NULL,
|
||||
`session_password` text NOT NULL,
|
||||
`session_timeout` integer NOT NULL,
|
||||
`metrics_prometheus` integer NOT NULL,
|
||||
`metrics_json` integer NOT NULL,
|
||||
`metrics_password` text,
|
||||
`created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
|
||||
`updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `hooks_table` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`pre_up` text NOT NULL,
|
||||
`post_up` text NOT NULL,
|
||||
`pre_down` text NOT NULL,
|
||||
`post_down` text NOT NULL,
|
||||
`created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
|
||||
`updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
|
||||
FOREIGN KEY (`id`) REFERENCES `interfaces_table`(`name`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `interfaces_table` (
|
||||
`name` text PRIMARY KEY NOT NULL,
|
||||
`device` text NOT NULL,
|
||||
`port` integer NOT NULL,
|
||||
`private_key` text NOT NULL,
|
||||
`public_key` text NOT NULL,
|
||||
`ipv4_cidr` text NOT NULL,
|
||||
`ipv6_cidr` text NOT NULL,
|
||||
`mtu` integer NOT NULL,
|
||||
`enabled` integer NOT NULL,
|
||||
`created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
|
||||
`updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `interfaces_table_port_unique` ON `interfaces_table` (`port`);--> statement-breakpoint
|
||||
CREATE TABLE `one_time_links_table` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`one_time_link` text NOT NULL,
|
||||
`expires_at` text NOT NULL,
|
||||
`client_id` integer NOT NULL,
|
||||
`created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
|
||||
`updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
|
||||
FOREIGN KEY (`client_id`) REFERENCES `clients_table`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `one_time_links_table_one_time_link_unique` ON `one_time_links_table` (`one_time_link`);--> statement-breakpoint
|
||||
CREATE TABLE `users_table` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`username` text NOT NULL,
|
||||
`password` text NOT NULL,
|
||||
`email` text,
|
||||
`name` text NOT NULL,
|
||||
`role` integer NOT NULL,
|
||||
`enabled` integer NOT NULL,
|
||||
`created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
|
||||
`updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `users_table_username_unique` ON `users_table` (`username`);--> statement-breakpoint
|
||||
CREATE TABLE `user_configs_table` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`default_mtu` integer NOT NULL,
|
||||
`default_persistent_keepalive` integer NOT NULL,
|
||||
`default_dns` text NOT NULL,
|
||||
`default_allowed_ips` text NOT NULL,
|
||||
`host` text NOT NULL,
|
||||
`port` integer NOT NULL,
|
||||
`created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
|
||||
`updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
|
||||
FOREIGN KEY (`id`) REFERENCES `interfaces_table`(`name`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
18
src/server/database/migrations/0001_classy_the_stranger.sql
Normal file
18
src/server/database/migrations/0001_classy_the_stranger.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
PRAGMA journal_mode=WAL;--> statement-breakpoint
|
||||
INSERT INTO `general_table` (`setup_step`, `session_password`, `session_timeout`, `metrics_prometheus`, `metrics_json`)
|
||||
VALUES (1, hex(randomblob(256)), 3600, 0, 0);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `interfaces_table` (`name`, `device`, `port`, `private_key`, `public_key`, `ipv4_cidr`, `ipv6_cidr`, `mtu`, `enabled`)
|
||||
VALUES ('wg0', 'eth0', 51820, '---default---', '---default---', '10.8.0.0/24', 'fdcc:ad94:bacf:61a4::cafe:0/112', 1420, 1);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `hooks_table` (`id`, `pre_up`, `post_up`, `pre_down`, `post_down`)
|
||||
VALUES (
|
||||
'wg0',
|
||||
'',
|
||||
'iptables -t nat -A POSTROUTING -s {{ipv4Cidr}} -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 {{ipv6Cidr}} -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;',
|
||||
'',
|
||||
'iptables -t nat -D POSTROUTING -s {{ipv4Cidr}} -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 {{ipv6Cidr}} -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;'
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `user_configs_table` (`id`, `default_mtu`, `default_persistent_keepalive`, `default_dns`, `default_allowed_ips`, `host`, `port`)
|
||||
VALUES ('wg0', 1420, 0, '["1.1.1.1","2606:4700:4700::1111"]', '["0.0.0.0/0","::/0"]', '', 51820)
|
||||
674
src/server/database/migrations/meta/0000_snapshot.json
Normal file
674
src/server/database/migrations/meta/0000_snapshot.json
Normal file
@@ -0,0 +1,674 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "b1dde023-d141-4eab-9226-89a832b2ed2b",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"clients_table": {
|
||||
"name": "clients_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ipv4_address": {
|
||||
"name": "ipv4_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ipv6_address": {
|
||||
"name": "ipv6_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"private_key": {
|
||||
"name": "private_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"public_key": {
|
||||
"name": "public_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"pre_shared_key": {
|
||||
"name": "pre_shared_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"allowed_ips": {
|
||||
"name": "allowed_ips",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"server_allowed_ips": {
|
||||
"name": "server_allowed_ips",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"persistent_keepalive": {
|
||||
"name": "persistent_keepalive",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"mtu": {
|
||||
"name": "mtu",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"dns": {
|
||||
"name": "dns",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"clients_table_ipv4_address_unique": {
|
||||
"name": "clients_table_ipv4_address_unique",
|
||||
"columns": [
|
||||
"ipv4_address"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"clients_table_ipv6_address_unique": {
|
||||
"name": "clients_table_ipv6_address_unique",
|
||||
"columns": [
|
||||
"ipv6_address"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"clients_table_user_id_users_table_id_fk": {
|
||||
"name": "clients_table_user_id_users_table_id_fk",
|
||||
"tableFrom": "clients_table",
|
||||
"tableTo": "users_table",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "restrict",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"general_table": {
|
||||
"name": "general_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 1
|
||||
},
|
||||
"setup_step": {
|
||||
"name": "setup_step",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"session_password": {
|
||||
"name": "session_password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"session_timeout": {
|
||||
"name": "session_timeout",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"metrics_prometheus": {
|
||||
"name": "metrics_prometheus",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"metrics_json": {
|
||||
"name": "metrics_json",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"metrics_password": {
|
||||
"name": "metrics_password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"hooks_table": {
|
||||
"name": "hooks_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"pre_up": {
|
||||
"name": "pre_up",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"post_up": {
|
||||
"name": "post_up",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"pre_down": {
|
||||
"name": "pre_down",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"post_down": {
|
||||
"name": "post_down",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"hooks_table_id_interfaces_table_name_fk": {
|
||||
"name": "hooks_table_id_interfaces_table_name_fk",
|
||||
"tableFrom": "hooks_table",
|
||||
"tableTo": "interfaces_table",
|
||||
"columnsFrom": [
|
||||
"id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"name"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"interfaces_table": {
|
||||
"name": "interfaces_table",
|
||||
"columns": {
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"device": {
|
||||
"name": "device",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"port": {
|
||||
"name": "port",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"private_key": {
|
||||
"name": "private_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"public_key": {
|
||||
"name": "public_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ipv4_cidr": {
|
||||
"name": "ipv4_cidr",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ipv6_cidr": {
|
||||
"name": "ipv6_cidr",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"mtu": {
|
||||
"name": "mtu",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"interfaces_table_port_unique": {
|
||||
"name": "interfaces_table_port_unique",
|
||||
"columns": [
|
||||
"port"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"one_time_links_table": {
|
||||
"name": "one_time_links_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"one_time_link": {
|
||||
"name": "one_time_link",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"client_id": {
|
||||
"name": "client_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"one_time_links_table_one_time_link_unique": {
|
||||
"name": "one_time_links_table_one_time_link_unique",
|
||||
"columns": [
|
||||
"one_time_link"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"one_time_links_table_client_id_clients_table_id_fk": {
|
||||
"name": "one_time_links_table_client_id_clients_table_id_fk",
|
||||
"tableFrom": "one_time_links_table",
|
||||
"tableTo": "clients_table",
|
||||
"columnsFrom": [
|
||||
"client_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"users_table": {
|
||||
"name": "users_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_table_username_unique": {
|
||||
"name": "users_table_username_unique",
|
||||
"columns": [
|
||||
"username"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"user_configs_table": {
|
||||
"name": "user_configs_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"default_mtu": {
|
||||
"name": "default_mtu",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"default_persistent_keepalive": {
|
||||
"name": "default_persistent_keepalive",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"default_dns": {
|
||||
"name": "default_dns",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"default_allowed_ips": {
|
||||
"name": "default_allowed_ips",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"host": {
|
||||
"name": "host",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"port": {
|
||||
"name": "port",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"user_configs_table_id_interfaces_table_name_fk": {
|
||||
"name": "user_configs_table_id_interfaces_table_name_fk",
|
||||
"tableFrom": "user_configs_table",
|
||||
"tableTo": "interfaces_table",
|
||||
"columnsFrom": [
|
||||
"id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"name"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
674
src/server/database/migrations/meta/0001_snapshot.json
Normal file
674
src/server/database/migrations/meta/0001_snapshot.json
Normal file
@@ -0,0 +1,674 @@
|
||||
{
|
||||
"id": "720d420c-361f-4427-a45b-db0ca613934d",
|
||||
"prevId": "b1dde023-d141-4eab-9226-89a832b2ed2b",
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"tables": {
|
||||
"clients_table": {
|
||||
"name": "clients_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ipv4_address": {
|
||||
"name": "ipv4_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ipv6_address": {
|
||||
"name": "ipv6_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"private_key": {
|
||||
"name": "private_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"public_key": {
|
||||
"name": "public_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"pre_shared_key": {
|
||||
"name": "pre_shared_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"allowed_ips": {
|
||||
"name": "allowed_ips",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"server_allowed_ips": {
|
||||
"name": "server_allowed_ips",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"persistent_keepalive": {
|
||||
"name": "persistent_keepalive",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"mtu": {
|
||||
"name": "mtu",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"dns": {
|
||||
"name": "dns",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"clients_table_ipv4_address_unique": {
|
||||
"name": "clients_table_ipv4_address_unique",
|
||||
"columns": [
|
||||
"ipv4_address"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"clients_table_ipv6_address_unique": {
|
||||
"name": "clients_table_ipv6_address_unique",
|
||||
"columns": [
|
||||
"ipv6_address"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"clients_table_user_id_users_table_id_fk": {
|
||||
"name": "clients_table_user_id_users_table_id_fk",
|
||||
"tableFrom": "clients_table",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"tableTo": "users_table",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "cascade",
|
||||
"onDelete": "restrict"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"general_table": {
|
||||
"name": "general_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 1
|
||||
},
|
||||
"setup_step": {
|
||||
"name": "setup_step",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"session_password": {
|
||||
"name": "session_password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"session_timeout": {
|
||||
"name": "session_timeout",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"metrics_prometheus": {
|
||||
"name": "metrics_prometheus",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"metrics_json": {
|
||||
"name": "metrics_json",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"metrics_password": {
|
||||
"name": "metrics_password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"hooks_table": {
|
||||
"name": "hooks_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"pre_up": {
|
||||
"name": "pre_up",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"post_up": {
|
||||
"name": "post_up",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"pre_down": {
|
||||
"name": "pre_down",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"post_down": {
|
||||
"name": "post_down",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"hooks_table_id_interfaces_table_name_fk": {
|
||||
"name": "hooks_table_id_interfaces_table_name_fk",
|
||||
"tableFrom": "hooks_table",
|
||||
"columnsFrom": [
|
||||
"id"
|
||||
],
|
||||
"tableTo": "interfaces_table",
|
||||
"columnsTo": [
|
||||
"name"
|
||||
],
|
||||
"onUpdate": "cascade",
|
||||
"onDelete": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"interfaces_table": {
|
||||
"name": "interfaces_table",
|
||||
"columns": {
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"device": {
|
||||
"name": "device",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"port": {
|
||||
"name": "port",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"private_key": {
|
||||
"name": "private_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"public_key": {
|
||||
"name": "public_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ipv4_cidr": {
|
||||
"name": "ipv4_cidr",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ipv6_cidr": {
|
||||
"name": "ipv6_cidr",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"mtu": {
|
||||
"name": "mtu",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"interfaces_table_port_unique": {
|
||||
"name": "interfaces_table_port_unique",
|
||||
"columns": [
|
||||
"port"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"one_time_links_table": {
|
||||
"name": "one_time_links_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"one_time_link": {
|
||||
"name": "one_time_link",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"client_id": {
|
||||
"name": "client_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"one_time_links_table_one_time_link_unique": {
|
||||
"name": "one_time_links_table_one_time_link_unique",
|
||||
"columns": [
|
||||
"one_time_link"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"one_time_links_table_client_id_clients_table_id_fk": {
|
||||
"name": "one_time_links_table_client_id_clients_table_id_fk",
|
||||
"tableFrom": "one_time_links_table",
|
||||
"columnsFrom": [
|
||||
"client_id"
|
||||
],
|
||||
"tableTo": "clients_table",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "cascade",
|
||||
"onDelete": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"users_table": {
|
||||
"name": "users_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_table_username_unique": {
|
||||
"name": "users_table_username_unique",
|
||||
"columns": [
|
||||
"username"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"user_configs_table": {
|
||||
"name": "user_configs_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"default_mtu": {
|
||||
"name": "default_mtu",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"default_persistent_keepalive": {
|
||||
"name": "default_persistent_keepalive",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"default_dns": {
|
||||
"name": "default_dns",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"default_allowed_ips": {
|
||||
"name": "default_allowed_ips",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"host": {
|
||||
"name": "host",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"port": {
|
||||
"name": "port",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"user_configs_table_id_interfaces_table_name_fk": {
|
||||
"name": "user_configs_table_id_interfaces_table_name_fk",
|
||||
"tableFrom": "user_configs_table",
|
||||
"columnsFrom": [
|
||||
"id"
|
||||
],
|
||||
"tableTo": "interfaces_table",
|
||||
"columnsTo": [
|
||||
"name"
|
||||
],
|
||||
"onUpdate": "cascade",
|
||||
"onDelete": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
20
src/server/database/migrations/meta/_journal.json
Normal file
20
src/server/database/migrations/meta/_journal.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1739266828300,
|
||||
"tag": "0000_short_skin",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "6",
|
||||
"when": 1739266837347,
|
||||
"tag": "0001_classy_the_stranger",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
47
src/server/database/repositories/client/schema.ts
Normal file
47
src/server/database/repositories/client/schema.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { sql, relations } from 'drizzle-orm';
|
||||
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
import { oneTimeLink, user } from '../../schema';
|
||||
|
||||
export const client = sqliteTable('clients_table', {
|
||||
id: int().primaryKey({ autoIncrement: true }),
|
||||
userId: int('user_id')
|
||||
.notNull()
|
||||
.references(() => user.id, {
|
||||
onDelete: 'restrict',
|
||||
onUpdate: 'cascade',
|
||||
}),
|
||||
name: text().notNull(),
|
||||
ipv4Address: text('ipv4_address').notNull().unique(),
|
||||
ipv6Address: text('ipv6_address').notNull().unique(),
|
||||
privateKey: text('private_key').notNull(),
|
||||
publicKey: text('public_key').notNull(),
|
||||
preSharedKey: text('pre_shared_key').notNull(),
|
||||
expiresAt: text('expires_at'),
|
||||
allowedIps: text('allowed_ips', { mode: 'json' }).$type<string[]>().notNull(),
|
||||
serverAllowedIps: text('server_allowed_ips', { mode: 'json' })
|
||||
.$type<string[]>()
|
||||
.notNull(),
|
||||
persistentKeepalive: int('persistent_keepalive').notNull(),
|
||||
mtu: int().notNull(),
|
||||
dns: text({ mode: 'json' }).$type<string[]>().notNull(),
|
||||
enabled: int({ mode: 'boolean' }).notNull(),
|
||||
createdAt: text('created_at')
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`),
|
||||
updatedAt: text('updated_at')
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),
|
||||
});
|
||||
|
||||
export const clientsRelations = relations(client, ({ one }) => ({
|
||||
oneTimeLink: one(oneTimeLink, {
|
||||
fields: [client.id],
|
||||
references: [oneTimeLink.clientId],
|
||||
}),
|
||||
user: one(user, {
|
||||
fields: [client.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
179
src/server/database/repositories/client/service.ts
Normal file
179
src/server/database/repositories/client/service.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { eq, sql } from 'drizzle-orm';
|
||||
import { parseCidr } from 'cidr-tools';
|
||||
import { client } from './schema';
|
||||
import type {
|
||||
ClientCreateFromExistingType,
|
||||
ClientCreateType,
|
||||
UpdateClientType,
|
||||
} from './types';
|
||||
import type { DBType } from '#db/sqlite';
|
||||
import { wgInterface, userConfig } from '#db/schema';
|
||||
|
||||
function createPreparedStatement(db: DBType) {
|
||||
return {
|
||||
findAll: db.query.client
|
||||
.findMany({
|
||||
with: {
|
||||
oneTimeLink: true,
|
||||
},
|
||||
})
|
||||
.prepare(),
|
||||
findById: db.query.client
|
||||
.findFirst({ where: eq(client.id, sql.placeholder('id')) })
|
||||
.prepare(),
|
||||
findByUserId: db.query.client
|
||||
.findMany({
|
||||
where: eq(client.userId, sql.placeholder('userId')),
|
||||
with: { oneTimeLink: true },
|
||||
})
|
||||
.prepare(),
|
||||
toggle: db
|
||||
.update(client)
|
||||
.set({ enabled: sql.placeholder('enabled') as never as boolean })
|
||||
.where(eq(client.id, sql.placeholder('id')))
|
||||
.prepare(),
|
||||
delete: db
|
||||
.delete(client)
|
||||
.where(eq(client.id, sql.placeholder('id')))
|
||||
.prepare(),
|
||||
};
|
||||
}
|
||||
|
||||
export class ClientService {
|
||||
#db: DBType;
|
||||
#statements: ReturnType<typeof createPreparedStatement>;
|
||||
|
||||
constructor(db: DBType) {
|
||||
this.#db = db;
|
||||
this.#statements = createPreparedStatement(db);
|
||||
}
|
||||
|
||||
async getForUser(userId: ID) {
|
||||
const result = await this.#statements.findByUserId.execute({ userId });
|
||||
return result.map((row) => ({
|
||||
...row,
|
||||
createdAt: new Date(row.createdAt),
|
||||
updatedAt: new Date(row.updatedAt),
|
||||
}));
|
||||
}
|
||||
|
||||
async getAll() {
|
||||
const result = await this.#statements.findAll.execute();
|
||||
return result.map((row) => ({
|
||||
...row,
|
||||
createdAt: new Date(row.createdAt),
|
||||
updatedAt: new Date(row.updatedAt),
|
||||
}));
|
||||
}
|
||||
|
||||
get(id: ID) {
|
||||
return this.#statements.findById.execute({ id });
|
||||
}
|
||||
|
||||
async create({ name, expiresAt }: ClientCreateType) {
|
||||
const privateKey = await wg.generatePrivateKey();
|
||||
const publicKey = await wg.getPublicKey(privateKey);
|
||||
const preSharedKey = await wg.generatePreSharedKey();
|
||||
|
||||
let parsedExpiresAt = expiresAt;
|
||||
if (parsedExpiresAt) {
|
||||
const expiresAtDate = new Date(parsedExpiresAt);
|
||||
expiresAtDate.setHours(23);
|
||||
expiresAtDate.setMinutes(59);
|
||||
expiresAtDate.setSeconds(59);
|
||||
parsedExpiresAt = expiresAtDate.toISOString();
|
||||
}
|
||||
|
||||
return this.#db.transaction(async (tx) => {
|
||||
const clients = await tx.query.client.findMany().execute();
|
||||
const clientInterface = await tx.query.wgInterface
|
||||
.findFirst({
|
||||
where: eq(wgInterface.name, 'wg0'),
|
||||
})
|
||||
.execute();
|
||||
|
||||
if (!clientInterface) {
|
||||
throw new Error('WireGuard interface not found');
|
||||
}
|
||||
|
||||
const clientConfig = await tx.query.userConfig
|
||||
.findFirst({
|
||||
where: eq(userConfig.id, clientInterface.name),
|
||||
})
|
||||
.execute();
|
||||
|
||||
if (!clientConfig) {
|
||||
throw new Error('WireGuard interface configuration not found');
|
||||
}
|
||||
|
||||
const ipv4Cidr = parseCidr(clientInterface.ipv4Cidr);
|
||||
const ipv4Address = nextIP(4, ipv4Cidr, clients);
|
||||
const ipv6Cidr = parseCidr(clientInterface.ipv6Cidr);
|
||||
const ipv6Address = nextIP(6, ipv6Cidr, clients);
|
||||
|
||||
await tx
|
||||
.insert(client)
|
||||
.values({
|
||||
name,
|
||||
// TODO: properly assign user id
|
||||
userId: 1,
|
||||
expiresAt: parsedExpiresAt,
|
||||
privateKey,
|
||||
publicKey,
|
||||
preSharedKey,
|
||||
ipv4Address,
|
||||
ipv6Address,
|
||||
mtu: clientConfig.defaultMtu,
|
||||
allowedIps: clientConfig.defaultAllowedIps,
|
||||
dns: clientConfig.defaultDns,
|
||||
persistentKeepalive: clientConfig.defaultPersistentKeepalive,
|
||||
serverAllowedIps: [],
|
||||
enabled: true,
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
toggle(id: ID, enabled: boolean) {
|
||||
return this.#statements.toggle.execute({ id, enabled });
|
||||
}
|
||||
|
||||
delete(id: ID) {
|
||||
return this.#statements.delete.execute({ id });
|
||||
}
|
||||
|
||||
update(id: ID, data: UpdateClientType) {
|
||||
return this.#db.update(client).set(data).where(eq(client.id, id)).execute();
|
||||
}
|
||||
|
||||
async createFromExisting({
|
||||
name,
|
||||
enabled,
|
||||
ipv4Address,
|
||||
ipv6Address,
|
||||
preSharedKey,
|
||||
privateKey,
|
||||
publicKey,
|
||||
}: ClientCreateFromExistingType) {
|
||||
const clientConfig = await Database.userConfigs.get();
|
||||
|
||||
return this.#db
|
||||
.insert(client)
|
||||
.values({
|
||||
name,
|
||||
userId: 1,
|
||||
privateKey,
|
||||
publicKey,
|
||||
preSharedKey,
|
||||
ipv4Address,
|
||||
ipv6Address,
|
||||
mtu: clientConfig.defaultMtu,
|
||||
allowedIps: clientConfig.defaultAllowedIps,
|
||||
dns: clientConfig.defaultDns,
|
||||
persistentKeepalive: clientConfig.defaultPersistentKeepalive,
|
||||
serverAllowedIps: [],
|
||||
enabled,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
83
src/server/database/repositories/client/types.ts
Normal file
83
src/server/database/repositories/client/types.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { InferSelectModel } from 'drizzle-orm';
|
||||
import z from 'zod';
|
||||
|
||||
import type { client } from './schema';
|
||||
|
||||
export type ClientType = InferSelectModel<typeof client>;
|
||||
|
||||
export type ClientNextIpType = Pick<ClientType, 'ipv4Address' | 'ipv6Address'>;
|
||||
|
||||
export type CreateClientType = Omit<
|
||||
ClientType,
|
||||
'createdAt' | 'updatedAt' | 'id'
|
||||
>;
|
||||
|
||||
export type UpdateClientType = Omit<
|
||||
CreateClientType,
|
||||
'privateKey' | 'publicKey' | 'preSharedKey' | 'userId'
|
||||
>;
|
||||
|
||||
const name = z
|
||||
.string({ message: t('zod.client.name') })
|
||||
.min(1, t('zod.client.name'))
|
||||
.pipe(safeStringRefine);
|
||||
|
||||
const expiresAt = z
|
||||
.string({ message: t('zod.client.expiresAt') })
|
||||
.min(1, t('zod.client.expiresAt'))
|
||||
.pipe(safeStringRefine)
|
||||
.nullable();
|
||||
|
||||
const address4 = z
|
||||
.string({ message: t('zod.client.address4') })
|
||||
.min(1, { message: t('zod.client.address4') })
|
||||
.pipe(safeStringRefine);
|
||||
|
||||
const address6 = z
|
||||
.string({ message: t('zod.client.address6') })
|
||||
.min(1, { message: t('zod.client.address6') })
|
||||
.pipe(safeStringRefine);
|
||||
|
||||
const serverAllowedIps = z.array(AddressSchema, {
|
||||
message: t('zod.client.serverAllowedIps'),
|
||||
});
|
||||
|
||||
export const ClientCreateSchema = z.object({
|
||||
name: name,
|
||||
expiresAt: expiresAt,
|
||||
});
|
||||
|
||||
export type ClientCreateType = z.infer<typeof ClientCreateSchema>;
|
||||
|
||||
export const ClientUpdateSchema = schemaForType<UpdateClientType>()(
|
||||
z.object({
|
||||
name: name,
|
||||
enabled: EnabledSchema,
|
||||
expiresAt: expiresAt,
|
||||
ipv4Address: address4,
|
||||
ipv6Address: address6,
|
||||
allowedIps: AllowedIpsSchema,
|
||||
serverAllowedIps: serverAllowedIps,
|
||||
mtu: MtuSchema,
|
||||
persistentKeepalive: PersistentKeepaliveSchema,
|
||||
dns: DnsSchema,
|
||||
})
|
||||
);
|
||||
|
||||
// TODO: investigate if coerce is bad
|
||||
const clientId = z.number({ message: t('zod.client.id'), coerce: true });
|
||||
|
||||
export const ClientGetSchema = z.object({
|
||||
clientId: clientId,
|
||||
});
|
||||
|
||||
export type ClientCreateFromExistingType = Pick<
|
||||
ClientType,
|
||||
| 'name'
|
||||
| 'ipv4Address'
|
||||
| 'ipv6Address'
|
||||
| 'privateKey'
|
||||
| 'preSharedKey'
|
||||
| 'publicKey'
|
||||
| 'enabled'
|
||||
>;
|
||||
23
src/server/database/repositories/general/schema.ts
Normal file
23
src/server/database/repositories/general/schema.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { sqliteTable, text, int } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
export const general = sqliteTable('general_table', {
|
||||
id: int().primaryKey({ autoIncrement: false }).default(1),
|
||||
|
||||
setupStep: int('setup_step').notNull(),
|
||||
|
||||
sessionPassword: text('session_password').notNull(),
|
||||
sessionTimeout: int('session_timeout').notNull(),
|
||||
|
||||
metricsPrometheus: int('metrics_prometheus', { mode: 'boolean' }).notNull(),
|
||||
metricsJson: int('metrics_json', { mode: 'boolean' }).notNull(),
|
||||
metricsPassword: text('metrics_password'),
|
||||
|
||||
createdAt: text('created_at')
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`),
|
||||
updatedAt: text('updated_at')
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),
|
||||
});
|
||||
123
src/server/database/repositories/general/service.ts
Normal file
123
src/server/database/repositories/general/service.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { general } from './schema';
|
||||
import type { GeneralUpdateType } from './types';
|
||||
import type { DBType } from '#db/sqlite';
|
||||
|
||||
function createPreparedStatement(db: DBType) {
|
||||
return {
|
||||
getSetupStep: db.query.general
|
||||
.findFirst({
|
||||
columns: {
|
||||
setupStep: true,
|
||||
},
|
||||
})
|
||||
.prepare(),
|
||||
getSessionConfig: db.query.general
|
||||
.findFirst({
|
||||
columns: {
|
||||
sessionPassword: true,
|
||||
sessionTimeout: true,
|
||||
},
|
||||
})
|
||||
.prepare(),
|
||||
getMetricsConfig: db.query.general
|
||||
.findFirst({
|
||||
columns: {
|
||||
metricsPrometheus: true,
|
||||
metricsJson: true,
|
||||
metricsPassword: true,
|
||||
},
|
||||
})
|
||||
.prepare(),
|
||||
getConfig: db.query.general
|
||||
.findFirst({
|
||||
columns: {
|
||||
sessionTimeout: true,
|
||||
metricsPrometheus: true,
|
||||
metricsJson: true,
|
||||
metricsPassword: true,
|
||||
},
|
||||
})
|
||||
.prepare(),
|
||||
updateSetupStep: db
|
||||
.update(general)
|
||||
.set({
|
||||
setupStep: sql.placeholder('setupStep') as never as number,
|
||||
})
|
||||
.prepare(),
|
||||
};
|
||||
}
|
||||
|
||||
export class GeneralService {
|
||||
#db: DBType;
|
||||
#statements: ReturnType<typeof createPreparedStatement>;
|
||||
|
||||
constructor(db: DBType) {
|
||||
this.#db = db;
|
||||
this.#statements = createPreparedStatement(db);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws
|
||||
*/
|
||||
async getSetupStep() {
|
||||
const result = await this.#statements.getSetupStep.execute();
|
||||
|
||||
if (!result) {
|
||||
throw new Error('General Config not found');
|
||||
}
|
||||
|
||||
return { step: result.setupStep, done: result.setupStep === 0 };
|
||||
}
|
||||
|
||||
setSetupStep(step: number) {
|
||||
return this.#statements.updateSetupStep.execute({ setupStep: step });
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws
|
||||
*/
|
||||
async getSessionConfig() {
|
||||
const result = await this.#statements.getSessionConfig.execute();
|
||||
|
||||
if (!result) {
|
||||
throw new Error('General Config not found');
|
||||
}
|
||||
|
||||
return {
|
||||
sessionPassword: result.sessionPassword,
|
||||
sessionTimeout: result.sessionTimeout,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws
|
||||
*/
|
||||
async getMetricsConfig() {
|
||||
const result = await this.#statements.getMetricsConfig.execute();
|
||||
|
||||
if (!result) {
|
||||
throw new Error('General Config not found');
|
||||
}
|
||||
|
||||
return {
|
||||
prometheus: result.metricsPrometheus,
|
||||
json: result.metricsJson,
|
||||
password: result.metricsPassword,
|
||||
};
|
||||
}
|
||||
|
||||
update(data: GeneralUpdateType) {
|
||||
return this.#db.update(general).set(data).execute();
|
||||
}
|
||||
|
||||
async getConfig() {
|
||||
const result = await this.#statements.getConfig.execute();
|
||||
|
||||
if (!result) {
|
||||
throw new Error('General Config not found');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
26
src/server/database/repositories/general/types.ts
Normal file
26
src/server/database/repositories/general/types.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { InferSelectModel } from 'drizzle-orm';
|
||||
import z from 'zod';
|
||||
import type { general } from './schema';
|
||||
|
||||
export type GeneralType = InferSelectModel<typeof general>;
|
||||
|
||||
const sessionTimeout = z.number({ message: t('zod.general.sessionTimeout') });
|
||||
|
||||
const metricsEnabled = z.boolean({ message: t('zod.general.metricsEnabled') });
|
||||
|
||||
const metricsPassword = z
|
||||
.string({ message: t('zod.general.metricsPassword') })
|
||||
.min(1, { message: t('zod.general.metricsPassword') })
|
||||
// TODO?: validate argon2 regex
|
||||
.nullable();
|
||||
|
||||
export const GeneralUpdateSchema = z.object({
|
||||
sessionTimeout: sessionTimeout,
|
||||
metricsPrometheus: metricsEnabled,
|
||||
metricsJson: metricsEnabled,
|
||||
metricsPassword: metricsPassword,
|
||||
});
|
||||
|
||||
export type GeneralUpdateType = z.infer<typeof GeneralUpdateSchema>;
|
||||
|
||||
export type SetupStepType = { step: number; done: boolean };
|
||||
24
src/server/database/repositories/hooks/schema.ts
Normal file
24
src/server/database/repositories/hooks/schema.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
import { wgInterface } from '../../schema';
|
||||
|
||||
export const hooks = sqliteTable('hooks_table', {
|
||||
id: text()
|
||||
.primaryKey()
|
||||
.references(() => wgInterface.name, {
|
||||
onDelete: 'cascade',
|
||||
onUpdate: 'cascade',
|
||||
}),
|
||||
preUp: text('pre_up').notNull(),
|
||||
postUp: text('post_up').notNull(),
|
||||
preDown: text('pre_down').notNull(),
|
||||
postDown: text('post_down').notNull(),
|
||||
createdAt: text('created_at')
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`),
|
||||
updatedAt: text('updated_at')
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),
|
||||
});
|
||||
38
src/server/database/repositories/hooks/service.ts
Normal file
38
src/server/database/repositories/hooks/service.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { eq, sql } from 'drizzle-orm';
|
||||
import { hooks } from './schema';
|
||||
import type { HooksUpdateType } from './types';
|
||||
import type { DBType } from '#db/sqlite';
|
||||
|
||||
function createPreparedStatement(db: DBType) {
|
||||
return {
|
||||
get: db.query.hooks
|
||||
.findFirst({ where: eq(hooks.id, sql.placeholder('interface')) })
|
||||
.prepare(),
|
||||
};
|
||||
}
|
||||
|
||||
export class HooksService {
|
||||
#db: DBType;
|
||||
#statements: ReturnType<typeof createPreparedStatement>;
|
||||
|
||||
constructor(db: DBType) {
|
||||
this.#db = db;
|
||||
this.#statements = createPreparedStatement(db);
|
||||
}
|
||||
|
||||
async get() {
|
||||
const hooks = await this.#statements.get.execute({ interface: 'wg0' });
|
||||
if (!hooks) {
|
||||
throw new Error('Hooks not found');
|
||||
}
|
||||
return hooks;
|
||||
}
|
||||
|
||||
update(data: HooksUpdateType) {
|
||||
return this.#db
|
||||
.update(hooks)
|
||||
.set(data)
|
||||
.where(eq(hooks.id, 'wg0'))
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
18
src/server/database/repositories/hooks/types.ts
Normal file
18
src/server/database/repositories/hooks/types.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { InferSelectModel } from 'drizzle-orm';
|
||||
import z from 'zod';
|
||||
import type { hooks } from './schema';
|
||||
|
||||
export type HooksType = InferSelectModel<typeof hooks>;
|
||||
|
||||
export type HooksUpdateType = Omit<HooksType, 'id' | 'createdAt' | 'updatedAt'>;
|
||||
|
||||
const hook = z.string({ message: t('zod.hook') }).pipe(safeStringRefine);
|
||||
|
||||
export const HooksUpdateSchema = schemaForType<HooksUpdateType>()(
|
||||
z.object({
|
||||
preUp: hook,
|
||||
postUp: hook,
|
||||
preDown: hook,
|
||||
postDown: hook,
|
||||
})
|
||||
);
|
||||
36
src/server/database/repositories/interface/schema.ts
Normal file
36
src/server/database/repositories/interface/schema.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { sql, relations } from 'drizzle-orm';
|
||||
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
import { userConfig, hooks } from '../../schema';
|
||||
|
||||
// maybe support multiple interfaces in the future
|
||||
export const wgInterface = sqliteTable('interfaces_table', {
|
||||
name: text().primaryKey(),
|
||||
device: text().notNull(),
|
||||
port: int().notNull().unique(),
|
||||
privateKey: text('private_key').notNull(),
|
||||
publicKey: text('public_key').notNull(),
|
||||
ipv4Cidr: text('ipv4_cidr').notNull(),
|
||||
ipv6Cidr: text('ipv6_cidr').notNull(),
|
||||
mtu: int().notNull(),
|
||||
// does nothing yet
|
||||
enabled: int({ mode: 'boolean' }).notNull(),
|
||||
createdAt: text('created_at')
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`),
|
||||
updatedAt: text('updated_at')
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),
|
||||
});
|
||||
|
||||
export const wgInterfaceRelations = relations(wgInterface, ({ one }) => ({
|
||||
hooks: one(hooks, {
|
||||
fields: [wgInterface.name],
|
||||
references: [hooks.id],
|
||||
}),
|
||||
userConfig: one(userConfig, {
|
||||
fields: [wgInterface.name],
|
||||
references: [userConfig.id],
|
||||
}),
|
||||
}));
|
||||
91
src/server/database/repositories/interface/service.ts
Normal file
91
src/server/database/repositories/interface/service.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import isCidr from 'is-cidr';
|
||||
import { eq, sql } from 'drizzle-orm';
|
||||
import { parseCidr } from 'cidr-tools';
|
||||
import { wgInterface } from './schema';
|
||||
import type { InterfaceCidrUpdateType, InterfaceUpdateType } from './types';
|
||||
import { client as clientSchema } from '#db/schema';
|
||||
import type { DBType } from '#db/sqlite';
|
||||
|
||||
function createPreparedStatement(db: DBType) {
|
||||
return {
|
||||
get: db.query.wgInterface
|
||||
.findFirst({ where: eq(wgInterface.name, sql.placeholder('interface')) })
|
||||
.prepare(),
|
||||
updateKeyPair: db
|
||||
.update(wgInterface)
|
||||
.set({
|
||||
privateKey: sql.placeholder('privateKey') as never as string,
|
||||
publicKey: sql.placeholder('publicKey') as never as string,
|
||||
})
|
||||
.where(eq(wgInterface.name, sql.placeholder('interface')))
|
||||
.prepare(),
|
||||
};
|
||||
}
|
||||
|
||||
export class InterfaceService {
|
||||
#db: DBType;
|
||||
#statements: ReturnType<typeof createPreparedStatement>;
|
||||
|
||||
constructor(db: DBType) {
|
||||
this.#db = db;
|
||||
this.#statements = createPreparedStatement(db);
|
||||
}
|
||||
|
||||
async get() {
|
||||
const wgInterface = await this.#statements.get.execute({
|
||||
interface: 'wg0',
|
||||
});
|
||||
if (!wgInterface) {
|
||||
throw new Error('Interface not found');
|
||||
}
|
||||
return wgInterface;
|
||||
}
|
||||
|
||||
updateKeyPair(privateKey: string, publicKey: string) {
|
||||
return this.#statements.updateKeyPair.execute({
|
||||
interface: 'wg0',
|
||||
privateKey,
|
||||
publicKey,
|
||||
});
|
||||
}
|
||||
|
||||
update(data: InterfaceUpdateType) {
|
||||
return this.#db
|
||||
.update(wgInterface)
|
||||
.set(data)
|
||||
.where(eq(wgInterface.name, 'wg0'))
|
||||
.execute();
|
||||
}
|
||||
|
||||
updateCidr(data: InterfaceCidrUpdateType) {
|
||||
if (!isCidr(data.ipv4Cidr) || !isCidr(data.ipv6Cidr)) {
|
||||
throw new Error('Invalid CIDR');
|
||||
}
|
||||
return this.#db.transaction(async (tx) => {
|
||||
await tx
|
||||
.update(wgInterface)
|
||||
.set(data)
|
||||
.where(eq(wgInterface.name, 'wg0'))
|
||||
.execute();
|
||||
|
||||
const clients = await tx.query.client.findMany().execute();
|
||||
|
||||
for (const client of clients) {
|
||||
// TODO: optimize
|
||||
const clients = await tx.query.client.findMany().execute();
|
||||
|
||||
const nextIpv4 = nextIP(4, parseCidr(data.ipv4Cidr), clients);
|
||||
const nextIpv6 = nextIP(6, parseCidr(data.ipv6Cidr), clients);
|
||||
|
||||
await tx
|
||||
.update(clientSchema)
|
||||
.set({
|
||||
ipv4Address: nextIpv4,
|
||||
ipv6Address: nextIpv6,
|
||||
})
|
||||
.where(eq(clientSchema.id, client.id))
|
||||
.execute();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
49
src/server/database/repositories/interface/types.ts
Normal file
49
src/server/database/repositories/interface/types.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { InferSelectModel } from 'drizzle-orm';
|
||||
import z from 'zod';
|
||||
import type { wgInterface } from './schema';
|
||||
|
||||
export type InterfaceType = InferSelectModel<typeof wgInterface>;
|
||||
|
||||
export type InterfaceCreateType = Omit<
|
||||
InterfaceType,
|
||||
'createdAt' | 'updatedAt'
|
||||
>;
|
||||
|
||||
export type InterfaceUpdateType = Omit<
|
||||
InterfaceCreateType,
|
||||
'name' | 'createdAt' | 'updatedAt' | 'privateKey' | 'publicKey'
|
||||
>;
|
||||
|
||||
const device = z
|
||||
.string({ message: t('zod.interface.device') })
|
||||
.min(1, t('zod.interface.device'))
|
||||
.pipe(safeStringRefine);
|
||||
|
||||
const cidr = z
|
||||
.string({ message: t('zod.interface.cidr') })
|
||||
.min(1, { message: t('zod.interface.cidr') })
|
||||
.pipe(safeStringRefine);
|
||||
|
||||
export const InterfaceUpdateSchema = schemaForType<InterfaceUpdateType>()(
|
||||
z.object({
|
||||
ipv4Cidr: cidr,
|
||||
ipv6Cidr: cidr,
|
||||
mtu: MtuSchema,
|
||||
port: PortSchema,
|
||||
device: device,
|
||||
enabled: EnabledSchema,
|
||||
})
|
||||
);
|
||||
|
||||
export type InterfaceCidrUpdateType = {
|
||||
ipv4Cidr: string;
|
||||
ipv6Cidr: string;
|
||||
};
|
||||
|
||||
export const InterfaceCidrUpdateSchema =
|
||||
schemaForType<InterfaceCidrUpdateType>()(
|
||||
z.object({
|
||||
ipv4Cidr: cidr,
|
||||
ipv6Cidr: cidr,
|
||||
})
|
||||
);
|
||||
27
src/server/database/repositories/oneTimeLink/schema.ts
Normal file
27
src/server/database/repositories/oneTimeLink/schema.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { sql, relations } from 'drizzle-orm';
|
||||
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
import { client } from '../../schema';
|
||||
|
||||
export const oneTimeLink = sqliteTable('one_time_links_table', {
|
||||
id: int().primaryKey({ autoIncrement: true }),
|
||||
oneTimeLink: text('one_time_link').notNull().unique(),
|
||||
expiresAt: text('expires_at').notNull(),
|
||||
clientId: int('client_id')
|
||||
.notNull()
|
||||
.references(() => client.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
createdAt: text('created_at')
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`),
|
||||
updatedAt: text('updated_at')
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),
|
||||
});
|
||||
|
||||
export const oneTimeLinksRelations = relations(oneTimeLink, ({ one }) => ({
|
||||
client: one(client, {
|
||||
fields: [oneTimeLink.clientId],
|
||||
references: [client.id],
|
||||
}),
|
||||
}));
|
||||
51
src/server/database/repositories/oneTimeLink/service.ts
Normal file
51
src/server/database/repositories/oneTimeLink/service.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { eq, sql } from 'drizzle-orm';
|
||||
import CRC32 from 'crc-32';
|
||||
import { oneTimeLink } from './schema';
|
||||
import type { DBType } from '#db/sqlite';
|
||||
|
||||
function createPreparedStatement(db: DBType) {
|
||||
return {
|
||||
delete: db
|
||||
.delete(oneTimeLink)
|
||||
.where(eq(oneTimeLink.id, sql.placeholder('id')))
|
||||
.prepare(),
|
||||
create: db
|
||||
.insert(oneTimeLink)
|
||||
.values({
|
||||
clientId: sql.placeholder('id'),
|
||||
oneTimeLink: sql.placeholder('oneTimeLink'),
|
||||
expiresAt: sql.placeholder('expiresAt'),
|
||||
})
|
||||
.prepare(),
|
||||
erase: db
|
||||
.update(oneTimeLink)
|
||||
.set({ expiresAt: sql.placeholder('expiresAt') as never as string })
|
||||
.where(eq(oneTimeLink.clientId, sql.placeholder('id')))
|
||||
.prepare(),
|
||||
};
|
||||
}
|
||||
|
||||
export class OneTimeLinkService {
|
||||
#statements: ReturnType<typeof createPreparedStatement>;
|
||||
|
||||
constructor(db: DBType) {
|
||||
this.#statements = createPreparedStatement(db);
|
||||
}
|
||||
|
||||
delete(id: ID) {
|
||||
return this.#statements.delete.execute({ id });
|
||||
}
|
||||
|
||||
generate(id: ID) {
|
||||
const key = `${id}-${Math.floor(Math.random() * 1000)}`;
|
||||
const oneTimeLink = Math.abs(CRC32.str(key)).toString(16);
|
||||
const expiresAt = new Date(Date.now() + 5 * 60 * 1000).toISOString();
|
||||
|
||||
return this.#statements.create.execute({ id, oneTimeLink, expiresAt });
|
||||
}
|
||||
|
||||
erase(id: ID) {
|
||||
const expiresAt = Date.now() + 10 * 1000;
|
||||
return this.#statements.erase.execute({ id, expiresAt });
|
||||
}
|
||||
}
|
||||
14
src/server/database/repositories/oneTimeLink/types.ts
Normal file
14
src/server/database/repositories/oneTimeLink/types.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { InferSelectModel } from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
import type { oneTimeLink } from './schema';
|
||||
|
||||
export type OneTimeLinkType = InferSelectModel<typeof oneTimeLink>;
|
||||
|
||||
const oneTimeLinkType = z
|
||||
.string({ message: t('zod.otl') })
|
||||
.min(1, t('zod.otl'))
|
||||
.pipe(safeStringRefine);
|
||||
|
||||
export const OneTimeLinkGetSchema = z.object({
|
||||
oneTimeLink: oneTimeLinkType,
|
||||
});
|
||||
25
src/server/database/repositories/user/schema.ts
Normal file
25
src/server/database/repositories/user/schema.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { sql, relations } from 'drizzle-orm';
|
||||
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
import { client } from '../../schema';
|
||||
|
||||
export const user = sqliteTable('users_table', {
|
||||
id: int().primaryKey({ autoIncrement: true }),
|
||||
username: text().notNull().unique(),
|
||||
password: text().notNull(),
|
||||
email: text(),
|
||||
name: text().notNull(),
|
||||
role: int().$type<Role>().notNull(),
|
||||
enabled: int({ mode: 'boolean' }).notNull(),
|
||||
createdAt: text('created_at')
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`),
|
||||
updatedAt: text('updated_at')
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),
|
||||
});
|
||||
|
||||
export const usersRelations = relations(user, ({ many }) => ({
|
||||
clients: many(client),
|
||||
}));
|
||||
108
src/server/database/repositories/user/service.ts
Normal file
108
src/server/database/repositories/user/service.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { eq, sql } from 'drizzle-orm';
|
||||
import { user } from './schema';
|
||||
import type { DBType } from '#db/sqlite';
|
||||
|
||||
function createPreparedStatement(db: DBType) {
|
||||
return {
|
||||
findAll: db.query.user.findMany().prepare(),
|
||||
findById: db.query.user
|
||||
.findFirst({ where: eq(user.id, sql.placeholder('id')) })
|
||||
.prepare(),
|
||||
findByUsername: db.query.user
|
||||
.findFirst({
|
||||
where: eq(user.username, sql.placeholder('username')),
|
||||
})
|
||||
.prepare(),
|
||||
update: db
|
||||
.update(user)
|
||||
.set({
|
||||
name: sql.placeholder('name') as never as string,
|
||||
email: sql.placeholder('email') as never as string,
|
||||
})
|
||||
.where(eq(user.id, sql.placeholder('id')))
|
||||
.prepare(),
|
||||
};
|
||||
}
|
||||
|
||||
export class UserService {
|
||||
#db: DBType;
|
||||
#statements: ReturnType<typeof createPreparedStatement>;
|
||||
|
||||
constructor(db: DBType) {
|
||||
this.#db = db;
|
||||
this.#statements = createPreparedStatement(db);
|
||||
}
|
||||
|
||||
async getAll() {
|
||||
return this.#statements.findAll.execute();
|
||||
}
|
||||
|
||||
async get(id: ID) {
|
||||
return this.#statements.findById.execute({ id });
|
||||
}
|
||||
|
||||
async getByUsername(username: string) {
|
||||
return this.#statements.findByUsername.execute({ username });
|
||||
}
|
||||
|
||||
async create(username: string, password: string) {
|
||||
const hash = await hashPassword(password);
|
||||
|
||||
return this.#db.transaction(async (tx) => {
|
||||
const oldUser = await tx.query.user
|
||||
.findFirst({
|
||||
where: eq(user.username, username),
|
||||
})
|
||||
.execute();
|
||||
|
||||
if (oldUser) {
|
||||
throw new Error('User already exists');
|
||||
}
|
||||
|
||||
const userCount = await tx.$count(user);
|
||||
|
||||
await tx.insert(user).values({
|
||||
password: hash,
|
||||
username,
|
||||
email: null,
|
||||
name: 'Administrator',
|
||||
role: userCount === 0 ? roles.ADMIN : roles.CLIENT,
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async update(id: ID, name: string, email: string | null) {
|
||||
return this.#statements.update.execute({ id, name, email });
|
||||
}
|
||||
|
||||
async updatePassword(id: ID, currentPassword: string, newPassword: string) {
|
||||
const hash = await hashPassword(newPassword);
|
||||
|
||||
return this.#db.transaction(async (tx) => {
|
||||
// get user again to avoid password changing while request
|
||||
const txUser = await tx.query.user
|
||||
.findFirst({ where: eq(user.id, id) })
|
||||
.execute();
|
||||
|
||||
if (!txUser) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const passwordValid = await isPasswordValid(
|
||||
currentPassword,
|
||||
txUser.password
|
||||
);
|
||||
|
||||
if (!passwordValid) {
|
||||
throw new Error('Invalid password');
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(user)
|
||||
.set({ password: hash })
|
||||
.where(eq(user.id, id))
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
}
|
||||
59
src/server/database/repositories/user/types.ts
Normal file
59
src/server/database/repositories/user/types.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { InferSelectModel } from 'drizzle-orm';
|
||||
import z from 'zod';
|
||||
import type { user } from './schema';
|
||||
|
||||
export type UserType = InferSelectModel<typeof user>;
|
||||
|
||||
const username = z
|
||||
.string({ message: t('zod.user.username') })
|
||||
.min(8, t('zod.user.username'))
|
||||
.pipe(safeStringRefine);
|
||||
|
||||
const password = z
|
||||
.string({ message: t('zod.user.password') })
|
||||
.min(12, t('zod.user.password'))
|
||||
.regex(/[A-Z]/, t('zod.user.passwordUppercase'))
|
||||
.regex(/[a-z]/, t('zod.user.passwordLowercase'))
|
||||
.regex(/\d/, t('zod.user.passwordNumber'))
|
||||
.regex(/[!@#$%^&*(),.?":{}|<>]/, t('zod.user.passwordSpecial'))
|
||||
.pipe(safeStringRefine);
|
||||
|
||||
const remember = z.boolean({ message: t('zod.user.remember') });
|
||||
|
||||
export const UserLoginSchema = z.object({
|
||||
username: username,
|
||||
password: password,
|
||||
remember: remember,
|
||||
});
|
||||
|
||||
export const UserSetupSchema = z.object({
|
||||
username: username,
|
||||
password: password,
|
||||
});
|
||||
|
||||
const name = z
|
||||
.string({ message: t('zod.user.name') })
|
||||
.min(1, 'zod.user.name')
|
||||
.pipe(safeStringRefine);
|
||||
|
||||
const email = z
|
||||
.string({ message: t('zod.user.email') })
|
||||
.min(5, t('zod.user.email'))
|
||||
.email({ message: t('zod.user.emailInvalid') })
|
||||
.pipe(safeStringRefine)
|
||||
.nullable();
|
||||
|
||||
export const UserUpdateSchema = z.object({
|
||||
name: name,
|
||||
email: email,
|
||||
});
|
||||
|
||||
export const UserUpdatePasswordSchema = z
|
||||
.object({
|
||||
currentPassword: password,
|
||||
newPassword: password,
|
||||
confirmPassword: password,
|
||||
})
|
||||
.refine((val) => val.newPassword === val.confirmPassword, {
|
||||
message: t('zod.user.passwordMatch'),
|
||||
});
|
||||
29
src/server/database/repositories/userConfig/schema.ts
Normal file
29
src/server/database/repositories/userConfig/schema.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
import { wgInterface } from '../../schema';
|
||||
|
||||
// default* means clients store it themselves
|
||||
export const userConfig = sqliteTable('user_configs_table', {
|
||||
id: text()
|
||||
.primaryKey()
|
||||
.references(() => wgInterface.name, {
|
||||
onDelete: 'cascade',
|
||||
onUpdate: 'cascade',
|
||||
}),
|
||||
defaultMtu: int('default_mtu').notNull(),
|
||||
defaultPersistentKeepalive: int('default_persistent_keepalive').notNull(),
|
||||
defaultDns: text('default_dns', { mode: 'json' }).$type<string[]>().notNull(),
|
||||
defaultAllowedIps: text('default_allowed_ips', { mode: 'json' })
|
||||
.$type<string[]>()
|
||||
.notNull(),
|
||||
host: text().notNull(),
|
||||
port: int().notNull(),
|
||||
createdAt: text('created_at')
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`),
|
||||
updatedAt: text('updated_at')
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),
|
||||
});
|
||||
56
src/server/database/repositories/userConfig/service.ts
Normal file
56
src/server/database/repositories/userConfig/service.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { eq, sql } from 'drizzle-orm';
|
||||
import { userConfig } from './schema';
|
||||
import type { UserConfigUpdateType } from './types';
|
||||
import type { DBType } from '#db/sqlite';
|
||||
|
||||
function createPreparedStatement(db: DBType) {
|
||||
return {
|
||||
get: db.query.userConfig
|
||||
.findFirst({ where: eq(userConfig.id, sql.placeholder('interface')) })
|
||||
.prepare(),
|
||||
updateHostPort: db
|
||||
.update(userConfig)
|
||||
.set({
|
||||
host: sql.placeholder('host') as never as string,
|
||||
port: sql.placeholder('port') as never as number,
|
||||
})
|
||||
.where(eq(userConfig.id, sql.placeholder('interface')))
|
||||
.prepare(),
|
||||
};
|
||||
}
|
||||
|
||||
export class UserConfigService {
|
||||
#db: DBType;
|
||||
#statements: ReturnType<typeof createPreparedStatement>;
|
||||
|
||||
constructor(db: DBType) {
|
||||
this.#db = db;
|
||||
this.#statements = createPreparedStatement(db);
|
||||
}
|
||||
|
||||
async get() {
|
||||
const userConfig = await this.#statements.get.execute({ interface: 'wg0' });
|
||||
|
||||
if (!userConfig) {
|
||||
throw new Error('User config not found');
|
||||
}
|
||||
|
||||
return userConfig;
|
||||
}
|
||||
|
||||
updateHostPort(host: string, port: number) {
|
||||
return this.#statements.updateHostPort.execute({
|
||||
interface: 'wg0',
|
||||
host,
|
||||
port,
|
||||
});
|
||||
}
|
||||
|
||||
update(data: UserConfigUpdateType) {
|
||||
return this.#db
|
||||
.update(userConfig)
|
||||
.set(data)
|
||||
.where(eq(userConfig.id, 'wg0'))
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
31
src/server/database/repositories/userConfig/types.ts
Normal file
31
src/server/database/repositories/userConfig/types.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { InferSelectModel } from 'drizzle-orm';
|
||||
import z from 'zod';
|
||||
import type { userConfig } from './schema';
|
||||
|
||||
export type UserConfigType = InferSelectModel<typeof userConfig>;
|
||||
|
||||
const host = z
|
||||
.string({ message: t('zod.userConfig.host') })
|
||||
.min(1, t('zod.userConfig.host'))
|
||||
.pipe(safeStringRefine);
|
||||
|
||||
export const UserConfigSetupSchema = z.object({
|
||||
host: host,
|
||||
port: PortSchema,
|
||||
});
|
||||
|
||||
export type UserConfigUpdateType = Omit<
|
||||
UserConfigType,
|
||||
'id' | 'createdAt' | 'updatedAt'
|
||||
>;
|
||||
|
||||
export const UserConfigUpdateSchema = schemaForType<UserConfigUpdateType>()(
|
||||
z.object({
|
||||
port: PortSchema,
|
||||
defaultMtu: MtuSchema,
|
||||
defaultPersistentKeepalive: PersistentKeepaliveSchema,
|
||||
defaultDns: DnsSchema,
|
||||
defaultAllowedIps: AllowedIpsSchema,
|
||||
host: host,
|
||||
})
|
||||
);
|
||||
8
src/server/database/schema.ts
Normal file
8
src/server/database/schema.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// Make sure to not use any Path Aliases in these files
|
||||
export * from './repositories/client/schema';
|
||||
export * from './repositories/general/schema';
|
||||
export * from './repositories/hooks/schema';
|
||||
export * from './repositories/interface/schema';
|
||||
export * from './repositories/oneTimeLink/schema';
|
||||
export * from './repositories/user/schema';
|
||||
export * from './repositories/userConfig/schema';
|
||||
60
src/server/database/sqlite.ts
Normal file
60
src/server/database/sqlite.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { drizzle } from 'drizzle-orm/libsql';
|
||||
import { migrate as drizzleMigrate } from 'drizzle-orm/libsql/migrator';
|
||||
import { createClient } from '@libsql/client';
|
||||
import debug from 'debug';
|
||||
|
||||
import * as schema from './schema';
|
||||
import { ClientService } from './repositories/client/service';
|
||||
import { GeneralService } from './repositories/general/service';
|
||||
import { UserService } from './repositories/user/service';
|
||||
import { UserConfigService } from './repositories/userConfig/service';
|
||||
import { InterfaceService } from './repositories/interface/service';
|
||||
import { HooksService } from './repositories/hooks/service';
|
||||
import { OneTimeLinkService } from './repositories/oneTimeLink/service';
|
||||
|
||||
const DB_DEBUG = debug('Database');
|
||||
|
||||
const client = createClient({ url: 'file:/etc/wireguard/wg-easy.db' });
|
||||
const db = drizzle({ client, schema });
|
||||
|
||||
export async function connect() {
|
||||
await migrate();
|
||||
return new DBService(db);
|
||||
}
|
||||
|
||||
class DBService {
|
||||
clients: ClientService;
|
||||
general: GeneralService;
|
||||
users: UserService;
|
||||
userConfigs: UserConfigService;
|
||||
interfaces: InterfaceService;
|
||||
hooks: HooksService;
|
||||
oneTimeLinks: OneTimeLinkService;
|
||||
|
||||
constructor(db: DBType) {
|
||||
this.clients = new ClientService(db);
|
||||
this.general = new GeneralService(db);
|
||||
this.users = new UserService(db);
|
||||
this.userConfigs = new UserConfigService(db);
|
||||
this.interfaces = new InterfaceService(db);
|
||||
this.hooks = new HooksService(db);
|
||||
this.oneTimeLinks = new OneTimeLinkService(db);
|
||||
}
|
||||
}
|
||||
|
||||
export type DBType = typeof db;
|
||||
export type DBServiceType = DBService;
|
||||
|
||||
async function migrate() {
|
||||
try {
|
||||
DB_DEBUG('Migrating database...');
|
||||
await drizzleMigrate(db, {
|
||||
migrationsFolder: './server/database/migrations',
|
||||
});
|
||||
DB_DEBUG('Migration complete');
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
DB_DEBUG('Failed to migrate database:', e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/server/middleware/setup.ts
Normal file
29
src/server/middleware/setup.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/* First setup of wg-easy */
|
||||
export default defineEventHandler(async (event) => {
|
||||
const url = getRequestURL(event);
|
||||
|
||||
// User can't be logged in, and public routes can be accessed whenever
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { step, done } = await Database.general.getSetupStep();
|
||||
if (!done) {
|
||||
const parsedSetup = url.pathname.match(/\/setup\/(\d)/);
|
||||
if (!parsedSetup) {
|
||||
return sendRedirect(event, `/setup/1`, 302);
|
||||
}
|
||||
const [_, currentSetup] = parsedSetup;
|
||||
|
||||
if (step.toString() === currentSetup) {
|
||||
return;
|
||||
}
|
||||
return sendRedirect(event, `/setup/${step}`, 302);
|
||||
} else {
|
||||
// If already set up
|
||||
if (!url.pathname.startsWith('/setup/')) {
|
||||
return;
|
||||
}
|
||||
return sendRedirect(event, '/login', 302);
|
||||
}
|
||||
});
|
||||
6
src/server/plugins/manager.ts
Normal file
6
src/server/plugins/manager.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default defineNitroPlugin((nitroApp) => {
|
||||
nitroApp.hooks.hook('close', async () => {
|
||||
console.log('Shutting down');
|
||||
await WireGuard.Shutdown();
|
||||
});
|
||||
});
|
||||
29
src/server/routes/cnf/[oneTimeLink].ts
Normal file
29
src/server/routes/cnf/[oneTimeLink].ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { OneTimeLinkGetSchema } from '#db/repositories/oneTimeLink/types';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { oneTimeLink } = await getValidatedRouterParams(
|
||||
event,
|
||||
validateZod(OneTimeLinkGetSchema, event)
|
||||
);
|
||||
const clients = await WireGuard.getAllClients();
|
||||
// TODO: filter on the database level
|
||||
const client = clients.find(
|
||||
(client) => client.oneTimeLink?.oneTimeLink === oneTimeLink
|
||||
);
|
||||
if (!client) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Invalid One Time Link',
|
||||
});
|
||||
}
|
||||
const clientId = client.id;
|
||||
const config = await WireGuard.getClientConfiguration({ clientId });
|
||||
await Database.oneTimeLinks.erase(clientId);
|
||||
setHeader(
|
||||
event,
|
||||
'Content-Disposition',
|
||||
`attachment; filename="${client.name}.conf"`
|
||||
);
|
||||
setHeader(event, 'Content-Type', 'text/plain');
|
||||
return config;
|
||||
});
|
||||
35
src/server/routes/metrics/json.get.ts
Normal file
35
src/server/routes/metrics/json.get.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export default defineMetricsHandler('json', async () => {
|
||||
return getMetricsJSON();
|
||||
});
|
||||
|
||||
async function getMetricsJSON() {
|
||||
const clients = await WireGuard.getAllClients();
|
||||
let wireguardPeerCount = 0;
|
||||
let wireguardEnabledPeersCount = 0;
|
||||
let wireguardConnectedPeersCount = 0;
|
||||
for (const client of clients) {
|
||||
wireguardPeerCount++;
|
||||
if (client.enabled === true) {
|
||||
wireguardEnabledPeersCount++;
|
||||
}
|
||||
if (isPeerConnected(client)) {
|
||||
wireguardConnectedPeersCount++;
|
||||
}
|
||||
}
|
||||
return {
|
||||
wireguard_configured_peers: wireguardPeerCount,
|
||||
wireguard_enabled_peers: wireguardEnabledPeersCount,
|
||||
wireguard_connected_peers: wireguardConnectedPeersCount,
|
||||
clients: clients.map((client) => ({
|
||||
name: client.name,
|
||||
enabled: client.enabled,
|
||||
ipv4Address: client.ipv4Address,
|
||||
ipv6Address: client.ipv6Address,
|
||||
publicKey: client.publicKey,
|
||||
endpoint: client.endpoint,
|
||||
latestHandshakeAt: client.latestHandshakeAt,
|
||||
transferRx: client.transferRx,
|
||||
transferTx: client.transferTx,
|
||||
})),
|
||||
};
|
||||
}
|
||||
70
src/server/routes/metrics/prometheus.get.ts
Normal file
70
src/server/routes/metrics/prometheus.get.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
export default defineMetricsHandler('prometheus', async ({ event }) => {
|
||||
setHeader(event, 'Content-Type', 'text/plain');
|
||||
return getPrometheusResponse();
|
||||
});
|
||||
|
||||
async function getPrometheusResponse() {
|
||||
const wgInterface = await Database.interfaces.get();
|
||||
const clients = await WireGuard.getAllClients();
|
||||
let wireguardPeerCount = 0;
|
||||
let wireguardEnabledPeersCount = 0;
|
||||
let wireguardConnectedPeersCount = 0;
|
||||
const wireguardSentBytes = [];
|
||||
const wireguardReceivedBytes = [];
|
||||
const wireguardLatestHandshakeSeconds = [];
|
||||
for (const client of clients) {
|
||||
wireguardPeerCount++;
|
||||
if (client.enabled === true) {
|
||||
wireguardEnabledPeersCount++;
|
||||
}
|
||||
|
||||
if (isPeerConnected(client)) {
|
||||
wireguardConnectedPeersCount++;
|
||||
}
|
||||
|
||||
const id = `interface="${wgInterface.name}",enabled="${client.enabled}",ipv4Address="${client.ipv4Address}",ipv6Address="${client.ipv6Address}",name="${client.name}"`;
|
||||
|
||||
wireguardSentBytes.push(
|
||||
`wireguard_sent_bytes{${id}} ${client.transferTx ?? 0}`
|
||||
);
|
||||
wireguardReceivedBytes.push(
|
||||
`wireguard_received_bytes{${id}} ${client.transferRx ?? 0}`
|
||||
);
|
||||
// TODO: if latestHandshakeAt is null this would result in client showing as online?
|
||||
wireguardLatestHandshakeSeconds.push(
|
||||
`wireguard_latest_handshake_seconds{${id}} ${client.latestHandshakeAt ? (Date.now() - client.latestHandshakeAt.getTime()) / 1000 : 0}`
|
||||
);
|
||||
}
|
||||
|
||||
const id = `interface="${wgInterface.name}"`;
|
||||
|
||||
const returnText = [
|
||||
'# HELP wg-easy and wireguard metrics',
|
||||
'',
|
||||
'# HELP wireguard_configured_peers',
|
||||
'# TYPE wireguard_configured_peers gauge',
|
||||
`wireguard_configured_peers{${id}} ${wireguardPeerCount}`,
|
||||
'',
|
||||
'# HELP wireguard_enabled_peers',
|
||||
'# TYPE wireguard_enabled_peers gauge',
|
||||
`wireguard_enabled_peers{${id}} ${wireguardEnabledPeersCount}`,
|
||||
'',
|
||||
'# HELP wireguard_connected_peers',
|
||||
'# TYPE wireguard_connected_peers gauge',
|
||||
`wireguard_connected_peers{${id}} ${wireguardConnectedPeersCount}`,
|
||||
'',
|
||||
'# HELP wireguard_sent_bytes Bytes sent to the peer',
|
||||
'# TYPE wireguard_sent_bytes counter',
|
||||
`${wireguardSentBytes.join('\n')}`,
|
||||
'',
|
||||
'# HELP wireguard_received_bytes Bytes received from the peer',
|
||||
'# TYPE wireguard_received_bytes counter',
|
||||
`${wireguardReceivedBytes.join('\n')}`,
|
||||
'',
|
||||
'# HELP wireguard_latest_handshake_seconds UNIX timestamp seconds of the last handshake',
|
||||
'# TYPE wireguard_latest_handshake_seconds gauge',
|
||||
`${wireguardLatestHandshakeSeconds.join('\n')}`,
|
||||
];
|
||||
|
||||
return returnText.join('\n');
|
||||
}
|
||||
3
src/server/tsconfig.json
Normal file
3
src/server/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../.nuxt/tsconfig.server.json"
|
||||
}
|
||||
24
src/server/utils/Database.ts
Normal file
24
src/server/utils/Database.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Changing the Database Provider
|
||||
* This design allows for easy swapping of different database implementations.
|
||||
*/
|
||||
import { connect, type DBServiceType } from '#db/sqlite';
|
||||
|
||||
const nullObject = new Proxy(
|
||||
{},
|
||||
{
|
||||
get() {
|
||||
throw new Error('Database not yet initialized');
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// eslint-disable-next-line import/no-mutable-exports
|
||||
let provider = nullObject as never as DBServiceType;
|
||||
|
||||
connect().then((db) => {
|
||||
provider = db;
|
||||
WireGuard.Startup();
|
||||
});
|
||||
|
||||
export default provider;
|
||||
236
src/server/utils/WireGuard.ts
Normal file
236
src/server/utils/WireGuard.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import debug from 'debug';
|
||||
import QRCode from 'qrcode';
|
||||
import type { InterfaceType } from '#db/repositories/interface/types';
|
||||
|
||||
const WG_DEBUG = debug('WireGuard');
|
||||
|
||||
class WireGuard {
|
||||
/**
|
||||
* Save and sync config
|
||||
*/
|
||||
async saveConfig() {
|
||||
const wgInterface = await Database.interfaces.get();
|
||||
await this.#saveWireguardConfig(wgInterface);
|
||||
await this.#syncWireguardConfig(wgInterface);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates and saves WireGuard config from database
|
||||
*
|
||||
* Make sure to pass an updated InterfaceType object
|
||||
*/
|
||||
async #saveWireguardConfig(wgInterface: InterfaceType) {
|
||||
const clients = await Database.clients.getAll();
|
||||
const hooks = await Database.hooks.get();
|
||||
|
||||
const result = [];
|
||||
result.push(wg.generateServerInterface(wgInterface, hooks));
|
||||
|
||||
for (const client of clients) {
|
||||
if (!client.enabled) {
|
||||
continue;
|
||||
}
|
||||
result.push(wg.generateServerPeer(client));
|
||||
}
|
||||
|
||||
WG_DEBUG('Saving Config...');
|
||||
await fs.writeFile(
|
||||
`/etc/wireguard/${wgInterface.name}.conf`,
|
||||
result.join('\n\n'),
|
||||
{
|
||||
mode: 0o600,
|
||||
}
|
||||
);
|
||||
WG_DEBUG('Config saved successfully.');
|
||||
}
|
||||
|
||||
async #syncWireguardConfig(wgInterface: InterfaceType) {
|
||||
WG_DEBUG('Syncing Config...');
|
||||
await wg.sync(wgInterface.name);
|
||||
WG_DEBUG('Config synced successfully.');
|
||||
}
|
||||
|
||||
async getClientsForUser(userId: ID) {
|
||||
const wgInterface = await Database.interfaces.get();
|
||||
|
||||
const dbClients = await Database.clients.getForUser(userId);
|
||||
|
||||
const clients = dbClients.map((client) => ({
|
||||
...client,
|
||||
latestHandshakeAt: null as Date | null,
|
||||
endpoint: null as string | null,
|
||||
transferRx: null as number | null,
|
||||
transferTx: null as number | null,
|
||||
}));
|
||||
|
||||
// Loop WireGuard status
|
||||
const dump = await wg.dump(wgInterface.name);
|
||||
dump.forEach(
|
||||
({ publicKey, latestHandshakeAt, endpoint, transferRx, transferTx }) => {
|
||||
const client = clients.find((client) => client.publicKey === publicKey);
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.latestHandshakeAt = latestHandshakeAt;
|
||||
client.endpoint = endpoint;
|
||||
client.transferRx = transferRx;
|
||||
client.transferTx = transferTx;
|
||||
}
|
||||
);
|
||||
|
||||
return clients;
|
||||
}
|
||||
|
||||
async getAllClients() {
|
||||
const wgInterface = await Database.interfaces.get();
|
||||
const dbClients = await Database.clients.getAll();
|
||||
const clients = dbClients.map((client) => ({
|
||||
...client,
|
||||
latestHandshakeAt: null as Date | null,
|
||||
endpoint: null as string | null,
|
||||
transferRx: null as number | null,
|
||||
transferTx: null as number | null,
|
||||
}));
|
||||
|
||||
// Loop WireGuard status
|
||||
const dump = await wg.dump(wgInterface.name);
|
||||
dump.forEach(
|
||||
({ publicKey, latestHandshakeAt, endpoint, transferRx, transferTx }) => {
|
||||
const client = clients.find((client) => client.publicKey === publicKey);
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.latestHandshakeAt = latestHandshakeAt;
|
||||
client.endpoint = endpoint;
|
||||
client.transferRx = transferRx;
|
||||
client.transferTx = transferTx;
|
||||
}
|
||||
);
|
||||
|
||||
return clients;
|
||||
}
|
||||
|
||||
async getClientConfiguration({ clientId }: { clientId: ID }) {
|
||||
const wgInterface = await Database.interfaces.get();
|
||||
const userConfig = await Database.userConfigs.get();
|
||||
|
||||
const client = await Database.clients.get(clientId);
|
||||
|
||||
if (!client) {
|
||||
throw new Error('Client not found');
|
||||
}
|
||||
|
||||
return wg.generateClientConfig(wgInterface, userConfig, client);
|
||||
}
|
||||
|
||||
async getClientQRCodeSVG({ clientId }: { clientId: ID }) {
|
||||
const config = await this.getClientConfiguration({ clientId });
|
||||
return QRCode.toString(config, {
|
||||
type: 'svg',
|
||||
width: 512,
|
||||
});
|
||||
}
|
||||
|
||||
async Startup() {
|
||||
WG_DEBUG('Starting WireGuard...');
|
||||
// let as it has to refetch if keys change
|
||||
let wgInterface = await Database.interfaces.get();
|
||||
|
||||
// default interface has no keys
|
||||
if (
|
||||
wgInterface.privateKey === '---default---' &&
|
||||
wgInterface.publicKey === '---default---'
|
||||
) {
|
||||
WG_DEBUG('Generating new Wireguard Keys...');
|
||||
const privateKey = await wg.generatePrivateKey();
|
||||
const publicKey = await wg.getPublicKey(privateKey);
|
||||
|
||||
await Database.interfaces.updateKeyPair(privateKey, publicKey);
|
||||
wgInterface = await Database.interfaces.get();
|
||||
WG_DEBUG('New Wireguard Keys generated successfully.');
|
||||
}
|
||||
WG_DEBUG(`Starting Wireguard Interface ${wgInterface.name}...`);
|
||||
await this.#saveWireguardConfig(wgInterface);
|
||||
await wg.down(wgInterface.name).catch(() => {});
|
||||
await wg.up(wgInterface.name).catch((err) => {
|
||||
if (
|
||||
err &&
|
||||
err.message &&
|
||||
err.message.includes(`Cannot find device "${wgInterface.name}"`)
|
||||
) {
|
||||
throw new Error(
|
||||
`WireGuard exited with the error: Cannot find device "${wgInterface.name}"\nThis usually means that your host's kernel does not support WireGuard!`,
|
||||
{ cause: err.message }
|
||||
);
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
await this.#syncWireguardConfig(wgInterface);
|
||||
WG_DEBUG(`Wireguard Interface ${wgInterface.name} started successfully.`);
|
||||
|
||||
WG_DEBUG('Starting Cron Job...');
|
||||
await this.startCronJob();
|
||||
WG_DEBUG('Cron Job started successfully.');
|
||||
}
|
||||
|
||||
// TODO: handle as worker_thread
|
||||
async startCronJob() {
|
||||
setIntervalImmediately(() => {
|
||||
this.cronJob().catch((err) => {
|
||||
WG_DEBUG('Running Cron Job failed.');
|
||||
console.error(err);
|
||||
});
|
||||
}, 60 * 1000);
|
||||
}
|
||||
|
||||
// Shutdown wireguard
|
||||
async Shutdown() {
|
||||
const wgInterface = await Database.interfaces.get();
|
||||
await wg.down(wgInterface.name).catch(() => {});
|
||||
}
|
||||
|
||||
async cronJob() {
|
||||
const clients = await Database.clients.getAll();
|
||||
// Expires Feature
|
||||
for (const client of clients) {
|
||||
if (client.enabled !== true) continue;
|
||||
if (
|
||||
client.expiresAt !== null &&
|
||||
new Date() > new Date(client.expiresAt)
|
||||
) {
|
||||
WG_DEBUG(`Client ${client.id} expired.`);
|
||||
await Database.clients.toggle(client.id, false);
|
||||
}
|
||||
}
|
||||
// One Time Link Feature
|
||||
for (const client of clients) {
|
||||
if (
|
||||
client.oneTimeLink !== null &&
|
||||
new Date() > new Date(client.oneTimeLink.expiresAt)
|
||||
) {
|
||||
WG_DEBUG(`Client ${client.id} One Time Link expired.`);
|
||||
await Database.oneTimeLinks.delete(client.oneTimeLink.id);
|
||||
}
|
||||
}
|
||||
|
||||
await this.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
if (OLD_ENV.PASSWORD || OLD_ENV.PASSWORD_HASH) {
|
||||
// TODO: change url before release
|
||||
throw new Error(
|
||||
`
|
||||
You are using an invalid Configuration for wg-easy
|
||||
Please follow the instructions on https://wg-easy.github.io/wg-easy/ to migrate
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: make static or object
|
||||
|
||||
export default new WireGuard();
|
||||
32
src/server/utils/cmd.ts
Normal file
32
src/server/utils/cmd.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import childProcess from 'child_process';
|
||||
import debug from 'debug';
|
||||
|
||||
const CMD_DEBUG = debug('CMD');
|
||||
|
||||
export function exec(
|
||||
cmd: string,
|
||||
{ log }: { log: boolean | string } = { log: true }
|
||||
) {
|
||||
if (typeof log === 'string') {
|
||||
CMD_DEBUG(`$ ${log}`);
|
||||
} else if (log === true) {
|
||||
CMD_DEBUG(`$ ${cmd}`);
|
||||
}
|
||||
|
||||
if (process.platform !== 'linux') {
|
||||
return Promise.resolve('');
|
||||
}
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
childProcess.exec(
|
||||
cmd,
|
||||
{
|
||||
shell: 'bash',
|
||||
},
|
||||
(err, stdout) => {
|
||||
if (err) return reject(err);
|
||||
return resolve(String(stdout).trim());
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
20
src/server/utils/config.ts
Normal file
20
src/server/utils/config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import debug from 'debug';
|
||||
import packageJson from '@@/package.json';
|
||||
|
||||
export const RELEASE = 'v' + packageJson.version;
|
||||
|
||||
export const SERVER_DEBUG = debug('Server');
|
||||
|
||||
export const OLD_ENV = {
|
||||
/** @deprecated Only for migration purposes */
|
||||
PASSWORD: process.env.PASSWORD,
|
||||
/** @deprecated Only for migration purposes */
|
||||
PASSWORD_HASH: process.env.PASSWORD_HASH,
|
||||
};
|
||||
|
||||
export const WG_ENV = {
|
||||
/** UI is hosted on HTTP instead of HTTPS */
|
||||
INSECURE: process.env.INSECURE === 'true',
|
||||
};
|
||||
|
||||
console.log(WG_ENV);
|
||||
181
src/server/utils/handler.ts
Normal file
181
src/server/utils/handler.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import type { EventHandlerRequest, EventHandlerResponse, H3Event } from 'h3';
|
||||
import type { UserType } from '#db/repositories/user/types';
|
||||
import type { SetupStepType } from '#db/repositories/general/types';
|
||||
import {
|
||||
type Permissions,
|
||||
hasPermissionsWithData,
|
||||
} from '#shared/utils/permissions';
|
||||
|
||||
type PermissionHandler<
|
||||
TReq extends EventHandlerRequest,
|
||||
TRes extends EventHandlerResponse,
|
||||
Resource extends keyof Permissions,
|
||||
> = {
|
||||
(params: {
|
||||
event: H3Event<TReq>;
|
||||
user: UserType;
|
||||
/**
|
||||
* check if user has permissions to access the resource
|
||||
*
|
||||
* see: {@link hasPermissionsWithData}
|
||||
*/
|
||||
checkPermissions: (data?: Permissions[Resource]['dataType']) => true;
|
||||
}): TRes;
|
||||
};
|
||||
|
||||
/**
|
||||
* get current user
|
||||
*/
|
||||
export const definePermissionEventHandler = <
|
||||
TReq extends EventHandlerRequest,
|
||||
TRes extends EventHandlerResponse,
|
||||
Resource extends keyof Permissions,
|
||||
>(
|
||||
resource: Resource,
|
||||
action: Permissions[Resource]['action'],
|
||||
handler: PermissionHandler<TReq, TRes, Resource>
|
||||
) => {
|
||||
return defineEventHandler(async (event) => {
|
||||
const user = await getCurrentUser(event);
|
||||
|
||||
const permissions = hasPermissionsWithData(user, resource, action);
|
||||
|
||||
// if no data is required, check permissions
|
||||
if (permissions.isBoolean()) {
|
||||
permissions.check();
|
||||
}
|
||||
|
||||
const response = await handler({
|
||||
event,
|
||||
user,
|
||||
checkPermissions: permissions.check,
|
||||
});
|
||||
|
||||
// if data is required, make sure permissions were checked
|
||||
if (!permissions.checked) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Permission was not checked',
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
};
|
||||
|
||||
// which api route is allowed for each setup step
|
||||
// 0 is done, 1 is start
|
||||
// 3 means step 2 is done
|
||||
const ValidSetupSteps = {
|
||||
1: [2] as const,
|
||||
3: [4, 'migrate'] as const,
|
||||
} as const;
|
||||
|
||||
type ValidSteps =
|
||||
(typeof ValidSetupSteps)[keyof typeof ValidSetupSteps][number];
|
||||
|
||||
type SetupHandler<
|
||||
TReq extends EventHandlerRequest,
|
||||
TRes extends EventHandlerResponse,
|
||||
> = { (params: { event: H3Event<TReq>; setup: SetupStepType }): TRes };
|
||||
|
||||
/**
|
||||
* check if the setup is done, if not, run the handler
|
||||
*/
|
||||
export const defineSetupEventHandler = <
|
||||
TReq extends EventHandlerRequest,
|
||||
TRes extends EventHandlerResponse,
|
||||
>(
|
||||
step: ValidSteps,
|
||||
handler: SetupHandler<TReq, TRes>
|
||||
) => {
|
||||
return defineEventHandler(async (event) => {
|
||||
const setup = await Database.general.getSetupStep();
|
||||
|
||||
if (setup.done) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Invalid state',
|
||||
});
|
||||
}
|
||||
|
||||
const validSetupSteps =
|
||||
ValidSetupSteps[setup.step as keyof typeof ValidSetupSteps];
|
||||
|
||||
if (!validSetupSteps) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Invalid setup step',
|
||||
});
|
||||
}
|
||||
|
||||
if (!validSetupSteps.includes(step as never)) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Invalid step',
|
||||
});
|
||||
}
|
||||
|
||||
return await handler({ event, setup });
|
||||
});
|
||||
};
|
||||
|
||||
type Metrics = 'prometheus' | 'json';
|
||||
|
||||
type MetricsHandler<
|
||||
TReq extends EventHandlerRequest,
|
||||
TRes extends EventHandlerResponse,
|
||||
> = { (params: { event: H3Event<TReq> }): TRes };
|
||||
|
||||
/**
|
||||
* check if the metrics are enabled and the token is correct
|
||||
*/
|
||||
export const defineMetricsHandler = <
|
||||
TReq extends EventHandlerRequest,
|
||||
TRes extends EventHandlerResponse,
|
||||
>(
|
||||
type: Metrics,
|
||||
handler: MetricsHandler<TReq, TRes>
|
||||
) => {
|
||||
return defineEventHandler(async (event) => {
|
||||
const auth = getHeader(event, 'Authorization');
|
||||
|
||||
if (!auth) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Unauthorized',
|
||||
});
|
||||
}
|
||||
|
||||
const [method, value] = auth.split(' ');
|
||||
|
||||
if (method !== 'Bearer' || !value) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Bearer Auth required',
|
||||
});
|
||||
}
|
||||
|
||||
const metricsConfig = await Database.general.getMetricsConfig();
|
||||
|
||||
if (metricsConfig[type] !== true) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Metrics not enabled',
|
||||
});
|
||||
}
|
||||
|
||||
if (metricsConfig.password) {
|
||||
const tokenValid = await isPasswordValid(value, metricsConfig.password);
|
||||
|
||||
if (!tokenValid) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Incorrect token',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await handler({ event });
|
||||
});
|
||||
};
|
||||
33
src/server/utils/ip.ts
Normal file
33
src/server/utils/ip.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { parseCidr } from 'cidr-tools';
|
||||
import { stringifyIp } from 'ip-bigint';
|
||||
|
||||
import type { ClientNextIpType } from '#db/repositories/client/types';
|
||||
|
||||
type ParsedCidr = ReturnType<typeof parseCidr>;
|
||||
|
||||
export function nextIP(
|
||||
version: 4 | 6,
|
||||
cidr: ParsedCidr,
|
||||
clients: ClientNextIpType[]
|
||||
) {
|
||||
let address;
|
||||
for (let i = cidr.start + 2n; i <= cidr.end - 1n; i++) {
|
||||
const currentIp = stringifyIp({ number: i, version: version });
|
||||
const client = clients.find((client) => {
|
||||
return client[`ipv${version}Address`] === currentIp;
|
||||
});
|
||||
|
||||
if (!client) {
|
||||
address = currentIp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!address) {
|
||||
throw new Error('Maximum number of clients reached', {
|
||||
cause: `IPv${version} Address Pool exhausted`,
|
||||
});
|
||||
}
|
||||
|
||||
return address;
|
||||
}
|
||||
18
src/server/utils/password.ts
Normal file
18
src/server/utils/password.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import argon2 from 'argon2';
|
||||
|
||||
/**
|
||||
* Checks if `password` matches the hash.
|
||||
*/
|
||||
export function isPasswordValid(
|
||||
password: string,
|
||||
hash: string
|
||||
): Promise<boolean> {
|
||||
return argon2.verify(hash, password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashes a password.
|
||||
*/
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
return argon2.hash(password);
|
||||
}
|
||||
56
src/server/utils/release.ts
Normal file
56
src/server/utils/release.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
type GithubRelease = {
|
||||
tag_name: string;
|
||||
body: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Cache function for 1 hour
|
||||
*/
|
||||
function cacheFunction<T>(fn: () => T): () => T {
|
||||
let cache: { value: T; expiry: number } | null = null;
|
||||
|
||||
return (): T => {
|
||||
const now = Date.now();
|
||||
|
||||
if (cache && cache.expiry > now) {
|
||||
return cache.value;
|
||||
}
|
||||
|
||||
const result = fn();
|
||||
cache = {
|
||||
value: result,
|
||||
expiry: now + 3600000,
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchLatestRelease() {
|
||||
try {
|
||||
const response = await $fetch<GithubRelease>(
|
||||
'https://api.github.com/repos/wg-easy/wg-easy/releases/latest',
|
||||
{ method: 'get', timeout: 5000 }
|
||||
);
|
||||
if (!response) {
|
||||
throw new Error('Empty Response');
|
||||
}
|
||||
const changelog = response.body.split('\r\n\r\n')[0] ?? '';
|
||||
return {
|
||||
version: response.tag_name,
|
||||
changelog,
|
||||
};
|
||||
} catch (e) {
|
||||
SERVER_DEBUG('Failed to fetch latest releases: ', e);
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to fetch latest release',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch latest release from GitHub
|
||||
* @cache Response is cached for 1 hour
|
||||
*/
|
||||
export const cachedFetchLatestRelease = cacheFunction(fetchLatestRelease);
|
||||
111
src/server/utils/session.ts
Normal file
111
src/server/utils/session.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { H3Event } from 'h3';
|
||||
import type { UserType } from '#db/repositories/user/types';
|
||||
|
||||
export type WGSession = Partial<{
|
||||
userId: ID;
|
||||
}>;
|
||||
|
||||
const name = 'wg-easy';
|
||||
|
||||
export async function useWGSession(event: H3Event, rememberMe = false) {
|
||||
const sessionConfig = await Database.general.getSessionConfig();
|
||||
return useSession<WGSession>(event, {
|
||||
password: sessionConfig.sessionPassword,
|
||||
name,
|
||||
// TODO: add session expiration
|
||||
// maxAge: undefined
|
||||
cookie: {
|
||||
maxAge: rememberMe ? sessionConfig.sessionTimeout : undefined,
|
||||
secure: !WG_ENV.INSECURE,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getWGSession(event: H3Event) {
|
||||
const sessionConfig = await Database.general.getSessionConfig();
|
||||
return getSession<WGSession>(event, {
|
||||
password: sessionConfig.sessionPassword,
|
||||
name,
|
||||
cookie: {
|
||||
secure: !WG_ENV.INSECURE,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws
|
||||
*/
|
||||
export async function getCurrentUser(event: H3Event) {
|
||||
const session = await getWGSession(event);
|
||||
|
||||
const authorization = getHeader(event, 'Authorization');
|
||||
|
||||
let user: UserType | undefined = undefined;
|
||||
if (session.data.userId) {
|
||||
// Handle if authenticating using Session
|
||||
user = await Database.users.get(session.data.userId);
|
||||
} else if (authorization) {
|
||||
// Handle if authenticating using Header
|
||||
const [method, value] = authorization.split(' ');
|
||||
// Support Basic Authentication
|
||||
// TODO: support personal access token or similar
|
||||
if (method !== 'Basic' || !value) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Invalid Basic Authorization',
|
||||
});
|
||||
}
|
||||
|
||||
const basicValue = Buffer.from(value, 'base64').toString('utf-8');
|
||||
|
||||
// Split by first ":"
|
||||
const index = basicValue.indexOf(':');
|
||||
const username = basicValue.substring(0, index);
|
||||
const password = basicValue.substring(index + 1);
|
||||
|
||||
if (!username || !password) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Invalid Basic Authorization',
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: timing can be used to enumerate usernames
|
||||
|
||||
const foundUser = await Database.users.getByUsername(username);
|
||||
|
||||
if (!foundUser) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Session failed',
|
||||
});
|
||||
}
|
||||
|
||||
const userHashPassword = foundUser.password;
|
||||
const passwordValid = await isPasswordValid(password, userHashPassword);
|
||||
|
||||
if (!passwordValid) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Session failed',
|
||||
});
|
||||
}
|
||||
user = foundUser;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Session failed. User not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (!user.enabled) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'User is disabled',
|
||||
});
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
26
src/server/utils/template.ts
Normal file
26
src/server/utils/template.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { InterfaceType } from '#db/repositories/interface/types';
|
||||
|
||||
/**
|
||||
* Replace all {{key}} in the template with the values[key]
|
||||
*/
|
||||
export function template(templ: string, values: Record<string, string>) {
|
||||
return templ.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
||||
return values[key] !== undefined ? values[key] : match;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Available keys:
|
||||
* - ipv4Cidr: IPv4 CIDR
|
||||
* - ipv6Cidr: IPv6 CIDR
|
||||
* - device: Network device
|
||||
* - port: Port number
|
||||
*/
|
||||
export function iptablesTemplate(templ: string, wgInterface: InterfaceType) {
|
||||
return template(templ, {
|
||||
ipv4Cidr: wgInterface.ipv4Cidr,
|
||||
ipv6Cidr: wgInterface.ipv6Cidr,
|
||||
device: wgInterface.device,
|
||||
port: wgInterface.port.toString(),
|
||||
});
|
||||
}
|
||||
145
src/server/utils/types.ts
Normal file
145
src/server/utils/types.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import type { ZodSchema } from 'zod';
|
||||
import z from 'zod';
|
||||
import type { H3Event, EventHandlerRequest } from 'h3';
|
||||
|
||||
export type ID = number;
|
||||
|
||||
/**
|
||||
* return the string as is
|
||||
*
|
||||
* used for i18n ally
|
||||
*/
|
||||
export const t = (v: string) => v;
|
||||
|
||||
export const safeStringRefine = z
|
||||
.string()
|
||||
.refine(
|
||||
(v) => v !== '__proto__' && v !== 'constructor' && v !== 'prototype',
|
||||
{ message: t('zod.stringMalformed') }
|
||||
);
|
||||
|
||||
export const EnabledSchema = z.boolean({ message: t('zod.enabled') });
|
||||
|
||||
export const MtuSchema = z
|
||||
.number({ message: t('zod.mtu') })
|
||||
.min(1280, { message: t('zod.mtu') })
|
||||
.max(9000, { message: t('zod.mtu') });
|
||||
|
||||
export const PortSchema = z
|
||||
.number({ message: t('zod.port') })
|
||||
.min(1, { message: t('zod.port') })
|
||||
.max(65535, { message: t('zod.port') });
|
||||
|
||||
export const PersistentKeepaliveSchema = z
|
||||
.number({ message: t('zod.persistentKeepalive') })
|
||||
.min(0, t('zod.persistentKeepalive'))
|
||||
.max(65535, t('zod.persistentKeepalive'));
|
||||
|
||||
export const AddressSchema = z
|
||||
.string({ message: t('zod.address') })
|
||||
.min(1, { message: t('zod.address') })
|
||||
.pipe(safeStringRefine);
|
||||
|
||||
export const DnsSchema = z
|
||||
.array(AddressSchema, { message: t('zod.dns') })
|
||||
.min(1, t('zod.dns'));
|
||||
|
||||
export const AllowedIpsSchema = z
|
||||
.array(AddressSchema, { message: t('zod.allowedIps') })
|
||||
.min(1, { message: t('zod.allowedIps') });
|
||||
|
||||
export const FileSchema = z.object({
|
||||
file: z.string({ message: t('zod.file') }),
|
||||
});
|
||||
|
||||
export const schemaForType =
|
||||
<T>() =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
<S extends z.ZodType<T, any, any>>(arg: S) => {
|
||||
return arg;
|
||||
};
|
||||
|
||||
export function validateZod<T>(
|
||||
schema: ZodSchema<T>,
|
||||
event: H3Event<EventHandlerRequest>
|
||||
) {
|
||||
return async (data: unknown) => {
|
||||
try {
|
||||
return await schema.parseAsync(data);
|
||||
} catch (error) {
|
||||
let message = 'Unexpected Error';
|
||||
if (error instanceof z.ZodError) {
|
||||
const t = await useTranslation(event);
|
||||
|
||||
message = error.issues
|
||||
.map((v) => {
|
||||
let m = v.message;
|
||||
|
||||
if (t) {
|
||||
let newMessage = null;
|
||||
if (v.message.startsWith('zod.')) {
|
||||
switch (v.code) {
|
||||
case 'too_small':
|
||||
switch (v.type) {
|
||||
case 'string':
|
||||
newMessage = t('zod.generic.stringMin', [
|
||||
t(v.message),
|
||||
v.minimum,
|
||||
]);
|
||||
break;
|
||||
case 'number':
|
||||
newMessage = t('zod.generic.numberMin', [
|
||||
t(v.message),
|
||||
v.minimum,
|
||||
]);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'invalid_type': {
|
||||
if (v.received === 'null' || v.received === 'undefined') {
|
||||
newMessage = t('zod.generic.required', [
|
||||
v.path.join('.'),
|
||||
]);
|
||||
} else {
|
||||
switch (v.expected) {
|
||||
case 'string':
|
||||
newMessage = t('zod.generic.validString', [
|
||||
t(v.message),
|
||||
]);
|
||||
break;
|
||||
case 'boolean':
|
||||
newMessage = t('zod.generic.validBoolean', [
|
||||
t(v.message),
|
||||
]);
|
||||
break;
|
||||
case 'number':
|
||||
newMessage = t('zod.generic.validNumber', [
|
||||
t(v.message),
|
||||
]);
|
||||
break;
|
||||
case 'array':
|
||||
newMessage = t('zod.generic.validArray', [
|
||||
t(v.message),
|
||||
]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newMessage) {
|
||||
m = newMessage;
|
||||
} else {
|
||||
m = t(v.message);
|
||||
}
|
||||
}
|
||||
|
||||
return m;
|
||||
})
|
||||
.join('; ');
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
};
|
||||
}
|
||||
140
src/server/utils/wgHelper.ts
Normal file
140
src/server/utils/wgHelper.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { parseCidr } from 'cidr-tools';
|
||||
import { stringifyIp } from 'ip-bigint';
|
||||
import type { ClientType } from '#db/repositories/client/types';
|
||||
import type { InterfaceType } from '#db/repositories/interface/types';
|
||||
import type { UserConfigType } from '#db/repositories/userConfig/types';
|
||||
import type { HooksType } from '#db/repositories/hooks/types';
|
||||
|
||||
export const wg = {
|
||||
generateServerPeer: (client: Omit<ClientType, 'createdAt' | 'updatedAt'>) => {
|
||||
const allowedIps = [
|
||||
`${client.ipv4Address}/32`,
|
||||
`${client.ipv6Address}/128`,
|
||||
...(client.serverAllowedIps ?? []),
|
||||
];
|
||||
|
||||
return `# Client: ${client.name} (${client.id})
|
||||
[Peer]
|
||||
PublicKey = ${client.publicKey}
|
||||
PresharedKey = ${client.preSharedKey}
|
||||
AllowedIPs = ${allowedIps.join(', ')}`;
|
||||
},
|
||||
|
||||
generateServerInterface: (wgInterface: InterfaceType, hooks: HooksType) => {
|
||||
const cidr4 = parseCidr(wgInterface.ipv4Cidr);
|
||||
const cidr6 = parseCidr(wgInterface.ipv6Cidr);
|
||||
const ipv4Addr = stringifyIp({ number: cidr4.start + 1n, version: 4 });
|
||||
const ipv6Addr = stringifyIp({ number: cidr6.start + 1n, version: 6 });
|
||||
|
||||
return `# Note: Do not edit this file directly.
|
||||
# Your changes will be overwritten!
|
||||
|
||||
# Server
|
||||
[Interface]
|
||||
PrivateKey = ${wgInterface.privateKey}
|
||||
Address = ${ipv4Addr}/${cidr4.prefix}, ${ipv6Addr}/${cidr6.prefix}
|
||||
ListenPort = ${wgInterface.port}
|
||||
MTU = ${wgInterface.mtu}
|
||||
PreUp = ${iptablesTemplate(hooks.preUp, wgInterface)}
|
||||
PostUp = ${iptablesTemplate(hooks.postUp, wgInterface)}
|
||||
PreDown = ${iptablesTemplate(hooks.preDown, wgInterface)}
|
||||
PostDown = ${iptablesTemplate(hooks.postDown, wgInterface)}`;
|
||||
},
|
||||
|
||||
generateClientConfig: (
|
||||
wgInterface: InterfaceType,
|
||||
userConfig: UserConfigType,
|
||||
client: ClientType
|
||||
) => {
|
||||
const cidr4Block = parseCidr(wgInterface.ipv4Cidr).prefix;
|
||||
const cidr6Block = parseCidr(wgInterface.ipv6Cidr).prefix;
|
||||
|
||||
return `[Interface]
|
||||
PrivateKey = ${client.privateKey}
|
||||
Address = ${client.ipv4Address}/${cidr4Block}, ${client.ipv6Address}/${cidr6Block}
|
||||
DNS = ${client.dns.join(', ')}
|
||||
MTU = ${client.mtu}
|
||||
|
||||
[Peer]
|
||||
PublicKey = ${wgInterface.publicKey}
|
||||
PresharedKey = ${client.preSharedKey}
|
||||
AllowedIPs = ${client.allowedIps.join(', ')}
|
||||
PersistentKeepalive = ${client.persistentKeepalive}
|
||||
Endpoint = ${userConfig.host}:${userConfig.port}`;
|
||||
},
|
||||
|
||||
generatePrivateKey: () => {
|
||||
return exec('wg genkey');
|
||||
},
|
||||
|
||||
getPublicKey: (privateKey: string) => {
|
||||
return exec(`echo ${privateKey} | wg pubkey`, {
|
||||
log: 'echo ***hidden*** | wg pubkey',
|
||||
});
|
||||
},
|
||||
|
||||
generatePreSharedKey: () => {
|
||||
return exec('wg genpsk');
|
||||
},
|
||||
|
||||
up: (infName: string) => {
|
||||
return exec(`wg-quick up ${infName}`);
|
||||
},
|
||||
|
||||
down: (infName: string) => {
|
||||
return exec(`wg-quick down ${infName}`);
|
||||
},
|
||||
|
||||
sync: (infName: string) => {
|
||||
return exec(`wg syncconf ${infName} <(wg-quick strip ${infName})`);
|
||||
},
|
||||
|
||||
dump: async (infName: string) => {
|
||||
const rawDump = await exec(`wg show ${infName} dump`, {
|
||||
log: false,
|
||||
});
|
||||
|
||||
type wgDumpLine = [
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
];
|
||||
|
||||
return rawDump
|
||||
.trim()
|
||||
.split('\n')
|
||||
.slice(1)
|
||||
.map((line) => {
|
||||
const splitLines = line.split('\t');
|
||||
const [
|
||||
publicKey,
|
||||
preSharedKey,
|
||||
endpoint,
|
||||
allowedIps,
|
||||
latestHandshakeAt,
|
||||
transferRx,
|
||||
transferTx,
|
||||
persistentKeepalive,
|
||||
] = splitLines as wgDumpLine;
|
||||
|
||||
return {
|
||||
publicKey,
|
||||
preSharedKey,
|
||||
endpoint: endpoint === '(none)' ? null : endpoint,
|
||||
allowedIps,
|
||||
latestHandshakeAt:
|
||||
latestHandshakeAt === '0'
|
||||
? null
|
||||
: new Date(Number.parseInt(`${latestHandshakeAt}000`)),
|
||||
transferRx: Number.parseInt(transferRx),
|
||||
transferTx: Number.parseInt(transferTx),
|
||||
persistentKeepalive: persistentKeepalive,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user