mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
fix(settings): gate the workspace on parental lock init, edit only a readable lock store, guard the deferred reload, validate backup lock entries
- The workspace route resolver awaits ParentalLockService.initialize() next to the settings load, so no route or catalog activates before the PIN and lock store are known. - Every lock write re-reads a failed store before building its edit, so a recovered store is edited rather than overwritten. - The deferred hydration reload runs under the publish guard of the request that deferred it. - Backup import validates every parental lock entry and rejects a damaged list instead of erasing the persisted locks on restore. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
d4d0754e12
commit
4bd4bf2fc2
12 files changed
+257
-60
No files matched your search
@@ -152,21 +152,58 @@ export function normalizeParentalLockStalkerCategories(
|
||||
return result;
|
||||
}
|
||||
|
||||
function isWellFormedList(
|
||||
value: unknown,
|
||||
isWellFormedEntry: (entry: unknown) => boolean
|
||||
/** A list whose every entry the matching normalizer would KEEP. */
|
||||
export function isWellFormedParentalLockXtreamCategories(
|
||||
value: unknown
|
||||
): boolean {
|
||||
return Array.isArray(value) && value.every(isWellFormedEntry);
|
||||
return (
|
||||
Array.isArray(value) &&
|
||||
value.every(
|
||||
(entry) =>
|
||||
isRecord(entry) &&
|
||||
PARENTAL_LOCK_XTREAM_CATEGORY_TYPES.includes(
|
||||
entry['categoryType'] as ParentalLockXtreamCategoryType
|
||||
) &&
|
||||
normalizeNumericId(entry['xtreamId']) !== null
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function isWellFormedParentalLockStalkerCategories(
|
||||
value: unknown
|
||||
): boolean {
|
||||
return (
|
||||
Array.isArray(value) &&
|
||||
value.every(
|
||||
(entry) =>
|
||||
isRecord(entry) &&
|
||||
PARENTAL_LOCK_STALKER_CATEGORY_TYPES.includes(
|
||||
entry['categoryType'] as ParentalLockStalkerCategoryType
|
||||
) &&
|
||||
((typeof entry['categoryId'] === 'string' &&
|
||||
entry['categoryId'].trim() !== '' &&
|
||||
entry['categoryId'].trim() !== '*') ||
|
||||
(typeof entry['categoryId'] === 'number' &&
|
||||
Number.isFinite(entry['categoryId'])))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function isWellFormedParentalLockGroupTitles(value: unknown): boolean {
|
||||
return (
|
||||
Array.isArray(value) &&
|
||||
value.every((entry) => typeof entry === 'string')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a persisted store has the shape `writeLocks` produces: every
|
||||
* playlist entry an object whose `xtream`/`stalker`/`m3u` lists are all
|
||||
* present (a missing list is corruption, not "empty") and hold only
|
||||
* entries the normalizers accept. The normalizers DROP what they
|
||||
* do not understand, which is right for user-supplied backups but turns a
|
||||
* corrupted persisted store into "nothing is locked"; a store that fails
|
||||
* this check is treated as unreadable instead.
|
||||
* entries the normalizers accept. The normalizers DROP what they do not
|
||||
* understand; on a persisted store (and on a backup's lock lists, which a
|
||||
* restore treats as authoritative) that would turn corruption into
|
||||
* "nothing is locked", so such data is rejected instead.
|
||||
*/
|
||||
export function isWellFormedParentalLockStore(value: unknown): boolean {
|
||||
if (!isRecord(value)) {
|
||||
@@ -175,29 +212,9 @@ export function isWellFormedParentalLockStore(value: unknown): boolean {
|
||||
return Object.values(value).every(
|
||||
(locks) =>
|
||||
isRecord(locks) &&
|
||||
isWellFormedList(
|
||||
locks['xtream'],
|
||||
(entry) =>
|
||||
isRecord(entry) &&
|
||||
PARENTAL_LOCK_XTREAM_CATEGORY_TYPES.includes(
|
||||
entry['categoryType'] as ParentalLockXtreamCategoryType
|
||||
) &&
|
||||
normalizeNumericId(entry['xtreamId']) !== null
|
||||
) &&
|
||||
isWellFormedList(
|
||||
locks['stalker'],
|
||||
(entry) =>
|
||||
isRecord(entry) &&
|
||||
PARENTAL_LOCK_STALKER_CATEGORY_TYPES.includes(
|
||||
entry['categoryType'] as ParentalLockStalkerCategoryType
|
||||
) &&
|
||||
((typeof entry['categoryId'] === 'string' &&
|
||||
entry['categoryId'].trim() !== '' &&
|
||||
entry['categoryId'].trim() !== '*') ||
|
||||
(typeof entry['categoryId'] === 'number' &&
|
||||
Number.isFinite(entry['categoryId'])))
|
||||
) &&
|
||||
isWellFormedList(locks['m3u'], (entry) => typeof entry === 'string')
|
||||
isWellFormedParentalLockXtreamCategories(locks['xtream']) &&
|
||||
isWellFormedParentalLockStalkerCategories(locks['stalker']) &&
|
||||
isWellFormedParentalLockGroupTitles(locks['m3u'])
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user