feat(auth): implement WebSocket authentication for admin access to logs
Build / windows-latest (push) Failing after 1m35s
Build / ubuntu-latest (push) Canceled after 1m46s

This commit is contained in:
2026-09-23 11:20:52 +08:00
parent a232ad518e
commit da467c9297
5 changed files with 152 additions and 55 deletions
+1 -1
View File
@@ -58,5 +58,5 @@ frontend/
## Caveats
- The backend's `/api/system/getLogs` websocket is currently unauthenticated — anyone who can reach the port can read logs. Consider gating it in a future backend change.
- The log websocket needs an `admin` token, and a browser cannot set headers on a WebSocket handshake, so the token rides in the query string (`/api/system/getLogs?token=…&timestamp=…`). That URL is a credential: it can end up in proxy and access logs, so don't paste it into third-party tools. The view reconnects with a fresh token from `localStorage` on every attempt.
- Registering more than one user is intentionally impossible; the backend only accepts the very first registration.
+14 -1
View File
@@ -1,5 +1,6 @@
<script setup>
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
import { getToken } from '../utils/auth.js';
import { LOG_LEVELS } from '../utils/fmt.js';
const MAX_LOGS = 1200;
@@ -38,7 +39,14 @@ const statusText = computed(() => {
function wsUrl() {
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
return `${proto}//${window.location.host}/api/system/getLogs`;
// The backend only upgrades the request for an admin token. A browser cannot
// set headers on a WebSocket handshake, so the credentials travel in the
// query string — the URL itself is therefore a secret.
const params = new URLSearchParams({
token: getToken(),
timestamp: String(Math.floor(Date.now() / 1000))
});
return `${proto}//${window.location.host}/api/system/getLogs?${params}`;
}
function connect() {
@@ -49,6 +57,11 @@ function connect() {
if (ws) {
try { ws.close(); } catch { /* ignore */ }
}
// Signed out: the handshake would be rejected, so don't spin on reconnects.
if (!getToken()) {
status.value = 'closed';
return;
}
status.value = 'connecting';
try {