mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 00:07:01 +00:00
fix: ask to sign in again when a password change ends the session
This commit is contained in:
@@ -1490,6 +1490,8 @@
|
||||
"description": "Change the password for this account.",
|
||||
"error": "Failed to change password.",
|
||||
"mismatch": "Passwords do not match.",
|
||||
"session_ended": "Changing the password ended this session. Sign in again with your new password.",
|
||||
"session_ended_title": "Sign in again",
|
||||
"toggle_visibility": "Show or hide password",
|
||||
"too_short": "New password must be at least 8 characters."
|
||||
},
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
pickClientCertAlias,
|
||||
} from '../../lib/client-cert';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
import { useAuthStore } from '../../stores/auth-store';
|
||||
import { passwordChangeOutcome } from './password-change';
|
||||
import {
|
||||
AccountSecurityForbiddenError,
|
||||
isLimitedStalwartSignIn,
|
||||
@@ -155,21 +157,45 @@ function PasswordChangeSection({ otpEnabled }: { otpEnabled: boolean }) {
|
||||
if (newPwd.length < 8) { setError(t('settings.security.password.too_short', "New password must be at least 8 characters.")); return; }
|
||||
if (newPwd !== confirmPwd) { setError(t('settings.security.password.mismatch', "Passwords do not match.")); return; }
|
||||
setSaving(true);
|
||||
const tokenSignIn = jmapClient.usesBearerAuth;
|
||||
let failure: unknown = null;
|
||||
try {
|
||||
const tokenSignIn = jmapClient.usesBearerAuth;
|
||||
await changePassword(currentPwd, newPwd, otpEnabled ? otpCode : undefined);
|
||||
setCurrentPwd(''); setOtpCode(''); setNewPwd(''); setConfirmPwd('');
|
||||
Alert.alert(
|
||||
t('settings.security.password.changed_title', "Password changed"),
|
||||
tokenSignIn
|
||||
? t('settings.security.password.changed_sign_in_again', "Your account password was updated. Sign in again with the new password.")
|
||||
: t('settings.security.password.changed', "Your account password was updated."),
|
||||
);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t('settings.security.password.error', "Failed to change password."));
|
||||
failure = err ?? new Error('Failed to change password');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
const outcome = passwordChangeOutcome(failure, tokenSignIn);
|
||||
if (outcome.kind === 'error') {
|
||||
setError(outcome.message || t('settings.security.password.error', "Failed to change password."));
|
||||
return;
|
||||
}
|
||||
setCurrentPwd(''); setOtpCode(''); setNewPwd(''); setConfirmPwd('');
|
||||
if (outcome.kind === 'changed') {
|
||||
Alert.alert(
|
||||
t('settings.security.password.changed_title', "Password changed"),
|
||||
t('settings.security.password.changed', "Your account password was updated."),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// The change ended the session (see password-change): say so, then open
|
||||
// the sign-in to this account, filled in, instead of letting the next
|
||||
// request fail with a generic error.
|
||||
const accountId = useAuthStore.getState().activeAccountId;
|
||||
Alert.alert(
|
||||
outcome.changed
|
||||
? t('settings.security.password.changed_title', "Password changed")
|
||||
: t('settings.security.password.session_ended_title', "Sign in again"),
|
||||
outcome.changed
|
||||
? t('settings.security.password.changed_sign_in_again', "Your account password was updated. Sign in again with the new password.")
|
||||
: t('settings.security.password.session_ended', "Changing the password ended this session. Sign in again with your new password."),
|
||||
[{
|
||||
text: t('login.mobile.continue', 'Continue'),
|
||||
onPress: () => { if (accountId) void useAuthStore.getState().requestReauth(accountId); },
|
||||
}],
|
||||
{ cancelable: false },
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { AuthenticationError } from '../../../api/jmap-client';
|
||||
import { passwordChangeOutcome } from '../password-change';
|
||||
|
||||
// A password change on Stalwart revokes the session's tokens (0.16 with
|
||||
// TOTP: the sign-in runs on OAuth tokens). The page says "sign in again"
|
||||
// instead of showing the 401 as a failed change.
|
||||
|
||||
describe('passwordChangeOutcome', () => {
|
||||
it('asks a token session to sign in again after the change', () => {
|
||||
expect(passwordChangeOutcome(null, true)).toEqual({ kind: 'sign-in-again', changed: true });
|
||||
expect(passwordChangeOutcome(null, false)).toEqual({ kind: 'changed' });
|
||||
});
|
||||
|
||||
it('takes a 401 on the change for the session it ended, not a failure', () => {
|
||||
expect(passwordChangeOutcome(new AuthenticationError('Session expired'), true))
|
||||
.toEqual({ kind: 'sign-in-again', changed: false });
|
||||
});
|
||||
|
||||
it('shows what the server refused', () => {
|
||||
expect(passwordChangeOutcome(new Error('Current secret is incorrect.'), true))
|
||||
.toEqual({ kind: 'error', message: 'Current secret is incorrect.' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* What the Security page says after a password change. Stalwart revokes the
|
||||
* account's tokens with the old password, refresh token included (webmail
|
||||
* #1432, checked on 0.16.27), so a session on tokens (a password plus TOTP
|
||||
* sign-in, OAuth) ends with the change. That is told as "sign in again with
|
||||
* the new password", and the app opens the sign-in to the account. A 401 on
|
||||
* the change itself is the same ending seen from the other side: the server
|
||||
* dropped the session as it changed the password. Anything else is an error
|
||||
* the form shows.
|
||||
*/
|
||||
export type PasswordChangeOutcome =
|
||||
| { kind: 'changed' }
|
||||
| { kind: 'sign-in-again'; changed: boolean }
|
||||
| { kind: 'error'; message: string | null };
|
||||
|
||||
export function passwordChangeOutcome(error: unknown, tokenSignIn: boolean): PasswordChangeOutcome {
|
||||
if (error === null || error === undefined) {
|
||||
return tokenSignIn ? { kind: 'sign-in-again', changed: true } : { kind: 'changed' };
|
||||
}
|
||||
if (error instanceof Error && error.name === 'AuthenticationError') {
|
||||
return { kind: 'sign-in-again', changed: false };
|
||||
}
|
||||
return { kind: 'error', message: error instanceof Error ? error.message : null };
|
||||
}
|
||||
Reference in New Issue
Block a user