mirror of https://github.com/h44z/wg-portal.git
Compare commits
1 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
d66a4b71b8 |
|
|
@ -16,6 +16,7 @@
|
||||||
"@vojtechlanka/vue-tags-input": "^3.1.1",
|
"@vojtechlanka/vue-tags-input": "^3.1.1",
|
||||||
"bootstrap": "^5.3.7",
|
"bootstrap": "^5.3.7",
|
||||||
"bootswatch": "^5.3.7",
|
"bootswatch": "^5.3.7",
|
||||||
|
"cidr-tools": "^11.0.3",
|
||||||
"flag-icons": "^7.3.2",
|
"flag-icons": "^7.3.2",
|
||||||
"ip-address": "^10.0.1",
|
"ip-address": "^10.0.1",
|
||||||
"is-cidr": "^5.1.1",
|
"is-cidr": "^5.1.1",
|
||||||
|
|
@ -920,7 +921,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/popperjs"
|
"url": "https://opencollective.com/popperjs"
|
||||||
|
|
@ -1492,6 +1492,18 @@
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cidr-tools": {
|
||||||
|
"version": "11.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/cidr-tools/-/cidr-tools-11.0.3.tgz",
|
||||||
|
"integrity": "sha512-7p0rp7B2P+nZfBkJlrQzUMDyUHeYK2h/XCJY80VUl1v5oxwLxQjZMy39BXVOXugwAX67l0oJ/QQ6OhANgUtUbw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-bigint": "^8.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/clone-regexp": {
|
"node_modules/clone-regexp": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-3.0.0.tgz",
|
||||||
|
|
@ -1706,6 +1718,15 @@
|
||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ip-bigint": {
|
||||||
|
"version": "8.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip-bigint/-/ip-bigint-8.2.2.tgz",
|
||||||
|
"integrity": "sha512-wPoOpHigOtoY29UCFA0L82cJVFcT7M+TsrgipUVpFw7HV9LpLEuNXCymt3623jzHPlIZzFaCyaVf9VACssFYew==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ip-regex": {
|
"node_modules/ip-regex": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz",
|
||||||
|
|
@ -2047,7 +2068,6 @@
|
||||||
"integrity": "sha512-FvQdkn2dZ8DGiLgi0Uf4zsj7r/BsiLImNa5QJ10eZalY6NfZyjrmWGFcuCN5jNwlDlXFJnftauv+UtvBKLvepQ==",
|
"integrity": "sha512-FvQdkn2dZ8DGiLgi0Uf4zsj7r/BsiLImNa5QJ10eZalY6NfZyjrmWGFcuCN5jNwlDlXFJnftauv+UtvBKLvepQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bufbuild/protobuf": "^2.5.0",
|
"@bufbuild/protobuf": "^2.5.0",
|
||||||
"buffer-builder": "^0.2.0",
|
"buffer-builder": "^0.2.0",
|
||||||
|
|
@ -2539,7 +2559,6 @@
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
@ -2581,7 +2600,6 @@
|
||||||
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
|
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
|
|
@ -2675,7 +2693,6 @@
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
@ -2688,7 +2705,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
|
||||||
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
|
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.22",
|
"@vue/compiler-dom": "3.5.22",
|
||||||
"@vue/compiler-sfc": "3.5.22",
|
"@vue/compiler-sfc": "3.5.22",
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
"@vojtechlanka/vue-tags-input": "^3.1.1",
|
"@vojtechlanka/vue-tags-input": "^3.1.1",
|
||||||
"bootstrap": "^5.3.7",
|
"bootstrap": "^5.3.7",
|
||||||
"bootswatch": "^5.3.7",
|
"bootswatch": "^5.3.7",
|
||||||
|
"cidr-tools": "^11.0.3",
|
||||||
"flag-icons": "^7.3.2",
|
"flag-icons": "^7.3.2",
|
||||||
"ip-address": "^10.0.1",
|
"ip-address": "^10.0.1",
|
||||||
"is-cidr": "^5.1.1",
|
"is-cidr": "^5.1.1",
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,9 @@ const userDisplayName = computed(() => {
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<RouterLink :to="{ name: 'key-generator' }" class="nav-link">{{ $t('menu.keygen') }}</RouterLink>
|
<RouterLink :to="{ name: 'key-generator' }" class="nav-link">{{ $t('menu.keygen') }}</RouterLink>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<RouterLink :to="{ name: 'ip-calculator' }" class="nav-link">{{ $t('menu.calculator') }}</RouterLink>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="navbar-nav d-flex justify-content-end">
|
<div class="navbar-nav d-flex justify-content-end">
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,8 @@
|
||||||
"audit": "Audit Log",
|
"audit": "Audit Log",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"logout": "Logout",
|
"logout": "Logout",
|
||||||
"keygen": "Key Generator"
|
"keygen": "Key Generator",
|
||||||
|
"calculator": "IP Calculator"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"headline": "WireGuard® VPN Portal",
|
"headline": "WireGuard® VPN Portal",
|
||||||
|
|
@ -269,6 +270,26 @@
|
||||||
"placeholder": "The pre-shared key"
|
"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": {
|
"modals": {
|
||||||
"user-view": {
|
"user-view": {
|
||||||
"headline": "User Account:",
|
"headline": "User Account:",
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,14 @@ const router = createRouter({
|
||||||
// this generates a separate chunk (About.[hash].js) for this route
|
// this generates a separate chunk (About.[hash].js) for this route
|
||||||
// which is lazy-loaded when the route is visited.
|
// which is lazy-loaded when the route is visited.
|
||||||
component: () => import('../views/KeyGeneraterView.vue')
|
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",
|
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
|
// 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)
|
const authRequired = !publicPages.includes(to.path)
|
||||||
|
|
||||||
if (authRequired && !auth.IsAuthenticated) {
|
if (authRequired && !auth.IsAuthenticated) {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -217,15 +217,6 @@ func (m Manager) RestoreInterfaceState(
|
||||||
if err != nil && !iface.IsDisabled() {
|
if err != nil && !iface.IsDisabled() {
|
||||||
slog.Debug("creating missing interface", "interface", iface.Identifier, "backend", controller.GetId())
|
slog.Debug("creating missing interface", "interface", iface.Identifier, "backend", controller.GetId())
|
||||||
|
|
||||||
// temporarily disable interface in database so that the current state is reflected correctly
|
|
||||||
_ = m.db.SaveInterface(ctx, iface.Identifier,
|
|
||||||
func(in *domain.Interface) (*domain.Interface, error) {
|
|
||||||
now := time.Now()
|
|
||||||
in.Disabled = &now // set
|
|
||||||
in.DisabledReason = domain.DisabledReasonInterfaceMissing
|
|
||||||
return in, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
// temporarily disable interface in database so that the current state is reflected correctly
|
// temporarily disable interface in database so that the current state is reflected correctly
|
||||||
_ = m.db.SaveInterface(ctx, iface.Identifier,
|
_ = m.db.SaveInterface(ctx, iface.Identifier,
|
||||||
func(in *domain.Interface) (*domain.Interface, error) {
|
func(in *domain.Interface) (*domain.Interface, error) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue