diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 31d962d0..191c33e2 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,9 +2,9 @@ name: "CodeQL" on: push: - branches: [ "master" ] + branches: ["master"] pull_request: - branches: [ "master" ] + branches: ["master"] schedule: - cron: "15 0 * * *" @@ -21,21 +21,21 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'javascript-typescript' ] + language: ["javascript-typescript"] steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} - - name: Autobuild - uses: github/codeql-action/autobuild@v3 + - name: Autobuild + uses: github/codeql-action/autobuild@v3 - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/deploy-nightly.yml b/.github/workflows/deploy-nightly.yml index 88404be3..03a75bba 100644 --- a/.github/workflows/deploy-nightly.yml +++ b/.github/workflows/deploy-nightly.yml @@ -54,4 +54,7 @@ jobs: git config --global user.name 'Docs Deploy Bot' git config --global user.email 'docs.deploy@users.noreply.github.com' - name: Build Docs Website - run: mike deploy --push nightly + run: | + cd docs + git fetch origin gh-pages --depth=1 + mike deploy --push --update-aliases nightly diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index aea89c16..321c72e1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,8 +3,8 @@ name: Build & Publish Latest on: workflow_dispatch: push: - branches: - - master + tags: + - "v*" jobs: deploy: @@ -27,7 +27,7 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@v3 + uses: docker/metadata-action@v5 with: images: | ghcr.io/wg-easy/wg-easy @@ -67,4 +67,7 @@ jobs: git config --global user.name 'Docs Deploy Bot' git config --global user.email 'docs.deploy@users.noreply.github.com' - name: Build Docs Website - run: mike deploy --push --update-aliases ${{ github.ref_name }} latest + run: | + cd docs + git fetch origin gh-pages --depth=1 + mike deploy --push --update-aliases ${{ github.ref_name }} latest diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 868fa296..17af45a9 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -8,11 +8,10 @@ name: Mark stale issues and pull requests on: workflow_dispatch: schedule: - - cron: '*/5 * * * *' + - cron: "*/5 * * * *" jobs: stale: - runs-on: ubuntu-latest if: github.repository_owner == 'wg-easy' permissions: @@ -20,8 +19,8 @@ jobs: pull-requests: write steps: - - uses: actions/stale@v9 - with: + - uses: actions/stale@v9 + with: days-before-issue-stale: 30 days-before-issue-close: 14 stale-issue-label: "stale" diff --git a/CHANGELOG.md b/CHANGELOG.md index e842e6bc..d4b50efa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,15 @@ This update is an entire rewrite to make it even easier to set up your own VPN. - Almost all Environment variables removed - New and Improved UI +- API Basic Authentication +- Added Docs +- Incrementing Version -> Semantic Versioning +- CIDR Support +- IPv6 Support +- Changed API Structure +- Changed Database Structure +- Deprecated Dockerless Installations +- Added Docker Volume Mount ## Minor Changes diff --git a/README.md b/README.md index 421fdda5..f89b57b7 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,8 @@ And log in again. ### 2. Run WireGuard Easy + + To setup the IPv6 Network, simply run once: ```bash @@ -82,6 +84,7 @@ To automatically install & run wg-easy, simply run: --ip6 fdcc:ad94:bacf:61a3::2a \ --ip 10.42.42.42 \ -v ~/.wg-easy:/etc/wireguard \ + -v /lib/modules:/lib/modules:ro \ -p 51820:51820/udp \ -p 51821:51821/tcp \ --cap-add NET_ADMIN \ @@ -97,7 +100,7 @@ To automatically install & run wg-easy, simply run: The Web UI will now be available on `http://0.0.0.0:51821`. -The Prometheus metrics will now be available on `http://0.0.0.0:51821/metrics`. Grafana dashboard [21733](https://grafana.com/grafana/dashboards/21733-wireguard/) +The Prometheus metrics will now be available on `http://0.0.0.0:51821/api/metrics`. Grafana dashboard [21733](https://grafana.com/grafana/dashboards/21733-wireguard/) > 💡 Your configuration files will be saved in `~/.wg-easy` diff --git a/src/app/app.vue b/src/app/app.vue index 636ca56d..0ad77a48 100644 --- a/src/app/app.vue +++ b/src/app/app.vue @@ -1,7 +1,12 @@ @@ -11,6 +16,7 @@ const globalStore = useGlobalStore(); globalStore.fetchFeatures(); globalStore.fetchRelease(); globalStore.setLanguage(); +globalStore.fetchStatistics(); useHead({ bodyAttrs: { class: 'bg-gray-50 dark:bg-neutral-800', diff --git a/src/app/components/Client/Charts.vue b/src/app/components/Client/Charts.vue index 1b64bdf7..f542be45 100644 --- a/src/app/components/Client/Charts.vue +++ b/src/app/components/Client/Charts.vue @@ -1,13 +1,13 @@ diff --git a/src/app/components/ui/Toast.vue b/src/app/components/ui/Toast.vue new file mode 100644 index 00000000..d5a3721f --- /dev/null +++ b/src/app/components/ui/Toast.vue @@ -0,0 +1,37 @@ + + + diff --git a/src/app/components/ui/UserMenu.vue b/src/app/components/ui/UserMenu.vue new file mode 100644 index 00000000..8b368ea1 --- /dev/null +++ b/src/app/components/ui/UserMenu.vue @@ -0,0 +1,92 @@ + + + diff --git a/src/app/layouts/Header.vue b/src/app/layouts/Header.vue index ccca6357..30335870 100644 --- a/src/app/layouts/Header.vue +++ b/src/app/layouts/Header.vue @@ -2,21 +2,23 @@
-

- WireGuard -

+ +

+ WireGuard +

