Feat: Suggest IP or Hostname (#1739)
* get ip and hostnames * use heroicons * add host field * get private info * unstyled prototype * styled select * add to setup * fix types
This commit is contained in:
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -6,6 +6,9 @@
|
||||
"nuxtr.vueFiles.style.addStyleTag": false,
|
||||
"nuxtr.piniaFiles.defaultTemplate": "setup",
|
||||
"nuxtr.monorepoMode.DirectoryName": "src",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "always"
|
||||
},
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@ services:
|
||||
- NET_ADMIN
|
||||
- SYS_MODULE
|
||||
environment:
|
||||
- INIT_ENABLED=true
|
||||
- INIT_ENABLED=false
|
||||
- INIT_HOST=test
|
||||
- INIT_PORT=51820
|
||||
- INIT_USERNAME=testtest
|
||||
|
||||
@@ -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.2"
|
||||
"packageManager": "pnpm@10.6.3"
|
||||
}
|
||||
|
||||
40
src/app/components/Admin/SuggestDialog.vue
Normal file
40
src/app/components/Admin/SuggestDialog.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<BaseDialog :trigger-class="triggerClass">
|
||||
<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">
|
||||
<p>{{ $t('admin.config.suggestDesc') }}</p>
|
||||
<BaseSelect v-model="selected" :options="values" />
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<DialogClose as-child>
|
||||
<BaseButton>{{ $t('dialog.cancel') }}</BaseButton>
|
||||
</DialogClose>
|
||||
<DialogClose as-child>
|
||||
<BaseButton @click="$emit('change', selected)">
|
||||
{{ $t('dialog.change') }}
|
||||
</BaseButton>
|
||||
</DialogClose>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineEmits(['change']);
|
||||
const props = defineProps<{
|
||||
triggerClass?: string;
|
||||
url: '/api/admin/ip-info' | '/api/setup/4';
|
||||
}>();
|
||||
|
||||
const { data } = await useFetch(props.url, {
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
const selected = ref<string>();
|
||||
const values = toRef(data.value);
|
||||
</script>
|
||||
37
src/app/components/Base/Select.vue
Normal file
37
src/app/components/Base/Select.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<SelectRoot v-model="selected">
|
||||
<SelectTrigger
|
||||
class="inline-flex h-8 items-center justify-around gap-2 rounded bg-gray-200 px-3 text-sm leading-none dark:bg-neutral-500 dark:text-neutral-200"
|
||||
aria-label="Choose option"
|
||||
>
|
||||
<SelectValue placeholder="Select..." />
|
||||
<IconsArrowDown class="size-3" />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectPortal>
|
||||
<SelectContent
|
||||
class="z-[100] min-w-28 rounded bg-gray-300 dark:bg-neutral-500"
|
||||
>
|
||||
<SelectViewport class="p-2">
|
||||
<SelectItem
|
||||
v-for="(option, index) in options"
|
||||
:key="index"
|
||||
:value="option.value"
|
||||
class="relative flex h-6 items-center rounded px-3 text-sm leading-none outline-none hover:bg-red-800 hover:text-white dark:text-white"
|
||||
>
|
||||
<SelectItemText>
|
||||
{{ option.value }} - {{ option.label }}
|
||||
</SelectItemText>
|
||||
</SelectItem>
|
||||
</SelectViewport>
|
||||
</SelectContent>
|
||||
</SelectPortal>
|
||||
</SelectRoot>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
options: { label: string; value: string }[];
|
||||
}>();
|
||||
const selected = defineModel<string>();
|
||||
</script>
|
||||
48
src/app/components/Form/HostField.vue
Normal file
48
src/app/components/Form/HostField.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<FormLabel :for="id">
|
||||
{{ label }}
|
||||
</FormLabel>
|
||||
<BaseTooltip v-if="description" :text="description">
|
||||
<IconsInfo class="size-4" />
|
||||
</BaseTooltip>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<BaseInput
|
||||
:id="id"
|
||||
v-model.trim="data"
|
||||
:name="id"
|
||||
type="text"
|
||||
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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
id: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
url: '/api/admin/ip-info' | '/api/setup/4';
|
||||
}>();
|
||||
|
||||
const data = defineModel<string | null>({
|
||||
set(value) {
|
||||
const temp = value?.trim() ?? null;
|
||||
if (temp === '') {
|
||||
return null;
|
||||
}
|
||||
return temp;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,13 +1,7 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.707 10.293a1 1 0 010 1.414l-6 6a1 1 0 01-1.414 0l-6-6a1 1 0 111.414-1.414L9 14.586V3a1 1 0 012 0v11.586l4.293-4.293a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<ArrowDownIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ArrowDownIcon from '@heroicons/vue/24/outline/esm/ArrowDownIcon';
|
||||
</script>
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
<template>
|
||||
<svg
|
||||
inline
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
|
||||
/>
|
||||
</svg>
|
||||
<ArrowPathIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ArrowPathIcon from '@heroicons/vue/24/outline/esm/ArrowPathIcon';
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m11.25 9-3 3m0 0 3 3m-3-3h7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||
/>
|
||||
</svg>
|
||||
<ArrowLeftCircleIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ArrowLeftCircleIcon from '@heroicons/vue/24/outline/esm/ArrowLeftCircleIcon';
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m12.75 15 3-3m0 0-3-3m3 3h-7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||
/>
|
||||
</svg>
|
||||
<ArrowRightCircleIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ArrowRightCircleIcon from '@heroicons/vue/24/outline/esm/ArrowLeftCircleIcon';
|
||||
</script>
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M3.293 9.707a1 1 0 010-1.414l6-6a1 1 0 011.414 0l6 6a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L4.707 9.707a1 1 0 01-1.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<ArrowUpIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ArrowUpIcon from '@heroicons/vue/24/outline/esm/ArrowUpIcon';
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M18.375 2.25c-1.035 0-1.875.84-1.875 1.875v15.75c0 1.035.84 1.875 1.875 1.875h.75c1.035 0 1.875-.84 1.875-1.875V4.125c0-1.036-.84-1.875-1.875-1.875h-.75ZM9.75 8.625c0-1.036.84-1.875 1.875-1.875h.75c1.036 0 1.875.84 1.875 1.875v11.25c0 1.035-.84 1.875-1.875 1.875h-.75a1.875 1.875 0 0 1-1.875-1.875V8.625ZM3 13.125c0-1.036.84-1.875 1.875-1.875h.75c1.036 0 1.875.84 1.875 1.875v6.75c0 1.035-.84 1.875-1.875 1.875h-.75A1.875 1.875 0 0 1 3 19.875v-6.75Z"
|
||||
/>
|
||||
</svg>
|
||||
<ChartBarIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ChartBarIcon from '@heroicons/vue/24/outline/esm/ChartBarIcon';
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||
/>
|
||||
</svg>
|
||||
<CheckCircleIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import CheckCircleIcon from '@heroicons/vue/24/outline/esm/CheckCircleIcon';
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
<XMarkIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import XMarkIcon from '@heroicons/vue/24/outline/esm/XMarkIcon';
|
||||
</script>
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<TrashIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import TrashIcon from '@heroicons/vue/24/outline/esm/TrashIcon';
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||
/>
|
||||
</svg>
|
||||
<ArrowDownTrayIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ArrowDownTrayIcon from '@heroicons/vue/24/outline/esm/ArrowDownTrayIcon';
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||
/>
|
||||
</svg>
|
||||
<PencilSquareIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import PencilSquareIcon from '@heroicons/vue/24/outline/esm/PencilSquareIcon';
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
|
||||
/>
|
||||
</svg>
|
||||
<InformationCircleIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import InformationCircleIcon from '@heroicons/vue/24/outline/esm/InformationCircleIcon';
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m10.5 21 5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 0 1 6-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 0 1-3.827-5.802"
|
||||
/>
|
||||
</svg>
|
||||
<LanguageIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import LanguageIcon from '@heroicons/vue/24/outline/esm/LanguageIcon';
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13.213 9.787a3.391 3.391 0 0 0-4.795 0l-3.425 3.426a3.39 3.39 0 0 0 4.795 4.794l.321-.304m-.321-4.49a3.39 3.39 0 0 0 4.795 0l3.424-3.426a3.39 3.39 0 0 0-4.794-4.795l-1.028.961"
|
||||
/>
|
||||
</svg>
|
||||
<LinkIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import LinkIcon from '@heroicons/vue/24/outline/esm/LinkIcon';
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
<ArrowRightStartOnRectangleIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ArrowRightStartOnRectangleIcon from '@heroicons/vue/24/outline/esm/ArrowRightStartOnRectangleIcon';
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z"
|
||||
/>
|
||||
</svg>
|
||||
<MoonIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MoonIcon from '@heroicons/vue/24/outline/esm/MoonIcon';
|
||||
</script>
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
<template>
|
||||
<svg
|
||||
inline
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||
/>
|
||||
</svg>
|
||||
<PlusIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import PlusIcon from '@heroicons/vue/24/outline/esm/PlusIcon';
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z"
|
||||
/>
|
||||
</svg>
|
||||
<QrCodeIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import QrCodeIcon from '@heroicons/vue/24/outline/esm/QrCodeIcon';
|
||||
</script>
|
||||
|
||||
7
src/app/components/Icons/Sparkles.vue
Normal file
7
src/app/components/Icons/Sparkles.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<SparklesIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import SparklesIcon from '@heroicons/vue/24/outline/esm/SparklesIcon';
|
||||
</script>
|
||||
@@ -1,16 +1,7 @@
|
||||
<template>
|
||||
<svg
|
||||
inline
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M5.25 14.25h13.5m-13.5 0a3 3 0 0 1-3-3m3 3a3 3 0 1 0 0 6h13.5a3 3 0 1 0 0-6m-16.5-3a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3m-19.5 0a4.5 4.5 0 0 1 .9-2.7L5.737 5.1a3.375 3.375 0 0 1 2.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 0 1 .9 2.7m0 0a3 3 0 0 1-3 3m0 3h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Zm-3 6h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Z"
|
||||
/>
|
||||
</svg>
|
||||
<ServerStackIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ServerStackIcon from '@heroicons/vue/24/outline/esm/ServerStackIcon';
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"
|
||||
/>
|
||||
</svg>
|
||||
<SunIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import SunIcon from '@heroicons/vue/24/outline/esm/SunIcon';
|
||||
</script>
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
<template>
|
||||
<!-- Heroicon name: outline/exclamation -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<ExclamationTriangleIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ExclamationTriangleIcon from '@heroicons/vue/24/outline/esm/ExclamationTriangleIcon';
|
||||
</script>
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
<FormElement @submit.prevent="submit">
|
||||
<FormGroup>
|
||||
<FormHeading>{{ $t('admin.config.connection') }}</FormHeading>
|
||||
<FormTextField
|
||||
<FormHostField
|
||||
id="host"
|
||||
v-model="data.host"
|
||||
:label="$t('general.host')"
|
||||
:description="$t('admin.config.hostDesc')"
|
||||
url="/api/admin/ip-info"
|
||||
/>
|
||||
<FormNumberField
|
||||
id="port"
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
</p>
|
||||
<div class="mt-8 flex flex-col gap-3">
|
||||
<div class="flex flex-col">
|
||||
<FormNullTextField
|
||||
<FormHostField
|
||||
id="host"
|
||||
v-model="host"
|
||||
:label="$t('general.host')"
|
||||
placeholder="vpn.example.com"
|
||||
:description="$t('setup.hostDesc')"
|
||||
url="/api/setup/4"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"port": "Port",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"confirmPassword": "Confirm Password"
|
||||
"confirmPassword": "Confirm Password",
|
||||
"loading": "Loading"
|
||||
},
|
||||
"setup": {
|
||||
"welcome": "Welcome to your first setup of wg-easy",
|
||||
@@ -147,7 +148,9 @@
|
||||
"allowedIpsDesc": "Allowed IPs clients will use (global config)",
|
||||
"dnsDesc": "DNS server clients will use (global config)",
|
||||
"mtuDesc": "MTU clients will use (only for new clients)",
|
||||
"persistentKeepaliveDesc": "Interval in seconds to send keepalives to the server. 0 = disabled (only for new clients)"
|
||||
"persistentKeepaliveDesc": "Interval in seconds to send keepalives to the server. 0 = disabled (only for new clients)",
|
||||
"suggest": "Suggest",
|
||||
"suggestDesc": "Choose a IP-Address or Hostname for the Host field"
|
||||
},
|
||||
"interface": {
|
||||
"cidrSuccess": "Changed CIDR",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@eschricht/nuxt-color-mode": "^1.1.5",
|
||||
"@heroicons/vue": "^2.2.0",
|
||||
"@libsql/client": "^0.14.0",
|
||||
"@nuxtjs/i18n": "^9.3.1",
|
||||
"@nuxtjs/tailwindcss": "^6.13.2",
|
||||
@@ -60,5 +61,5 @@
|
||||
"typescript": "^5.8.2",
|
||||
"vue-tsc": "^2.2.8"
|
||||
},
|
||||
"packageManager": "pnpm@10.6.2"
|
||||
"packageManager": "pnpm@10.6.3"
|
||||
}
|
||||
|
||||
13
src/pnpm-lock.yaml
generated
13
src/pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
||||
'@eschricht/nuxt-color-mode':
|
||||
specifier: ^1.1.5
|
||||
version: 1.1.5(magicast@0.3.5)
|
||||
'@heroicons/vue':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0(vue@3.5.13(typescript@5.8.2))
|
||||
'@libsql/client':
|
||||
specifier: ^0.14.0
|
||||
version: 0.14.0
|
||||
@@ -791,6 +794,11 @@ packages:
|
||||
'@floating-ui/vue@1.1.6':
|
||||
resolution: {integrity: sha512-XFlUzGHGv12zbgHNk5FN2mUB7ROul3oG2ENdTpWdE+qMFxyNxWSRmsoyhiEnpmabNm6WnUvR1OvJfUfN4ojC1A==}
|
||||
|
||||
'@heroicons/vue@2.2.0':
|
||||
resolution: {integrity: sha512-G3dbSxoeEKqbi/DFalhRxJU4mTXJn7GwZ7ae8NuEQzd1bqdd0jAbdaBZlHPcvPD2xI1iGzNVB4k20Un2AguYPw==}
|
||||
peerDependencies:
|
||||
vue: '>= 3'
|
||||
|
||||
'@humanfs/core@0.19.1':
|
||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
@@ -3403,7 +3411,6 @@ packages:
|
||||
|
||||
libsql@0.4.7:
|
||||
resolution: {integrity: sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw==}
|
||||
cpu: [x64, arm64, wasm32]
|
||||
os: [darwin, linux, win32]
|
||||
|
||||
lilconfig@3.1.3:
|
||||
@@ -5761,6 +5768,10 @@ snapshots:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
|
||||
'@heroicons/vue@2.2.0(vue@3.5.13(typescript@5.8.2))':
|
||||
dependencies:
|
||||
vue: 3.5.13(typescript@5.8.2)
|
||||
|
||||
'@humanfs/core@0.19.1': {}
|
||||
|
||||
'@humanfs/node@0.16.6':
|
||||
|
||||
4
src/server/api/admin/ip-info.get.ts
Normal file
4
src/server/api/admin/ip-info.get.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePermissionEventHandler('admin', 'any', async () => {
|
||||
const result = await cachedGetIpInformation();
|
||||
return result;
|
||||
});
|
||||
4
src/server/api/setup/4.get.ts
Normal file
4
src/server/api/setup/4.get.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default defineSetupEventHandler(4, async () => {
|
||||
const result = await cachedGetIpInformation();
|
||||
return result;
|
||||
});
|
||||
29
src/server/utils/cache.ts
Normal file
29
src/server/utils/cache.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
type Opts = {
|
||||
/**
|
||||
* Expiry time in milliseconds
|
||||
*/
|
||||
expiry: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Cache function for 1 hour
|
||||
*/
|
||||
export function cacheFunction<T>(fn: () => T, { expiry }: Opts): () => T {
|
||||
let cache: { value: T; expiry: number } | null = null;
|
||||
|
||||
return (): T => {
|
||||
const now = Date.now();
|
||||
|
||||
if (cache && cache.expiry > now) {
|
||||
return cache.value;
|
||||
}
|
||||
|
||||
const result = fn();
|
||||
cache = {
|
||||
value: result,
|
||||
expiry: now + expiry,
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { parseCidr } from 'cidr-tools';
|
||||
import { Resolver } from 'node:dns/promises';
|
||||
import { networkInterfaces } from 'node:os';
|
||||
import { stringifyIp } from 'ip-bigint';
|
||||
import type { parseCidr } from 'cidr-tools';
|
||||
|
||||
import type { ClientNextIpType } from '#db/repositories/client/types';
|
||||
|
||||
@@ -31,3 +33,140 @@ export function nextIP(
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
// use opendns to get public ip
|
||||
const dnsServers = {
|
||||
ip4: ['208.67.222.222'],
|
||||
ip6: ['2620:119:35::35'],
|
||||
ip: 'myip.opendns.com',
|
||||
};
|
||||
|
||||
async function getPublicInformation() {
|
||||
const ipv4 = await getPublicIpv4();
|
||||
const ipv6 = await getPublicIpv6();
|
||||
|
||||
const ptr4 = ipv4 ? await getReverseDns(ipv4) : [];
|
||||
const ptr6 = ipv6 ? await getReverseDns(ipv6) : [];
|
||||
const hostnames = [...new Set([...ptr4, ...ptr6])];
|
||||
|
||||
return { ipv4, ipv6, hostnames };
|
||||
}
|
||||
|
||||
async function getPublicIpv4() {
|
||||
try {
|
||||
const resolver = new Resolver();
|
||||
resolver.setServers(dnsServers.ip4);
|
||||
const ipv4 = await resolver.resolve4(dnsServers.ip);
|
||||
return ipv4[0];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getPublicIpv6() {
|
||||
try {
|
||||
const resolver = new Resolver();
|
||||
resolver.setServers(dnsServers.ip6);
|
||||
const ipv6 = await resolver.resolve6(dnsServers.ip);
|
||||
return ipv6[0];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getReverseDns(ip: string) {
|
||||
try {
|
||||
const resolver = new Resolver();
|
||||
resolver.setServers([...dnsServers.ip4, ...dnsServers.ip6]);
|
||||
const ptr = await resolver.reverse(ip);
|
||||
return ptr;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function getPrivateInformation() {
|
||||
const interfaces = networkInterfaces();
|
||||
|
||||
const interfaceNames = Object.keys(interfaces);
|
||||
|
||||
const obj: Record<string, { ipv4: string[]; ipv6: string[] }> = {};
|
||||
|
||||
for (const name of interfaceNames) {
|
||||
if (name === 'wg0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const iface = interfaces[name];
|
||||
if (!iface) continue;
|
||||
|
||||
for (const { family, internal, address } of iface) {
|
||||
if (internal) {
|
||||
continue;
|
||||
}
|
||||
if (!obj[name]) {
|
||||
obj[name] = {
|
||||
ipv4: [],
|
||||
ipv6: [],
|
||||
};
|
||||
}
|
||||
if (family === 'IPv4') {
|
||||
obj[name].ipv4.push(address);
|
||||
} else if (family === 'IPv6') {
|
||||
obj[name].ipv6.push(address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
async function getIpInformation() {
|
||||
const results = [];
|
||||
|
||||
const publicInfo = await getPublicInformation();
|
||||
if (publicInfo.ipv4) {
|
||||
results.push({
|
||||
value: publicInfo.ipv4,
|
||||
label: 'IPv4 - Public',
|
||||
});
|
||||
}
|
||||
if (publicInfo.ipv6) {
|
||||
results.push({
|
||||
value: `[${publicInfo.ipv6}]`,
|
||||
label: 'IPv6 - Public',
|
||||
});
|
||||
}
|
||||
for (const hostname of publicInfo.hostnames) {
|
||||
results.push({
|
||||
value: hostname,
|
||||
label: 'Hostname - Public',
|
||||
});
|
||||
}
|
||||
|
||||
const privateInfo = getPrivateInformation();
|
||||
for (const [name, { ipv4, ipv6 }] of Object.entries(privateInfo)) {
|
||||
for (const ip of ipv4) {
|
||||
results.push({
|
||||
value: ip,
|
||||
label: `IPv4 - ${name}`,
|
||||
});
|
||||
}
|
||||
for (const ip of ipv6) {
|
||||
results.push({
|
||||
value: `[${ip}]`,
|
||||
label: `IPv6 - ${name}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch IP Information
|
||||
* @cache Response is cached for 15 min
|
||||
*/
|
||||
export const cachedGetIpInformation = cacheFunction(getIpInformation, {
|
||||
expiry: 15 * 60 * 1000,
|
||||
});
|
||||
|
||||
@@ -3,29 +3,6 @@ type GithubRelease = {
|
||||
body: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Cache function for 1 hour
|
||||
*/
|
||||
function cacheFunction<T>(fn: () => T): () => T {
|
||||
let cache: { value: T; expiry: number } | null = null;
|
||||
|
||||
return (): T => {
|
||||
const now = Date.now();
|
||||
|
||||
if (cache && cache.expiry > now) {
|
||||
return cache.value;
|
||||
}
|
||||
|
||||
const result = fn();
|
||||
cache = {
|
||||
value: result,
|
||||
expiry: now + 3600000,
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchLatestRelease() {
|
||||
try {
|
||||
const response = await $fetch<GithubRelease>(
|
||||
@@ -53,4 +30,6 @@ async function fetchLatestRelease() {
|
||||
* Fetch latest release from GitHub
|
||||
* @cache Response is cached for 1 hour
|
||||
*/
|
||||
export const cachedFetchLatestRelease = cacheFunction(fetchLatestRelease);
|
||||
export const cachedFetchLatestRelease = cacheFunction(fetchLatestRelease, {
|
||||
expiry: 60 * 60 * 1000,
|
||||
});
|
||||
|
||||
@@ -91,6 +91,11 @@ export async function getCurrentUser(event: H3Event) {
|
||||
});
|
||||
}
|
||||
user = foundUser;
|
||||
} else {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Session failed. No Authorization',
|
||||
});
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
|
||||
Reference in New Issue
Block a user