fix: surface server errors on password change and TOTP toggle
This commit is contained in:
@@ -222,6 +222,20 @@ describe('account-security-store', () => {
|
|||||||
expect(useAccountSecurityStore.getState().error).toBe('forbidden');
|
expect(useAccountSecurityStore.getState().error).toBe('forbidden');
|
||||||
expect(useAccountSecurityStore.getState().isSaving).toBe(false);
|
expect(useAccountSecurityStore.getState().isSaving).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('throws the server message when the set comes back as notUpdated (HTTP 200)', async () => {
|
||||||
|
mockedJmap.mockResolvedValueOnce([
|
||||||
|
['x:AccountPassword/set', {
|
||||||
|
notUpdated: { singleton: { type: 'invalidProperties', description: 'Current password is incorrect' } },
|
||||||
|
}, '0'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
useAccountSecurityStore.getState().changePassword('wrong', 'new'),
|
||||||
|
).rejects.toThrow('Current password is incorrect');
|
||||||
|
expect(useAccountSecurityStore.getState().error).toBe('Current password is incorrect');
|
||||||
|
expect(useAccountSecurityStore.getState().isSaving).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('updateDisplayName', () => {
|
describe('updateDisplayName', () => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { debug } from '@/lib/debug';
|
import { debug } from '@/lib/debug';
|
||||||
import { useAuthStore } from '@/stores/auth-store';
|
import { useAuthStore } from '@/stores/auth-store';
|
||||||
import { stalwartJmap, requireResult } from '@/lib/stalwart/jmap-passthrough';
|
import { stalwartJmap, requireResult, type JmapMethodResponse } from '@/lib/stalwart/jmap-passthrough';
|
||||||
|
|
||||||
export type EncryptionType = 'Disabled' | 'Aes128' | 'Aes256';
|
export type EncryptionType = 'Disabled' | 'Aes128' | 'Aes256';
|
||||||
|
|
||||||
@@ -173,6 +173,23 @@ async function removeCredential(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A JMAP `/set` reports per-object failures (wrong current password, weak
|
||||||
|
* password, …) inside `notUpdated` with an HTTP 200 — `stalwartJmap` does not
|
||||||
|
* throw for these. Inspect the response and surface the server's message so the
|
||||||
|
* UI doesn't report a failed change as successful.
|
||||||
|
*/
|
||||||
|
function requireAccountPasswordUpdate(responses: JmapMethodResponse[], fallbackError: string): void {
|
||||||
|
const result = requireResult<{
|
||||||
|
updated?: Record<string, unknown>;
|
||||||
|
notUpdated?: Record<string, { type?: string; description?: string }>;
|
||||||
|
}>(responses, 'x:AccountPassword/set');
|
||||||
|
const failed = result.notUpdated?.singleton;
|
||||||
|
if (failed) {
|
||||||
|
throw new Error(failed.description || failed.type || fallbackError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function extractEncryptionType(raw: unknown): EncryptionType {
|
function extractEncryptionType(raw: unknown): EncryptionType {
|
||||||
if (!raw || typeof raw !== 'object') return 'Disabled';
|
if (!raw || typeof raw !== 'object') return 'Disabled';
|
||||||
const type = (raw as { ['@type']?: string })['@type'];
|
const type = (raw as { ['@type']?: string })['@type'];
|
||||||
@@ -344,7 +361,7 @@ export const useAccountSecurityStore = create<AccountSecurityState>()((set, get)
|
|||||||
set({ isSaving: true, error: null });
|
set({ isSaving: true, error: null });
|
||||||
try {
|
try {
|
||||||
const accountId = getPrimaryAccountId();
|
const accountId = getPrimaryAccountId();
|
||||||
await stalwartJmap([
|
const responses = await stalwartJmap([
|
||||||
[
|
[
|
||||||
'x:AccountPassword/set',
|
'x:AccountPassword/set',
|
||||||
{
|
{
|
||||||
@@ -354,6 +371,7 @@ export const useAccountSecurityStore = create<AccountSecurityState>()((set, get)
|
|||||||
'0',
|
'0',
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
requireAccountPasswordUpdate(responses, 'Failed to change password');
|
||||||
set({ isSaving: false });
|
set({ isSaving: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({
|
set({
|
||||||
@@ -389,7 +407,7 @@ export const useAccountSecurityStore = create<AccountSecurityState>()((set, get)
|
|||||||
set({ isSaving: true, error: null });
|
set({ isSaving: true, error: null });
|
||||||
try {
|
try {
|
||||||
const accountId = getPrimaryAccountId();
|
const accountId = getPrimaryAccountId();
|
||||||
await stalwartJmap([
|
const responses = await stalwartJmap([
|
||||||
[
|
[
|
||||||
'x:AccountPassword/set',
|
'x:AccountPassword/set',
|
||||||
{
|
{
|
||||||
@@ -404,6 +422,7 @@ export const useAccountSecurityStore = create<AccountSecurityState>()((set, get)
|
|||||||
'0',
|
'0',
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
requireAccountPasswordUpdate(responses, 'Failed to enable TOTP');
|
||||||
set({ otpEnabled: true, isSaving: false });
|
set({ otpEnabled: true, isSaving: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({
|
set({
|
||||||
@@ -418,7 +437,7 @@ export const useAccountSecurityStore = create<AccountSecurityState>()((set, get)
|
|||||||
set({ isSaving: true, error: null });
|
set({ isSaving: true, error: null });
|
||||||
try {
|
try {
|
||||||
const accountId = getPrimaryAccountId();
|
const accountId = getPrimaryAccountId();
|
||||||
await stalwartJmap([
|
const responses = await stalwartJmap([
|
||||||
[
|
[
|
||||||
'x:AccountPassword/set',
|
'x:AccountPassword/set',
|
||||||
{
|
{
|
||||||
@@ -433,6 +452,7 @@ export const useAccountSecurityStore = create<AccountSecurityState>()((set, get)
|
|||||||
'0',
|
'0',
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
requireAccountPasswordUpdate(responses, 'Failed to disable TOTP');
|
||||||
set({ otpEnabled: false, isSaving: false });
|
set({ otpEnabled: false, isSaving: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({
|
set({
|
||||||
|
|||||||
Reference in New Issue
Block a user