Compare commits

..

10 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
Bernd Storath c133446f9c Bump version to 15.0.0-beta.10 2025-03-14 14:28:12 +01:00
Bernd Storath e8b3e54228 fix libsql bundling issue 2025-03-14 14:26:08 +01:00
19 changed files with 1278 additions and 1360 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ COPY --from=build /app/.output /app
# Copy migrations # Copy migrations
COPY --from=build /app/server/database/migrations /app/server/database/migrations COPY --from=build /app/server/database/migrations /app/server/database/migrations
# libsql # libsql
RUN npm install --no-save libsql RUN cd /app/server && npm install --no-save libsql
# Install Linux packages # Install Linux packages
RUN apk add --no-cache \ RUN apk add --no-cache \
+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", "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" "scripts:version": "bash scripts/version.sh"
}, },
"packageManager": "pnpm@10.6.3" "packageManager": "pnpm@10.7.0"
} }
+5 -6
View File
@@ -3,12 +3,12 @@
<template #trigger><slot /></template> <template #trigger><slot /></template>
<template #title>{{ $t('admin.config.suggest') }}</template> <template #title>{{ $t('admin.config.suggest') }}</template>
<template #description> <template #description>
<p v-if="!values"> <div class="flex flex-col items-start gap-2">
<p>{{ $t('admin.config.suggestDesc') }}</p>
<p v-if="!data">
{{ $t('general.loading') }} {{ $t('general.loading') }}
</p> </p>
<div v-else class="flex flex-col items-start gap-2"> <BaseSelect v-else v-model="selected" :options="data" />
<p>{{ $t('admin.config.suggestDesc') }}</p>
<BaseSelect v-model="selected" :options="values" />
</div> </div>
</template> </template>
<template #actions> <template #actions>
@@ -31,10 +31,9 @@ const props = defineProps<{
url: '/api/admin/ip-info' | '/api/setup/4'; url: '/api/admin/ip-info' | '/api/setup/4';
}>(); }>();
const { data } = await useFetch(props.url, { const { data } = useFetch(props.url, {
method: 'get', method: 'get',
}); });
const selected = ref<string>(); const selected = ref<string>();
const values = toRef(data.value);
</script> </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" 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 as-child
> >
<button @click="open = !open"> <button type="button" @click="open = !open">
<slot /> <slot />
</button> </button>
</TooltipTrigger> </TooltipTrigger>
+2
View File
@@ -16,6 +16,7 @@
class="w-full" class="w-full"
:placeholder="placeholder" :placeholder="placeholder"
/> />
<ClientOnly>
<AdminSuggestDialog :url="url" @change="data = $event"> <AdminSuggestDialog :url="url" @change="data = $event">
<BaseButton as="span"> <BaseButton as="span">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
@@ -24,6 +25,7 @@
</div> </div>
</BaseButton> </BaseButton>
</AdminSuggestDialog> </AdminSuggestDialog>
</ClientOnly>
</div> </div>
</template> </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> <template>
<div <div
v-if=" v-if="
globalStore.release?.updateAvailable && globalStore.information?.updateAvailable &&
authStore.userData && authStore.userData &&
hasPermissions(authStore.userData, 'admin', 'any') 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.information.currentRelease} → v${globalStore.information.latestRelease.version}`"
> >
<div class="container mx-auto flex flex-auto flex-row items-center"> <div class="container mx-auto flex flex-auto flex-row items-center">
<div class="flex-grow"> <div class="flex-grow">
<p class="font-bold">{{ $t('update.updateAvailable') }}</p> <p class="font-bold">{{ $t('update.updateAvailable') }}</p>
<p>{{ globalStore.release.latestRelease.changelog }}</p> <p>{{ globalStore.information.latestRelease.changelog }}</p>
</div> </div>
<a <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" 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" 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" href="https://github.com/wg-easy/wg-easy"
>WireGuard Easy</a >WireGuard Easy</a
> >
({{ globalStore.release?.currentRelease }}) © 2021-2025 by ({{ globalStore.information?.currentRelease }}) © 2021-2025 by
<a <a
class="hover:underline" class="hover:underline"
target="_blank" target="_blank"
+6 -6
View File
@@ -18,18 +18,18 @@
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormHeading :description="$t('admin.config.allowedIpsDesc')">{{ <FormHeading :description="$t('admin.config.allowedIpsDesc')">
$t('general.allowedIps') {{ $t('general.allowedIps') }}
}}</FormHeading> </FormHeading>
<FormArrayField <FormArrayField
v-model="data.defaultAllowedIps" v-model="data.defaultAllowedIps"
name="defaultAllowedIps" name="defaultAllowedIps"
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormHeading :description="$t('admin.config.dnsDesc')">{{ <FormHeading :description="$t('admin.config.dnsDesc')">
$t('general.dns') {{ $t('general.dns') }}
}}</FormHeading> </FormHeading>
<FormArrayField v-model="data.defaultDns" name="defaultDns" /> <FormArrayField v-model="data.defaultDns" name="defaultDns" />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
+1
View File
@@ -1,6 +1,7 @@
<template> <template>
<main> <main>
<UiBanner /> <UiBanner />
<HeaderInsecure />
<form <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" 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" @submit.prevent="submit"
+2 -2
View File
@@ -1,5 +1,5 @@
export const useGlobalStore = defineStore('Global', () => { export const useGlobalStore = defineStore('Global', () => {
const { data: release } = useFetch('/api/release', { const { data: information } = useFetch('/api/information', {
method: 'get', method: 'get',
}); });
@@ -21,7 +21,7 @@ export const useGlobalStore = defineStore('Global', () => {
return { return {
sortClient, sortClient,
release, information,
uiShowCharts, uiShowCharts,
toggleCharts, toggleCharts,
uiChartType, uiChartType,
+4 -7
View File
@@ -33,7 +33,7 @@
"yes": "Yes", "yes": "Yes",
"no": "No", "no": "No",
"confirmPassword": "Confirm Password", "confirmPassword": "Confirm Password",
"loading": "Loading" "loading": "Loading..."
}, },
"setup": { "setup": {
"welcome": "Welcome to your first setup of wg-easy", "welcome": "Welcome to your first setup of wg-easy",
@@ -65,7 +65,8 @@
"login": { "login": {
"signIn": "Sign In", "signIn": "Sign In",
"rememberMe": "Remember me", "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": { "error": {
"clear": "Clear", "clear": "Clear",
@@ -135,7 +136,7 @@
"sessionTimeoutDesc": "Session duration for Remember Me (seconds)", "sessionTimeoutDesc": "Session duration for Remember Me (seconds)",
"metrics": "Metrics", "metrics": "Metrics",
"metricsPassword": "Password", "metricsPassword": "Password",
"metricsPasswordDesc": "Bearer Password for the metrics endpoint (argon2 hash)", "metricsPasswordDesc": "Bearer Password for the metrics endpoint (password or argon2 hash)",
"json": "JSON", "json": "JSON",
"jsonDesc": "Route for metrics in JSON format", "jsonDesc": "Route for metrics in JSON format",
"prometheus": "Prometheus", "prometheus": "Prometheus",
@@ -189,10 +190,6 @@
"user": { "user": {
"username": "Username", "username": "Username",
"password": "Password", "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", "remember": "Remember",
"name": "Name", "name": "Name",
"email": "Email", "email": "Email",
+12 -10
View File
@@ -1,6 +1,6 @@
{ {
"name": "wg-easy", "name": "wg-easy",
"version": "15.0.0-beta.9", "version": "15.0.0-beta.11",
"description": "The easiest way to run WireGuard VPN + Web-based Admin UI.", "description": "The easiest way to run WireGuard VPN + Web-based Admin UI.",
"private": true, "private": true,
"type": "module", "type": "module",
@@ -20,9 +20,10 @@
"dependencies": { "dependencies": {
"@eschricht/nuxt-color-mode": "^1.1.5", "@eschricht/nuxt-color-mode": "^1.1.5",
"@heroicons/vue": "^2.2.0", "@heroicons/vue": "^2.2.0",
"@libsql/client": "^0.14.0", "@libsql/client": "^0.15.1",
"@nuxtjs/i18n": "^9.3.1", "@nuxtjs/i18n": "^9.4.0",
"@nuxtjs/tailwindcss": "^6.13.2", "@nuxtjs/tailwindcss": "^6.13.2",
"@phc/format": "^1.0.0",
"@pinia/nuxt": "^0.10.1", "@pinia/nuxt": "^0.10.1",
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/forms": "^0.5.10",
"apexcharts": "^4.5.0", "apexcharts": "^4.5.0",
@@ -31,13 +32,13 @@
"cidr-tools": "^11.0.3", "cidr-tools": "^11.0.3",
"crc-32": "^1.2.2", "crc-32": "^1.2.2",
"debug": "^4.4.0", "debug": "^4.4.0",
"drizzle-orm": "^0.40.0", "drizzle-orm": "^0.41.0",
"ip-bigint": "^8.2.1", "ip-bigint": "^8.2.1",
"is-cidr": "^5.1.1", "is-cidr": "^5.1.1",
"is-ip": "^5.0.1", "is-ip": "^5.0.1",
"js-sha256": "^0.11.0", "js-sha256": "^0.11.0",
"lowdb": "^7.0.1", "lowdb": "^7.0.1",
"nuxt": "^3.16.0", "nuxt": "^3.16.1",
"pinia": "^3.0.1", "pinia": "^3.0.1",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"radix-vue": "^1.9.17", "radix-vue": "^1.9.17",
@@ -49,17 +50,18 @@
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/eslint": "1.2.0", "@nuxt/eslint": "1.3.0",
"@types/debug": "^4.1.12", "@types/debug": "^4.1.12",
"@types/phc__format": "^1.0.1",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@types/semver": "^7.5.8", "@types/semver": "^7.7.0",
"drizzle-kit": "^0.30.5", "drizzle-kit": "^0.30.6",
"eslint": "^9.22.0", "eslint": "^9.23.0",
"eslint-config-prettier": "^10.1.1", "eslint-config-prettier": "^10.1.1",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11", "prettier-plugin-tailwindcss": "^0.6.11",
"typescript": "^5.8.2", "typescript": "^5.8.2",
"vue-tsc": "^2.2.8" "vue-tsc": "^2.2.8"
}, },
"packageManager": "pnpm@10.6.3" "packageManager": "pnpm@10.7.0"
} }
+1176 -1305
View File
File diff suppressed because it is too large Load Diff
@@ -3,9 +3,11 @@ import { gt } from 'semver';
export default defineEventHandler(async () => { export default defineEventHandler(async () => {
const latestRelease = await cachedFetchLatestRelease(); const latestRelease = await cachedFetchLatestRelease();
const updateAvailable = gt(latestRelease.version, RELEASE); const updateAvailable = gt(latestRelease.version, RELEASE);
const insecure = WG_ENV.INSECURE;
return { return {
currentRelease: RELEASE, currentRelease: RELEASE,
latestRelease: latestRelease, latestRelease: latestRelease,
updateAvailable, 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(); return this.#db.update(general).set(data).execute();
} }
@@ -11,7 +11,6 @@ const metricsEnabled = z.boolean({ message: t('zod.general.metricsEnabled') });
const metricsPassword = z const metricsPassword = z
.string({ message: t('zod.general.metricsPassword') }) .string({ message: t('zod.general.metricsPassword') })
.min(1, { message: t('zod.general.metricsPassword') }) .min(1, { message: t('zod.general.metricsPassword') })
// TODO?: validate argon2 regex
.nullable(); .nullable();
export const GeneralUpdateSchema = z.object({ export const GeneralUpdateSchema = z.object({
@@ -6,16 +6,12 @@ export type UserType = InferSelectModel<typeof user>;
const username = z const username = z
.string({ message: t('zod.user.username') }) .string({ message: t('zod.user.username') })
.min(8, t('zod.user.username')) .min(2, t('zod.user.username'))
.pipe(safeStringRefine); .pipe(safeStringRefine);
const password = z const password = z
.string({ message: t('zod.user.password') }) .string({ message: t('zod.user.password') })
.min(12, 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); .pipe(safeStringRefine);
const remember = z.boolean({ message: t('zod.user.remember') }); const remember = z.boolean({ message: t('zod.user.remember') });
+19
View File
@@ -1,4 +1,5 @@
import argon2 from 'argon2'; import argon2 from 'argon2';
import { deserialize } from '@phc/format';
/** /**
* Checks if `password` matches the hash. * Checks if `password` matches the hash.
@@ -16,3 +17,21 @@ export function isPasswordValid(
export async function hashPassword(password: string): Promise<string> { export async function hashPassword(password: string): Promise<string> {
return argon2.hash(password); 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;
}
}