feat: add logout button to header when auth is enabled

- adds logout button next to connection status indicator
- implements /api/logout endpoint to clear sessions
- button only shows when authentication is configured
- clears session cookie and invalidates server-side session

implements #315
This commit is contained in:
Pulse Monitor
2025-08-14 14:39:26 +00:00
parent 541e8f3c64
commit 57df26668a
2 changed files with 101 additions and 0 deletions
+47
View File
@@ -34,6 +34,7 @@ function App() {
// Simple auth state
const [isLoading, setIsLoading] = createSignal(true);
const [needsAuth, setNeedsAuth] = createSignal(false);
const [hasAuth, setHasAuth] = createSignal(false);
// Don't initialize WebSocket until after auth check
const [wsStore, setWsStore] = createSignal<EnhancedStore | null>(null);
@@ -97,6 +98,16 @@ function App() {
// Check auth on mount
onMount(() => {
// First check security status to see if auth is configured
fetch('/api/security/status')
.then(res => res.json())
.then(data => {
setHasAuth(data.hasAuthentication || false);
})
.catch(() => {
setHasAuth(false);
});
fetch('/api/state', {
headers: {
'X-Requested-With': 'XMLHttpRequest',
@@ -131,6 +142,30 @@ function App() {
const handleLogin = () => {
window.location.reload();
};
const handleLogout = async () => {
try {
// Clear any session data
await fetch('/api/logout', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
},
credentials: 'include'
});
} catch (error) {
console.error('Logout error:', error);
}
// Clear WebSocket connection
setWsStore(null);
// Set auth required
setNeedsAuth(true);
// Reload to clear any cached data
window.location.reload();
};
// Pass through the store directly (only when initialized)
const enhancedStore = () => wsStore();
@@ -211,6 +246,18 @@ function App() {
</Show>
{connected() ? 'Connected' : reconnecting() ? 'Reconnecting...' : 'Disconnected'}
</div>
<Show when={hasAuth() && !needsAuth()}>
<button
onClick={handleLogout}
class="text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors flex items-center gap-1"
title="Logout"
>
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
<span>Logout</span>
</button>
</Show>
</div>
</div>
</div>
+54
View File
@@ -191,6 +191,7 @@ func (r *Router) setupRoutes() {
// Security routes
r.mux.HandleFunc("/api/security/change-password", r.handleChangePassword)
r.mux.HandleFunc("/api/security/remove-password", r.handleRemovePassword)
r.mux.HandleFunc("/api/logout", r.handleLogout)
r.mux.HandleFunc("/api/security/status", func(w http.ResponseWriter, req *http.Request) {
if req.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
@@ -1119,6 +1120,59 @@ func (r *Router) handleRemovePassword(w http.ResponseWriter, req *http.Request)
})
}
// handleLogout handles logout requests
func (r *Router) handleLogout(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
writeErrorResponse(w, http.StatusMethodNotAllowed, "method_not_allowed",
"Only POST method is allowed", nil)
return
}
// Get session token from cookie
var sessionToken string
if cookie, err := req.Cookie("pulse_session"); err == nil {
sessionToken = cookie.Value
}
// Delete the session if it exists
if sessionToken != "" {
sessionMu.Lock()
delete(sessions, sessionToken)
sessionMu.Unlock()
// Also delete CSRF token if exists
csrfMu.Lock()
delete(csrfTokens, sessionToken)
csrfMu.Unlock()
}
// Clear the session cookie
http.SetCookie(w, &http.Cookie{
Name: "pulse_session",
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
Secure: req.TLS != nil || req.Header.Get("X-Forwarded-Proto") == "https",
SameSite: http.SameSiteStrictMode,
})
// Audit log logout (use admin as username since we have single user for now)
LogAuditEvent("logout", "admin", GetClientIP(req), req.URL.Path, true, "User logged out")
log.Info().
Str("user", "admin").
Str("ip", GetClientIP(req)).
Msg("User logged out")
// Return success
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"message": "Successfully logged out",
})
}
// handleState handles state requests
func (r *Router) handleState(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {