mirror of https://github.com/h44z/wg-portal.git
				
				
				
			Compare commits
	
		
			19 Commits
		
	
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								 | 
						b1637b0c4e | |
| 
							
							
								 | 
						0cc7ebb83e | |
| 
							
							
								 | 
						eb6a787cfc | |
| 
							
							
								 | 
						b546eec4ed | |
| 
							
							
								 | 
						9be2133220 | |
| 
							
							
								 | 
						b05837b2d9 | |
| 
							
							
								 | 
						08c8f8eac0 | |
| 
							
							
								 | 
						d864e24145 | |
| 
							
							
								 | 
						5b56e58fe9 | |
| 
							
							
								 | 
						930ef7b573 | |
| 
							
							
								 | 
						18296673d7 | |
| 
							
							
								 | 
						4ccc59c109 | |
| 
							
							
								 | 
						e6b01a9903 | |
| 
							
							
								 | 
						2f79dd04c0 | |
| 
							
							
								 | 
						e5ed9736b3 | |
| 
							
							
								 | 
						c8353b85ae | |
| 
							
							
								 | 
						6142031387 | |
| 
							
							
								 | 
						dd86d0ff49 | |
| 
							
							
								 | 
						bdd426a679 | 
| 
						 | 
				
			
			@ -16,7 +16,6 @@
 | 
			
		|||
        "@vojtechlanka/vue-tags-input": "^3.1.1",
 | 
			
		||||
        "bootstrap": "^5.3.7",
 | 
			
		||||
        "bootswatch": "^5.3.7",
 | 
			
		||||
        "cidr-tools": "^11.0.3",
 | 
			
		||||
        "flag-icons": "^7.3.2",
 | 
			
		||||
        "ip-address": "^10.0.1",
 | 
			
		||||
        "is-cidr": "^5.1.1",
 | 
			
		||||
| 
						 | 
				
			
			@ -921,6 +920,7 @@
 | 
			
		|||
      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
 | 
			
		||||
      "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "peer": true,
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "type": "opencollective",
 | 
			
		||||
        "url": "https://opencollective.com/popperjs"
 | 
			
		||||
| 
						 | 
				
			
			@ -1492,18 +1492,6 @@
 | 
			
		|||
        "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": {
 | 
			
		||||
      "version": "3.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-3.0.0.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -1718,15 +1706,6 @@
 | 
			
		|||
        "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": {
 | 
			
		||||
      "version": "5.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -2068,6 +2047,7 @@
 | 
			
		|||
      "integrity": "sha512-FvQdkn2dZ8DGiLgi0Uf4zsj7r/BsiLImNa5QJ10eZalY6NfZyjrmWGFcuCN5jNwlDlXFJnftauv+UtvBKLvepQ==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "peer": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@bufbuild/protobuf": "^2.5.0",
 | 
			
		||||
        "buffer-builder": "^0.2.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -2559,6 +2539,7 @@
 | 
			
		|||
      "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "peer": true,
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=12"
 | 
			
		||||
      },
 | 
			
		||||
| 
						 | 
				
			
			@ -2600,6 +2581,7 @@
 | 
			
		|||
      "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "peer": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "esbuild": "^0.25.0",
 | 
			
		||||
        "fdir": "^6.4.4",
 | 
			
		||||
| 
						 | 
				
			
			@ -2693,6 +2675,7 @@
 | 
			
		|||
      "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "peer": true,
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=12"
 | 
			
		||||
      },
 | 
			
		||||
| 
						 | 
				
			
			@ -2705,6 +2688,7 @@
 | 
			
		|||
      "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
 | 
			
		||||
      "integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "peer": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@vue/compiler-dom": "3.5.22",
 | 
			
		||||
        "@vue/compiler-sfc": "3.5.22",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,7 +16,6 @@
 | 
			
		|||
    "@vojtechlanka/vue-tags-input": "^3.1.1",
 | 
			
		||||
    "bootstrap": "^5.3.7",
 | 
			
		||||
    "bootswatch": "^5.3.7",
 | 
			
		||||
    "cidr-tools": "^11.0.3",
 | 
			
		||||
    "flag-icons": "^7.3.2",
 | 
			
		||||
    "ip-address": "^10.0.1",
 | 
			
		||||
    "is-cidr": "^5.1.1",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -133,9 +133,6 @@ 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">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -42,8 +42,7 @@
 | 
			
		|||
    "audit": "Audit Log",
 | 
			
		||||
    "login": "Login",
 | 
			
		||||
    "logout": "Logout",
 | 
			
		||||
    "keygen": "Key Generator",
 | 
			
		||||
    "calculator": "IP Calculator"
 | 
			
		||||
    "keygen": "Key Generator"
 | 
			
		||||
  },
 | 
			
		||||
  "home": {
 | 
			
		||||
    "headline": "WireGuard® VPN Portal",
 | 
			
		||||
| 
						 | 
				
			
			@ -270,26 +269,6 @@
 | 
			
		|||
        "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:",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -72,14 +72,6 @@ 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",
 | 
			
		||||
| 
						 | 
				
			
			@ -130,7 +122,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', '/ip-calculator']
 | 
			
		||||
  const publicPages = ['/', '/login', '/key-generator']
 | 
			
		||||
  const authRequired = !publicPages.includes(to.path)
 | 
			
		||||
 | 
			
		||||
  if (authRequired && !auth.IsAuthenticated) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,139 +0,0 @@
 | 
			
		|||
<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,6 +217,15 @@ func (m Manager) RestoreInterfaceState(
 | 
			
		|||
		if err != nil && !iface.IsDisabled() {
 | 
			
		||||
			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
 | 
			
		||||
			_ = m.db.SaveInterface(ctx, iface.Identifier,
 | 
			
		||||
				func(in *domain.Interface) (*domain.Interface, error) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue