From 881d4abbfe43c37a3258252c13130d1cee0a1246 Mon Sep 17 00:00:00 2001 From: Bernd Storath <999999bst@gmail.com> Date: Tue, 13 Aug 2024 10:01:28 +0200 Subject: [PATCH] Split into Components, migrate to nuxt fixup shutdown wireguard properly fix styling, fix store split even more clear interval split even more split even more handle auth middleware on server avoid flicker of login page --- docker-compose.yml | 2 +- src/app.vue | 2 - src/assets/css/app.css | 6 - src/components/Client/Address.vue | 71 ++ src/components/Client/Avatar.vue | 42 + src/components/Client/Charts.vue | 140 +++ src/components/Client/Client.vue | 55 + src/components/Client/Config.vue | 40 + src/components/Client/Delete.vue | 28 + src/components/Client/InlineTransfer.vue | 51 + src/components/Client/LastSeen.vue | 20 + src/components/Client/Name.vue | 76 ++ src/components/Client/QRCode.vue | 38 + src/components/Client/Switch.vue | 40 + src/components/Client/Transfer.vue | 67 + src/components/Clients/Clients.vue | 13 + src/components/Clients/Empty.vue | 33 + src/components/Clients/New.vue | 30 + src/components/ui/Chart.vue | 17 + src/eslint.config.mjs | 8 +- src/layouts/Footer.vue | 11 +- src/layouts/Header.vue | 64 +- src/pages/index.vue | 1450 ++++------------------ src/pages/login.vue | 57 +- src/server/api/session.delete.ts | 2 +- src/server/api/session.post.ts | 2 +- src/server/middleware/auth.ts | 14 + src/server/middleware/session.ts | 21 +- src/server/plugins/shutdown.ts | 6 + src/stores/auth.ts | 34 + src/stores/clients.ts | 110 ++ src/stores/global.ts | 81 ++ src/stores/modal.ts | 36 + src/tailwind.config.ts | 6 +- src/utils/chart.ts | 15 + src/utils/math.ts | 31 + 36 files changed, 1414 insertions(+), 1305 deletions(-) delete mode 100644 src/assets/css/app.css create mode 100644 src/components/Client/Address.vue create mode 100644 src/components/Client/Avatar.vue create mode 100644 src/components/Client/Charts.vue create mode 100644 src/components/Client/Client.vue create mode 100644 src/components/Client/Config.vue create mode 100644 src/components/Client/Delete.vue create mode 100644 src/components/Client/InlineTransfer.vue create mode 100644 src/components/Client/LastSeen.vue create mode 100644 src/components/Client/Name.vue create mode 100644 src/components/Client/QRCode.vue create mode 100644 src/components/Client/Switch.vue create mode 100644 src/components/Client/Transfer.vue create mode 100644 src/components/Clients/Clients.vue create mode 100644 src/components/Clients/Empty.vue create mode 100644 src/components/Clients/New.vue create mode 100644 src/components/ui/Chart.vue create mode 100644 src/server/middleware/auth.ts create mode 100644 src/server/plugins/shutdown.ts create mode 100644 src/stores/auth.ts create mode 100644 src/stores/clients.ts create mode 100644 src/stores/global.ts create mode 100644 src/stores/modal.ts create mode 100644 src/utils/chart.ts create mode 100644 src/utils/math.ts diff --git a/docker-compose.yml b/docker-compose.yml index dd450ed9..3f11cc6b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,7 +39,7 @@ services: cap_add: - NET_ADMIN - SYS_MODULE - # - NET_RAW # ⚠️ Uncomment if using Podman + # - NET_RAW # ⚠️ Uncomment if using Podman sysctls: - net.ipv4.ip_forward=1 - net.ipv4.conf.all.src_valid_mark=1 diff --git a/src/app.vue b/src/app.vue index 637bf667..1395ec0b 100644 --- a/src/app.vue +++ b/src/app.vue @@ -9,8 +9,6 @@ diff --git a/src/components/Client/Avatar.vue b/src/components/Client/Avatar.vue new file mode 100644 index 00000000..25f64669 --- /dev/null +++ b/src/components/Client/Avatar.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/components/Client/Charts.vue b/src/components/Client/Charts.vue new file mode 100644 index 00000000..9f1350eb --- /dev/null +++ b/src/components/Client/Charts.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/src/components/Client/Client.vue b/src/components/Client/Client.vue new file mode 100644 index 00000000..aeb18513 --- /dev/null +++ b/src/components/Client/Client.vue @@ -0,0 +1,55 @@ + + + diff --git a/src/components/Client/Config.vue b/src/components/Client/Config.vue new file mode 100644 index 00000000..1acd2185 --- /dev/null +++ b/src/components/Client/Config.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/components/Client/Delete.vue b/src/components/Client/Delete.vue new file mode 100644 index 00000000..91a8cce0 --- /dev/null +++ b/src/components/Client/Delete.vue @@ -0,0 +1,28 @@ + + + diff --git a/src/components/Client/InlineTransfer.vue b/src/components/Client/InlineTransfer.vue new file mode 100644 index 00000000..a0e1539b --- /dev/null +++ b/src/components/Client/InlineTransfer.vue @@ -0,0 +1,51 @@ + + + diff --git a/src/components/Client/LastSeen.vue b/src/components/Client/LastSeen.vue new file mode 100644 index 00000000..aec14361 --- /dev/null +++ b/src/components/Client/LastSeen.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/components/Client/Name.vue b/src/components/Client/Name.vue new file mode 100644 index 00000000..f274fa56 --- /dev/null +++ b/src/components/Client/Name.vue @@ -0,0 +1,76 @@ + + + diff --git a/src/components/Client/QRCode.vue b/src/components/Client/QRCode.vue new file mode 100644 index 00000000..7a600123 --- /dev/null +++ b/src/components/Client/QRCode.vue @@ -0,0 +1,38 @@ + + + diff --git a/src/components/Client/Switch.vue b/src/components/Client/Switch.vue new file mode 100644 index 00000000..95be64ff --- /dev/null +++ b/src/components/Client/Switch.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/components/Client/Transfer.vue b/src/components/Client/Transfer.vue new file mode 100644 index 00000000..59583f98 --- /dev/null +++ b/src/components/Client/Transfer.vue @@ -0,0 +1,67 @@ + + + diff --git a/src/components/Clients/Clients.vue b/src/components/Clients/Clients.vue new file mode 100644 index 00000000..dccbe043 --- /dev/null +++ b/src/components/Clients/Clients.vue @@ -0,0 +1,13 @@ + + + diff --git a/src/components/Clients/Empty.vue b/src/components/Clients/Empty.vue new file mode 100644 index 00000000..e2016b28 --- /dev/null +++ b/src/components/Clients/Empty.vue @@ -0,0 +1,33 @@ + + + diff --git a/src/components/Clients/New.vue b/src/components/Clients/New.vue new file mode 100644 index 00000000..0bf2c72f --- /dev/null +++ b/src/components/Clients/New.vue @@ -0,0 +1,30 @@ + + + diff --git a/src/components/ui/Chart.vue b/src/components/ui/Chart.vue new file mode 100644 index 00000000..4910c653 --- /dev/null +++ b/src/components/ui/Chart.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/eslint.config.mjs b/src/eslint.config.mjs index ad6fe8e4..f169e53b 100644 --- a/src/eslint.config.mjs +++ b/src/eslint.config.mjs @@ -1,4 +1,10 @@ import { createConfigForNuxt } from '@nuxt/eslint-config/flat'; import eslintConfigPrettier from 'eslint-config-prettier'; -export default createConfigForNuxt().append(eslintConfigPrettier); +export default createConfigForNuxt() + .append({ + rules: { + 'vue/no-multiple-template-root': 'off', + }, + }) + .append(eslintConfigPrettier); diff --git a/src/layouts/Footer.vue b/src/layouts/Footer.vue index 146e8e7a..3a3108b9 100644 --- a/src/layouts/Footer.vue +++ b/src/layouts/Footer.vue @@ -1,9 +1,6 @@ - + diff --git a/src/layouts/Header.vue b/src/layouts/Header.vue index 10beaee0..c6562600 100644 --- a/src/layouts/Header.vue +++ b/src/layouts/Header.vue @@ -1,5 +1,5 @@ diff --git a/src/pages/index.vue b/src/pages/index.vue index 3432f16f..0e2417e6 100644 --- a/src/pages/index.vue +++ b/src/pages/index.vue @@ -1,608 +1,97 @@ diff --git a/src/pages/login.vue b/src/pages/login.vue index fd231779..bc2b0063 100644 --- a/src/pages/login.vue +++ b/src/pages/login.vue @@ -64,15 +64,16 @@ - @@ -80,43 +81,29 @@ diff --git a/src/server/api/session.delete.ts b/src/server/api/session.delete.ts index 06cd5ad3..1ee6ddce 100644 --- a/src/server/api/session.delete.ts +++ b/src/server/api/session.delete.ts @@ -3,7 +3,7 @@ export default defineEventHandler(async (event) => { const sessionId = session.id; if (sessionId === undefined) { - return createError({ + throw createError({ statusCode: 401, statusMessage: 'Not logged in', }); diff --git a/src/server/api/session.post.ts b/src/server/api/session.post.ts index c267712c..74e8a092 100644 --- a/src/server/api/session.post.ts +++ b/src/server/api/session.post.ts @@ -26,5 +26,5 @@ export default defineEventHandler(async (event) => { SERVER_DEBUG(`New Session: ${data.id}`); - return { success: true }; + return { success: true, requiresPassword: REQUIRES_PASSWORD }; }); diff --git a/src/server/middleware/auth.ts b/src/server/middleware/auth.ts new file mode 100644 index 00000000..cba58b4e --- /dev/null +++ b/src/server/middleware/auth.ts @@ -0,0 +1,14 @@ +export default defineEventHandler(async (event) => { + const url = getRequestURL(event); + const session = await useWGSession(event); + if (url.pathname === '/login') { + if (!REQUIRES_PASSWORD || session.data.authenticated) { + return sendRedirect(event, '/', 302) + } + } + if (url.pathname === '/') { + if (!session.data.authenticated) { + return sendRedirect(event, '/login', 302) + } + } +}); \ No newline at end of file diff --git a/src/server/middleware/session.ts b/src/server/middleware/session.ts index add2fcac..65c4bb3f 100644 --- a/src/server/middleware/session.ts +++ b/src/server/middleware/session.ts @@ -1,18 +1,13 @@ export default defineEventHandler(async (event) => { - if (event.node.req.url === undefined) { - throw createError({ - statusCode: 400, - statusMessage: 'Invalid request', - }); - } + const url = getRequestURL(event); if ( !REQUIRES_PASSWORD || - !event.node.req.url.startsWith('/api/') || - event.node.req.url === '/api/session' || - event.node.req.url === '/api/lang' || - event.node.req.url === '/api/release' || - event.node.req.url === '/api/ui-chart-type' || - event.node.req.url === '/api/ui-traffic-stats' + !url.pathname.startsWith('/api/') || + url.pathname === '/api/session' || + url.pathname === '/api/lang' || + url.pathname === '/api/release' || + url.pathname === '/api/ui-chart-type' || + url.pathname === '/api/ui-traffic-stats' ) { return; } @@ -22,7 +17,7 @@ export default defineEventHandler(async (event) => { } const authorization = getHeader(event, 'Authorization'); - if (event.node.req.url.startsWith('/api/') && authorization) { + if (url.pathname.startsWith('/api/') && authorization) { if (isPasswordValid(authorization)) { return; } diff --git a/src/server/plugins/shutdown.ts b/src/server/plugins/shutdown.ts new file mode 100644 index 00000000..9dd80fa2 --- /dev/null +++ b/src/server/plugins/shutdown.ts @@ -0,0 +1,6 @@ +export default defineNitroPlugin((nitroApp) => { + nitroApp.hooks.hook('close', () => { + console.log('Shutting down'); + WireGuard.Shutdown(); + }); +}); diff --git a/src/stores/auth.ts b/src/stores/auth.ts new file mode 100644 index 00000000..c471122a --- /dev/null +++ b/src/stores/auth.ts @@ -0,0 +1,34 @@ +export const useAuthStore = defineStore('Auth', () => { + const authenticated = ref(false); + const requiresPassword = ref(true); + + /** + * @throws if unsuccessful + */ + async function login(password: string) { + const response = await api.createSession({ password }); + authenticated.value = response.success; + requiresPassword.value = response.requiresPassword; + return true as const; + } + + /** + * @throws if unsuccessful + */ + async function logout() { + const response = await api.deleteSession(); + authenticated.value = !response.success; + return response.success; + } + + /** + * @throws if unsuccessful + */ + async function update() { + const response = await api.getSession(); + authenticated.value = response.authenticated; + requiresPassword.value = response.requiresPassword; + } + + return { requiresPassword, authenticated, login, logout, update }; +}); diff --git a/src/stores/clients.ts b/src/stores/clients.ts new file mode 100644 index 00000000..70bcd2c5 --- /dev/null +++ b/src/stores/clients.ts @@ -0,0 +1,110 @@ +import { defineStore } from 'pinia'; +import { sha256 } from 'js-sha256'; + +export type LocalClient = WGClient & { + avatar?: string; + transferMax?: number; +} & Omit; + +export type ClientPersist = { + transferRxHistory: number[]; + transferRxPrevious: number; + transferRxCurrent: number; + transferRxSeries: { name: string; data: number[] }[]; + hoverRx?: unknown; + transferTxHistory: number[]; + transferTxPrevious: number; + transferTxCurrent: number; + transferTxSeries: { name: string; data: number[] }[]; + hoverTx?: unknown; +}; + +export const useClientsStore = defineStore('Clients', () => { + const clients = ref(null); + const clientsPersist = ref>({}); + + async function refresh({ updateCharts = false } = {}) { + const _clients = await api.getClients(); + clients.value = _clients.map((client) => { + let avatar = undefined; + if (client.name.includes('@') && client.name.includes('.')) { + avatar = `https://gravatar.com/avatar/${sha256(client.name.toLowerCase().trim())}.jpg`; + } + + if (!clientsPersist.value[client.id]) { + clientsPersist.value[client.id] = { + transferRxHistory: Array(50).fill(0), + transferRxPrevious: client.transferRx ?? 0, + transferTxHistory: Array(50).fill(0), + transferTxPrevious: client.transferTx ?? 0, + transferRxCurrent: 0, + transferTxCurrent: 0, + transferRxSeries: [], + transferTxSeries: [], + }; + } + + const clientPersist = clientsPersist.value[client.id]; + + // Debug + // client.transferRx = this.clientsPersist[client.id].transferRxPrevious + Math.random() * 1000; + // client.transferTx = this.clientsPersist[client.id].transferTxPrevious + Math.random() * 1000; + // client.latestHandshakeAt = new Date(); + // this.requiresPassword = true; + + clientPersist.transferRxCurrent = + (client.transferRx ?? 0) - clientPersist.transferRxPrevious; + + clientPersist.transferRxPrevious = client.transferRx ?? 0; + + clientPersist.transferTxCurrent = + (client.transferTx ?? 0) - clientPersist.transferTxPrevious; + + clientPersist.transferTxPrevious = client.transferTx ?? 0; + + let transferMax = undefined; + + if (updateCharts) { + clientPersist.transferRxHistory.push(clientPersist.transferRxCurrent); + clientPersist.transferRxHistory.shift(); + + clientPersist.transferTxHistory.push(clientPersist.transferTxCurrent); + clientPersist.transferTxHistory.shift(); + + clientPersist.transferTxSeries = [ + { + name: 'Tx', + data: clientPersist.transferTxHistory, + }, + ]; + + clientPersist.transferRxSeries = [ + { + name: 'Rx', + data: clientPersist.transferRxHistory, + }, + ]; + + transferMax = Math.max( + ...clientPersist.transferTxHistory, + ...clientPersist.transferRxHistory + ); + } + + return { + ...client, + avatar, + transferTxHistory: clientPersist.transferTxHistory, + transferRxHistory: clientPersist.transferRxHistory, + transferMax, + transferTxSeries: clientPersist.transferTxSeries, + transferRxSeries: clientPersist.transferRxSeries, + transferTxCurrent: clientPersist.transferTxCurrent, + transferRxCurrent: clientPersist.transferRxCurrent, + hoverTx: clientPersist.hoverTx, + hoverRx: clientPersist.hoverRx, + }; + }); + } + return { clients, clientsPersist, refresh }; +}); diff --git a/src/stores/global.ts b/src/stores/global.ts new file mode 100644 index 00000000..d3851fa6 --- /dev/null +++ b/src/stores/global.ts @@ -0,0 +1,81 @@ +import { defineStore } from 'pinia'; + +export const useGlobalStore = defineStore('Global', () => { + const uiChartType = ref(0); + const uiShowCharts = ref(getItem('uiShowCharts') === '1'); + const currentRelease = ref(null); + const latestRelease = ref( + null + ); + const uiTrafficStats = ref(false); + + const { availableLocales, locale } = useI18n(); + + async function fetchRelease() { + const lang = await api.getLang(); + if (lang !== getItem('lang') && availableLocales.includes(lang)) { + setItem('lang', lang); + locale.value = lang; + } + + const _currentRelease = await api.getRelease(); + const _latestRelease = await fetch( + 'https://wg-easy.github.io/wg-easy/changelog.json' + ) + .then((res) => res.json()) + .then((releases) => { + const releasesArray = Object.entries(releases).map( + ([version, changelog]) => ({ + version: parseInt(version, 10), + changelog: changelog as string, + }) + ); + releasesArray.sort((a, b) => { + return b.version - a.version; + }); + + return releasesArray[0]; + }); + + if (_currentRelease >= _latestRelease.version) return; + + currentRelease.value = _currentRelease; + latestRelease.value = _latestRelease; + } + + async function fetchChartType() { + api + .getChartType() + .then((res) => { + uiChartType.value = res; + }) + .catch(() => { + uiChartType.value = 0; + }); + } + + async function fetchUITrafficStats() { + api + .getUITrafficStats() + .then((res) => { + uiTrafficStats.value = res; + }) + .catch(() => { + uiTrafficStats.value = false; + }); + } + + const updateCharts = computed(() => { + return uiChartType.value > 0 && uiShowCharts.value; + }); + + return { + uiChartType, + uiShowCharts, + uiTrafficStats, + updateCharts, + fetchRelease, + fetchChartType, + fetchUITrafficStats, + }; +}); diff --git a/src/stores/modal.ts b/src/stores/modal.ts new file mode 100644 index 00000000..e0a0eaf2 --- /dev/null +++ b/src/stores/modal.ts @@ -0,0 +1,36 @@ +import { defineStore } from 'pinia'; + +export const useModalStore = defineStore('Modal', () => { + const clientsStore = useClientsStore(); + const clientDelete = ref(null); + const clientCreate = ref(null); + const clientCreateName = ref(''); + const qrcode = ref(null); + + function createClient() { + const name = clientCreateName.value; + if (!name) return; + + api + .createClient({ name }) + .catch((err) => alert(err.message || err.toString())) + .finally(() => clientsStore.refresh().catch(console.error)); + } + function deleteClient(client: WGClient | null) { + if (client === null) { + return; + } + api + .deleteClient({ clientId: client.id }) + .catch((err) => alert(err.message || err.toString())) + .finally(() => clientsStore.refresh().catch(console.error)); + } + return { + clientDelete, + clientCreate, + clientCreateName, + qrcode, + createClient, + deleteClient, + }; +}); diff --git a/src/tailwind.config.ts b/src/tailwind.config.ts index 4aa8b111..4f722326 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 * as colors from 'tailwindcss/colors.js'; +// import { red } from 'tailwindcss/colors.js'; export default { darkMode: 'selector', @@ -17,8 +17,8 @@ export default { }, extend: { colors: { - DEFAULT: colors.red[800], - primary: colors.red[800], + // DEFAULT: red[800], + // primary: red[800], }, }, }, diff --git a/src/utils/chart.ts b/src/utils/chart.ts new file mode 100644 index 00000000..d65df5b4 --- /dev/null +++ b/src/utils/chart.ts @@ -0,0 +1,15 @@ +export const UI_CHART_TYPES = [ + { type: false, strokeWidth: 0 }, + { type: 'line', strokeWidth: 3 }, + { type: 'area', strokeWidth: 0 }, + { type: 'bar', strokeWidth: 0 }, +]; + +export const CHART_COLORS = { + rx: { light: 'rgba(128,128,128,0.3)', dark: 'rgba(255,255,255,0.3)' }, + tx: { light: 'rgba(128,128,128,0.4)', dark: 'rgba(255,255,255,0.3)' }, + gradient: { + light: ['rgba(0,0,0,1.0)', 'rgba(0,0,0,1.0)'], + dark: ['rgba(128,128,128,0)', 'rgba(128,128,128,0)'], + }, +}; diff --git a/src/utils/math.ts b/src/utils/math.ts new file mode 100644 index 00000000..2aba68f9 --- /dev/null +++ b/src/utils/math.ts @@ -0,0 +1,31 @@ +export function bytes( + bytes: number, + decimals = 2, + kib = false, + maxunit?: string +) { + if (bytes === 0) return '0 B'; + if (Number.isNaN(bytes) && !Number.isFinite(bytes)) return 'NaN'; + const k = kib ? 1024 : 1000; + const dm = + decimals != null && !Number.isNaN(decimals) && decimals >= 0 ? decimals : 2; + const sizes = kib + ? ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB', 'BiB'] + : ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'BB']; + let i = Math.floor(Math.log(bytes) / Math.log(k)); + if (maxunit !== undefined) { + const index = sizes.indexOf(maxunit); + if (index !== -1) i = index; + } + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; +} + +export function dateTime(value: Date) { + return new Intl.DateTimeFormat(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + }).format(value); +}