Add metrics password override and implement frontend UI indicators

Backend changes:
- Added WG_METRICS_PASSWORD environment variable override
- Updated applyGeneralOverrides() to include metrics password
- Updated /api/admin/overrides endpoint to include metrics password

Frontend changes:
- Added override indicators (warning icons with tooltips) to all form fields
- Updated TextField, NumberField, NullTextField, SwitchField, HostField, ArrayField components
- Added overridden prop support to all form components
- Fetched /api/admin/overrides in all admin pages (interface, general, config, hooks)
- Warning icon displays when field is overridden by environment variable
- ArrayField shows banner when overridden
- Updated documentation with WG_METRICS_PASSWORD

Co-authored-by: kaaax0815 <32197462+kaaax0815@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-11-14 14:30:56 +00:00
parent c1d5822f41
commit fbf24410db
13 changed files with 91 additions and 5 deletions
@@ -51,6 +51,7 @@ These environment variables allow you to override settings that would normally b
| Env | Example | Description | | Env | Example | Description |
| ----------------------- | ----------------- | ------------------------- | | ----------------------- | ----------------- | ------------------------- |
| `WG_SESSION_TIMEOUT` | `3600` | Session timeout (seconds) | | `WG_SESSION_TIMEOUT` | `3600` | Session timeout (seconds) |
| `WG_METRICS_PASSWORD` | `mypassword123` | Metrics endpoint password |
| `WG_METRICS_PROMETHEUS` | `true` or `false` | Enable Prometheus metrics | | `WG_METRICS_PROMETHEUS` | `true` or `false` | Enable Prometheus metrics |
| `WG_METRICS_JSON` | `true` or `false` | Enable JSON metrics | | `WG_METRICS_JSON` | `true` or `false` | Enable JSON metrics |
+9 -1
View File
@@ -1,5 +1,9 @@
<template> <template>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div v-if="overridden" class="flex items-center gap-2 p-2 bg-amber-50 dark:bg-amber-900/20 rounded-lg text-amber-700 dark:text-amber-400 text-sm">
<IconsWarning class="size-4" />
<span>This field is overridden by an environment variable</span>
</div>
<div v-if="data?.length === 0"> <div v-if="data?.length === 0">
{{ emptyText || $t('form.noItems') }} {{ emptyText || $t('form.noItems') }}
</div> </div>
@@ -35,7 +39,11 @@
<script lang="ts" setup> <script lang="ts" setup>
const data = defineModel<string[]>(); const data = defineModel<string[]>();
defineProps<{ emptyText?: string[]; name: string }>(); defineProps<{
emptyText?: string[];
name: string;
overridden?: boolean;
}>();
function update(e: Event, i: number) { function update(e: Event, i: number) {
const v = (e.target as HTMLInputElement).value; const v = (e.target as HTMLInputElement).value;
+4
View File
@@ -6,6 +6,9 @@
<BaseTooltip v-if="description" :text="description"> <BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" /> <IconsInfo class="size-4" />
</BaseTooltip> </BaseTooltip>
<BaseTooltip v-if="overridden" text="This field is overridden by an environment variable">
<IconsWarning class="size-4 ml-1 text-amber-500" />
</BaseTooltip>
</div> </div>
<div class="flex gap-1"> <div class="flex gap-1">
<BaseInput <BaseInput
@@ -38,6 +41,7 @@ defineProps<{
description?: string; description?: string;
placeholder?: string; placeholder?: string;
url: '/api/admin/ip-info' | '/api/setup/4'; url: '/api/admin/ip-info' | '/api/setup/4';
overridden?: boolean;
}>(); }>();
const data = defineModel<string | null>({ const data = defineModel<string | null>({
@@ -6,6 +6,9 @@
<BaseTooltip v-if="description" :text="description"> <BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" /> <IconsInfo class="size-4" />
</BaseTooltip> </BaseTooltip>
<BaseTooltip v-if="overridden" text="This field is overridden by an environment variable">
<IconsWarning class="size-4 ml-1 text-amber-500" />
</BaseTooltip>
</div> </div>
<BaseInput <BaseInput
:id="id" :id="id"
@@ -24,6 +27,7 @@ defineProps<{
description?: string; description?: string;
autocomplete?: string; autocomplete?: string;
placeholder?: string; placeholder?: string;
overridden?: boolean;
}>(); }>();
const data = defineModel<string | null>({ const data = defineModel<string | null>({
+9 -1
View File
@@ -6,12 +6,20 @@
<BaseTooltip v-if="description" :text="description"> <BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" /> <IconsInfo class="size-4" />
</BaseTooltip> </BaseTooltip>
<BaseTooltip v-if="overridden" text="This field is overridden by an environment variable">
<IconsWarning class="size-4 ml-1 text-amber-500" />
</BaseTooltip>
</div> </div>
<BaseInput :id="id" v-model.number="data" :name="id" type="number" /> <BaseInput :id="id" v-model.number="data" :name="id" type="number" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
defineProps<{ id: string; label: string; description?: string }>(); defineProps<{
id: string;
label: string;
description?: string;
overridden?: boolean;
}>();
const data = defineModel<number>(); const data = defineModel<number>();
</script> </script>
+9 -1
View File
@@ -6,11 +6,19 @@
<BaseTooltip v-if="description" :text="description"> <BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" /> <IconsInfo class="size-4" />
</BaseTooltip> </BaseTooltip>
<BaseTooltip v-if="overridden" text="This field is overridden by an environment variable">
<IconsWarning class="size-4 ml-1 text-amber-500" />
</BaseTooltip>
</div> </div>
<BaseSwitch :id="id" v-model="data" /> <BaseSwitch :id="id" v-model="data" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
defineProps<{ id: string; label: string; description?: string }>(); defineProps<{
id: string;
label: string;
description?: string;
overridden?: boolean;
}>();
const data = defineModel<boolean>(); const data = defineModel<boolean>();
</script> </script>
+4
View File
@@ -6,6 +6,9 @@
<BaseTooltip v-if="description" :text="description"> <BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" /> <IconsInfo class="size-4" />
</BaseTooltip> </BaseTooltip>
<BaseTooltip v-if="overridden" text="This field is overridden by an environment variable">
<IconsWarning class="size-4 ml-1 text-amber-500" />
</BaseTooltip>
</div> </div>
<BaseInput <BaseInput
:id="id" :id="id"
@@ -24,6 +27,7 @@ defineProps<{
description?: string; description?: string;
autocomplete?: string; autocomplete?: string;
disabled?: boolean; disabled?: boolean;
overridden?: boolean;
}>(); }>();
const data = defineModel<string>(); const data = defineModel<string>();
+16 -1
View File
@@ -9,12 +9,14 @@
:label="$t('general.host')" :label="$t('general.host')"
:description="$t('admin.config.hostDesc')" :description="$t('admin.config.hostDesc')"
url="/api/admin/ip-info" url="/api/admin/ip-info"
:overridden="overrides.host"
/> />
<FormNumberField <FormNumberField
id="port" id="port"
v-model="data.port" v-model="data.port"
:label="$t('general.port')" :label="$t('general.port')"
:description="$t('admin.config.portDesc')" :description="$t('admin.config.portDesc')"
:overridden="overrides.port"
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
@@ -24,13 +26,18 @@
<FormArrayField <FormArrayField
v-model="data.defaultAllowedIps" v-model="data.defaultAllowedIps"
name="defaultAllowedIps" name="defaultAllowedIps"
:overridden="overrides.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"
:overridden="overrides.defaultDns"
/>
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormHeading>{{ $t('form.sectionAdvanced') }}</FormHeading> <FormHeading>{{ $t('form.sectionAdvanced') }}</FormHeading>
@@ -39,12 +46,14 @@
v-model="data.defaultMtu" v-model="data.defaultMtu"
:label="$t('general.mtu')" :label="$t('general.mtu')"
:description="$t('admin.config.mtuDesc')" :description="$t('admin.config.mtuDesc')"
:overridden="overrides.defaultMtu"
/> />
<FormNumberField <FormNumberField
id="defaultPersistentKeepalive" id="defaultPersistentKeepalive"
v-model="data.defaultPersistentKeepalive" v-model="data.defaultPersistentKeepalive"
:label="$t('general.persistentKeepalive')" :label="$t('general.persistentKeepalive')"
:description="$t('admin.config.persistentKeepaliveDesc')" :description="$t('admin.config.persistentKeepaliveDesc')"
:overridden="overrides.defaultPersistentKeepalive"
/> />
</FormGroup> </FormGroup>
<FormGroup v-if="globalStore.information?.isAwg"> <FormGroup v-if="globalStore.information?.isAwg">
@@ -118,6 +127,12 @@ const { data: _data, refresh } = await useFetch(`/api/admin/userconfig`, {
method: 'get', method: 'get',
}); });
const { data: overridesData } = await useFetch(`/api/admin/overrides`, {
method: 'get',
});
const overrides = computed(() => overridesData.value?.userConfig || {});
const data = toRef(_data.value); const data = toRef(_data.value);
const _submit = useSubmit( const _submit = useSubmit(
+11
View File
@@ -7,6 +7,7 @@
v-model="data.sessionTimeout" v-model="data.sessionTimeout"
:label="$t('admin.general.sessionTimeout')" :label="$t('admin.general.sessionTimeout')"
:description="$t('admin.general.sessionTimeoutDesc')" :description="$t('admin.general.sessionTimeoutDesc')"
:overridden="overrides.sessionTimeout"
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
@@ -16,18 +17,21 @@
v-model="data.metricsPassword" v-model="data.metricsPassword"
:label="$t('admin.general.metricsPassword')" :label="$t('admin.general.metricsPassword')"
:description="$t('admin.general.metricsPasswordDesc')" :description="$t('admin.general.metricsPasswordDesc')"
:overridden="overrides.metricsPassword"
/> />
<FormSwitchField <FormSwitchField
id="prometheus" id="prometheus"
v-model="data.metricsPrometheus" v-model="data.metricsPrometheus"
:label="$t('admin.general.prometheus')" :label="$t('admin.general.prometheus')"
:description="$t('admin.general.prometheusDesc')" :description="$t('admin.general.prometheusDesc')"
:overridden="overrides.metricsPrometheus"
/> />
<FormSwitchField <FormSwitchField
id="json" id="json"
v-model="data.metricsJson" v-model="data.metricsJson"
:label="$t('admin.general.json')" :label="$t('admin.general.json')"
:description="$t('admin.general.jsonDesc')" :description="$t('admin.general.jsonDesc')"
:overridden="overrides.metricsJson"
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
@@ -43,6 +47,13 @@
const { data: _data, refresh } = await useFetch(`/api/admin/general`, { const { data: _data, refresh } = await useFetch(`/api/admin/general`, {
method: 'get', method: 'get',
}); });
const { data: overridesData } = await useFetch(`/api/admin/overrides`, {
method: 'get',
});
const overrides = computed(() => overridesData.value?.general || {});
const data = toRef(_data.value); const data = toRef(_data.value);
const _submit = useSubmit( const _submit = useSubmit(
+10
View File
@@ -6,21 +6,25 @@
id="PreUp" id="PreUp"
v-model="data.preUp" v-model="data.preUp"
:label="$t('hooks.preUp')" :label="$t('hooks.preUp')"
:overridden="overrides.preUp"
/> />
<FormTextField <FormTextField
id="PostUp" id="PostUp"
v-model="data.postUp" v-model="data.postUp"
:label="$t('hooks.postUp')" :label="$t('hooks.postUp')"
:overridden="overrides.postUp"
/> />
<FormTextField <FormTextField
id="PreDown" id="PreDown"
v-model="data.preDown" v-model="data.preDown"
:label="$t('hooks.preDown')" :label="$t('hooks.preDown')"
:overridden="overrides.preDown"
/> />
<FormTextField <FormTextField
id="PostDown" id="PostDown"
v-model="data.postDown" v-model="data.postDown"
:label="$t('hooks.postDown')" :label="$t('hooks.postDown')"
:overridden="overrides.postDown"
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
@@ -37,6 +41,12 @@ const { data: _data, refresh } = await useFetch(`/api/admin/hooks`, {
method: 'get', method: 'get',
}); });
const { data: overridesData } = await useFetch(`/api/admin/overrides`, {
method: 'get',
});
const overrides = computed(() => overridesData.value?.hooks || {});
const data = toRef(_data.value); const data = toRef(_data.value);
const _submit = useSubmit( const _submit = useSubmit(
+9
View File
@@ -7,18 +7,21 @@
v-model="data.mtu" v-model="data.mtu"
:label="$t('general.mtu')" :label="$t('general.mtu')"
:description="$t('admin.interface.mtuDesc')" :description="$t('admin.interface.mtuDesc')"
:overridden="overrides.mtu"
/> />
<FormNumberField <FormNumberField
id="port" id="port"
v-model="data.port" v-model="data.port"
:label="$t('general.port')" :label="$t('general.port')"
:description="$t('admin.interface.portDesc')" :description="$t('admin.interface.portDesc')"
:overridden="overrides.port"
/> />
<FormTextField <FormTextField
id="device" id="device"
v-model="data.device" v-model="data.device"
:label="$t('admin.interface.device')" :label="$t('admin.interface.device')"
:description="$t('admin.interface.deviceDesc')" :description="$t('admin.interface.deviceDesc')"
:overridden="overrides.device"
/> />
</FormGroup> </FormGroup>
<FormGroup v-if="globalStore.information?.isAwg"> <FormGroup v-if="globalStore.information?.isAwg">
@@ -164,6 +167,12 @@ const { data: _data, refresh } = await useFetch(`/api/admin/interface`, {
method: 'get', method: 'get',
}); });
const { data: overridesData } = await useFetch(`/api/admin/overrides`, {
method: 'get',
});
const overrides = computed(() => overridesData.value?.interface || {});
const data = toRef(_data.value); const data = toRef(_data.value);
const _submit = useSubmit( const _submit = useSubmit(
+1
View File
@@ -17,6 +17,7 @@ export default definePermissionEventHandler('admin', 'any', async () => {
}, },
general: { general: {
sessionTimeout: WG_GENERAL_OVERRIDE_ENV.SESSION_TIMEOUT !== undefined, sessionTimeout: WG_GENERAL_OVERRIDE_ENV.SESSION_TIMEOUT !== undefined,
metricsPassword: WG_GENERAL_OVERRIDE_ENV.METRICS_PASSWORD !== undefined,
metricsPrometheus: WG_GENERAL_OVERRIDE_ENV.METRICS_PROMETHEUS !== undefined, metricsPrometheus: WG_GENERAL_OVERRIDE_ENV.METRICS_PROMETHEUS !== undefined,
metricsJson: WG_GENERAL_OVERRIDE_ENV.METRICS_JSON !== undefined, metricsJson: WG_GENERAL_OVERRIDE_ENV.METRICS_JSON !== undefined,
}, },
+4 -1
View File
@@ -97,6 +97,8 @@ export const WG_GENERAL_OVERRIDE_ENV = {
SESSION_TIMEOUT: process.env.WG_SESSION_TIMEOUT SESSION_TIMEOUT: process.env.WG_SESSION_TIMEOUT
? Number.parseInt(process.env.WG_SESSION_TIMEOUT, 10) ? Number.parseInt(process.env.WG_SESSION_TIMEOUT, 10)
: undefined, : undefined,
/** Override metrics password */
METRICS_PASSWORD: process.env.WG_METRICS_PASSWORD,
/** Override metrics Prometheus enabled status */ /** Override metrics Prometheus enabled status */
METRICS_PROMETHEUS: process.env.WG_METRICS_PROMETHEUS === 'true' ? true : METRICS_PROMETHEUS: process.env.WG_METRICS_PROMETHEUS === 'true' ? true :
process.env.WG_METRICS_PROMETHEUS === 'false' ? false : process.env.WG_METRICS_PROMETHEUS === 'false' ? false :
@@ -165,11 +167,12 @@ export function applyUserConfigOverrides<
* Apply environment variable overrides to a general config object * Apply environment variable overrides to a general config object
*/ */
export function applyGeneralOverrides< export function applyGeneralOverrides<
T extends { sessionTimeout: number; metricsPrometheus: boolean; metricsJson: boolean }, T extends { sessionTimeout: number; metricsPassword: string | null; metricsPrometheus: boolean; metricsJson: boolean },
>(generalConfig: T): T { >(generalConfig: T): T {
return { return {
...generalConfig, ...generalConfig,
sessionTimeout: WG_GENERAL_OVERRIDE_ENV.SESSION_TIMEOUT ?? generalConfig.sessionTimeout, sessionTimeout: WG_GENERAL_OVERRIDE_ENV.SESSION_TIMEOUT ?? generalConfig.sessionTimeout,
metricsPassword: WG_GENERAL_OVERRIDE_ENV.METRICS_PASSWORD ?? generalConfig.metricsPassword,
metricsPrometheus: WG_GENERAL_OVERRIDE_ENV.METRICS_PROMETHEUS ?? generalConfig.metricsPrometheus, metricsPrometheus: WG_GENERAL_OVERRIDE_ENV.METRICS_PROMETHEUS ?? generalConfig.metricsPrometheus,
metricsJson: WG_GENERAL_OVERRIDE_ENV.METRICS_JSON ?? generalConfig.metricsJson, metricsJson: WG_GENERAL_OVERRIDE_ENV.METRICS_JSON ?? generalConfig.metricsJson,
}; };