diff --git a/libs/portal/xtream/data-access/src/lib/data-sources/pwa-xtream-data-source.spec.ts b/libs/portal/xtream/data-access/src/lib/data-sources/pwa-xtream-data-source.spec.ts index 12f5bb943..cc220c222 100644 --- a/libs/portal/xtream/data-access/src/lib/data-sources/pwa-xtream-data-source.spec.ts +++ b/libs/portal/xtream/data-access/src/lib/data-sources/pwa-xtream-data-source.spec.ts @@ -41,6 +41,31 @@ describe('PwaXtreamDataSource', () => { localStorage.clear(); }); + it('keeps Xtream passwords out of localStorage playlist metadata', async () => { + await dataSource.createPlaylist({ + id: 'playlist-1', + name: 'Xtream PWA', + serverUrl: credentials.serverUrl, + username: credentials.username, + password: credentials.password, + type: 'xtream', + }); + + const storedPlaylists = JSON.parse( + localStorage.getItem('xtream-playlists') || '[]' + ); + expect(storedPlaylists).toEqual([ + expect.not.objectContaining({ + password: credentials.password, + }), + ]); + await expect(dataSource.getPlaylist('playlist-1')).resolves.toEqual( + expect.objectContaining({ + password: credentials.password, + }) + ); + }); + it('normalizes Xtream API stream identifiers for PWA catalog navigation', async () => { apiService.getStreams.mockImplementation( (_credentials: XtreamCredentials, type: string) => { diff --git a/libs/portal/xtream/data-access/src/lib/data-sources/pwa-xtream-data-source.ts b/libs/portal/xtream/data-access/src/lib/data-sources/pwa-xtream-data-source.ts index 687a1b720..768d9f2ab 100644 --- a/libs/portal/xtream/data-access/src/lib/data-sources/pwa-xtream-data-source.ts +++ b/libs/portal/xtream/data-access/src/lib/data-sources/pwa-xtream-data-source.ts @@ -63,6 +63,10 @@ interface StoredRecentItem { readonly backdropUrl?: string; } +type StoredXtreamPlaylistData = Omit & { + readonly password?: string; +}; + /** * PWA implementation of the Xtream data source. * Uses API-only strategy: always fetch from API, no database caching. @@ -77,6 +81,7 @@ export class PwaXtreamDataSource implements IXtreamDataSource { // In-memory cache for the current session private categoryCache = new Map(); private contentCache = new Map(); + private playlistPasswords = new Map(); // ========================================================================= // Playlist Operations (localStorage) @@ -84,10 +89,12 @@ export class PwaXtreamDataSource implements IXtreamDataSource { async getPlaylist(playlistId: string): Promise { const playlists = this.getPlaylistsFromStorage(); - return playlists.find((p) => p.id === playlistId) || null; + const playlist = playlists.find((p) => p.id === playlistId); + return playlist?.password ? playlist : null; } async createPlaylist(playlist: XtreamPlaylistData): Promise { + this.rememberPlaylistPassword(playlist); const playlists = this.getPlaylistsFromStorage(); playlists.push(playlist); this.savePlaylistsToStorage(playlists); @@ -97,6 +104,10 @@ export class PwaXtreamDataSource implements IXtreamDataSource { playlistId: string, updates: Partial ): Promise { + if (updates.password) { + this.playlistPasswords.set(playlistId, updates.password); + } + const playlists = this.getPlaylistsFromStorage(); const index = playlists.findIndex((p) => p.id === playlistId); if (index !== -1) { @@ -122,14 +133,75 @@ export class PwaXtreamDataSource implements IXtreamDataSource { private getPlaylistsFromStorage(): XtreamPlaylistData[] { try { const data = localStorage.getItem(STORAGE_KEYS.PLAYLISTS); - return data ? JSON.parse(data) : []; + const playlists = data + ? (JSON.parse(data) as StoredXtreamPlaylistData[]) + : []; + + return playlists.map((playlist) => + this.fromStoredPlaylist(playlist) + ); } catch { return []; } } private savePlaylistsToStorage(playlists: XtreamPlaylistData[]): void { - localStorage.setItem(STORAGE_KEYS.PLAYLISTS, JSON.stringify(playlists)); + const persistedPlaylists = playlists.map((playlist) => + this.toStoredPlaylist(playlist) + ); + localStorage.setItem( + STORAGE_KEYS.PLAYLISTS, + JSON.stringify(persistedPlaylists) + ); + } + + private toStoredPlaylist( + playlist: XtreamPlaylistData + ): StoredXtreamPlaylistData { + return { + id: playlist.id, + name: playlist.name, + title: playlist.title, + updateDate: playlist.updateDate, + serverUrl: playlist.serverUrl, + username: playlist.username, + type: playlist.type, + userAgent: playlist.userAgent, + referrer: playlist.referrer, + origin: playlist.origin, + serverTimezone: playlist.serverTimezone, + }; + } + + private fromStoredPlaylist( + playlist: StoredXtreamPlaylistData + ): XtreamPlaylistData { + const legacyPassword = + typeof playlist.password === 'string' ? playlist.password : ''; + if (legacyPassword) { + this.playlistPasswords.set(playlist.id, legacyPassword); + } + + return { + id: playlist.id, + name: playlist.name, + title: playlist.title, + updateDate: playlist.updateDate, + serverUrl: playlist.serverUrl, + username: playlist.username, + password: this.playlistPasswords.get(playlist.id) ?? legacyPassword, + type: playlist.type, + userAgent: playlist.userAgent, + referrer: playlist.referrer, + origin: playlist.origin, + serverTimezone: playlist.serverTimezone, + }; + } + + private rememberPlaylistPassword(playlist: XtreamPlaylistData): void { + if (playlist.password) { + this.playlistPasswords.set(playlist.id, playlist.password); + } } // =========================================================================