Add /api/auth/logout endpoint and improve auth flow.

- Implemented `handleAuthLogout` to support user logout by clearing session cookies.
- Improved `/api/auth/status` with authentication cookie validation for persistent login state.
- Enhanced `App.jsx` to prevent UI flash during auth status checks and streamline logout flow.
- Refined user profile handling and permission fetch logic for better reliability.
This commit is contained in:
2025-09-20 20:30:14 +01:00
parent effeae4495
commit 82665444f4
2 changed files with 64 additions and 11 deletions

View File

@@ -113,6 +113,7 @@ func (s *Server) UserInterface() {
s.mux.HandleFunc("/api/auth/challenge", s.handleAuthChallenge) s.mux.HandleFunc("/api/auth/challenge", s.handleAuthChallenge)
s.mux.HandleFunc("/api/auth/login", s.handleAuthLogin) s.mux.HandleFunc("/api/auth/login", s.handleAuthLogin)
s.mux.HandleFunc("/api/auth/status", s.handleAuthStatus) s.mux.HandleFunc("/api/auth/status", s.handleAuthStatus)
s.mux.HandleFunc("/api/auth/logout", s.handleAuthLogout)
s.mux.HandleFunc("/api/permissions/", s.handlePermissions) s.mux.HandleFunc("/api/permissions/", s.handlePermissions)
} }
@@ -206,7 +207,16 @@ func (s *Server) handleAuthLogin(w http.ResponseWriter, r *http.Request) {
return return
} }
// Authentication successful // Authentication successful: set a simple session cookie with the pubkey
cookie := &http.Cookie{
Name: "orly_auth",
Value: hex.Enc(evt.Pubkey),
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
MaxAge: 60 * 60 * 24 * 30, // 30 days
}
http.SetCookie(w, cookie)
w.Write([]byte(`{"success": true, "pubkey": "` + hex.Enc(evt.Pubkey) + `", "message": "Authentication successful"}`)) w.Write([]byte(`{"success": true, "pubkey": "` + hex.Enc(evt.Pubkey) + `", "message": "Authentication successful"}`))
} }
@@ -216,11 +226,38 @@ func (s *Server) handleAuthStatus(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return return
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
// Check for auth cookie
if c, err := r.Cookie("orly_auth"); err == nil && c.Value != "" {
// Validate pubkey format (hex)
if _, err := hex.Dec(c.Value); !chk.E(err) {
w.Write([]byte(`{"authenticated": true, "pubkey": "` + c.Value + `"}`))
return
}
}
w.Write([]byte(`{"authenticated": false}`)) w.Write([]byte(`{"authenticated": false}`))
} }
// handleAuthLogout clears the auth cookie
func (s *Server) handleAuthLogout(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Expire the cookie
http.SetCookie(w, &http.Cookie{
Name: "orly_auth",
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"success": true}`))
}
// handlePermissions returns the permission level for a given pubkey // handlePermissions returns the permission level for a given pubkey
func (s *Server) handlePermissions(w http.ResponseWriter, r *http.Request) { func (s *Server) handlePermissions(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {

View File

@@ -6,9 +6,14 @@ function App() {
const [statusType, setStatusType] = useState('info'); const [statusType, setStatusType] = useState('info');
const [profileData, setProfileData] = useState(null); const [profileData, setProfileData] = useState(null);
const [checkingAuth, setCheckingAuth] = useState(true);
useEffect(() => { useEffect(() => {
// Check authentication status on page load // Check authentication status on page load
checkStatus(); (async () => {
await checkStatus();
setCheckingAuth(false);
})();
}, []); }, []);
// Effect to fetch profile when user changes // Effect to fetch profile when user changes
@@ -30,17 +35,20 @@ function App() {
try { try {
const response = await fetch('/api/auth/status'); const response = await fetch('/api/auth/status');
const data = await response.json(); const data = await response.json();
if (data.authenticated) { if (data.authenticated && data.pubkey) {
setUser(data.pubkey); // Fetch permission first, then set user and profile
updateStatus(`Already authenticated as: ${data.pubkey.slice(0, 16)}...`, 'success'); try {
// Check permissions if authenticated
if (data.pubkey) {
const permResponse = await fetch(`/api/permissions/${data.pubkey}`); const permResponse = await fetch(`/api/permissions/${data.pubkey}`);
const permData = await permResponse.json(); const permData = await permResponse.json();
if (permData && permData.permission) { if (permData && permData.permission) {
setUser({...data, permission: permData.permission}); const fullUser = { pubkey: data.pubkey, permission: permData.permission };
setUser(fullUser);
updateStatus(`Already authenticated as: ${data.pubkey.slice(0, 16)}...`, 'success');
// Fire and forget profile fetch
fetchUserProfile(data.pubkey);
} }
} catch (_) {
// ignore permission fetch errors
} }
} }
} catch (error) { } catch (error) {
@@ -306,11 +314,19 @@ function App() {
} }
} }
function logout() { async function logout() {
try {
await fetch('/api/auth/logout', { method: 'POST' });
} catch (_) {}
setUser(null); setUser(null);
updateStatus('Logged out', 'info'); updateStatus('Logged out', 'info');
} }
// Prevent UI flash: wait until we checked auth status
if (checkingAuth) {
return null;
}
return ( return (
<> <>
{user?.permission ? ( {user?.permission ? (