Feat: Initial Setup through env vars (#1736)

* initial support for initial setup

* improve setup

* improve mobile view

* move base admin route

* admin panel mobile view

* set initial host and port

* add docs

* properly setup everything, use for dev env

* change userconfig and interface port on setup, note users afterwards
This commit is contained in:
Bernd Storath
2025-03-13 11:28:05 +01:00
committed by GitHub
parent 4890bb28e5
commit 86bdbe4c3d
26 changed files with 277 additions and 129 deletions

View File

@@ -47,6 +47,7 @@ ENV DEBUG=Server,WireGuard,Database,CMD
ENV PORT=51821 ENV PORT=51821
ENV HOST=0.0.0.0 ENV HOST=0.0.0.0
ENV INSECURE=false ENV INSECURE=false
ENV INIT_ENABLED=false
LABEL org.opencontainers.image.source=https://github.com/wg-easy/wg-easy LABEL org.opencontainers.image.source=https://github.com/wg-easy/wg-easy

View File

@@ -26,7 +26,8 @@ RUN update-alternatives --install /usr/sbin/ip6tables ip6tables /usr/sbin/ip6tab
ENV DEBUG=Server,WireGuard,Database,CMD ENV DEBUG=Server,WireGuard,Database,CMD
ENV PORT=51821 ENV PORT=51821
ENV HOST=0.0.0.0 ENV HOST=0.0.0.0
ENV INSECURE=false ENV INSECURE=true
ENV INIT_ENABLED=false
# Install Dependencies # Install Dependencies
COPY src/package.json src/pnpm-lock.yaml ./ COPY src/package.json src/pnpm-lock.yaml ./

View File

@@ -15,6 +15,12 @@ services:
cap_add: cap_add:
- NET_ADMIN - NET_ADMIN
- SYS_MODULE - SYS_MODULE
environment:
- INIT_ENABLED=true
- INIT_HOST=test
- INIT_PORT=51820
- INIT_USERNAME=testtest
- INIT_PASSWORD=Qweasdyxcv!2
# folders should be generated inside container # folders should be generated inside container
volumes: volumes:

View File

@@ -0,0 +1,32 @@
---
title: Unattended Setup
---
If you want to run the setup without any user interaction, e.g. with a tool like Ansible, you can use these environment variables to configure the setup.
These will only be used during the first start of the container. After that, the setup will be disabled.
| Env | Example | Description | Group |
| ---------------- | ----------------- | --------------------------------------------------------- | ----- |
| `INIT_ENABLED` | `true` | Enables the below env vars | 0 |
| `INIT_USERNAME` | `admin` | Sets admin username | 1 |
| `INIT_PASSWORD` | `Se!ureP%ssw` | Sets admin password | 1 |
| `INIT_HOST` | `vpn.example.com` | Host clients will connect to | 1 |
| `INIT_PORT` | `51820` | Port clients will connect to and wireguard will listen on | 1 |
| `INIT_DNS` | `1.1.1.1,8.8.8.8` | Sets global dns setting | 2 |
| `INIT_IPV4_CIDR` | `10.8.0.0/24` | Sets IPv4 cidr | 3 |
| `INIT_IPV6_CIDR` | `2001:0DB8::/32` | Sets IPv6 cidr | 3 |
/// warning | Variables have to be used together
If variables are in the same group, you have to set all of them. For example, if you set `INIT_IPV4_CIDR`, you also have to set `INIT_IPV6_CIDR`.
If you want to skip the setup process, you have to configure group `1`
///
/// note | Security
The initial username and password is not checked for complexity. Make sure to set a long enough username and a secure password. Otherwise, the user won't be able to log in.
Its recommended to remove the variables after the setup is done to prevent the password from being exposed.
///

View File

@@ -1,14 +1,17 @@
<template> <template>
<TooltipProvider> <TooltipProvider>
<TooltipRoot> <TooltipRoot :open="open" @update:open="open = $event">
<TooltipTrigger <TooltipTrigger
class="inline-flex h-8 w-8 items-center justify-center rounded-full text-gray-400 outline-none focus:shadow-sm focus:shadow-black" class="mx-2 inline-flex h-4 w-4 items-center justify-center rounded-full text-gray-400 outline-none focus:shadow-sm focus:shadow-black"
as-child
> >
<slot /> <button @click="open = !open">
<slot />
</button>
</TooltipTrigger> </TooltipTrigger>
<TooltipPortal> <TooltipPortal>
<TooltipContent <TooltipContent
class="select-none rounded bg-gray-600 px-3 py-2 text-sm leading-none text-white shadow-lg will-change-[transform,opacity]" class="select-none whitespace-pre-line rounded bg-gray-600 px-3 py-2 text-sm leading-none text-white shadow-lg will-change-[transform,opacity]"
:side-offset="5" :side-offset="5"
> >
{{ text }} {{ text }}
@@ -21,4 +24,6 @@
<script lang="ts" setup> <script lang="ts" setup>
defineProps<{ text: string }>(); defineProps<{ text: string }>();
const open = ref(false);
</script> </script>

View File

@@ -1,5 +1,5 @@
<template> <template>
<section class="grid grid-cols-1 gap-4 md:grid-cols-2"> <section class="grid grid-cols-2 gap-4">
<slot /> <slot />
<Separator <Separator
decorative decorative

View File

@@ -1,5 +1,5 @@
<template> <template>
<NuxtLink to="/" class="mb-4 flex-grow self-start"> <NuxtLink to="/" class="mb-4">
<h1 class="text-4xl font-medium dark:text-neutral-200"> <h1 class="text-4xl font-medium dark:text-neutral-200">
<img <img
src="/logo.png" src="/logo.png"

View File

@@ -1,6 +1,10 @@
<template> <template>
<div <div
v-if="globalStore.release?.updateAvailable" v-if="
globalStore.release?.updateAvailable &&
authStore.userData &&
hasPermissions(authStore.userData, 'admin', 'any')
"
class="font-small mb-10 rounded-md bg-red-800 p-4 text-sm text-white shadow-lg dark:bg-red-100 dark:text-red-600" class="font-small mb-10 rounded-md bg-red-800 p-4 text-sm text-white shadow-lg dark:bg-red-100 dark:text-red-600"
:title="`v${globalStore.release.currentRelease} → v${globalStore.release.latestRelease.version}`" :title="`v${globalStore.release.currentRelease} → v${globalStore.release.latestRelease.version}`"
> >
@@ -23,6 +27,5 @@
<script lang="ts" setup> <script lang="ts" setup>
const globalStore = useGlobalStore(); const globalStore = useGlobalStore();
const authStore = useAuthStore();
// TODO: only show this to admins
</script> </script>

View File

@@ -1,23 +1,23 @@
<template> <template>
<div> <div>
<header class="container mx-auto mt-4 max-w-3xl px-3 xs:mt-6 md:px-0"> <header class="mx-auto mt-4 flex max-w-3xl flex-col justify-center">
<div <div
class="mb-5" class="mb-5 w-full"
:class=" :class="
loggedIn loggedIn
? 'flex flex-auto flex-col-reverse items-center gap-3 xxs:flex-row' ? 'flex flex-col items-center justify-between sm:flex-row'
: 'flex justify-end' : 'flex justify-end'
" "
> >
<HeaderLogo v-if="loggedIn" /> <HeaderLogo v-if="loggedIn" />
<div class="flex grow-0 items-center gap-3 self-end xxs:self-center"> <div class="flex flex-row gap-3">
<HeaderLangSelector /> <HeaderLangSelector />
<HeaderThemeSwitch /> <HeaderThemeSwitch />
<HeaderChartToggle v-if="loggedIn" /> <HeaderChartToggle v-if="loggedIn" />
<UiUserMenu v-if="loggedIn" /> <UiUserMenu v-if="loggedIn" />
</div> </div>
</div> </div>
<HeaderUpdate class="mt-5" /> <HeaderUpdate class="mt-4" />
</header> </header>
<slot /> <slot />
<UiFooter /> <UiFooter />

View File

@@ -11,8 +11,8 @@
</header> </header>
<main> <main>
<Panel> <Panel>
<PanelBody class="mx-auto mt-10 p-4 md:w-[70%] lg:w-[60%]"> <PanelBody class="m-4 mx-auto mt-10 md:w-[70%] lg:w-[60%]">
<h2 class="mb-16 mt-8 text-3xl font-medium"> <h2 class="mb-16 mt-8 text-center text-3xl font-medium">
{{ $t('setup.welcome') }} {{ $t('setup.welcome') }}
</h2> </h2>

View File

@@ -1,8 +1,8 @@
<template> <template>
<div> <div>
<div class="container mx-auto p-4"> <div class="container mx-auto p-4">
<div class="flex"> <div class="flex flex-col gap-4 lg:flex-row">
<div class="mr-4 w-64 rounded-lg bg-white p-4 dark:bg-neutral-700"> <div class="rounded-lg bg-white p-4 lg:w-64 dark:bg-neutral-700">
<NuxtLink to="/admin"> <NuxtLink to="/admin">
<h2 class="mb-4 text-xl font-bold dark:text-neutral-200"> <h2 class="mb-4 text-xl font-bold dark:text-neutral-200">
{{ t('pages.admin.panel') }} {{ t('pages.admin.panel') }}
@@ -13,6 +13,7 @@
v-for="(item, index) in menuItems" v-for="(item, index) in menuItems"
:key="index" :key="index"
:to="`/admin/${item.id}`" :to="`/admin/${item.id}`"
active-class="bg-red-800 rounded"
> >
<BaseButton <BaseButton
as="span" as="span"
@@ -27,7 +28,7 @@
<div <div
class="flex-1 rounded-lg bg-white p-6 dark:bg-neutral-700 dark:text-neutral-200" class="flex-1 rounded-lg bg-white p-6 dark:bg-neutral-700 dark:text-neutral-200"
> >
<h1 class="mb-6 text-3xl font-bold">{{ activeMenuItem?.name }}</h1> <h1 class="mb-6 text-3xl font-bold">{{ activeMenuItem.name }}</h1>
<NuxtPage /> <NuxtPage />
</div> </div>
</div> </div>
@@ -44,13 +45,17 @@ const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const menuItems = [ const menuItems = [
{ id: '', name: t('pages.admin.general') }, { id: 'general', name: t('pages.admin.general') },
{ id: 'config', name: t('pages.admin.config') }, { id: 'config', name: t('pages.admin.config') },
{ id: 'interface', name: t('pages.admin.interface') }, { id: 'interface', name: t('pages.admin.interface') },
{ id: 'hooks', name: t('pages.admin.hooks') }, { id: 'hooks', name: t('pages.admin.hooks') },
]; ];
const defaultItem = { id: '', name: t('pages.admin.panel') };
const activeMenuItem = computed(() => { const activeMenuItem = computed(() => {
return menuItems.find((item) => route.path === `/admin/${item.id}`); return (
menuItems.find((item) => route.path === `/admin/${item.id}`) ?? defaultItem
);
}); });
</script> </script>

View File

@@ -0,0 +1,64 @@
<template>
<main v-if="data">
<FormElement @submit.prevent="submit">
<FormGroup>
<FormNumberField
id="session"
v-model="data.sessionTimeout"
:label="$t('admin.general.sessionTimeout')"
:description="$t('admin.general.sessionTimeoutDesc')"
/>
</FormGroup>
<FormGroup>
<FormHeading>{{ $t('admin.general.metrics') }}</FormHeading>
<FormNullTextField
id="password"
v-model="data.metricsPassword"
:label="$t('admin.general.metricsPassword')"
:description="$t('admin.general.metricsPasswordDesc')"
/>
<FormSwitchField
id="prometheus"
v-model="data.metricsPrometheus"
:label="$t('admin.general.prometheus')"
:description="$t('admin.general.prometheusDesc')"
/>
<FormSwitchField
id="json"
v-model="data.metricsJson"
:label="$t('admin.general.json')"
:description="$t('admin.general.jsonDesc')"
/>
</FormGroup>
<FormGroup>
<FormHeading>{{ $t('form.actions') }}</FormHeading>
<FormActionField type="submit" :label="$t('form.save')" />
<FormActionField :label="$t('form.revert')" @click="revert" />
</FormGroup>
</FormElement>
</main>
</template>
<script setup lang="ts">
const { data: _data, refresh } = await useFetch(`/api/admin/general`, {
method: 'get',
});
const data = toRef(_data.value);
const _submit = useSubmit(
`/api/admin/general`,
{
method: 'post',
},
{ revert }
);
function submit() {
return _submit(data.value);
}
async function revert() {
await refresh();
data.value = toRef(_data.value).value;
}
</script>

View File

@@ -1,64 +1,5 @@
<template> <template>
<main v-if="data"> <main class="flex flex-col gap-3">
<FormElement @submit.prevent="submit"> <p class="whitespace-pre-line">{{ $t('admin.introText') }}</p>
<FormGroup>
<FormNumberField
id="session"
v-model="data.sessionTimeout"
:label="$t('admin.general.sessionTimeout')"
:description="$t('admin.general.sessionTimeoutDesc')"
/>
</FormGroup>
<FormGroup>
<FormHeading>{{ $t('admin.general.metrics') }}</FormHeading>
<FormNullTextField
id="password"
v-model="data.metricsPassword"
:label="$t('admin.general.metricsPassword')"
:description="$t('admin.general.metricsPasswordDesc')"
/>
<FormSwitchField
id="prometheus"
v-model="data.metricsPrometheus"
:label="$t('admin.general.prometheus')"
:description="$t('admin.general.prometheusDesc')"
/>
<FormSwitchField
id="json"
v-model="data.metricsJson"
:label="$t('admin.general.json')"
:description="$t('admin.general.jsonDesc')"
/>
</FormGroup>
<FormGroup>
<FormHeading>{{ $t('form.actions') }}</FormHeading>
<FormActionField type="submit" :label="$t('form.save')" />
<FormActionField :label="$t('form.revert')" @click="revert" />
</FormGroup>
</FormElement>
</main> </main>
</template> </template>
<script setup lang="ts">
const { data: _data, refresh } = await useFetch(`/api/admin/general`, {
method: 'get',
});
const data = toRef(_data.value);
const _submit = useSubmit(
`/api/admin/general`,
{
method: 'post',
},
{ revert }
);
function submit() {
return _submit(data.value);
}
async function revert() {
await refresh();
data.value = toRef(_data.value).value;
}
</script>

View File

@@ -54,6 +54,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const authStore = useAuthStore();
authStore.update();
const authenticating = ref(false); const authenticating = ref(false);
const remember = ref(false); const remember = ref(false);
const username = ref<null | string>(null); const username = ref<null | string>(null);

View File

@@ -1,9 +1,9 @@
<template> <template>
<div> <div class="flex flex-col items-center">
<p class="px-8 pt-8 text-center text-2xl"> <p class="px-8 text-center text-2xl">
{{ $t('setup.welcomeDesc') }} {{ $t('setup.welcomeDesc') }}
</p> </p>
<NuxtLink to="/setup/2"> <NuxtLink to="/setup/2" class="mt-8">
<BaseButton as="span">{{ $t('general.continue') }}</BaseButton> <BaseButton as="span">{{ $t('general.continue') }}</BaseButton>
</NuxtLink> </NuxtLink>
</div> </div>

View File

@@ -1,9 +1,9 @@
<template> <template>
<div> <div>
<p class="p-8 text-center text-lg"> <p class="text-center text-lg">
{{ $t('setup.createAdminDesc') }} {{ $t('setup.createAdminDesc') }}
</p> </p>
<div class="flex flex-col gap-3"> <div class="mt-8 flex flex-col gap-3">
<div class="flex flex-col"> <div class="flex flex-col">
<FormNullTextField <FormNullTextField
id="username" id="username"
@@ -28,7 +28,7 @@
:label="$t('general.confirmPassword')" :label="$t('general.confirmPassword')"
/> />
</div> </div>
<div> <div class="mt-4 flex justify-center">
<BaseButton @click="submit">{{ $t('setup.createAccount') }}</BaseButton> <BaseButton @click="submit">{{ $t('setup.createAccount') }}</BaseButton>
</div> </div>
</div> </div>

View File

@@ -1,14 +1,18 @@
<template> <template>
<div> <div>
<p class="p-8 text-center text-lg"> <p class="text-center text-lg">
{{ $t('setup.existingSetup') }} {{ $t('setup.existingSetup') }}
</p> </p>
<div class="mb-8 flex justify-center"> <div class="mt-4 flex justify-center gap-3">
<NuxtLink to="/setup/4"> <NuxtLink to="/setup/4" class="w-20">
<BaseButton as="span">{{ $t('general.no') }}</BaseButton> <BaseButton as="span" class="w-full justify-center">
{{ $t('general.no') }}
</BaseButton>
</NuxtLink> </NuxtLink>
<NuxtLink to="/setup/migrate"> <NuxtLink to="/setup/migrate" class="w-20">
<BaseButton as="span">{{ $t('general.yes') }}</BaseButton> <BaseButton as="span" class="w-full justify-center">
{{ $t('general.yes') }}
</BaseButton>
</NuxtLink> </NuxtLink>
</div> </div>
</div> </div>

View File

@@ -1,21 +1,27 @@
<template> <template>
<div> <div>
<p class="p-8 text-center text-lg"> <p class="text-center text-lg">
{{ $t('setup.setupConfigDesc') }} {{ $t('setup.setupConfigDesc') }}
</p> </p>
<div class="flex flex-col gap-3"> <div class="mt-8 flex flex-col gap-3">
<div class="flex flex-col"> <div class="flex flex-col">
<FormNullTextField <FormNullTextField
id="host" id="host"
v-model="host" v-model="host"
:label="$t('general.host')" :label="$t('general.host')"
placeholder="vpn.example.com" placeholder="vpn.example.com"
:description="$t('setup.hostDesc')"
/> />
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<FormNumberField id="port" v-model="port" :label="$t('general.port')" /> <FormNumberField
id="port"
v-model="port"
:label="$t('general.port')"
:description="$t('setup.portDesc')"
/>
</div> </div>
<div> <div class="mt-4 flex justify-center">
<BaseButton @click="submit">{{ $t('general.continue') }}</BaseButton> <BaseButton @click="submit">{{ $t('general.continue') }}</BaseButton>
</div> </div>
</div> </div>

View File

@@ -1,13 +1,15 @@
<template> <template>
<div> <div class="flex flex-col items-center">
<p class="p-8 text-center text-lg"> <p class="text-center text-lg">
{{ $t('setup.setupMigrationDesc') }} {{ $t('setup.setupMigrationDesc') }}
</p> </p>
<div> <div class="mt-8 flex gap-3">
<Label for="migration">{{ $t('setup.migration') }}</Label> <Label for="migration">{{ $t('setup.migration') }}</Label>
<input id="migration" type="file" @change="onChangeFile" /> <input id="migration" type="file" @change="onChangeFile" />
</div> </div>
<BaseButton @click="submit">{{ $t('setup.upload') }}</BaseButton> <div class="mt-4">
<BaseButton @click="submit">{{ $t('setup.upload') }}</BaseButton>
</div>
</div> </div>
</template> </template>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div> <div class="flex flex-col items-center">
<p>{{ $t('setup.successful') }}</p> <p>{{ $t('setup.successful') }}</p>
<NuxtLink to="/login"> <NuxtLink to="/login" class="mt-4">
<BaseButton as="span">{{ $t('login.signIn') }}</BaseButton> <BaseButton as="span">{{ $t('login.signIn') }}</BaseButton>
</NuxtLink> </NuxtLink>
</div> </div>

View File

@@ -35,16 +35,18 @@
"confirmPassword": "Confirm Password" "confirmPassword": "Confirm Password"
}, },
"setup": { "setup": {
"welcome": "Welcome to your first setup of wg-easy !", "welcome": "Welcome to your first setup of wg-easy",
"welcomeDesc": "You have found the easiest way to install and manage WireGuard on any Linux host!", "welcomeDesc": "You have found the easiest way to install and manage WireGuard on any Linux host",
"existingSetup": "Do you have an existing setup?", "existingSetup": "Do you have an existing setup?",
"createAdminDesc": "Please first enter an admin username and a strong secure password. This information will be used to log in to your administration panel.", "createAdminDesc": "Please first enter an admin username and a strong secure password. This information will be used to log in to your administration panel.",
"setupConfigDesc": "Please enter the host and port information. This will be used for the client configuration when setting up WireGuard on their devices.", "setupConfigDesc": "Please enter the host and port information. This will be used for the client configuration when setting up WireGuard on their devices.",
"setupMigrationDesc": "Please provide the backup file if you want to migrate your data from your previous wg-easy version to your new setup.", "setupMigrationDesc": "Please provide the backup file if you want to migrate your data from your previous wg-easy version to your new setup.",
"upload": "Upload", "upload": "Upload",
"migration": "Restore the backup", "migration": "Restore the backup:",
"createAccount": "Create Account", "createAccount": "Create Account",
"successful": "Setup successful" "successful": "Setup successful",
"hostDesc": "Public hostname clients will connect to",
"portDesc": "Public UDP port clients will connect to and WireGuard will listen on"
}, },
"update": { "update": {
"updateAvailable": "There is an update available!", "updateAvailable": "There is an update available!",
@@ -141,7 +143,7 @@
"config": { "config": {
"connection": "Connection", "connection": "Connection",
"hostDesc": "Public hostname clients will connect to (invalidates config)", "hostDesc": "Public hostname clients will connect to (invalidates config)",
"portDesc": "Public UDP port clients will connect to (invalidates config)", "portDesc": "Public UDP port clients will connect to (invalidates config, you probably want to change Interface Port too)",
"allowedIpsDesc": "Allowed IPs clients will use (global config)", "allowedIpsDesc": "Allowed IPs clients will use (global config)",
"dnsDesc": "DNS server clients will use (global config)", "dnsDesc": "DNS server clients will use (global config)",
"mtuDesc": "MTU clients will use (only for new clients)", "mtuDesc": "MTU clients will use (only for new clients)",
@@ -153,9 +155,10 @@
"device": "Device", "device": "Device",
"deviceDesc": "Ethernet device the wireguard traffic should be forwarded through", "deviceDesc": "Ethernet device the wireguard traffic should be forwarded through",
"mtuDesc": "MTU WireGuard will use", "mtuDesc": "MTU WireGuard will use",
"portDesc": "UDP Port WireGuard will listen on (could invalidate config)", "portDesc": "UDP Port WireGuard will listen on (you probably want to change Config Port too)",
"changeCidr": "Change CIDR" "changeCidr": "Change CIDR"
} },
"introText": "Welcome to the admin panel.\n\nHere you can manage the general settings, the configuration, the interface settings and the hooks.\n\nStart by choosing one of the sections in the sidebar."
}, },
"zod": { "zod": {
"generic": { "generic": {

View File

@@ -28,7 +28,9 @@ export default defineNuxtConfig({
}, },
locales: [ locales: [
{ {
// same as i18n.config.ts
code: 'en', code: 'en',
// BCP 47 language tag
language: 'en-US', language: 'en-US',
name: 'English', name: 'English',
}, },

View File

@@ -2,11 +2,10 @@ export default defineEventHandler(async (event) => {
const session = await useWGSession(event); const session = await useWGSession(event);
if (!session.data.userId) { if (!session.data.userId) {
throw createError({ // not logged in
statusCode: 401, return null;
statusMessage: 'Not logged in',
});
} }
const user = await Database.users.get(session.data.userId); const user = await Database.users.get(session.data.userId);
if (!user) { if (!user) {
throw createError({ throw createError({

View File

@@ -1,6 +1,7 @@
import { eq, sql } from 'drizzle-orm'; import { eq, sql } from 'drizzle-orm';
import { userConfig } from './schema'; import { userConfig } from './schema';
import type { UserConfigUpdateType } from './types'; import type { UserConfigUpdateType } from './types';
import { wgInterface } from '#db/schema';
import type { DBType } from '#db/sqlite'; import type { DBType } from '#db/sqlite';
function createPreparedStatement(db: DBType) { function createPreparedStatement(db: DBType) {
@@ -8,14 +9,6 @@ function createPreparedStatement(db: DBType) {
get: db.query.userConfig get: db.query.userConfig
.findFirst({ where: eq(userConfig.id, sql.placeholder('interface')) }) .findFirst({ where: eq(userConfig.id, sql.placeholder('interface')) })
.prepare(), .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(),
}; };
} }
@@ -38,11 +31,26 @@ export class UserConfigService {
return userConfig; return userConfig;
} }
// TODO: wrap ipv6 host in square brackets
/**
* sets host of user config
*
* sets port of user config and interface
*/
updateHostPort(host: string, port: number) { updateHostPort(host: string, port: number) {
return this.#statements.updateHostPort.execute({ return this.#db.transaction(async (tx) => {
interface: 'wg0', await tx
host, .update(userConfig)
port, .set({ host, port })
.where(eq(userConfig.id, 'wg0'))
.execute();
await tx
.update(wgInterface)
.set({ port })
.where(eq(wgInterface.name, 'wg0'))
.execute();
}); });
} }

View File

@@ -19,7 +19,13 @@ const db = drizzle({ client, schema });
export async function connect() { export async function connect() {
await migrate(); await migrate();
return new DBService(db); const dbService = new DBService(db);
if (WG_INITIAL_ENV.ENABLED) {
await initialSetup(dbService);
}
return dbService;
} }
class DBService { class DBService {
@@ -58,3 +64,47 @@ async function migrate() {
} }
} }
} }
async function initialSetup(db: DBServiceType) {
const setup = await db.general.getSetupStep();
if (setup.done) {
DB_DEBUG('Setup already done. Skiping initial setup.');
return;
}
if (WG_INITIAL_ENV.IPV4_CIDR && WG_INITIAL_ENV.IPV6_CIDR) {
DB_DEBUG('Setting initial CIDR...');
await db.interfaces.updateCidr({
ipv4Cidr: WG_INITIAL_ENV.IPV4_CIDR,
ipv6Cidr: WG_INITIAL_ENV.IPV6_CIDR,
});
}
if (WG_INITIAL_ENV.DNS) {
DB_DEBUG('Setting initial DNS...');
const userConfig = await db.userConfigs.get();
await db.userConfigs.update({
...userConfig,
defaultDns: WG_INITIAL_ENV.DNS,
});
}
if (
WG_INITIAL_ENV.USERNAME &&
WG_INITIAL_ENV.PASSWORD &&
WG_INITIAL_ENV.HOST &&
WG_INITIAL_ENV.PORT
) {
DB_DEBUG('Creating initial user...');
await db.users.create(WG_INITIAL_ENV.USERNAME, WG_INITIAL_ENV.PASSWORD);
DB_DEBUG('Setting initial host and port...');
await db.userConfigs.updateHostPort(
WG_INITIAL_ENV.HOST,
WG_INITIAL_ENV.PORT
);
await db.general.setSetupStep(0);
}
}

View File

@@ -19,6 +19,19 @@ export const WG_ENV = {
PORT: assertEnv('PORT'), PORT: assertEnv('PORT'),
}; };
export const WG_INITIAL_ENV = {
ENABLED: process.env.INIT_ENABLED === 'true',
USERNAME: process.env.INIT_USERNAME,
PASSWORD: process.env.INIT_PASSWORD,
DNS: process.env.INIT_DNS?.split(',').map((x) => x.trim()),
IPV4_CIDR: process.env.INIT_IPV4_CIDR,
IPV6_CIDR: process.env.INIT_IPV6_CIDR,
HOST: process.env.INIT_HOST,
PORT: process.env.INIT_PORT
? Number.parseInt(process.env.INIT_PORT, 10)
: undefined,
};
function assertEnv<T extends string>(env: T) { function assertEnv<T extends string>(env: T) {
const val = process.env[env]; const val = process.env[env];