Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c7f64ebd5 | |||
| 589ec1fe9a | |||
| 6e0d758e36 | |||
| 940edb2b0c | |||
| d51f12a82f | |||
| 4a3747fa12 | |||
| 499fb096b6 | |||
| c5c3a65bbf |
+1
-1
@@ -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"
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
<template #trigger><slot /></template>
|
||||
<template #title>{{ $t('admin.config.suggest') }}</template>
|
||||
<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') }}
|
||||
</p>
|
||||
<div v-else class="flex flex-col items-start gap-2">
|
||||
<p>{{ $t('admin.config.suggestDesc') }}</p>
|
||||
<BaseSelect v-model="selected" :options="values" />
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
class="w-full"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
<ClientOnly>
|
||||
<AdminSuggestDialog :url="url" @change="data = $event">
|
||||
<BaseButton as="span">
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -24,6 +25,7 @@
|
||||
</div>
|
||||
</BaseButton>
|
||||
</AdminSuggestDialog>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
Generated
+1138
-1242
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') });
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user