mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-22 11:13:26 +00:00
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:
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user