Browse Source

Feat: Migration (#1663)

* show error for old env vars

* reorder setup, be able to migrate

* fix type issue
pull/1665/head
Bernd Storath 6 months ago
committed by GitHub
parent
commit
413baa1a1f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 11
      src/app/pages/setup/2.vue
  2. 75
      src/app/pages/setup/3.vue
  3. 72
      src/app/pages/setup/4.vue
  4. 6
      src/app/stores/setup.ts
  5. 3
      src/i18n/locales/en.json
  6. 2
      src/server/api/setup/3.post.ts
  7. 87
      src/server/api/setup/migrate.post.ts
  8. 37
      src/server/database/repositories/client/service.ts
  9. 13
      src/server/database/repositories/client/types.ts
  10. 4
      src/server/database/repositories/oneTimeLink/types.ts
  11. 1
      src/server/routes/cnf/[oneTimeLink].ts
  12. 10
      src/server/utils/WireGuard.ts
  13. 7
      src/server/utils/config.ts
  14. 4
      src/server/utils/ip.ts
  15. 8
      src/server/utils/types.ts

11
src/app/pages/setup/2.vue

@ -1,16 +1,13 @@
<template>
<div>
<p class="p-8 text-center text-lg">
{{ 'Do you have a existing Setup?' }}
<p class="px-8 pt-8 text-center text-2xl">
{{ $t('setup.messageWelcome.whatIs') }}
</p>
<div class="mb-8 flex justify-center">
<NuxtLink to="/setup/3"><BaseButton>No</BaseButton></NuxtLink>
<NuxtLink to="/setup/migrate"><BaseButton>Yes</BaseButton></NuxtLink>
</div>
<NuxtLink to="/setup/3"><BaseButton>Continue</BaseButton></NuxtLink>
</div>
</template>
<script lang="ts" setup>
<script setup lang="ts">
definePageMeta({
layout: 'setup',
});

75
src/app/pages/setup/3.vue

@ -1,16 +1,83 @@
<template>
<div>
<p class="px-8 pt-8 text-center text-2xl">
{{ $t('setup.messageWelcome.whatIs') }}
<p class="p-8 text-center text-lg">
{{ $t('setup.messageSetupCreateAdminUser') }}
</p>
<NuxtLink to="/setup/4"><BaseButton>Continue</BaseButton></NuxtLink>
<form id="newAccount"></form>
<div>
<Label for="username">{{ $t('username') }}</Label>
<input
id="username"
v-model="username"
form="newAccount"
type="text"
autocomplete="username"
class="mb-5 w-full rounded-lg border-2 border-gray-100 px-3 py-2 text-sm text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-gray-200 dark:placeholder:text-neutral-400 dark:focus:border-red-800"
/>
</div>
<div>
<Label for="password">{{ $t('setup.newPassword') }}</Label>
<input
id="password"
v-model="password"
form="newAccount"
type="password"
autocomplete="new-password"
class="mb-5 w-full rounded-lg border-2 border-gray-100 px-3 py-2 text-sm text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-gray-200 dark:placeholder:text-neutral-400 dark:focus:border-red-800"
/>
</div>
<div>
<Label for="accept">{{ $t('setup.accept') }}</Label>
<input
id="accept"
v-model="accept"
form="newAccount"
type="checkbox"
class="ml-2"
/>
</div>
<BaseButton @click="newAccount">Create Account</BaseButton>
</div>
</template>
<script setup lang="ts">
<script lang="ts" setup>
import { FetchError } from 'ofetch';
const { t } = useI18n();
definePageMeta({
layout: 'setup',
});
const setupStore = useSetupStore();
setupStore.setStep(3);
const router = useRouter();
const username = ref<null | string>(null);
const password = ref<null | string>(null);
const accept = ref<boolean>(true);
const toast = useToast();
async function newAccount() {
try {
if (!username.value || !password.value) {
toast.showToast({
type: 'error',
title: t('setup.requirements'),
message: t('setup.emptyFields'),
});
return;
}
await setupStore.step3(username.value, password.value, accept.value);
await router.push('/setup/4');
} catch (error) {
if (error instanceof FetchError) {
toast.showToast({
type: 'error',
title: t('setup.requirements'),
message: error.data.message,
});
}
}
}
</script>

72
src/app/pages/setup/4.vue

@ -1,83 +1,19 @@
<template>
<div>
<p class="p-8 text-center text-lg">
{{ $t('setup.messageSetupCreateAdminUser') }}
{{ 'Do you have a existing Setup?' }}
</p>
<form id="newAccount"></form>
<div>
<Label for="username">{{ $t('username') }}</Label>
<input
id="username"
v-model="username"
form="newAccount"
type="text"
autocomplete="username"
class="mb-5 w-full rounded-lg border-2 border-gray-100 px-3 py-2 text-sm text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-gray-200 dark:placeholder:text-neutral-400 dark:focus:border-red-800"
/>
<div class="mb-8 flex justify-center">
<NuxtLink to="/setup/5"><BaseButton>No</BaseButton></NuxtLink>
<NuxtLink to="/setup/migrate"><BaseButton>Yes</BaseButton></NuxtLink>
</div>
<div>
<Label for="password">{{ $t('setup.newPassword') }}</Label>
<input
id="password"
v-model="password"
form="newAccount"
type="password"
autocomplete="new-password"
class="mb-5 w-full rounded-lg border-2 border-gray-100 px-3 py-2 text-sm text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-gray-200 dark:placeholder:text-neutral-400 dark:focus:border-red-800"
/>
</div>
<div>
<Label for="accept">{{ $t('setup.accept') }}</Label>
<input
id="accept"
v-model="accept"
form="newAccount"
type="checkbox"
class="ml-2"
/>
</div>
<BaseButton @click="newAccount">Create Account</BaseButton>
</div>
</template>
<script lang="ts" setup>
import { FetchError } from 'ofetch';
const { t } = useI18n();
definePageMeta({
layout: 'setup',
});
const setupStore = useSetupStore();
setupStore.setStep(4);
const router = useRouter();
const username = ref<null | string>(null);
const password = ref<null | string>(null);
const accept = ref<boolean>(true);
const toast = useToast();
async function newAccount() {
try {
if (!username.value || !password.value) {
toast.showToast({
type: 'error',
title: t('setup.requirements'),
message: t('setup.emptyFields'),
});
return;
}
await setupStore.step4(username.value, password.value, accept.value);
await router.push('/setup/5');
} catch (error) {
if (error instanceof FetchError) {
toast.showToast({
type: 'error',
title: t('setup.requirements'),
message: error.data.message,
});
}
}
}
</script>

6
src/app/stores/setup.ts

@ -4,8 +4,8 @@ export const useSetupStore = defineStore('Setup', () => {
/**
* @throws if unsuccessful
*/
async function step4(username: string, password: string, accept: boolean) {
const response = await $fetch('/api/setup/4', {
async function step3(username: string, password: string, accept: boolean) {
const response = await $fetch('/api/setup/3', {
method: 'post',
body: { username, password, accept },
});
@ -41,7 +41,7 @@ export const useSetupStore = defineStore('Setup', () => {
}
return {
step4,
step3,
step5,
runMigration,
step,

3
src/i18n/locales/en.json

@ -145,6 +145,7 @@
"persistentKeepalive": "Persistent Keepalive",
"address": "IP Address",
"dns": "DNS",
"allowedIps": "Allowed IPs"
"allowedIps": "Allowed IPs",
"file": "File"
}
}

2
src/server/api/setup/4.post.ts → src/server/api/setup/3.post.ts

@ -9,6 +9,6 @@ export default defineSetupEventHandler(async ({ event }) => {
// TODO: validate setup step
await Database.users.create(username, password);
await Database.general.setSetupStep(5);
await Database.general.setSetupStep(4);
return { success: true };
});

87
src/server/api/setup/migrate.post.ts

@ -1,21 +1,25 @@
/*import { parseCidr } from 'cidr-tools';
import { parseCidr } from 'cidr-tools';
import { stringifyIp } from 'ip-bigint';
import { z } from 'zod';*/
import { z } from 'zod';
export default defineSetupEventHandler(async (/*{ event }*/) => {
// TODO: Implement
/*
export default defineSetupEventHandler(async ({ event }) => {
const { file } = await readValidatedBody(
event,
validateZod(FileSchema, event)
);
const { file } = await readValidatedBody(event, validateZod(fileType, 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(),
@ -31,49 +35,42 @@ export default defineSetupEventHandler(async (/*{ event }*/) => {
if (!res.success) {
throw new Error('Invalid Config');
}
const system = await Database.system.get();
const oldConfig = res.data;
const oldCidr = parseCidr(oldConfig.server.address + '/24');
const db = {
system: {
...system,
// TODO: migrate to db calls
interface: {
...system.interface,
address4: oldConfig.server.address,
privateKey: oldConfig.server.privateKey,
publicKey: oldConfig.server.publicKey,
},
userConfig: {
...system.userConfig,
defaultDns: [...system.userConfig.defaultDns],
allowedIps: [...system.userConfig.allowedIps],
address4Range:
stringifyIp({ number: oldCidr.start, version: 4 }) + '/24',
},
} satisfies Partial<Database['system']>,
clients: {} as Database['clients'],
};
for (const oldClient of Object.values(oldConfig.clients)) {
const address6 = nextIPv6(db.system, db.clients);
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];
await Database.client.create({
address4: oldClient.address,
enabled: oldClient.enabled,
name: oldClient.name,
preSharedKey: oldClient.preSharedKey,
privateKey: oldClient.privateKey,
publicKey: oldClient.publicKey,
expiresAt: null,
oneTimeLink: null,
allowedIps: [...db.system.userConfig.allowedIps],
serverAllowedIPs: [],
persistentKeepalive: 0,
address6: address6,
mtu: 1420,
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 };
});

37
src/server/database/repositories/client/service.ts

@ -1,7 +1,11 @@
import type { DBType } from '#db/sqlite';
import { eq, sql } from 'drizzle-orm';
import { client } from './schema';
import type { ClientCreateType, UpdateClientType } from './types';
import type {
ClientCreateFromExistingType,
ClientCreateType,
UpdateClientType,
} from './types';
import type { ID } from '#db/schema';
import { wgInterface, userConfig } from '#db/schema';
import { parseCidr } from 'cidr-tools';
@ -142,4 +146,35 @@ export class ClientService {
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();
}
}

13
src/server/database/repositories/client/types.ts

@ -5,6 +5,8 @@ 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'
@ -68,3 +70,14 @@ 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'
>;

4
src/server/database/repositories/oneTimeLink/types.ts

@ -5,8 +5,8 @@ import { z } from 'zod';
export type OneTimeLinkType = InferSelectModel<typeof oneTimeLink>;
const oneTimeLinkType = z
.string({ message: t('zod.otl.otl') })
.min(1, t('zod.otl.otl'))
.string({ message: t('zod.otl') })
.min(1, t('zod.otl'))
.pipe(safeStringRefine);
export const OneTimeLinkGetSchema = z.object(

1
src/server/routes/cnf/[oneTimeLink].ts

@ -6,6 +6,7 @@ export default defineEventHandler(async (event) => {
validateZod(OneTimeLinkGetSchema, event)
);
const clients = await WireGuard.getAllClients();
// TODO: filter on the database level
const client = clients.find(
(client) => client.oneTimeLink?.oneTimeLink === oneTimeLink
);

10
src/server/utils/WireGuard.ts

@ -243,4 +243,14 @@ class WireGuard {
}
}
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
`
);
}
export default new WireGuard();

7
src/server/utils/config.ts

@ -4,3 +4,10 @@ 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,
};

4
src/server/utils/ip.ts

@ -1,14 +1,14 @@
import type { parseCidr } from 'cidr-tools';
import { stringifyIp } from 'ip-bigint';
import type { ClientType } from '#db/repositories/client/types';
import type { ClientNextIpType } from '#db/repositories/client/types';
type ParsedCidr = ReturnType<typeof parseCidr>;
export function nextIP(
version: 4 | 6,
cidr: ParsedCidr,
clients: ClientType[]
clients: ClientNextIpType[]
) {
let address;
for (let i = cidr.start + 2n; i <= cidr.end - 1n; i++) {

8
src/server/utils/types.ts

@ -9,6 +9,7 @@ import type { H3Event, EventHandlerRequest } from 'h3';
*/
export const t = (v: string) => v;
// TODO: use everywhere or remove
export const objectMessage = t('zod.body');
export const safeStringRefine = z
@ -50,6 +51,13 @@ 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') }),
},
{ message: objectMessage }
);
export const schemaForType =
<T>() =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any

Loading…
Cancel
Save