diff --git a/src/backend/utils/auth-manager.ts b/src/backend/utils/auth-manager.ts index e1aa0cd1..3c66137d 100644 --- a/src/backend/utils/auth-manager.ts +++ b/src/backend/utils/auth-manager.ts @@ -578,19 +578,6 @@ class AuthManager { sessionId, }); } - } else { - try { - await db.delete(sessions).where(eq(sessions.userId, userId)); - } catch (error) { - databaseLogger.error( - "Failed to delete user sessions on logout", - error, - { - operation: "sessions_delete_logout_failed", - userId, - }, - ); - } } } diff --git a/src/ui/desktop/admin/AdminSettings.tsx b/src/ui/desktop/admin/AdminSettings.tsx index f520d6fe..02c45dfd 100644 --- a/src/ui/desktop/admin/AdminSettings.tsx +++ b/src/ui/desktop/admin/AdminSettings.tsx @@ -52,6 +52,9 @@ import { getUserInfo, getCookie, isElectron, + getSessions, + revokeSession, + revokeAllUserSessions, } from "@/ui/main-axios.ts"; interface AdminSettingsProps { @@ -126,6 +129,7 @@ export function AdminSettings({ expiresAt: string; lastActiveAt: string; jwtToken: string; + isRevoked?: boolean; }> >([]); const [sessionsLoading, setSessionsLoading] = React.useState(false); @@ -565,35 +569,8 @@ export function AdminSettings({ setSessionsLoading(true); try { - const isDev = - !isElectron() && - process.env.NODE_ENV === "development" && - (window.location.port === "3000" || - window.location.port === "5173" || - window.location.port === "" || - window.location.hostname === "localhost" || - window.location.hostname === "127.0.0.1"); - - const apiUrl = isElectron() - ? `${(window as { configuredServerUrl?: string }).configuredServerUrl}/users/sessions` - : isDev - ? `http://localhost:30001/users/sessions` - : `/users/sessions`; - - const response = await fetch(apiUrl, { - method: "GET", - credentials: "include", - headers: { - Authorization: `Bearer ${getCookie("jwt")}`, - }, - }); - - if (response.ok) { - const data = await response.json(); - setSessions(data.sessions || []); - } else { - toast.error(t("admin.failedToFetchSessions")); - } + const data = await getSessions(); + setSessions(data.sessions || []); } catch (err) { if (!err?.message?.includes("No server configured")) { toast.error(t("admin.failedToFetchSessions")); @@ -612,41 +589,15 @@ export function AdminSettings({ t("admin.confirmRevokeSession"), async () => { try { - const isDev = - !isElectron() && - process.env.NODE_ENV === "development" && - (window.location.port === "3000" || - window.location.port === "5173" || - window.location.port === "" || - window.location.hostname === "localhost" || - window.location.hostname === "127.0.0.1"); + await revokeSession(sessionId); + toast.success(t("admin.sessionRevokedSuccessfully")); - const apiUrl = isElectron() - ? `${(window as { configuredServerUrl?: string }).configuredServerUrl}/users/sessions/${sessionId}` - : isDev - ? `http://localhost:30001/users/sessions/${sessionId}` - : `/users/sessions/${sessionId}`; - - const response = await fetch(apiUrl, { - method: "DELETE", - credentials: "include", - headers: { - Authorization: `Bearer ${getCookie("jwt")}`, - }, - }); - - if (response.ok) { - toast.success(t("admin.sessionRevokedSuccessfully")); - - if (isCurrentSession) { - setTimeout(() => { - window.location.reload(); - }, 1000); - } else { - fetchSessions(); - } + if (isCurrentSession) { + setTimeout(() => { + window.location.reload(); + }, 1000); } else { - toast.error(t("admin.failedToRevokeSession")); + fetchSessions(); } } catch { toast.error(t("admin.failedToRevokeSession")); @@ -663,49 +614,15 @@ export function AdminSettings({ t("admin.confirmRevokeAllSessions"), async () => { try { - const isDev = - !isElectron() && - process.env.NODE_ENV === "development" && - (window.location.port === "3000" || - window.location.port === "5173" || - window.location.port === "" || - window.location.hostname === "localhost" || - window.location.hostname === "127.0.0.1"); + const data = await revokeAllUserSessions(userId); + toast.success(data.message || t("admin.sessionsRevokedSuccessfully")); - const apiUrl = isElectron() - ? `${(window as { configuredServerUrl?: string }).configuredServerUrl}/users/sessions/revoke-all` - : isDev - ? `http://localhost:30001/users/sessions/revoke-all` - : `/users/sessions/revoke-all`; - - const response = await fetch(apiUrl, { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${getCookie("jwt")}`, - }, - body: JSON.stringify({ - targetUserId: userId, - exceptCurrent: false, - }), - }); - - if (response.ok) { - const data = await response.json(); - toast.success( - data.message || t("admin.sessionsRevokedSuccessfully"), - ); - - if (isCurrentUser) { - setTimeout(() => { - window.location.reload(); - }, 1000); - } else { - fetchSessions(); - } + if (isCurrentUser) { + setTimeout(() => { + window.location.reload(); + }, 1000); } else { - toast.error(t("admin.failedToRevokeSessions")); + fetchSessions(); } } catch { toast.error(t("admin.failedToRevokeSessions")); diff --git a/src/ui/desktop/authentication/Auth.tsx b/src/ui/desktop/authentication/Auth.tsx index eaa82600..f6f186bb 100644 --- a/src/ui/desktop/authentication/Auth.tsx +++ b/src/ui/desktop/authentication/Auth.tsx @@ -270,6 +270,7 @@ export function Auth({ "*", ); setWebviewAuthSuccess(true); + setTimeout(() => window.location.reload(), 100); setLoading(false); return; } catch (e) {} @@ -446,6 +447,7 @@ export function Auth({ "*", ); setWebviewAuthSuccess(true); + setTimeout(() => window.location.reload(), 100); setTotpLoading(false); return; } catch (e) {} @@ -553,6 +555,7 @@ export function Auth({ "*", ); setWebviewAuthSuccess(true); + setTimeout(() => window.location.reload(), 100); setOidcLoading(false); return; } catch (e) {} diff --git a/src/ui/desktop/authentication/ElectronLoginForm.tsx b/src/ui/desktop/authentication/ElectronLoginForm.tsx index 905c08cc..7ab2dee8 100644 --- a/src/ui/desktop/authentication/ElectronLoginForm.tsx +++ b/src/ui/desktop/authentication/ElectronLoginForm.tsx @@ -3,23 +3,7 @@ import { Button } from "@/components/ui/button.tsx"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx"; import { useTranslation } from "react-i18next"; import { AlertCircle, Loader2, ArrowLeft, RefreshCw } from "lucide-react"; -import { getCookie, getUserInfo } from "@/ui/main-axios.ts"; - -declare global { - namespace JSX { - interface IntrinsicElements { - webview: React.DetailedHTMLProps< - React.HTMLAttributes, - HTMLElement - > & { - src?: string; - partition?: string; - allowpopups?: string; - ref?: React.Ref; - }; - } - } -} +import { getCookie } from "@/ui/main-axios.ts"; interface ElectronLoginFormProps { serverUrl: string; @@ -36,12 +20,10 @@ export function ElectronLoginForm({ const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [isAuthenticating, setIsAuthenticating] = useState(false); - const webviewRef = useRef(null); + const iframeRef = useRef(null); const hasAuthenticatedRef = useRef(false); const [currentUrl, setCurrentUrl] = useState(serverUrl); const hasLoadedOnce = useRef(false); - const urlCheckInterval = useRef(null); - const loadTimeout = useRef(null); useEffect(() => { const handleMessage = async (event: MessageEvent) => { @@ -57,7 +39,8 @@ export function ElectronLoginForm({ if ( data.type === "AUTH_SUCCESS" && data.token && - !hasAuthenticatedRef.current + !hasAuthenticatedRef.current && + !isAuthenticating ) { hasAuthenticatedRef.current = true; setIsAuthenticating(true); @@ -70,32 +53,11 @@ export function ElectronLoginForm({ throw new Error("Failed to save JWT to localStorage"); } - try { - await getUserInfo(); - } catch (verifyErr) { - localStorage.removeItem("jwt"); - const errorMsg = - verifyErr instanceof Error - ? verifyErr.message - : "Failed to verify authentication"; - console.error("Authentication verification failed:", verifyErr); - throw new Error( - errorMsg.includes("registration") || - errorMsg.includes("allowed") - ? "Authentication failed. Please check your server connection and try again." - : errorMsg, - ); - } - await new Promise((resolve) => setTimeout(resolve, 500)); onAuthSuccess(); } catch (err) { - const errorMessage = - err instanceof Error - ? err.message - : t("errors.authTokenSaveFailed"); - setError(errorMessage); + setError(t("errors.authTokenSaveFailed")); setIsAuthenticating(false); hasAuthenticatedRef.current = false; } @@ -109,190 +71,127 @@ export function ElectronLoginForm({ return () => { window.removeEventListener("message", handleMessage); }; - }, [serverUrl, onAuthSuccess, t]); + }, [serverUrl, isAuthenticating, onAuthSuccess, t]); useEffect(() => { - const checkWebviewUrl = () => { - const webview = webviewRef.current; - if (!webview) return; - - try { - const webviewUrl = webview.getURL(); - if (webviewUrl && webviewUrl !== currentUrl) { - setCurrentUrl(webviewUrl); - } - } catch (e) {} - }; - - urlCheckInterval.current = setInterval(checkWebviewUrl, 500); - - return () => { - if (urlCheckInterval.current) { - clearInterval(urlCheckInterval.current); - urlCheckInterval.current = null; - } - }; - }, [currentUrl]); - - useEffect(() => { - const webview = webviewRef.current; - if (!webview) return; - - loadTimeout.current = setTimeout(() => { - if (!hasLoadedOnce.current && loading) { - setLoading(false); - setError( - "Unable to connect to server. Please check the server URL and try again.", - ); - } - }, 15000); + const iframe = iframeRef.current; + if (!iframe) return; const handleLoad = () => { - if (loadTimeout.current) { - clearTimeout(loadTimeout.current); - loadTimeout.current = null; - } - setLoading(false); hasLoadedOnce.current = true; setError(null); try { - const webviewUrl = webview.getURL(); - setCurrentUrl(webviewUrl || serverUrl); + if (iframe.contentWindow) { + setCurrentUrl(iframe.contentWindow.location.href); + } } catch (e) { setCurrentUrl(serverUrl); } - const injectedScript = ` - (function() { - window.IS_ELECTRON = true; - window.IS_ELECTRON_WEBVIEW = true; - if (typeof window.electronAPI === 'undefined') { - window.electronAPI = { isElectron: true }; - } + try { + const injectedScript = ` + (function() { + let hasNotified = false; - let hasNotified = false; + function postJWTToParent(token, source) { + if (hasNotified) { + return; + } + hasNotified = true; - function postJWTToParent(token, source) { - if (hasNotified) { - return; + try { + window.parent.postMessage({ + type: 'AUTH_SUCCESS', + token: token, + source: source, + platform: 'desktop', + timestamp: Date.now() + }, '*'); + } catch (e) { + } } - hasNotified = true; - try { - window.parent.postMessage({ - type: 'AUTH_SUCCESS', - token: token, - source: source, - platform: 'desktop', - timestamp: Date.now() - }, '*'); - } catch (e) { - } - } - - function clearAuthData() { - try { - localStorage.removeItem('jwt'); - sessionStorage.removeItem('jwt'); - - const cookies = document.cookie.split(';'); - for (let i = 0; i < cookies.length; i++) { - const cookie = cookies[i]; - const eqPos = cookie.indexOf('='); - const name = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim(); - if (name === 'jwt') { - document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/'; - document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=' + window.location.hostname; + function checkAuth() { + try { + const localToken = localStorage.getItem('jwt'); + if (localToken && localToken.length > 20) { + postJWTToParent(localToken, 'localStorage'); + return true; } - } - } catch (error) { - } - } - window.addEventListener('message', function(event) { - try { - if (event.data && typeof event.data === 'object') { - if (event.data.type === 'CLEAR_AUTH_DATA') { - clearAuthData(); + const sessionToken = sessionStorage.getItem('jwt'); + if (sessionToken && sessionToken.length > 20) { + postJWTToParent(sessionToken, 'sessionStorage'); + return true; } - } - } catch (error) { - } - }); - function checkAuth() { - try { - const localToken = localStorage.getItem('jwt'); - if (localToken && localToken.length > 20) { - postJWTToParent(localToken, 'localStorage'); - return true; - } + const cookies = document.cookie; + if (cookies && cookies.length > 0) { + const cookieArray = cookies.split('; '); + const tokenCookie = cookieArray.find(row => row.startsWith('jwt=')); - const sessionToken = sessionStorage.getItem('jwt'); - if (sessionToken && sessionToken.length > 20) { - postJWTToParent(sessionToken, 'sessionStorage'); - return true; - } - - const cookies = document.cookie; - if (cookies && cookies.length > 0) { - const cookieArray = cookies.split('; '); - const tokenCookie = cookieArray.find(row => row.startsWith('jwt=')); - - if (tokenCookie) { - const token = tokenCookie.split('=')[1]; - if (token && token.length > 20) { - postJWTToParent(token, 'cookie'); - return true; + if (tokenCookie) { + const token = tokenCookie.split('=')[1]; + if (token && token.length > 20) { + postJWTToParent(token, 'cookie'); + return true; + } } } + } catch (error) { } - } catch (error) { + return false; + } + + const originalSetItem = localStorage.setItem; + localStorage.setItem = function(key, value) { + originalSetItem.apply(this, arguments); + if (key === 'jwt' && value && value.length > 20 && !hasNotified) { + setTimeout(() => checkAuth(), 100); + } + }; + + const originalSessionSetItem = sessionStorage.setItem; + sessionStorage.setItem = function(key, value) { + originalSessionSetItem.apply(this, arguments); + if (key === 'jwt' && value && value.length > 20 && !hasNotified) { + setTimeout(() => checkAuth(), 100); + } + }; + + const intervalId = setInterval(() => { + if (hasNotified) { + clearInterval(intervalId); + return; + } + if (checkAuth()) { + clearInterval(intervalId); + } + }, 500); + + setTimeout(() => { + clearInterval(intervalId); + }, 300000); + + setTimeout(() => checkAuth(), 500); + })(); + `; + + try { + if (iframe.contentWindow) { + try { + iframe.contentWindow.eval(injectedScript); + } catch (evalError) { + iframe.contentWindow.postMessage( + { type: "INJECT_SCRIPT", script: injectedScript }, + "*", + ); } - return false; } - - const originalSetItem = localStorage.setItem; - localStorage.setItem = function(key, value) { - originalSetItem.apply(this, arguments); - if (key === 'jwt' && value && value.length > 20 && !hasNotified) { - setTimeout(() => checkAuth(), 100); - } - }; - - const originalSessionSetItem = sessionStorage.setItem; - sessionStorage.setItem = function(key, value) { - originalSessionSetItem.apply(this, arguments); - if (key === 'jwt' && value && value.length > 20 && !hasNotified) { - setTimeout(() => checkAuth(), 100); - } - }; - - const intervalId = setInterval(() => { - if (hasNotified) { - clearInterval(intervalId); - return; - } - if (checkAuth()) { - clearInterval(intervalId); - } - }, 500); - - setTimeout(() => { - clearInterval(intervalId); - }, 300000); - - setTimeout(() => checkAuth(), 500); - })(); - `; - - try { - webview.executeJavaScript(injectedScript); - } catch (err) { - console.error("Failed to inject authentication script:", err); - } + } catch (err) {} + } catch (err) {} }; const handleError = () => { @@ -302,27 +201,18 @@ export function ElectronLoginForm({ } }; - webview.addEventListener("did-finish-load", handleLoad); - webview.addEventListener("did-fail-load", handleError); + iframe.addEventListener("load", handleLoad); + iframe.addEventListener("error", handleError); return () => { - webview.removeEventListener("did-finish-load", handleLoad); - webview.removeEventListener("did-fail-load", handleError); - if (loadTimeout.current) { - clearTimeout(loadTimeout.current); - loadTimeout.current = null; - } + iframe.removeEventListener("load", handleLoad); + iframe.removeEventListener("error", handleError); }; - }, [t, loading, serverUrl]); + }, [t]); const handleRefresh = () => { - if (webviewRef.current) { - if (loadTimeout.current) { - clearTimeout(loadTimeout.current); - loadTimeout.current = null; - } - - webviewRef.current.src = serverUrl; + if (iframeRef.current) { + iframeRef.current.src = serverUrl; setLoading(true); setError(null); } @@ -385,28 +275,14 @@ export function ElectronLoginForm({ )} - {isAuthenticating && ( -
-
- - - {t("auth.authenticating")} - -
-
- )} -
-
diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index 689023e7..d311874c 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -342,6 +342,7 @@ function createApiInstance( import("sonner").then(({ toast }) => { toast.warning("Session expired. Please log in again."); + window.location.reload(); }); const currentPath = window.location.pathname; @@ -1944,6 +1945,53 @@ export async function getUserList(): Promise<{ users: UserInfo[] }> { } } +export async function getSessions(): Promise<{ + sessions: { + id: string; + userId: string; + username?: string; + deviceType: string; + deviceInfo: string; + createdAt: string; + expiresAt: string; + lastActiveAt: string; + jwtToken: string; + isRevoked?: boolean; + }[]; +}> { + try { + const response = await authApi.get("/users/sessions"); + return response.data; + } catch (error) { + handleApiError(error, "fetch sessions"); + } +} + +export async function revokeSession( + sessionId: string, +): Promise<{ success: boolean; message: string }> { + try { + const response = await authApi.delete(`/users/sessions/${sessionId}`); + return response.data; + } catch (error) { + handleApiError(error, "revoke session"); + } +} + +export async function revokeAllUserSessions( + userId: string, +): Promise<{ success: boolean; message: string }> { + try { + const response = await authApi.post("/users/sessions/revoke-all", { + targetUserId: userId, + exceptCurrent: false, + }); + return response.data; + } catch (error) { + handleApiError(error, "revoke all user sessions"); + } +} + export async function makeUserAdmin( username: string, ): Promise> { diff --git a/src/ui/mobile/authentication/Auth.tsx b/src/ui/mobile/authentication/Auth.tsx index 95a2622c..b4b4b709 100644 --- a/src/ui/mobile/authentication/Auth.tsx +++ b/src/ui/mobile/authentication/Auth.tsx @@ -244,12 +244,6 @@ export function Auth({ setUsername(meRes.username || null); setUserId(meRes.userId || null); setDbError(null); - onAuthSuccess({ - isAdmin: !!meRes.is_admin, - username: meRes.username || null, - userId: meRes.userId || null, - }); - postJWTToWebView(); if (isReactNativeWebView()) { @@ -258,6 +252,12 @@ export function Auth({ return; } + onAuthSuccess({ + isAdmin: !!meRes.is_admin, + username: meRes.username || null, + userId: meRes.userId || null, + }); + setInternalLoggedIn(true); if (tab === "signup") { setSignupConfirmPassword(""); @@ -417,11 +417,6 @@ export function Auth({ setUserId(res.userId || null); setDbError(null); - onAuthSuccess({ - isAdmin: !!res.is_admin, - username: res.username || null, - userId: res.userId || null, - }); postJWTToWebView(); if (isReactNativeWebView()) { @@ -430,6 +425,12 @@ export function Auth({ return; } + onAuthSuccess({ + isAdmin: !!res.is_admin, + username: res.username || null, + userId: res.userId || null, + }); + setInternalLoggedIn(true); setTotpRequired(false); setTotpCode(""); @@ -510,12 +511,6 @@ export function Auth({ setUsername(meRes.username || null); setUserId(meRes.userId || null); setDbError(null); - onAuthSuccess({ - isAdmin: !!meRes.is_admin, - username: meRes.username || null, - userId: meRes.userId || null, - }); - postJWTToWebView(); if (isReactNativeWebView()) { @@ -529,6 +524,12 @@ export function Auth({ return; } + onAuthSuccess({ + isAdmin: !!meRes.is_admin, + username: meRes.username || null, + userId: meRes.userId || null, + }); + setInternalLoggedIn(true); window.history.replaceState( {},