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); +}