+
@@ -86,11 +81,16 @@ diff --git a/src/app/pages/admin.vue b/src/app/pages/admin.vue new file mode 100644 index 00000000..e9429d14 --- /dev/null +++ b/src/app/pages/admin.vue @@ -0,0 +1,52 @@ + + + diff --git a/src/app/pages/admin/features.vue b/src/app/pages/admin/features.vue new file mode 100644 index 00000000..88cac238 --- /dev/null +++ b/src/app/pages/admin/features.vue @@ -0,0 +1,83 @@ + + + diff --git a/src/app/pages/admin/index.vue b/src/app/pages/admin/index.vue new file mode 100644 index 00000000..cc9ba8be --- /dev/null +++ b/src/app/pages/admin/index.vue @@ -0,0 +1,10 @@ + + + diff --git a/src/app/pages/admin/statistics.vue b/src/app/pages/admin/statistics.vue new file mode 100644 index 00000000..0f4a828e --- /dev/null +++ b/src/app/pages/admin/statistics.vue @@ -0,0 +1,115 @@ + + + diff --git a/src/app/pages/index.vue b/src/app/pages/index.vue index 3f8ef5e2..1510109b 100644 --- a/src/app/pages/index.vue +++ b/src/app/pages/index.vue @@ -54,6 +54,8 @@ const intervalId = ref(null); clientsStore.refresh(); onMounted(() => { + // TODO: remove (to avoid console spam) + return; // TODO?: replace with websocket or similar intervalId.value = setInterval(() => { clientsStore diff --git a/src/app/pages/me.vue b/src/app/pages/me.vue new file mode 100644 index 00000000..dc49f7d4 --- /dev/null +++ b/src/app/pages/me.vue @@ -0,0 +1,54 @@ + + + diff --git a/src/app/stores/auth.ts b/src/app/stores/auth.ts index 403cb542..1f282b03 100644 --- a/src/app/stores/auth.ts +++ b/src/app/stores/auth.ts @@ -1,5 +1,10 @@ export const useAuthStore = defineStore('Auth', () => { - const requiresPassword = ref(true); + const userData = ref(); /** * @throws if unsuccessful @@ -13,8 +18,7 @@ export const useAuthStore = defineStore('Auth', () => { * @throws if unsuccessful */ async function login(username: string, password: string, remember: boolean) { - const response = await api.createSession({ username, password, remember }); - requiresPassword.value = response.requiresPassword; + await api.createSession({ username, password, remember }); return true as const; } @@ -26,13 +30,11 @@ export const useAuthStore = defineStore('Auth', () => { return response.success; } - /** - * @throws if unsuccessful - */ async function update() { - const session = await api.getSession(); - requiresPassword.value = session.requiresPassword; + // store role etc + const { data: response } = await api.getSession(); + userData.value = response.value; } - return { requiresPassword, login, logout, update, signup }; + return { userData, login, logout, update, signup }; }); diff --git a/src/app/stores/global.ts b/src/app/stores/global.ts index 7dc21848..9339e119 100644 --- a/src/app/stores/global.ts +++ b/src/app/stores/global.ts @@ -8,10 +8,6 @@ export const useGlobalStore = defineStore('Global', () => { ); const updateAvailable = ref(false); const features = ref({ - trafficStats: { - enabled: false, - type: 0, - }, sortClients: { enabled: false, }, @@ -22,12 +18,18 @@ export const useGlobalStore = defineStore('Global', () => { enabled: false, }, }); + const statistics = ref({ + enabled: false, + chartType: 0, + }); const sortClient = ref(true); // Sort clients by name, true = asc, false = desc const { availableLocales, locale } = useI18n(); async function setLanguage() { - const { data: lang } = await api.getLang(); + const { data: lang } = await useFetch('/api/lang', { + method: 'get', + }); if ( lang.value !== getItem('lang') && availableLocales.includes(lang.value!) @@ -38,7 +40,9 @@ export const useGlobalStore = defineStore('Global', () => { } async function fetchRelease() { - const { data: release } = await api.getRelease(); + const { data: release } = await useFetch('/api/release', { + method: 'get', + }); if (!release.value) { return; @@ -50,14 +54,25 @@ export const useGlobalStore = defineStore('Global', () => { } async function fetchFeatures() { - const { data: apiFeatures } = await api.getFeatures(); + const { data: apiFeatures } = await useFetch('/api/features', { + method: 'get', + }); if (apiFeatures.value) { features.value = apiFeatures.value; } } + async function fetchStatistics() { + const { data: apiStatistics } = await useFetch('/api/statistics', { + method: 'get', + }); + if (apiStatistics.value) { + statistics.value = apiStatistics.value; + } + } + const updateCharts = computed(() => { - return features.value.trafficStats.type > 0 && uiShowCharts.value; + return statistics.value.chartType > 0 && uiShowCharts.value; }); return { @@ -68,8 +83,10 @@ export const useGlobalStore = defineStore('Global', () => { currentRelease, latestRelease, updateAvailable, + statistics, fetchRelease, fetchFeatures, setLanguage, + fetchStatistics, }; }); diff --git a/src/app/utils/api.ts b/src/app/utils/api.ts index fcfa99a1..7ef85a89 100644 --- a/src/app/utils/api.ts +++ b/src/app/utils/api.ts @@ -1,19 +1,6 @@ class API { - async getRelease() { - return useFetch('/api/release', { - method: 'get', - }); - } - - async getLang() { - return useFetch('/api/lang', { - method: 'get', - }); - } - async getSession() { - // TODO?: use useFetch - return $fetch('/api/session', { + return useFetch('/api/session', { method: 'get', }); } @@ -140,12 +127,6 @@ class API { body: { username, password }, }); } - - async getFeatures() { - return useFetch('/api/features', { - method: 'get', - }); - } } type WGClientReturn = Awaited< diff --git a/src/app/utils/math.ts b/src/app/utils/math.ts index d43ac1b5..9592e333 100644 --- a/src/app/utils/math.ts +++ b/src/app/utils/math.ts @@ -21,6 +21,7 @@ export function bytes( } export function dateTime(value: Date) { + // TODO: results in mismatch because of different locales return new Intl.DateTimeFormat(undefined, { year: 'numeric', month: 'short', diff --git a/src/nuxt.config.ts b/src/nuxt.config.ts index df563b77..2b300d41 100644 --- a/src/nuxt.config.ts +++ b/src/nuxt.config.ts @@ -10,6 +10,7 @@ export default defineNuxtConfig({ '@nuxtjs/tailwindcss', '@pinia/nuxt', '@eschricht/nuxt-color-mode', + 'radix-vue/nuxt', ], colorMode: { preference: 'system', diff --git a/src/package.json b/src/package.json index 26d3af41..75cf2696 100644 --- a/src/package.json +++ b/src/package.json @@ -34,6 +34,7 @@ "nuxt": "^3.13.0", "pinia": "^2.2.2", "qrcode": "^1.5.4", + "radix-vue": "^1.9.5", "semver": "^7.6.3", "tailwindcss": "^3.4.10", "timeago.js": "^4.0.2", diff --git a/src/pnpm-lock.yaml b/src/pnpm-lock.yaml index 2cd028b4..0f00e7d8 100644 --- a/src/pnpm-lock.yaml +++ b/src/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: qrcode: specifier: ^1.5.4 version: 1.5.4 + radix-vue: + specifier: ^1.9.5 + version: 1.9.5(vue@3.4.38(typescript@5.5.4)) semver: specifier: ^7.6.3 version: 7.6.3 @@ -751,6 +754,18 @@ packages: resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} + '@floating-ui/core@1.6.7': + resolution: {integrity: sha512-yDzVT/Lm101nQ5TCVeK65LtdN7Tj4Qpr9RTXJ2vPFLqtLxwOrpoxAHAJI8J3yYWUc40J0BDBheaitK5SJmno2g==} + + '@floating-ui/dom@1.6.10': + resolution: {integrity: sha512-fskgCFv8J8OamCmyun8MfjB1Olfn+uZKjOKZ0vhYF3gRmEUXcGOjxWL8bBr7i4kIuPZ2KD2S3EUIOxnjC8kl2A==} + + '@floating-ui/utils@0.2.7': + resolution: {integrity: sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==} + + '@floating-ui/vue@1.1.4': + resolution: {integrity: sha512-ammH7T3vyCx7pmm9OF19Wc42zrGnUw0QvLoidgypWsCLJMtGXEwY7paYIHO+K+oLC3mbWpzIHzeTVienYenlNg==} + '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} @@ -759,6 +774,12 @@ packages: resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==} engines: {node: '>=18.18'} + '@internationalized/date@3.5.5': + resolution: {integrity: sha512-H+CfYvOZ0LTJeeLOqm19E3uj/4YjrmOFtBufDHPfvtI80hFAMqtrp7oCACpe4Cil5l8S0Qu/9dYfZc/5lY8WQQ==} + + '@internationalized/number@3.5.3': + resolution: {integrity: sha512-rd1wA3ebzlp0Mehj5YTuTI50AQEx80gWFyHcQu+u91/5NgdwBecO8BH6ipPfE+lmQ9d63vpB3H9SHoIUiupllw==} + '@intlify/bundle-utils@7.5.1': resolution: {integrity: sha512-UovJl10oBIlmYEcWw+VIHdKY5Uv5sdPG0b/b6bOYxGLln3UwB75+2dlc0F3Fsa0RhoznQ5Rp589/BZpABpE4Xw==} engines: {node: '>= 14.16'} @@ -1209,11 +1230,22 @@ packages: peerDependencies: eslint: '>=8.40.0' + '@swc/helpers@0.5.13': + resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==} + '@tailwindcss/forms@0.5.9': resolution: {integrity: sha512-tM4XVr2+UVTxXJzey9Twx48c1gcxFStqn1pQz0tRsX8o3DvxhN5oY5pvyAbUx7VTaZxpej4Zzvc6h+1RJBzpIg==} peerDependencies: tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20' + '@tanstack/virtual-core@3.10.7': + resolution: {integrity: sha512-ND5dfsU0n9F4gROzwNNDJmg6y8n9pI8YWxtgbfJ5UcNn7Hx+MxEXtXcQ189tS7sh8pmCObgz2qSiyRKTZxT4dg==} + + '@tanstack/vue-virtual@3.10.7': + resolution: {integrity: sha512-OSK1fkvz4GaBhF80KVmBsJZoMI9ncVaUU//pI8OqTdBnepw467zcuF2Y+Ia1VC0CPYfUEALyS8n4Ar0RI/7ASg==} + peerDependencies: + vue: ^2.7.0 || ^3.0.0 + '@trysound/sax@0.2.0': resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} @@ -1251,6 +1283,9 @@ packages: '@types/semver@7.5.8': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + '@types/web-bluetooth@0.0.20': + resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + '@typescript-eslint/eslint-plugin@8.4.0': resolution: {integrity: sha512-rg8LGdv7ri3oAlenMACk9e+AR4wUV0yrrG+XKsGKOK0EVgeEDqurkXMPILG2836fW4ibokTB5v4b6Z9+GYQDEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1430,6 +1465,15 @@ packages: '@vue/shared@3.4.38': resolution: {integrity: sha512-q0xCiLkuWWQLzVrecPb0RMsNWyxICOjPrcrwxTUEHb1fsnvni4dcuyG7RT/Ie7VPTvnjzIaWzRMUBsrqNj/hhw==} + '@vueuse/core@10.11.1': + resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==} + + '@vueuse/metadata@10.11.1': + resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==} + + '@vueuse/shared@10.11.1': + resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==} + '@yr/monotone-cubic-spline@1.0.3': resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==} @@ -1534,6 +1578,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-hidden@1.2.4: + resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} + engines: {node: '>=10'} + ast-kit@1.1.0: resolution: {integrity: sha512-RlNqd4u6c/rJ5R+tN/ZTtyNrH8X0NHCvyt6gD8RHa3JjzxxHWoyaU0Ujk3Zjbh7IZqrYl1Sxm6XzZifmVxXxHQ==} engines: {node: '>=16.14.0'} @@ -3553,6 +3601,11 @@ packages: queue-tick@1.0.1: resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + radix-vue@1.9.5: + resolution: {integrity: sha512-vtCq+WDAZj5BQtJiChGf/oC7w3y7jaod3agcntgph7fD6aqdcghLZYcUWdgT/XNJs2bEsk+3cjK3ONPRNeFcuQ==} + peerDependencies: + vue: '>= 3.2.0' + radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} @@ -4992,10 +5045,38 @@ snapshots: '@fastify/busboy@2.1.1': {} + '@floating-ui/core@1.6.7': + dependencies: + '@floating-ui/utils': 0.2.7 + + '@floating-ui/dom@1.6.10': + dependencies: + '@floating-ui/core': 1.6.7 + '@floating-ui/utils': 0.2.7 + + '@floating-ui/utils@0.2.7': {} + + '@floating-ui/vue@1.1.4(vue@3.4.38(typescript@5.5.4))': + dependencies: + '@floating-ui/dom': 1.6.10 + '@floating-ui/utils': 0.2.7 + vue-demi: 0.14.10(vue@3.4.38(typescript@5.5.4)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + '@humanwhocodes/module-importer@1.0.1': {} '@humanwhocodes/retry@0.3.0': {} + '@internationalized/date@3.5.5': + dependencies: + '@swc/helpers': 0.5.13 + + '@internationalized/number@3.5.3': + dependencies: + '@swc/helpers': 0.5.13 + '@intlify/bundle-utils@7.5.1(vue-i18n@9.14.0(vue@3.4.38(typescript@5.5.4)))': dependencies: '@intlify/message-compiler': 9.14.0 @@ -5658,11 +5739,22 @@ snapshots: - supports-color - typescript + '@swc/helpers@0.5.13': + dependencies: + tslib: 2.7.0 + '@tailwindcss/forms@0.5.9(tailwindcss@3.4.10)': dependencies: mini-svg-data-uri: 1.4.4 tailwindcss: 3.4.10 + '@tanstack/virtual-core@3.10.7': {} + + '@tanstack/vue-virtual@3.10.7(vue@3.4.38(typescript@5.5.4))': + dependencies: + '@tanstack/virtual-core': 3.10.7 + vue: 3.4.38(typescript@5.5.4) + '@trysound/sax@0.2.0': {} '@types/debug@4.1.12': @@ -5698,6 +5790,8 @@ snapshots: '@types/semver@7.5.8': {} + '@types/web-bluetooth@0.0.20': {} + '@typescript-eslint/eslint-plugin@8.4.0(@typescript-eslint/parser@8.4.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4))(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4)': dependencies: '@eslint-community/regexpp': 4.11.0 @@ -5992,6 +6086,25 @@ snapshots: '@vue/shared@3.4.38': {} + '@vueuse/core@10.11.1(vue@3.4.38(typescript@5.5.4))': + dependencies: + '@types/web-bluetooth': 0.0.20 + '@vueuse/metadata': 10.11.1 + '@vueuse/shared': 10.11.1(vue@3.4.38(typescript@5.5.4)) + vue-demi: 0.14.10(vue@3.4.38(typescript@5.5.4)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/metadata@10.11.1': {} + + '@vueuse/shared@10.11.1(vue@3.4.38(typescript@5.5.4))': + dependencies: + vue-demi: 0.14.10(vue@3.4.38(typescript@5.5.4)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + '@yr/monotone-cubic-spline@1.0.3': {} abbrev@1.1.1: {} @@ -6104,6 +6217,10 @@ snapshots: argparse@2.0.1: {} + aria-hidden@1.2.4: + dependencies: + tslib: 2.7.0 + ast-kit@1.1.0: dependencies: '@babel/parser': 7.25.6 @@ -8309,6 +8426,23 @@ snapshots: queue-tick@1.0.1: {} + radix-vue@1.9.5(vue@3.4.38(typescript@5.5.4)): + dependencies: + '@floating-ui/dom': 1.6.10 + '@floating-ui/vue': 1.1.4(vue@3.4.38(typescript@5.5.4)) + '@internationalized/date': 3.5.5 + '@internationalized/number': 3.5.3 + '@tanstack/vue-virtual': 3.10.7(vue@3.4.38(typescript@5.5.4)) + '@vueuse/core': 10.11.1(vue@3.4.38(typescript@5.5.4)) + '@vueuse/shared': 10.11.1(vue@3.4.38(typescript@5.5.4)) + aria-hidden: 1.2.4 + defu: 6.1.4 + fast-deep-equal: 3.1.3 + nanoid: 5.0.7 + vue: 3.4.38(typescript@5.5.4) + transitivePeerDependencies: + - '@vue/composition-api' + radix3@1.1.2: {} randombytes@2.1.0: diff --git a/src/server/api/admin/features.post.ts b/src/server/api/admin/features.post.ts new file mode 100644 index 00000000..f87119aa --- /dev/null +++ b/src/server/api/admin/features.post.ts @@ -0,0 +1,8 @@ +export default defineEventHandler(async (event) => { + const { features } = await readValidatedBody( + event, + validateZod(featuresType) + ); + await Database.system.updateFeatures(features); + return { success: true }; +}); diff --git a/src/server/api/admin/statistics.post.ts b/src/server/api/admin/statistics.post.ts new file mode 100644 index 00000000..5865580e --- /dev/null +++ b/src/server/api/admin/statistics.post.ts @@ -0,0 +1,8 @@ +export default defineEventHandler(async (event) => { + const { statistics } = await readValidatedBody( + event, + validateZod(statisticsType) + ); + await Database.system.updateStatistics(statistics); + return { success: true }; +}); diff --git a/src/server/api/cnf/[oneTimeLink].ts b/src/server/api/cnf/[oneTimeLink].ts index 64d39c32..b09719b0 100644 --- a/src/server/api/cnf/[oneTimeLink].ts +++ b/src/server/api/cnf/[oneTimeLink].ts @@ -1,6 +1,6 @@ export default defineEventHandler(async (event) => { const system = await Database.system.get(); - if (!system.oneTimeLinks.enabled) { + if (!system.features.oneTimeLinks.enabled) { throw createError({ statusCode: 404, statusMessage: 'Invalid state', diff --git a/src/server/api/features.get.ts b/src/server/api/features.get.ts index 0016e2bf..ef20118d 100644 --- a/src/server/api/features.get.ts +++ b/src/server/api/features.get.ts @@ -1,9 +1,4 @@ export default defineEventHandler(async () => { const system = await Database.system.get(); - return { - trafficStats: system.trafficStats, - sortClients: system.sortClients, - clientExpiration: system.clientExpiration, - oneTimeLinks: system.oneTimeLinks, - }; + return system.features; }); diff --git a/src/server/api/lang.get.ts b/src/server/api/lang.get.ts index d16ac483..a42620de 100644 --- a/src/server/api/lang.get.ts +++ b/src/server/api/lang.get.ts @@ -1,5 +1,5 @@ export default defineEventHandler(async (event) => { setHeader(event, 'Content-Type', 'application/json'); const system = await Database.system.get(); - return system.lang; + return system.general.lang; }); diff --git a/src/server/api/session.get.ts b/src/server/api/session.get.ts index 9aa91658..dee40f88 100644 --- a/src/server/api/session.get.ts +++ b/src/server/api/session.get.ts @@ -1,9 +1,24 @@ export default defineEventHandler(async (event) => { const session = await useWGSession(event); - const authenticated = session.data.authenticated; + + if (!session.data.userId) { + throw createError({ + statusCode: 401, + statusMessage: 'Not logged in', + }); + } + const user = await Database.user.findById(session.data.userId); + if (!user) { + throw createError({ + statusCode: 404, + statusMessage: 'Not found in Database', + }); + } return { - requiresPassword: true, - authenticated, + role: user.role, + username: user.username, + name: user.name, + email: user.email, }; }); diff --git a/src/server/api/session.post.ts b/src/server/api/session.post.ts index ea9e1a36..6c76ef2d 100644 --- a/src/server/api/session.post.ts +++ b/src/server/api/session.post.ts @@ -30,16 +30,15 @@ export default defineEventHandler(async (event) => { if (remember) { conf.cookie = { ...(system.sessionConfig.cookie ?? {}), - maxAge: system.sessionTimeout, + maxAge: system.general.sessionTimeout, }; } - const session = await useSession(event, { + const session = await useSession(event, { ...system.sessionConfig, }); const data = await session.update({ - authenticated: true, userId: user.id, }); diff --git a/src/server/api/statistics.get.ts b/src/server/api/statistics.get.ts new file mode 100644 index 00000000..487f5e6a --- /dev/null +++ b/src/server/api/statistics.get.ts @@ -0,0 +1,4 @@ +export default defineEventHandler(async () => { + const system = await Database.system.get(); + return system.statistics; +}); diff --git a/src/server/api/wireguard/client/[clientId]/generateOneTimeLink.post.ts b/src/server/api/wireguard/client/[clientId]/generateOneTimeLink.post.ts index 221874e4..d827ef54 100644 --- a/src/server/api/wireguard/client/[clientId]/generateOneTimeLink.post.ts +++ b/src/server/api/wireguard/client/[clientId]/generateOneTimeLink.post.ts @@ -1,6 +1,6 @@ export default defineEventHandler(async (event) => { const system = await Database.system.get(); - if (!system.oneTimeLinks.enabled) { + if (!system.features.oneTimeLinks.enabled) { throw createError({ status: 404, message: 'Invalid state', diff --git a/src/server/middleware/auth.ts b/src/server/middleware/auth.ts index ce90ece2..2b987b4a 100644 --- a/src/server/middleware/auth.ts +++ b/src/server/middleware/auth.ts @@ -1,14 +1,35 @@ export default defineEventHandler(async (event) => { const url = getRequestURL(event); const session = await useWGSession(event); + + // Api handled by session, Setup handled with setup middleware + if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/setup')) { + return; + } + if (url.pathname === '/login') { - if (session.data.authenticated) { + if (session.data.userId) { return sendRedirect(event, '/', 302); } + return; + } + + // Require auth for every page other than Login + // TODO: investigate /__nuxt_error (error page when unauthenticated) + if (!session.data.userId) { + return sendRedirect(event, '/login', 302); } - if (url.pathname === '/') { - if (!session.data.authenticated) { + + if (url.pathname.startsWith('/admin')) { + const user = await Database.user.findById(session.data.userId); + if (!user) { return sendRedirect(event, '/login', 302); } + if (user.role !== 'ADMIN') { + throw createError({ + statusCode: 403, + statusMessage: 'Not allowed to access Admin Panel', + }); + } } }); diff --git a/src/server/middleware/session.ts b/src/server/middleware/session.ts index 3611aaae..a03e0a96 100644 --- a/src/server/middleware/session.ts +++ b/src/server/middleware/session.ts @@ -1,5 +1,9 @@ +import type { User } from '~~/services/database/repositories/user'; + export default defineEventHandler(async (event) => { const url = getRequestURL(event); + // If one method of a route is public, every method is public! + // Handle api routes if ( !url.pathname.startsWith('/api/') || url.pathname === '/api/account/setup' || @@ -12,37 +16,81 @@ export default defineEventHandler(async (event) => { } const system = await Database.system.get(); - const session = await getSession(event, system.sessionConfig); - if (session.id && session.data.authenticated) { - return; - } - + const session = await getSession(event, system.sessionConfig); const authorization = getHeader(event, 'Authorization'); - if (url.pathname.startsWith('/api/') && authorization) { + + let user: User | undefined = undefined; + if (session.data.userId) { + // Handle if authenticating using Session + user = await Database.user.findById(session.data.userId); + } else if (authorization) { + // Handle if authenticating using Header + const [method, value] = authorization.split(' '); + // Support Basic Authentication + // TODO: support personal access token or similar + if (method !== 'Basic' || !value) { + throw createError({ + statusCode: 401, + statusMessage: 'Session failed', + }); + } + + const basicValue = Buffer.from(value, 'base64').toString('utf-8'); + + // Split by first ":" + const index = basicValue.indexOf(':'); + const username = basicValue.substring(0, index); + const password = basicValue.substring(index + 1); + + if (!username || !password) { + throw createError({ + statusCode: 401, + statusMessage: 'Session failed', + }); + } + const users = await Database.user.findAll(); - const user = users.find((user) => user.id == session.data.userId); - if (!user) + const foundUser = users.find((v) => v.username === username); + + if (!foundUser) { throw createError({ statusCode: 401, statusMessage: 'Session failed', }); + } - const userHashPassword = user.password; - const passwordValid = await isPasswordValid( - authorization, - userHashPassword - ); - if (passwordValid) { - return; + const userHashPassword = foundUser.password; + const passwordValid = await isPasswordValid(password, userHashPassword); + + if (!passwordValid) { + throw createError({ + statusCode: 401, + statusMessage: 'Incorrect Password', + }); } + user = foundUser; + } + + if (!user) { throw createError({ statusCode: 401, - statusMessage: 'Incorrect Password', + statusMessage: 'Not logged in', }); } - throw createError({ - statusCode: 401, - statusMessage: 'Not logged in', - }); + if (!user.enabled) { + throw createError({ + statusCode: 403, + statusMessage: 'Account is disabled', + }); + } + + if (url.pathname.startsWith('/api/admin')) { + if (user.role !== 'ADMIN') { + throw createError({ + statusCode: 403, + statusMessage: 'Missing Permissions', + }); + } + } }); diff --git a/src/server/middleware/setup.ts b/src/server/middleware/setup.ts index 6c28988d..5377895f 100644 --- a/src/server/middleware/setup.ts +++ b/src/server/middleware/setup.ts @@ -2,16 +2,17 @@ export default defineEventHandler(async (event) => { const url = getRequestURL(event); - if ( - url.pathname === '/setup' || - url.pathname === '/api/account/setup' || - url.pathname === '/api/features' - ) { + // User can't be logged in, and public routes can be accessed whenever + if (url.pathname.startsWith('/api/')) { return; } const users = await Database.user.findAll(); if (users.length === 0) { + // If not setup + if (url.pathname.startsWith('/setup')) { + return; + } if (url.pathname.startsWith('/api/')) { throw createError({ statusCode: 400, @@ -19,5 +20,11 @@ export default defineEventHandler(async (event) => { }); } return sendRedirect(event, '/setup', 302); + } else { + // If already set up + if (!url.pathname.startsWith('/setup')) { + return; + } + return sendRedirect(event, '/login', 302); } }); diff --git a/src/server/utils/WireGuard.ts b/src/server/utils/WireGuard.ts index cd8da359..02fc5f2d 100644 --- a/src/server/utils/WireGuard.ts +++ b/src/server/utils/WireGuard.ts @@ -318,7 +318,7 @@ class WireGuard { const clients = await Database.client.findAll(); const system = await Database.system.get(); // Expires Feature - if (system.clientExpiration.enabled) { + if (system.features.clientExpiration.enabled) { for (const client of Object.values(clients)) { if (client.enabled !== true) continue; if ( @@ -331,7 +331,7 @@ class WireGuard { } } // One Time Link Feature - if (system.oneTimeLinks.enabled) { + if (system.features.oneTimeLinks.enabled) { for (const client of Object.values(clients)) { if ( client.oneTimeLink !== null && diff --git a/src/server/utils/session.ts b/src/server/utils/session.ts index bb7e4aa0..1058f9cc 100644 --- a/src/server/utils/session.ts +++ b/src/server/utils/session.ts @@ -1,10 +1,10 @@ import type { H3Event } from 'h3'; -export type WGSession = { - authenticated: boolean; -}; +export type WGSession = Partial<{ + userId: string; +}>; export async function useWGSession(event: H3Event) { const system = await Database.system.get(); - return useSession>(event, system.sessionConfig); + return useSession(event, system.sessionConfig); } diff --git a/src/server/utils/types.ts b/src/server/utils/types.ts index 8e701f92..3e1697de 100644 --- a/src/server/utils/types.ts +++ b/src/server/utils/types.ts @@ -58,6 +58,27 @@ const oneTimeLink = z .min(1, 'oneTimeLink must be at least 1 Character') .pipe(safeStringRefine); +const features = z.record( + z.string({ message: 'key must be a valid string' }), + z.object( + { + enabled: z.boolean({ message: 'enabled must be a valid boolean' }), + }, + { message: 'value must be a valid object' } + ), + { message: 'features must be a valid record' } +); + +const statistics = z.object( + { + enabled: z.boolean({ message: 'enabled must be a valid boolean' }), + chartType: z.number({ message: 'chartType must be a valid number' }), + }, + { message: 'statistics must be a valid object' } +); + +const objectMessage = 'Body must be a valid object'; + export const clientIdType = z.object( { clientId: id, @@ -69,28 +90,28 @@ export const address4Type = z.object( { address4: address4, }, - { message: 'Body must be a valid object' } + { message: objectMessage } ); export const nameType = z.object( { name: name, }, - { message: 'Body must be a valid object' } + { message: objectMessage } ); export const expireDateType = z.object( { expireDate: expireDate, }, - { message: 'Body must be a valid object' } + { message: objectMessage } ); export const oneTimeLinkType = z.object( { oneTimeLink: oneTimeLink, }, - { message: 'Body must be a valid object' } + { message: objectMessage } ); export const createType = z.object( @@ -98,14 +119,14 @@ export const createType = z.object( name: name, expireDate: expireDate, }, - { message: 'Body must be a valid object' } + { message: objectMessage } ); export const fileType = z.object( { file: file, }, - { message: 'Body must be a valid object' } + { message: objectMessage } ); export const credentialsType = z.object( @@ -114,7 +135,7 @@ export const credentialsType = z.object( password: password, remember: remember, }, - { message: 'Body must be a valid object' } + { message: objectMessage } ); export const passwordType = z.object( @@ -122,7 +143,21 @@ export const passwordType = z.object( username: username, password: password, }, - { message: 'Body must be a valid object' } + { message: objectMessage } +); + +export const featuresType = z.object( + { + features: features, + }, + { message: objectMessage } +); + +export const statisticsType = z.object( + { + statistics: statistics, + }, + { message: objectMessage } ); export function validateZod(schema: ZodSchema) { diff --git a/src/server/utils/wgHelper.ts b/src/server/utils/wgHelper.ts index 2ec9eb57..7c0c0ff5 100644 --- a/src/server/utils/wgHelper.ts +++ b/src/server/utils/wgHelper.ts @@ -28,8 +28,8 @@ AllowedIPs = ${allowedIps.join(', ')}`; [Interface] PrivateKey = ${system.interface.privateKey} Address = ${system.interface.address4}/${cidr4Block}, ${system.interface.address6}/${cidr6Block} -ListenPort = ${system.wgPort} -MTU = ${system.userConfig.serverMtu} +ListenPort = ${system.interface.port} +MTU = ${system.interface.mtu} PreUp = ${system.iptables.PreUp} PostUp = ${system.iptables.PostUp} PreDown = ${system.iptables.PreDown} @@ -51,7 +51,7 @@ PublicKey = ${system.interface.publicKey} PresharedKey = ${client.preSharedKey} AllowedIPs = ${client.allowedIPs.join(', ')} PersistentKeepalive = ${client.persistentKeepalive} -Endpoint = ${system.wgHost}:${system.wgConfigPort}`; +Endpoint = ${system.userConfig.host}:${system.userConfig.port}`; }, generatePrivateKey: () => { diff --git a/src/services/database/lowdb.ts b/src/services/database/lowdb.ts index 5645795f..762dcff2 100644 --- a/src/services/database/lowdb.ts +++ b/src/services/database/lowdb.ts @@ -18,7 +18,14 @@ import { type NewClient, type OneTimeLink, } from './repositories/client'; -import { SystemRepository } from './repositories/system'; +import { + AvailableFeatures, + ChartType, + SystemRepository, + type Feature, + type Features, + type Statistics, +} from './repositories/system'; const DEBUG = debug('LowDB'); @@ -37,6 +44,31 @@ export class LowDBSystem extends SystemRepository { } return system; } + + async updateFeatures(features: Record) { + DEBUG('Update Features'); + this.#db.update((v) => { + for (const key in features) { + if (AvailableFeatures.includes(key as keyof Features)) { + v.system.features[key as keyof Features].enabled = + features[key]!.enabled; + } + } + }); + } + + async updateStatistics(statistics: Statistics) { + DEBUG('Update Statistics'); + this.#db.update((v) => { + v.system.statistics.enabled = statistics.enabled; + if ( + statistics.chartType >= ChartType.None && + statistics.chartType <= ChartType.Bar + ) { + v.system.statistics.chartType = statistics.chartType; + } + }); + } } export class LowDBUser extends UserRepository { @@ -77,6 +109,7 @@ export class LowDBUser extends UserRepository { id: crypto.randomUUID(), password: hash, username, + email: null, name: 'Administrator', role: isUserEmpty ? 'ADMIN' : 'CLIENT', enabled: true, diff --git a/src/services/database/migrations/1.ts b/src/services/database/migrations/1.ts index 39a9c6c2..20575cdc 100644 --- a/src/services/database/migrations/1.ts +++ b/src/services/database/migrations/1.ts @@ -16,52 +16,62 @@ export async function run1(db: Low) { const database: Database = { migrations: [], system: { + general: { + sessionTimeout: 3600, // 1 hour + lang: 'en', + }, + // Config to configure Server interface: { privateKey: privateKey, publicKey: publicKey, address4: stringifyIp({ number: cidr4.start + 1n, version: 4 }), address6: stringifyIp({ number: cidr6.start + 1n, version: 6 }), + mtu: 1420, + port: 51820, + device: 'eth0', }, - sessionTimeout: 3600, // 1 hour - lang: 'en', + // Config to configure Peer & Client Config userConfig: { mtu: 1420, - serverMtu: 1420, persistentKeepalive: 0, address4Range: address4Range, address6Range: address6Range, defaultDns: ['1.1.1.1', '2606:4700:4700::1111'], allowedIps: ['0.0.0.0/0', '::/0'], + // TODO: host has to be configured when onboarding + host: '', + port: 51820, }, - wgDevice: 'eth0', - // TODO: wgHost has to be configured when onboarding - wgHost: '', - wgPort: 51820, - wgConfigPort: 51820, + // Config to configure Firewall iptables: { PreUp: '', PostUp: '', PreDown: '', PostDown: '', }, - trafficStats: { - enabled: false, - type: ChartType.None, - }, - clientExpiration: { - enabled: false, - }, - oneTimeLinks: { - enabled: false, + features: { + clientExpiration: { + enabled: false, + }, + oneTimeLinks: { + enabled: false, + }, + sortClients: { + enabled: false, + }, }, - sortClients: { + statistics: { enabled: false, + chartType: ChartType.None, }, - prometheus: { - enabled: false, - password: null, + metrics: { + prometheus: { + enabled: false, + password: null, + }, }, sessionConfig: { + // TODO: be able to invalidate all sessions password: getRandomHex(256), name: 'wg-easy', cookie: {}, @@ -71,26 +81,25 @@ export async function run1(db: Low) { clients: {}, }; - // TODO: properly check if ipv6 support database.system.iptables.PostUp = - `iptables -t nat -A POSTROUTING -s ${database.system.userConfig.address4Range} -o ${database.system.wgDevice} -j MASQUERADE; -iptables -A INPUT -p udp -m udp --dport ${database.system.wgPort} -j ACCEPT; + `iptables -t nat -A POSTROUTING -s ${database.system.userConfig.address4Range} -o ${database.system.interface.device} -j MASQUERADE; +iptables -A INPUT -p udp -m udp --dport ${database.system.interface.port} -j ACCEPT; iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; -ip6tables -t nat -A POSTROUTING -s ${database.system.userConfig.address6Range} -o ${database.system.wgDevice} -j MASQUERADE; -ip6tables -A INPUT -p udp -m udp --dport ${database.system.wgPort} -j ACCEPT; +ip6tables -t nat -A POSTROUTING -s ${database.system.userConfig.address6Range} -o ${database.system.interface.device} -j MASQUERADE; +ip6tables -A INPUT -p udp -m udp --dport ${database.system.interface.port} -j ACCEPT; ip6tables -A FORWARD -i wg0 -j ACCEPT; ip6tables -A FORWARD -o wg0 -j ACCEPT;` .split('\n') .join(' '); database.system.iptables.PostDown = - `iptables -t nat -D POSTROUTING -s ${database.system.userConfig.address4Range} -o ${database.system.wgDevice} -j MASQUERADE; -iptables -D INPUT -p udp -m udp --dport ${database.system.wgPort} -j ACCEPT; + `iptables -t nat -D POSTROUTING -s ${database.system.userConfig.address4Range} -o ${database.system.interface.device} -j MASQUERADE; +iptables -D INPUT -p udp -m udp --dport ${database.system.interface.port} -j ACCEPT; iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; -ip6tables -t nat -D POSTROUTING -s ${database.system.userConfig.address6Range} -o ${database.system.wgDevice} -j MASQUERADE; -ip6tables -D INPUT -p udp -m udp --dport ${database.system.wgPort} -j ACCEPT; +ip6tables -t nat -D POSTROUTING -s ${database.system.userConfig.address6Range} -o ${database.system.interface.device} -j MASQUERADE; +ip6tables -D INPUT -p udp -m udp --dport ${database.system.interface.port} -j ACCEPT; ip6tables -D FORWARD -i wg0 -j ACCEPT; ip6tables -D FORWARD -o wg0 -j ACCEPT;` .split('\n') diff --git a/src/services/database/repositories/system.ts b/src/services/database/repositories/system.ts index cf9c8aab..2a82d0eb 100644 --- a/src/services/database/repositories/system.ts +++ b/src/services/database/repositories/system.ts @@ -14,16 +14,20 @@ export type WGInterface = { publicKey: string; address4: string; address6: string; + mtu: number; + port: number; + device: string; }; export type WGConfig = { mtu: number; - serverMtu: number; persistentKeepalive: number; address4Range: string; address6Range: string; defaultDns: string[]; allowedIps: string[]; + host: string; + port: number; }; export enum ChartType { @@ -33,9 +37,9 @@ export enum ChartType { Bar = 3, } -export type TrafficStats = { +export type Statistics = { enabled: boolean; - type: ChartType; + chartType: ChartType; }; export type Prometheus = { @@ -47,31 +51,45 @@ export type Feature = { enabled: boolean; }; +export type Metrics = { + prometheus: Prometheus; +}; + +export type General = { + sessionTimeout: number; + lang: Lang; +}; + +export type Features = { + clientExpiration: Feature; + oneTimeLinks: Feature; + sortClients: Feature; +}; + +export const AvailableFeatures: (keyof Features)[] = [ + 'clientExpiration', + 'oneTimeLinks', + 'sortClients', +] as const; + /** * Representing the WireGuard network configuration data structure of a computer interface system. */ export type System = { - interface: WGInterface; + general: General; - // maxAge - sessionTimeout: number; - lang: Lang; + interface: WGInterface; userConfig: WGConfig; - wgDevice: string; - wgHost: string; - wgPort: number; - wgConfigPort: number; - iptables: IpTables; - trafficStats: TrafficStats; - clientExpiration: Feature; - oneTimeLinks: Feature; - sortClients: Feature; + features: Features; + + statistics: Statistics; + + metrics: Metrics; - prometheus: Prometheus; sessionConfig: SessionConfig; }; @@ -85,4 +103,7 @@ export abstract class SystemRepository { * Retrieves the system configuration data from the database. */ abstract get(): Promise; + + abstract updateFeatures(features: Record): Promise; + abstract updateStatistics(statistics: Statistics): Promise; } diff --git a/src/services/database/repositories/user.ts b/src/services/database/repositories/user.ts index c09285e0..1bad8a28 100644 --- a/src/services/database/repositories/user.ts +++ b/src/services/database/repositories/user.ts @@ -17,6 +17,7 @@ export type User = { username: string; password: string; name: string; + email: string | null; /** ISO String */ createdAt: string; /** ISO String */ diff --git a/src/tailwind.config.ts b/src/tailwind.config.ts index 4f722326..3b41208f 100644 --- a/src/tailwind.config.ts +++ b/src/tailwind.config.ts @@ -1,6 +1,6 @@ import type { Config } from 'tailwindcss'; import type { PluginAPI } from 'tailwindcss/types/config'; -// import { red } from 'tailwindcss/colors.js'; +import tailwindForms from '@tailwindcss/forms'; export default { darkMode: 'selector', @@ -15,12 +15,6 @@ export default { xl: '1280px', '2xl': '1536px', }, - extend: { - colors: { - // DEFAULT: red[800], - // primary: red[800], - }, - }, }, plugins: [ function addDisabledClass({ addUtilities }: PluginAPI) { @@ -32,5 +26,6 @@ export default { }; addUtilities(newUtilities); }, + tailwindForms, ], } satisfies Config;