mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 13:46:16 -08:00
fix(server-storage): positions by content, acknowledged keepalive, serial re-reads, token changes
- Moved playback positions are matched by content type and Xtream id and name the server playlist. - Page-hide writes report whether the server took them; refused ones go back into the queue. Re-reads run one at a time. - The access guard's 401 carries WWW-Authenticate: Bearer realm="iptvnator"; the interceptor and PwaService's raw fetches turn it into sign-in, so a token changed on the server is noticed on provider calls too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fc32934ce3
commit
6820b87005
17 files changed
+414
-55
No files matched your search
@@ -20,6 +20,7 @@ import {
|
||||
getStalkerRequestErrorStatus,
|
||||
isStalkerProbeTimeout,
|
||||
} from '@iptvnator/portal/stalker/data-access';
|
||||
import { ServerStorageStatusService } from '@iptvnator/services';
|
||||
import { PwaService } from './pwa.service';
|
||||
|
||||
describe('PwaService', () => {
|
||||
@@ -279,6 +280,47 @@ describe('PwaService', () => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it('asks for a new server token when the access guard refuses a Stalker call', async () => {
|
||||
const status = TestBed.inject(ServerStorageStatusService);
|
||||
status.activate(
|
||||
{
|
||||
service: 'iptvnator',
|
||||
apiVersion: 1,
|
||||
instanceId: 'i',
|
||||
storage: 'server',
|
||||
auth: 'token',
|
||||
},
|
||||
() => undefined
|
||||
);
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = jest.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 401,
|
||||
statusText: 'Unauthorized',
|
||||
headers: new Headers({
|
||||
'WWW-Authenticate': 'Bearer realm="iptvnator"',
|
||||
}),
|
||||
json: async () => ({ code: 'unauthorized' }),
|
||||
} as unknown as Response) as unknown as typeof fetch;
|
||||
|
||||
const outcome = (
|
||||
service.sendIpcEvent(STALKER_REQUEST, {
|
||||
url: 'http://portal.example/portal.php',
|
||||
macAddress: '00:1A:79:AA:BB:CC',
|
||||
params: { action: 'get_genres' },
|
||||
silent: true,
|
||||
}) as Promise<unknown>
|
||||
).catch((error: unknown) => error);
|
||||
await Promise.resolve();
|
||||
http.expectOne((candidate) =>
|
||||
candidate.url.endsWith('/provider-targets')
|
||||
).flush({ targetId: 'target-1' });
|
||||
await outcome;
|
||||
|
||||
expect(status.state()).toBe('unauthorized');
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it('sends Stalker credentials as /stalker control params, including the serial', async () => {
|
||||
// JSDOM ships no fetch; install one for the proxy call.
|
||||
const fetchMock = jest.fn().mockResolvedValue({
|
||||
|
||||
@@ -13,7 +13,12 @@ import {
|
||||
switchMap,
|
||||
throwError,
|
||||
} from 'rxjs';
|
||||
import { DataService, serverAuthorizationHeaders } from '@iptvnator/services';
|
||||
import {
|
||||
DataService,
|
||||
isServerAccessChallenge,
|
||||
serverAuthorizationHeaders,
|
||||
ServerStorageStatusService,
|
||||
} from '@iptvnator/services';
|
||||
import {
|
||||
CONNECTIVITY_GUARD_RESET,
|
||||
ERROR,
|
||||
@@ -83,6 +88,7 @@ export class PwaService extends DataService {
|
||||
private readonly store = inject(Store);
|
||||
private readonly swUpdate = inject(SwUpdate);
|
||||
private readonly translateService = inject(TranslateService);
|
||||
private readonly serverStatus = inject(ServerStorageStatusService);
|
||||
private readonly logger = createLogger('PwaService');
|
||||
private readonly providerTargetIds = new Map<string, Promise<string>>();
|
||||
private readonly silentXtreamActions = new Set<string>([
|
||||
@@ -207,6 +213,7 @@ export class PwaService extends DataService {
|
||||
signal: controller.signal,
|
||||
}
|
||||
);
|
||||
this.noteServerAccess(response);
|
||||
|
||||
if (!response.ok) {
|
||||
return { reset: false };
|
||||
@@ -643,6 +650,7 @@ export class PwaService extends DataService {
|
||||
const response = await fetch(requestUrl, {
|
||||
headers: serverAuthorizationHeaders(),
|
||||
});
|
||||
this.noteServerAccess(response);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
@@ -794,4 +802,17 @@ export class PwaService extends DataService {
|
||||
getAppEnvironment(): string {
|
||||
return 'pwa';
|
||||
}
|
||||
|
||||
/** A token changed on the server shows up on any backend call. */
|
||||
private noteServerAccess(response: Response): void {
|
||||
if (
|
||||
response.status === 401 &&
|
||||
isServerAccessChallenge(
|
||||
response.status,
|
||||
response.headers.get('WWW-Authenticate')
|
||||
)
|
||||
) {
|
||||
this.serverStatus.reportAccessDenied();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user