+
-
+
-
-
-
+
+
+
-
-
-
+
-
-
- {{ $t('deleteClient') }}
-
-
-
- {{ $t('deleteDialog1') }}
- {{ clientDelete.name }}? {{ $t('deleteDialog2') }}
-
-
+
+
+
+
+
+ {{ $t('deleteClient') }}
+
+
+
+ {{ $t('deleteDialog1') }}
+ {{ modalStore.clientDelete.name }}? {{ $t('deleteDialog2') }}
+
-
+
+
+ {{ $t('deleteClient') }}
+
+
+ {{ $t('cancel') }}
+
-
-
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);
+}