diff --git a/frontend/src/desktop/components/shared/config/configSections/AccountSection.tsx b/frontend/src/desktop/components/shared/config/configSections/AccountSection.tsx deleted file mode 100644 index dbd366947..000000000 --- a/frontend/src/desktop/components/shared/config/configSections/AccountSection.tsx +++ /dev/null @@ -1,298 +0,0 @@ -import React, { useCallback, useMemo, useState } from 'react'; -import { Alert, Button, Group, Modal, Paper, PasswordInput, Stack, Text, TextInput } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import LocalIcon from '@app/components/shared/LocalIcon'; -import { alert as showToast } from '@app/components/toast'; -import { useAuth } from '@app/auth/UseSession'; -import { accountService } from '@app/services/accountService'; -import { connectionModeService } from '@app/services/connectionModeService'; -import { STIRLING_SAAS_URL } from '@app/constants/connection'; -import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex'; - -const AccountSection: React.FC = () => { - const { t } = useTranslation(); - const { user, signOut } = useAuth(); - const [passwordModalOpen, setPasswordModalOpen] = useState(false); - const [usernameModalOpen, setUsernameModalOpen] = useState(false); - - const [currentPassword, setCurrentPassword] = useState(''); - const [newPassword, setNewPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [passwordError, setPasswordError] = useState(''); - const [passwordSubmitting, setPasswordSubmitting] = useState(false); - - const [currentPasswordForUsername, setCurrentPasswordForUsername] = useState(''); - const [newUsername, setNewUsername] = useState(''); - const [usernameError, setUsernameError] = useState(''); - const [usernameSubmitting, setUsernameSubmitting] = useState(false); - - const authTypeFromMetadata = useMemo(() => { - const metadata = user?.app_metadata as { authType?: string; authenticationType?: string } | undefined; - return metadata?.authenticationType ?? metadata?.authType; - }, [user?.app_metadata]); - - const normalizedAuthType = useMemo( - () => (user?.authenticationType ?? authTypeFromMetadata ?? '').toLowerCase(), - [authTypeFromMetadata, user?.authenticationType] - ); - const isSsoUser = useMemo(() => ['sso', 'oauth2', 'saml2'].includes(normalizedAuthType), [normalizedAuthType]); - - const userIdentifier = useMemo(() => user?.email || user?.username || '', [user?.email, user?.username]); - - const redirectToLogin = useCallback(() => { - window.location.assign('/login'); - }, []); - - const handleLogout = useCallback(async () => { - try { - await signOut(); - - // Desktop-specific: mirror the Connection Settings logout flow to avoid stale config and bad login screens - await connectionModeService.switchToSaaS(STIRLING_SAAS_URL); - await connectionModeService.resetSetupCompletion().catch(() => {}); - window.history.replaceState({}, '', '/'); - window.location.reload(); - return; - } catch (desktopErr) { - console.warn('[Desktop AccountSection] Logout fallback failed, redirecting to login', desktopErr); - } - - redirectToLogin(); - }, [redirectToLogin, signOut]); - - const handlePasswordSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - - if (isSsoUser) { - setPasswordError(t('settings.security.password.ssoDisabled', 'Password changes are managed by your identity provider.')); - return; - } - - if (!currentPassword || !newPassword || !confirmPassword) { - setPasswordError(t('settings.security.password.required', 'All fields are required.')); - return; - } - - if (newPassword !== confirmPassword) { - setPasswordError(t('settings.security.password.mismatch', 'New passwords do not match.')); - return; - } - - try { - setPasswordSubmitting(true); - setPasswordError(''); - - await accountService.changePassword(currentPassword, newPassword); - - showToast({ - alertType: 'success', - title: t('settings.security.password.success', 'Password updated successfully. Please sign in again.'), - }); - - setCurrentPassword(''); - setNewPassword(''); - setConfirmPassword(''); - setPasswordModalOpen(false); - await handleLogout(); - } catch (err) { - const axiosError = err as { response?: { data?: { message?: string } } }; - setPasswordError( - axiosError.response?.data?.message || - t('settings.security.password.error', 'Unable to update password. Please verify your current password and try again.') - ); - } finally { - setPasswordSubmitting(false); - } - }; - - const handleUsernameSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - - if (isSsoUser) { - setUsernameError(t('changeCreds.ssoManaged', 'Your account is managed by your identity provider.')); - return; - } - - if (!currentPasswordForUsername || !newUsername) { - setUsernameError(t('settings.security.password.required', 'All fields are required.')); - return; - } - - try { - setUsernameSubmitting(true); - setUsernameError(''); - - await accountService.changeUsername(newUsername, currentPasswordForUsername); - - showToast({ - alertType: 'success', - title: t('changeCreds.credsUpdated', 'Account updated'), - body: t('changeCreds.description', 'Changes saved. Please log in again.'), - }); - - setNewUsername(''); - setCurrentPasswordForUsername(''); - setUsernameModalOpen(false); - await handleLogout(); - } catch (err) { - const axiosError = err as { response?: { data?: { message?: string } } }; - setUsernameError( - axiosError.response?.data?.message || - t('changeCreds.error', 'Unable to update username. Please verify your password and try again.') - ); - } finally { - setUsernameSubmitting(false); - } - }; - - return ( - -
- - {t('account.accountSettings', 'Account')} - - - {t('changeCreds.header', 'Update Your Account Details')} - -
- - - - - {userIdentifier - ? t('settings.general.user', 'User') + ': ' + userIdentifier - : t('account.accountSettings', 'Account Settings')} - - - - {isSsoUser && ( - } color="blue" variant="light"> - {t('changeCreds.ssoManaged', 'Your account is managed by your identity provider.')} - - )} - - - {!isSsoUser && ( - - )} - - {!isSsoUser && ( - - )} - - - - - - - - setPasswordModalOpen(false)} - title={t('settings.security.title', 'Change password')} - withinPortal - zIndex={Z_INDEX_OVER_CONFIG_MODAL} - > -
- - - {t('settings.security.password.subtitle', 'Change your password. You will be logged out after updating.')} - - - {passwordError && ( - } color="red" variant="light"> - {passwordError} - - )} - - setCurrentPassword(event.currentTarget.value)} - required - /> - - setNewPassword(event.currentTarget.value)} - required - /> - - setConfirmPassword(event.currentTarget.value)} - required - /> - - - - - -
-
- - setUsernameModalOpen(false)} - title={t('account.changeUsername', 'Change username')} - withinPortal - zIndex={Z_INDEX_OVER_CONFIG_MODAL} - > -
- - - {t('account.changeUsernameDescription', 'Update your username. You will be logged out after updating.')} - - - {usernameError && ( - } color="red" variant="light"> - {usernameError} - - )} - - setCurrentPasswordForUsername(event.currentTarget.value)} - required - /> - - setNewUsername(event.currentTarget.value)} - required - /> - - - - - -
-
-
- ); -}; - -export default AccountSection; diff --git a/frontend/src/desktop/extensions/accountLogout.ts b/frontend/src/desktop/extensions/accountLogout.ts new file mode 100644 index 000000000..59099ac1b --- /dev/null +++ b/frontend/src/desktop/extensions/accountLogout.ts @@ -0,0 +1,31 @@ +import { connectionModeService } from '@app/services/connectionModeService'; +import { STIRLING_SAAS_URL } from '@app/constants/connection'; + +type SignOutFn = () => Promise; + +interface AccountLogoutDeps { + signOut: SignOutFn; + redirectToLogin: () => void; +} + +/** + * Desktop-specific logout: mirrors Connection Settings flow to avoid stale state. + */ +export function useAccountLogout() { + return async ({ signOut, redirectToLogin }: AccountLogoutDeps): Promise => { + try { + await signOut(); + + await connectionModeService.switchToSaaS(STIRLING_SAAS_URL); + await connectionModeService.resetSetupCompletion().catch(() => {}); + + window.history.replaceState({}, '', '/'); + window.location.reload(); + return; + } catch (err) { + console.warn('[Desktop AccountLogout] Desktop-specific logout failed, falling back to redirect', err); + } + + redirectToLogin(); + }; +} diff --git a/frontend/src/proprietary/components/shared/config/configSections/AccountSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AccountSection.tsx index 95a9ee57f..81130ec69 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AccountSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AccountSection.tsx @@ -6,10 +6,12 @@ import { alert as showToast } from '@app/components/toast'; import { useAuth } from '@app/auth/UseSession'; import { accountService } from '@app/services/accountService'; import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex'; +import { useAccountLogout } from '@app/extensions/accountLogout'; const AccountSection: React.FC = () => { const { t } = useTranslation(); const { user, signOut } = useAuth(); + const accountLogout = useAccountLogout(); const [passwordModalOpen, setPasswordModalOpen] = useState(false); const [usernameModalOpen, setUsernameModalOpen] = useState(false); @@ -42,12 +44,8 @@ const AccountSection: React.FC = () => { }, []); const handleLogout = useCallback(async () => { - try { - await signOut(); - } finally { - redirectToLogin(); - } - }, [redirectToLogin, signOut]); + await accountLogout({ signOut, redirectToLogin }); + }, [accountLogout, redirectToLogin, signOut]); const handlePasswordSubmit = async (event: React.FormEvent) => { event.preventDefault(); diff --git a/frontend/src/proprietary/extensions/accountLogout.ts b/frontend/src/proprietary/extensions/accountLogout.ts new file mode 100644 index 000000000..fbff0108e --- /dev/null +++ b/frontend/src/proprietary/extensions/accountLogout.ts @@ -0,0 +1,20 @@ +type SignOutFn = () => Promise; + +interface AccountLogoutDeps { + signOut: SignOutFn; + redirectToLogin: () => void; +} + +/** + * Default (web/proprietary) logout handler: sign out and redirect to /login. + * Desktop builds override this file via path resolution. + */ +export function useAccountLogout() { + return async ({ signOut, redirectToLogin }: AccountLogoutDeps): Promise => { + try { + await signOut(); + } finally { + redirectToLogin(); + } + }; +}