mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 13:46:16 -08:00
fix(server-storage): merge user data on move, keep failed writes, sign-in route gate
- A merged playlist gains the browser document's favorites and history; Xtream values for one server entry are combined (lists unioned, maps merged, the server winning conflicts) and written once. - A failed user-data write stays queued and survives a re-read; the page-hide flush waits for a batch still travelling. - The start-up screen lets the sign-in route through whatever its check reports. - The backend refuses IPTVNATOR_ACCESS_TOKEN without server storage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
676e3adcd4
commit
fc32934ce3
17 files changed
+474
-90
No files matched your search
@@ -6,7 +6,7 @@ import {
|
||||
selectPlaylistsLoadFailed,
|
||||
selectPlaylistsLoadingFlag,
|
||||
} from '@iptvnator/m3u-state';
|
||||
import { ServerStorageStatusService } from '@iptvnator/services';
|
||||
import { provideRouter, Router } from '@angular/router';
|
||||
import { AppStartupStatusComponent } from './app-startup-status.component';
|
||||
|
||||
describe('AppStartupStatusComponent', () => {
|
||||
@@ -14,6 +14,7 @@ describe('AppStartupStatusComponent', () => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [AppStartupStatusComponent, TranslateModule.forRoot()],
|
||||
providers: [
|
||||
provideRouter([{ path: 'server-sign-in', children: [] }]),
|
||||
provideMockStore({
|
||||
selectors: [
|
||||
{ selector: selectPlaylistsLoadingFlag, value: false },
|
||||
@@ -49,26 +50,18 @@ describe('AppStartupStatusComponent', () => {
|
||||
expect(fixture.componentInstance.complete()).toBe(true);
|
||||
});
|
||||
|
||||
it('shows the server sign-in page although sources could not load', () => {
|
||||
it('shows the server sign-in page although sources could not load', async () => {
|
||||
TestBed.inject(MockStore).overrideSelector(
|
||||
selectPlaylistsLoadFailed,
|
||||
true
|
||||
);
|
||||
const status = TestBed.inject(ServerStorageStatusService);
|
||||
status.activate(
|
||||
{
|
||||
service: 'iptvnator',
|
||||
apiVersion: 1,
|
||||
instanceId: 'i',
|
||||
storage: 'server',
|
||||
auth: 'token',
|
||||
},
|
||||
() => undefined
|
||||
);
|
||||
status.reportUnauthorized();
|
||||
const fixture = TestBed.createComponent(AppStartupStatusComponent);
|
||||
fixture.componentRef.setInput('routeReady', true);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.componentInstance.complete()).toBe(false);
|
||||
|
||||
await TestBed.inject(Router).navigateByUrl('/server-sign-in');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.complete()).toBe(true);
|
||||
expect(fixture.nativeElement.querySelector('section')).toBeNull();
|
||||
|
||||
@@ -5,16 +5,19 @@ import {
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { filter, map } from 'rxjs';
|
||||
import {
|
||||
PlaylistActions,
|
||||
selectPlaylistsLoadFailed,
|
||||
selectPlaylistsLoadingFlag,
|
||||
} from '@iptvnator/m3u-state';
|
||||
import { ServerStorageStatusService } from '@iptvnator/services';
|
||||
import { SERVER_SIGN_IN_PATH } from '@iptvnator/services';
|
||||
|
||||
/** The first route also waits for settings and XMLTV source reconciliation. */
|
||||
@Component({
|
||||
@@ -131,14 +134,23 @@ export class AppStartupStatusComponent {
|
||||
selectPlaylistsLoadingFlag
|
||||
);
|
||||
readonly failed = this.store.selectSignal(selectPlaylistsLoadFailed);
|
||||
private readonly serverStatus = inject(ServerStorageStatusService);
|
||||
/** Server storage waiting for a token: only its sign-in page can load. */
|
||||
private readonly signInRequired = computed(
|
||||
() => this.serverStatus.state() === 'unauthorized'
|
||||
private readonly router = inject(Router);
|
||||
private readonly url = toSignal(
|
||||
this.router.events.pipe(
|
||||
filter((event) => event instanceof NavigationEnd),
|
||||
map((event) => event.urlAfterRedirects)
|
||||
),
|
||||
{ initialValue: this.router.url }
|
||||
);
|
||||
/**
|
||||
* Server storage asking for a token: the playlists cannot load, and the
|
||||
* sign-in page must stay up whatever its check reports.
|
||||
*/
|
||||
private readonly onSignInPage = computed(() =>
|
||||
this.url().startsWith(SERVER_SIGN_IN_PATH)
|
||||
);
|
||||
readonly complete = computed(
|
||||
() =>
|
||||
this.routeReady() && (this.sourcesReady() || this.signInRequired())
|
||||
() => this.routeReady() && (this.sourcesReady() || this.onSignInPage())
|
||||
);
|
||||
|
||||
retry(): void {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { CanActivateFn, Router } from '@angular/router';
|
||||
import { ServerStorageStatusService } from '@iptvnator/services';
|
||||
import {
|
||||
SERVER_SIGN_IN_PATH,
|
||||
ServerStorageStatusService,
|
||||
} from '@iptvnator/services';
|
||||
|
||||
/** Without a valid token the workspace would show an empty library. */
|
||||
export const serverAccessGuard: CanActivateFn = () =>
|
||||
inject(ServerStorageStatusService).state() === 'unauthorized'
|
||||
? inject(Router).parseUrl('/server-sign-in')
|
||||
? inject(Router).parseUrl(SERVER_SIGN_IN_PATH)
|
||||
: true;
|
||||
|
||||
export const serverSignInGuard: CanActivateFn = () =>
|
||||
|
||||
Reference in new issue
Block a user