Browse Source

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
pull/1740/head
Bernd Storath 3 weeks ago
committed by GitHub
parent
commit
198b240755
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      .vscode/settings.json
  2. 2
      docker-compose.dev.yml
  3. 2
      package.json
  4. 40
      src/app/components/Admin/SuggestDialog.vue
  5. 37
      src/app/components/Base/Select.vue
  6. 48
      src/app/components/Form/HostField.vue
  7. 16
      src/app/components/Icons/ArrowDown.vue
  8. 19
      src/app/components/Icons/ArrowInf.vue
  9. 18
      src/app/components/Icons/ArrowLeftCircle.vue
  10. 18
      src/app/components/Icons/ArrowRightCircle.vue
  11. 16
      src/app/components/Icons/ArrowUp.vue
  12. 15
      src/app/components/Icons/Chart.vue
  13. 18
      src/app/components/Icons/CheckCircle.vue
  14. 18
      src/app/components/Icons/Close.vue
  15. 16
      src/app/components/Icons/Delete.vue
  16. 18
      src/app/components/Icons/Download.vue
  17. 18
      src/app/components/Icons/Edit.vue
  18. 18
      src/app/components/Icons/Info.vue
  19. 18
      src/app/components/Icons/Language.vue
  20. 18
      src/app/components/Icons/Link.vue
  21. 18
      src/app/components/Icons/Logout.vue
  22. 18
      src/app/components/Icons/Moon.vue
  23. 19
      src/app/components/Icons/Plus.vue
  24. 18
      src/app/components/Icons/QRCode.vue
  25. 7
      src/app/components/Icons/Sparkles.vue
  26. 19
      src/app/components/Icons/Stack.vue
  27. 18
      src/app/components/Icons/Sun.vue
  28. 20
      src/app/components/Icons/Warning.vue
  29. 3
      src/app/pages/admin/config.vue
  30. 3
      src/app/pages/setup/4.vue
  31. 7
      src/i18n/locales/en.json
  32. 3
      src/package.json
  33. 13
      src/pnpm-lock.yaml
  34. 4
      src/server/api/admin/ip-info.get.ts
  35. 4
      src/server/api/setup/4.get.ts
  36. 29
      src/server/utils/cache.ts
  37. 141
      src/server/utils/ip.ts
  38. 27
      src/server/utils/release.ts
  39. 5
      src/server/utils/session.ts

3
.vscode/settings.json

@ -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"
},

2
docker-compose.dev.yml

@ -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

2
package.json

@ -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": "[email protected].2"
"packageManager": "[email protected].3"
}

40
src/app/components/Admin/SuggestDialog.vue

@ -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

@ -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

@ -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>

16
src/app/components/Icons/ArrowDown.vue

@ -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>

19
src/app/components/Icons/ArrowInf.vue

@ -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>

18
src/app/components/Icons/ArrowLeftCircle.vue

@ -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>

18
src/app/components/Icons/ArrowRightCircle.vue

@ -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>

16
src/app/components/Icons/ArrowUp.vue

@ -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>

15
src/app/components/Icons/Chart.vue

@ -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>

18
src/app/components/Icons/CheckCircle.vue

@ -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>

18
src/app/components/Icons/Close.vue

@ -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>

16
src/app/components/Icons/Delete.vue

@ -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>

18
src/app/components/Icons/Download.vue

@ -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>

18
src/app/components/Icons/Edit.vue

@ -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>

18
src/app/components/Icons/Info.vue

@ -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>

18
src/app/components/Icons/Language.vue

@ -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>

18
src/app/components/Icons/Link.vue

@ -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>

18
src/app/components/Icons/Logout.vue

@ -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>

18
src/app/components/Icons/Moon.vue

@ -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>

19
src/app/components/Icons/Plus.vue

@ -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>

18
src/app/components/Icons/QRCode.vue

@ -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

@ -0,0 +1,7 @@
<template>
<SparklesIcon />
</template>
<script lang="ts" setup>
import SparklesIcon from '@heroicons/vue/24/outline/esm/SparklesIcon';
</script>

19
src/app/components/Icons/Stack.vue

@ -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>

18
src/app/components/Icons/Sun.vue

@ -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>

20
src/app/components/Icons/Warning.vue

@ -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
src/app/pages/admin/config.vue

@ -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"

3
src/app/pages/setup/4.vue

@ -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">

7
src/i18n/locales/en.json

@ -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",

3
src/package.json

@ -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": "[email protected].2"
"packageManager": "[email protected].3"
}

13
src/pnpm-lock.yaml

@ -11,6 +11,9 @@ importers:
'@eschricht/nuxt-color-mode':
specifier: ^1.1.5
version: 1.1.5([email protected])
'@heroicons/vue':
specifier: ^2.2.0
version: 2.2.0([email protected]([email protected]))
'@libsql/client':
specifier: ^0.14.0
version: 0.14.0
@ -791,6 +794,11 @@ packages:
'@floating-ui/[email protected]':
resolution: {integrity: sha512-XFlUzGHGv12zbgHNk5FN2mUB7ROul3oG2ENdTpWdE+qMFxyNxWSRmsoyhiEnpmabNm6WnUvR1OvJfUfN4ojC1A==}
'@heroicons/[email protected]':
resolution: {integrity: sha512-G3dbSxoeEKqbi/DFalhRxJU4mTXJn7GwZ7ae8NuEQzd1bqdd0jAbdaBZlHPcvPD2xI1iGzNVB4k20Un2AguYPw==}
peerDependencies:
vue: '>= 3'
'@humanfs/[email protected]':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@ -3403,7 +3411,6 @@ packages:
[email protected]:
resolution: {integrity: sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw==}
cpu: [x64, arm64, wasm32]
os: [darwin, linux, win32]
[email protected]:
@ -5761,6 +5768,10 @@ snapshots:
- '@vue/composition-api'
- vue
'@heroicons/[email protected]([email protected]([email protected]))':
dependencies:
vue: 3.5.13([email protected])
'@humanfs/[email protected]': {}
'@humanfs/[email protected]':

4
src/server/api/admin/ip-info.get.ts

@ -0,0 +1,4 @@
export default definePermissionEventHandler('admin', 'any', async () => {
const result = await cachedGetIpInformation();
return result;
});

4
src/server/api/setup/4.get.ts

@ -0,0 +1,4 @@
export default defineSetupEventHandler(4, async () => {
const result = await cachedGetIpInformation();
return result;
});

29
src/server/utils/cache.ts

@ -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;
};
}

141
src/server/utils/ip.ts

@ -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,
});

27
src/server/utils/release.ts

@ -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,
});

5
src/server/utils/session.ts

@ -91,6 +91,11 @@ export async function getCurrentUser(event: H3Event) {
});
}
user = foundUser;
} else {
throw createError({
statusCode: 401,
statusMessage: 'Session failed. No Authorization',
});
}
if (!user) {

Loading…
Cancel
Save