Compare commits

..

8 Commits

Author SHA1 Message Date
Bernd Storath 1c7f64ebd5 Bump version to 15.0.0-beta.11 2025-03-31 10:32:27 +02:00
Bernd Storath 589ec1fe9a Feat: Show insecure warning (#1779)
show insecure warning
2025-03-31 10:29:22 +02:00
Bernd Storath 6e0d758e36 Feat: Hash metrics password (#1778)
hash the metrics password

if it is not already hashed
2025-03-31 09:58:02 +02:00
Pokydko Oleksandr 940edb2b0c Improvements to username and password validations (#1745)
* Fix: Improve special character regex (#1744)
* update password special character regex to support
( `-` `_` `=` `+` `[` `]` `;` `'` `\` `/` )

* Fix: Allow usernames starting from 2 characters (#1744)
* update username validation to support short usernames starting from 2 characters

* remove char validation altogether

---------

Co-authored-by: Bernd Storath <999999bst@gmail.com>
2025-03-31 08:58:24 +02:00
Bernd Storath d51f12a82f update packages 2025-03-31 07:51:21 +02:00
Bernd Storath 4a3747fa12 update packages 2025-03-24 07:53:02 +01:00
Bernd Storath 499fb096b6 Fix: button triggering form (#1750)
prevent button from triggering form
2025-03-19 11:50:48 +01:00
Bernd Storath c5c3a65bbf Fix: slow suggest Host Address (#1746)
load on client instead of block
2025-03-17 14:28:03 +01:00
18 changed files with 1239 additions and 1296 deletions
+1 -1
View File
@@ -7,5 +7,5 @@
"docs:preview": "docker run --rm -it -p 8080:8080 -v ./docs:/docs squidfunk/mkdocs-material serve -a 0.0.0.0:8080",
"scripts:version": "bash scripts/version.sh"
},
"packageManager": "pnpm@10.6.3"
"packageManager": "pnpm@10.7.0"
}
+6 -7
View File
@@ -3,12 +3,12 @@
<template #trigger><slot /></template>
<template #title>{{ $t('admin.config.suggest') }}</template>
<template #description>
<p v-if="!values">
{{ $t('general.loading') }}
</p>
<div v-else class="flex flex-col items-start gap-2">
<div class="flex flex-col items-start gap-2">
<p>{{ $t('admin.config.suggestDesc') }}</p>
<BaseSelect v-model="selected" :options="values" />
<p v-if="!data">
{{ $t('general.loading') }}
</p>
<BaseSelect v-else v-model="selected" :options="data" />
</div>
</template>
<template #actions>
@@ -31,10 +31,9 @@ const props = defineProps<{
url: '/api/admin/ip-info' | '/api/setup/4';
}>();
const { data } = await useFetch(props.url, {
const { data } = useFetch(props.url, {
method: 'get',
});
const selected = ref<string>();
const values = toRef(data.value);
</script>
+1 -1
View File
@@ -5,7 +5,7 @@
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
>
<button @click="open = !open">
<button type="button" @click="open = !open">
<slot />
</button>
</TooltipTrigger>
+10 -8
View File
@@ -16,14 +16,16 @@
class="w-full"
:placeholder="placeholder"
/>
<AdminSuggestDialog :url="url" @change="data = $event">
<BaseButton as="span">
<div class="flex items-center gap-3">
<IconsSparkles class="w-4" />
<span>{{ $t('admin.config.suggest') }}</span>
</div>
</BaseButton>
</AdminSuggestDialog>
<ClientOnly>
<AdminSuggestDialog :url="url" @change="data = $event">
<BaseButton as="span">
<div class="flex items-center gap-3">
<IconsSparkles class="w-4" />
<span>{{ $t('admin.config.suggest') }}</span>
</div>
</BaseButton>
</AdminSuggestDialog>
</ClientOnly>
</div>
</template>
+22
View File
@@ -0,0 +1,22 @@
<template>
<div
v-if="!globalStore.information?.insecure && !https"
class="container mx-auto w-fit rounded-md bg-red-800 p-4 text-white shadow-lg dark:bg-red-100 dark:text-red-600"
>
<p class="text-center">{{ $t('login.insecure') }}</p>
</div>
</template>
<script lang="ts" setup>
const globalStore = useGlobalStore();
const https = ref(false);
onMounted(() => {
if (window.location.protocol === 'https:') {
https.value = true;
} else {
https.value = false;
}
});
</script>
+4 -4
View File
@@ -1,21 +1,21 @@
<template>
<div
v-if="
globalStore.release?.updateAvailable &&
globalStore.information?.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"
:title="`v${globalStore.release.currentRelease} → v${globalStore.release.latestRelease.version}`"
:title="`v${globalStore.information.currentRelease} → v${globalStore.information.latestRelease.version}`"
>
<div class="container mx-auto flex flex-auto flex-row items-center">
<div class="flex-grow">
<p class="font-bold">{{ $t('update.updateAvailable') }}</p>
<p>{{ globalStore.release.latestRelease.changelog }}</p>
<p>{{ globalStore.information.latestRelease.changelog }}</p>
</div>
<a
:href="`https://github.com/wg-easy/wg-easy/releases/tag/${globalStore.release.latestRelease.version}`"
:href="`https://github.com/wg-easy/wg-easy/releases/tag/${globalStore.information.latestRelease.version}`"
target="_blank"
class="font-sm float-right flex-shrink-0 rounded-md border-2 border-red-800 bg-white p-3 font-semibold text-red-800 transition-all hover:border-white hover:bg-red-800 hover:text-white dark:border-red-600 dark:bg-red-100 dark:text-red-600 dark:hover:border-red-600 dark:hover:bg-red-600 dark:hover:text-red-100"
>
+1 -1
View File
@@ -7,7 +7,7 @@
href="https://github.com/wg-easy/wg-easy"
>WireGuard Easy</a
>
({{ globalStore.release?.currentRelease }}) © 2021-2025 by
({{ globalStore.information?.currentRelease }}) © 2021-2025 by
<a
class="hover:underline"
target="_blank"
+6 -6
View File
@@ -18,18 +18,18 @@
/>
</FormGroup>
<FormGroup>
<FormHeading :description="$t('admin.config.allowedIpsDesc')">{{
$t('general.allowedIps')
}}</FormHeading>
<FormHeading :description="$t('admin.config.allowedIpsDesc')">
{{ $t('general.allowedIps') }}
</FormHeading>
<FormArrayField
v-model="data.defaultAllowedIps"
name="defaultAllowedIps"
/>
</FormGroup>
<FormGroup>
<FormHeading :description="$t('admin.config.dnsDesc')">{{
$t('general.dns')
}}</FormHeading>
<FormHeading :description="$t('admin.config.dnsDesc')">
{{ $t('general.dns') }}
</FormHeading>
<FormArrayField v-model="data.defaultDns" name="defaultDns" />
</FormGroup>
<FormGroup>
+1
View File
@@ -1,6 +1,7 @@
<template>
<main>
<UiBanner />
<HeaderInsecure />
<form
class="mx-auto mt-10 flex w-64 flex-col gap-5 overflow-hidden rounded-md bg-white p-5 text-gray-700 shadow dark:bg-neutral-700 dark:text-neutral-200"
@submit.prevent="submit"
+2 -2
View File
@@ -1,5 +1,5 @@
export const useGlobalStore = defineStore('Global', () => {
const { data: release } = useFetch('/api/release', {
const { data: information } = useFetch('/api/information', {
method: 'get',
});
@@ -21,7 +21,7 @@ export const useGlobalStore = defineStore('Global', () => {
return {
sortClient,
release,
information,
uiShowCharts,
toggleCharts,
uiChartType,
+4 -7
View File
@@ -33,7 +33,7 @@
"yes": "Yes",
"no": "No",
"confirmPassword": "Confirm Password",
"loading": "Loading"
"loading": "Loading..."
},
"setup": {
"welcome": "Welcome to your first setup of wg-easy",
@@ -65,7 +65,8 @@
"login": {
"signIn": "Sign In",
"rememberMe": "Remember me",
"rememberMeDesc": "Stay logged after closing the browser"
"rememberMeDesc": "Stay logged after closing the browser",
"insecure": "You can't log in with an insecure connection. Use HTTPS."
},
"error": {
"clear": "Clear",
@@ -135,7 +136,7 @@
"sessionTimeoutDesc": "Session duration for Remember Me (seconds)",
"metrics": "Metrics",
"metricsPassword": "Password",
"metricsPasswordDesc": "Bearer Password for the metrics endpoint (argon2 hash)",
"metricsPasswordDesc": "Bearer Password for the metrics endpoint (password or argon2 hash)",
"json": "JSON",
"jsonDesc": "Route for metrics in JSON format",
"prometheus": "Prometheus",
@@ -189,10 +190,6 @@
"user": {
"username": "Username",
"password": "Password",
"passwordUppercase": "Password must have at least 1 uppercase letter",
"passwordLowercase": "Password must have at least 1 lowercase letter",
"passwordNumber": "Password must have at least 1 number",
"passwordSpecial": "Password must have at least 1 special character",
"remember": "Remember",
"name": "Name",
"email": "Email",
+12 -10
View File
@@ -1,6 +1,6 @@
{
"name": "wg-easy",
"version": "15.0.0-beta.10",
"version": "15.0.0-beta.11",
"description": "The easiest way to run WireGuard VPN + Web-based Admin UI.",
"private": true,
"type": "module",
@@ -20,9 +20,10 @@
"dependencies": {
"@eschricht/nuxt-color-mode": "^1.1.5",
"@heroicons/vue": "^2.2.0",
"@libsql/client": "^0.14.0",
"@nuxtjs/i18n": "^9.3.1",
"@libsql/client": "^0.15.1",
"@nuxtjs/i18n": "^9.4.0",
"@nuxtjs/tailwindcss": "^6.13.2",
"@phc/format": "^1.0.0",
"@pinia/nuxt": "^0.10.1",
"@tailwindcss/forms": "^0.5.10",
"apexcharts": "^4.5.0",
@@ -31,13 +32,13 @@
"cidr-tools": "^11.0.3",
"crc-32": "^1.2.2",
"debug": "^4.4.0",
"drizzle-orm": "^0.40.0",
"drizzle-orm": "^0.41.0",
"ip-bigint": "^8.2.1",
"is-cidr": "^5.1.1",
"is-ip": "^5.0.1",
"js-sha256": "^0.11.0",
"lowdb": "^7.0.1",
"nuxt": "^3.16.0",
"nuxt": "^3.16.1",
"pinia": "^3.0.1",
"qrcode": "^1.5.4",
"radix-vue": "^1.9.17",
@@ -49,17 +50,18 @@
"zod": "^3.24.2"
},
"devDependencies": {
"@nuxt/eslint": "1.2.0",
"@nuxt/eslint": "1.3.0",
"@types/debug": "^4.1.12",
"@types/phc__format": "^1.0.1",
"@types/qrcode": "^1.5.5",
"@types/semver": "^7.5.8",
"drizzle-kit": "^0.30.5",
"eslint": "^9.22.0",
"@types/semver": "^7.7.0",
"drizzle-kit": "^0.30.6",
"eslint": "^9.23.0",
"eslint-config-prettier": "^10.1.1",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"typescript": "^5.8.2",
"vue-tsc": "^2.2.8"
},
"packageManager": "pnpm@10.6.3"
"packageManager": "pnpm@10.7.0"
}
+1138 -1242
View File
File diff suppressed because it is too large Load Diff
@@ -3,9 +3,11 @@ import { gt } from 'semver';
export default defineEventHandler(async () => {
const latestRelease = await cachedFetchLatestRelease();
const updateAvailable = gt(latestRelease.version, RELEASE);
const insecure = WG_ENV.INSECURE;
return {
currentRelease: RELEASE,
latestRelease: latestRelease,
updateAvailable,
insecure,
};
});
@@ -107,7 +107,15 @@ export class GeneralService {
};
}
update(data: GeneralUpdateType) {
async update(data: GeneralUpdateType) {
// only hash the password if it is not already hashed
if (
data.metricsPassword !== null &&
!isValidPasswordHash(data.metricsPassword)
) {
data.metricsPassword = await hashPassword(data.metricsPassword);
}
return this.#db.update(general).set(data).execute();
}
@@ -11,7 +11,6 @@ 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({
@@ -6,16 +6,12 @@ export type UserType = InferSelectModel<typeof user>;
const username = z
.string({ message: t('zod.user.username') })
.min(8, t('zod.user.username'))
.min(2, 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') });
+19
View File
@@ -1,4 +1,5 @@
import argon2 from 'argon2';
import { deserialize } from '@phc/format';
/**
* Checks if `password` matches the hash.
@@ -16,3 +17,21 @@ export function isPasswordValid(
export async function hashPassword(password: string): Promise<string> {
return argon2.hash(password);
}
/**
* Checks if the password hash is valid.
* This only checks if the hash is a valid PHC formatted string using argon2.
*/
export function isValidPasswordHash(hash: string): boolean {
try {
const obj = deserialize(hash);
if (obj.id !== 'argon2i' && obj.id !== 'argon2d' && obj.id !== 'argon2id') {
return false;
}
return true;
} catch {
return false;
}
}