mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 11:06:16 -08:00
fix(settings): retry a failed settings read before any parental-lock settings write
updateSettings writes the whole settings object, which after a failed startup read is the defaults; enabling the lock or changing the relock timeout then replaced the user's persisted preferences. The read is retried first and the write refused while settings stay unreadable. The settings writes move to parental-lock-settings-writer.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
54f27fe656
commit
b32940f690
4 files changed
+131
-48
No files matched your search
@@ -137,7 +137,12 @@ lock store are known (in the PWA the settings read can be the slower one).
|
||||
Every lock write re-reads a failed store BEFORE building its edit, so a
|
||||
recovered store is edited, never overwritten by an edit built on the empty
|
||||
fail-closed one. The feature switch itself is
|
||||
persisted through one guarded path (`persistEnabled`): `updateSettings`
|
||||
persisted through one guarded path (`persistEnabled`), and every
|
||||
parental-lock settings write first retries a failed startup settings read
|
||||
(`ensureSettingsReadable`) and is refused while settings stay unreadable —
|
||||
`updateSettings` writes the whole settings object, which after a failed
|
||||
read is the defaults and would replace the user's persisted preferences.
|
||||
Within that path `updateSettings`
|
||||
patches memory before it writes, so a failed write is undone in memory and
|
||||
`setupPin`/`disable` report false (whether to persist is decided from the
|
||||
settings switch BEFORE the PIN is stored, since `enabled` follows `hasPin`
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { normalizeParentalLockRelockMinutes } from '@iptvnator/shared/interfaces';
|
||||
import { SettingsStore } from '../settings-store.service';
|
||||
import { mirrorParentalLockEnabledSetting } from './parental-lock-bridge';
|
||||
|
||||
type SettingsWriter = Pick<
|
||||
InstanceType<typeof SettingsStore>,
|
||||
'updateSettings' | 'loadSettings'
|
||||
> & { storageFailure?: () => 'load' | 'save' | null };
|
||||
|
||||
/**
|
||||
* `updateSettings` writes the WHOLE settings object. After a failed startup
|
||||
* read that object is the defaults, so a parental-lock write would replace
|
||||
* the user's persisted player, portal and UI preferences. The read is
|
||||
* retried first (a transient failure recovers here), and the write is
|
||||
* refused while settings stay unreadable.
|
||||
*/
|
||||
export async function ensureParentalLockSettingsReadable(
|
||||
settings: SettingsWriter
|
||||
): Promise<boolean> {
|
||||
if (settings.storageFailure?.() !== 'load') {
|
||||
return true;
|
||||
}
|
||||
await settings.loadSettings();
|
||||
return settings.storageFailure?.() !== 'load';
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the feature switch. `updateSettings` patches the in-memory value
|
||||
* before the write; on a failed write that patch is undone (the second write
|
||||
* fails the same way and is ignored), so the toggle cannot show a state the
|
||||
* next launch will not have. The Electron mirror is written after the
|
||||
* settings and undoes them the same way when it fails: a reloaded renderer
|
||||
* and a restarted worker start from the mirror.
|
||||
*/
|
||||
export async function persistParentalLockEnabled(
|
||||
settings: SettingsWriter,
|
||||
enabled: boolean
|
||||
): Promise<boolean> {
|
||||
if (!(await ensureParentalLockSettingsReadable(settings))) {
|
||||
return false;
|
||||
}
|
||||
const undo = () =>
|
||||
settings
|
||||
.updateSettings({ parentalLockEnabled: !enabled })
|
||||
.catch(() => undefined);
|
||||
try {
|
||||
await settings.updateSettings({ parentalLockEnabled: enabled });
|
||||
} catch (error) {
|
||||
console.error('Failed to persist the parental lock switch.', error);
|
||||
await undo();
|
||||
return false;
|
||||
}
|
||||
if (!(await mirrorParentalLockEnabledSetting(enabled))) {
|
||||
await undo();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* False when the value could not be persisted; the in-memory patch is undone
|
||||
* so the timer on screen never differs from the one the next launch uses.
|
||||
*/
|
||||
export async function persistParentalLockRelockMinutes(
|
||||
settings: SettingsWriter,
|
||||
minutes: number,
|
||||
previous: number
|
||||
): Promise<boolean> {
|
||||
if (!(await ensureParentalLockSettingsReadable(settings))) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await settings.updateSettings({
|
||||
parentalLockRelockMinutes:
|
||||
normalizeParentalLockRelockMinutes(minutes),
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to persist the relock timeout.', error);
|
||||
await settings
|
||||
.updateSettings({
|
||||
parentalLockRelockMinutes:
|
||||
normalizeParentalLockRelockMinutes(previous),
|
||||
})
|
||||
.catch(() => undefined);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -226,12 +226,19 @@ describe('ParentalLockService', () => {
|
||||
expect(updateBridgeSettings).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('still persists the switch when settings could not be read', async () => {
|
||||
it('retries a failed settings read before persisting the switch', async () => {
|
||||
storageFailure.set('load');
|
||||
prompt.requestPin.mockResolvedValue('1234');
|
||||
const service = await createService();
|
||||
// Storage recovers: the retry reads the persisted settings back.
|
||||
const loadSettings = TestBed.inject(SettingsStore)
|
||||
.loadSettings as jest.Mock;
|
||||
loadSettings.mockImplementationOnce(async () => {
|
||||
storageFailure.set(null);
|
||||
});
|
||||
|
||||
await expect(service.setupPin()).resolves.toBe(true);
|
||||
expect(loadSettings).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(updateSettings).toHaveBeenCalledWith({
|
||||
parentalLockEnabled: true,
|
||||
@@ -241,6 +248,18 @@ describe('ParentalLockService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses parental-lock settings writes while settings stay unreadable', async () => {
|
||||
storageFailure.set('load');
|
||||
prompt.requestPin.mockResolvedValue('1234');
|
||||
const service = await createService();
|
||||
|
||||
// Writing now would replace the persisted settings with defaults.
|
||||
await expect(service.setupPin()).resolves.toBe(false);
|
||||
await expect(service.setRelockMinutes(30)).resolves.toBe(false);
|
||||
expect(updateSettings).not.toHaveBeenCalled();
|
||||
expect(prompt.requestPin).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps the session locked on a failed PIN read and retries before unlocking', async () => {
|
||||
storage.pinHash = await hashParentalLockPin('1234');
|
||||
parentalLockEnabled.set(true);
|
||||
|
||||
@@ -24,10 +24,12 @@ import {
|
||||
PARENTAL_LOCK_PROMPT,
|
||||
ParentalLockPromptRequest,
|
||||
} from './parental-lock-prompt.token';
|
||||
import { syncParentalLockStateToMainProcess } from './parental-lock-bridge';
|
||||
import {
|
||||
mirrorParentalLockEnabledSetting,
|
||||
syncParentalLockStateToMainProcess,
|
||||
} from './parental-lock-bridge';
|
||||
ensureParentalLockSettingsReadable,
|
||||
persistParentalLockEnabled,
|
||||
persistParentalLockRelockMinutes,
|
||||
} from './parental-lock-settings-writer';
|
||||
import { ParentalLockStorageService } from './parental-lock-storage';
|
||||
|
||||
/**
|
||||
@@ -238,7 +240,7 @@ export class ParentalLockService {
|
||||
*/
|
||||
async setupPin(): Promise<boolean> {
|
||||
await this.initialize();
|
||||
if (!this.prompt) {
|
||||
if (!this.prompt || !(await this.ensureSettingsReadable())) {
|
||||
return false;
|
||||
}
|
||||
// Decided BEFORE the PIN is stored: with the settings switch
|
||||
@@ -262,34 +264,12 @@ export class ParentalLockService {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the feature switch. `updateSettings` patches the in-memory
|
||||
* value before the write; on a failed write that patch is undone (the
|
||||
* second write fails the same way and is ignored), so the toggle
|
||||
* cannot show a state the next launch will not have, and the Electron
|
||||
* mirror is only updated for a persisted switch.
|
||||
*/
|
||||
private async persistEnabled(enabled: boolean): Promise<boolean> {
|
||||
try {
|
||||
await this.settingsStore.updateSettings({
|
||||
parentalLockEnabled: enabled,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to persist the parental lock switch.', error);
|
||||
await this.settingsStore
|
||||
.updateSettings({ parentalLockEnabled: !enabled })
|
||||
.catch(() => undefined);
|
||||
return false;
|
||||
}
|
||||
// The mirror is what a reloaded renderer and a restarted worker
|
||||
// start from; a switch persisted on one side only is undone.
|
||||
if (!(await mirrorParentalLockEnabledSetting(enabled))) {
|
||||
await this.settingsStore
|
||||
.updateSettings({ parentalLockEnabled: !enabled })
|
||||
.catch(() => undefined);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
private persistEnabled(enabled: boolean): Promise<boolean> {
|
||||
return persistParentalLockEnabled(this.settingsStore, enabled);
|
||||
}
|
||||
|
||||
private ensureSettingsReadable(): Promise<boolean> {
|
||||
return ensureParentalLockSettingsReadable(this.settingsStore);
|
||||
}
|
||||
|
||||
/** Verifies the current PIN, then replaces it. */
|
||||
@@ -352,20 +332,11 @@ export class ParentalLockService {
|
||||
* timer on screen never differs from the one the next launch uses.
|
||||
*/
|
||||
async setRelockMinutes(minutes: number): Promise<boolean> {
|
||||
const previous = this.relockMinutes();
|
||||
try {
|
||||
await this.settingsStore.updateSettings({
|
||||
parentalLockRelockMinutes:
|
||||
normalizeParentalLockRelockMinutes(minutes),
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to persist the relock timeout.', error);
|
||||
await this.settingsStore
|
||||
.updateSettings({ parentalLockRelockMinutes: previous })
|
||||
.catch(() => undefined);
|
||||
return false;
|
||||
}
|
||||
return persistParentalLockRelockMinutes(
|
||||
this.settingsStore,
|
||||
minutes,
|
||||
this.relockMinutes()
|
||||
);
|
||||
}
|
||||
|
||||
// -- Lock store (ParentalLockLockStore; predicates add `active`) -------
|
||||
|
||||
Reference in new issue
Block a user