add IPCalculator View (#557)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled

* create IPCalculator View + add cidr_tools package

* fixed translation and comma separated ip as placeholder
This commit is contained in:
Tomáš Lukča
2025-11-03 17:40:42 +01:00
committed by GitHub
parent da76327569
commit d66a4b71b8
6 changed files with 196 additions and 8 deletions

View File

@@ -133,6 +133,9 @@ const userDisplayName = computed(() => {
<li class="nav-item">
<RouterLink :to="{ name: 'key-generator' }" class="nav-link">{{ $t('menu.keygen') }}</RouterLink>
</li>
<li class="nav-item">
<RouterLink :to="{ name: 'ip-calculator' }" class="nav-link">{{ $t('menu.calculator') }}</RouterLink>
</li>
</ul>
<div class="navbar-nav d-flex justify-content-end">

View File

@@ -42,7 +42,8 @@
"audit": "Audit Log",
"login": "Login",
"logout": "Logout",
"keygen": "Key Generator"
"keygen": "Key Generator",
"calculator": "IP Calculator"
},
"home": {
"headline": "WireGuard® VPN Portal",
@@ -269,6 +270,26 @@
"placeholder": "The pre-shared key"
}
},
"calculator": {
"headline": "WireGuard IP Calculator",
"abstract": "Generate a WireGuard Allowed IPs. The IP subnets are generated in your local browser and are never sent to the server.",
"headline-allowed-ip": "New Allowed IPs",
"button-exclude-private": "Exclude Private IP Ranges",
"allowed-ip": {
"label": "Allowed IPs",
"placeholder": "0.0.0.0/0, ::/0",
"empty": "Value cannot be empty"
},
"dissallowed-ip": {
"label": "Disallowed IPs",
"placeholder": "10.0.0.0/8, 192.168.0.0/16",
"invalid": "Invalid address: {addr}"
},
"new-allowed-ip": {
"label": "Allowed IPs",
"placeholder": ""
}
},
"modals": {
"user-view": {
"headline": "User Account:",

View File

@@ -72,6 +72,14 @@ const router = createRouter({
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/KeyGeneraterView.vue')
},
{
path: '/ip-calculator',
name: 'ip-calculator',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/IPCalculatorView.vue')
}
],
linkActiveClass: "active",
@@ -122,7 +130,7 @@ router.beforeEach(async (to) => {
}
// redirect to login page if not logged in and trying to access a restricted page
const publicPages = ['/', '/login', '/key-generator']
const publicPages = ['/', '/login', '/key-generator', '/ip-calculator']
const authRequired = !publicPages.includes(to.path)
if (authRequired && !auth.IsAuthenticated) {

View File

@@ -0,0 +1,139 @@
<script setup>
import {ref, watch, computed} from "vue";
import isCidr from "is-cidr";
import {isIP} from "is-ip";
import {excludeCidr} from "cidr-tools";
import {useI18n} from 'vue-i18n';
const allowedIp = ref("")
const dissallowedIp = ref("")
const privateIP = ref("10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16")
const {t} = useI18n()
const errorAllowed = ref("")
const errorDissallowed = ref("")
/**
* Validate a comma-separated list of IP and/or CIDR addresses.
* @function validateIpAndCidrList
* @param {string} value - Comma-separated string (e.g. "10.0.0.0/8, 192.168.0.1")
* @returns {true|string} Returns true if all values are valid, otherwise an error message.
*/
function validateIpAndCidrList(value) {
const list = value.split(",").map(v => v.trim()).filter(Boolean);
if (list.length === 0) {
return t('calculator.allowed-ip.empty');
}
for (const addr of list) {
if (!isIP(addr) && !isCidr(addr)) {
return t('calculator.dissallowed-ip.invalid', {addr});
}
}
return true;
}
/**
* Watcher that validates allowed IPs input in real-time.
* Updates `errorAllowed` whenever `allowedIp` changes.
*/
watch(allowedIp, (newValue) => {
const result = validateIpAndCidrList(newValue);
errorAllowed.value = result === true ? "" : result;
});
/**
* Watcher that validates disallowed IPs input in real-time.
* Updates `errorDissallowed` whenever `dissallowedIp` changes.
*/
watch(dissallowedIp, (newValue) => {
if (!allowedIp.value || allowedIp.value.trim() === "") {
allowedIp.value = "0.0.0.0/0";
}
const result = validateIpAndCidrList(newValue);
errorDissallowed.value = result === true ? "" : result;
});
/**
* Dynamically computes the resulting "Allowed IPs" list
* by excluding the disallowed ranges from the allowed ranges.
* @constant
* @type {ComputedRef<string>}
* @returns {string} A comma-separated string of resulting CIDR blocks.
*/
const newAllowedIp = computed(() => {
if (errorAllowed.value || errorDissallowed.value) return "";
try {
const allowedList = allowedIp.value.split(",").map(v => v.trim()).filter(Boolean);
const disallowedList = dissallowedIp.value.split(",").map(v => v.trim()).filter(Boolean);
const result = excludeCidr(allowedList, disallowedList);
return result.join(", ");
} catch (e) {
console.error("Allowed IPs calculation error:", e);
return "";
}
});
/**
* Append private IP ranges to disallowed IPs.
* If any already exist, they are preserved and new ones are appended only if not present.
* @function addPrivateIPs
*/
function addPrivateIPs() {
const privateList = privateIP.value.split(",").map(v => v.trim());
const currentList = dissallowedIp.value.split(",").map(v => v.trim()).filter(Boolean);
const combined = Array.from(new Set([...currentList, ...privateList]));
dissallowedIp.value = combined.join(", ");
}
</script>
<template>
<div class="page-header">
<h1>{{ $t('calculator.headline') }}</h1>
</div>
<p class="lead">{{ $t('calculator.abstract') }}</p>
<div class="mt-4 row">
<div class="col-12 col-lg-5">
<fieldset>
<div class="form-group">
<label class="form-label mt-4">{{ $t('calculator.allowed-ip.label') }}</label>
<input class="form-control" v-model="allowedIp" :placeholder="$t('calculator.allowed-ip.placeholder')" :class="{ 'is-invalid': errorAllowed }">
<div v-if="errorAllowed" class="text-danger mt-1">{{ errorAllowed }}</div>
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('calculator.dissallowed-ip.label') }}</label>
<input class="form-control" v-model="dissallowedIp" :placeholder="$t('calculator.dissallowed-ip.placeholder')" :class="{ 'is-invalid': errorDissallowed }">
<div v-if="errorDissallowed" class="text-danger mt-1">{{ errorDissallowed }}</div>
</div>
</fieldset>
<fieldset>
<hr class="mt-4">
<button class="btn btn-primary mb-4" type="button" @click="addPrivateIPs">{{ $t('calculator.button-exclude-private') }}</button>
</fieldset>
</div>
<div class="col-12 col-lg-2 mt-sm-4">
</div>
<div class="col-12 col-lg-5">
<h1>{{ $t('calculator.headline-allowed-ip') }}</h1>
<fieldset>
<div class="form-group">
<textarea class="form-control" :value="newAllowedIp" rows="6" :placeholder="$t('calculator.new-allowed-ip.placeholder')" readonly></textarea>
</div>
</fieldset>
</div>
</div>
</template>
<style scoped>
</style>