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:
Bernd Storath
2025-03-14 10:33:02 +01:00
committed by GitHub
parent 86bdbe4c3d
commit 198b240755
39 changed files with 450 additions and 302 deletions

View File

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

View File

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

29
src/server/utils/cache.ts Normal file
View 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;
};
}

View File

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

View File

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

View File

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