mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 21:26:15 -08:00
fix(server-storage): check a remembered instance before sign-in sends the token
A page started from memory (startedOffline) withholds the token until the instance is verified, so sign-in on that page sent no token and the server's 401 cleared the right one. Sign-in now first calls ServerStorageSession.ensureVerified(): it reads /instance without a token, marks the status verified when the instance id matches, and reloads the page when another server answers at the address. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b8838c1c09
commit
a94673b4ab
6 files changed
+83
-4
No files matched your search
@@ -13,7 +13,7 @@ const TOKEN_KEY = 'iptvnator-server:access-token';
|
||||
describe('ServerSignInComponent', () => {
|
||||
let fixture: ComponentFixture<ServerSignInComponent>;
|
||||
let component: ServerSignInComponent;
|
||||
const session = { fetchStatus: jest.fn() };
|
||||
const session = { fetchStatus: jest.fn(), ensureVerified: jest.fn() };
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
@@ -152,4 +152,16 @@ describe('ServerSignInComponent', () => {
|
||||
expect(localStorage.getItem(TOKEN_KEY)).toBeNull();
|
||||
expect(feedbackText()).toBe('SERVER_STORAGE.SIGN_IN_STORAGE');
|
||||
});
|
||||
|
||||
it('sends the token only after the server proved to be the remembered one', async () => {
|
||||
session.ensureVerified.mockRejectedValue(new Error('another server'));
|
||||
|
||||
component.edit('right');
|
||||
await component.submit(new Event('submit'));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(session.fetchStatus).not.toHaveBeenCalled();
|
||||
expect(localStorage.getItem(TOKEN_KEY)).toBeNull();
|
||||
expect(feedbackText()).toBe('SERVER_STORAGE.SIGN_IN_UNREACHABLE');
|
||||
});
|
||||
});
|
||||
@@ -56,6 +56,17 @@ export class ServerSignInComponent {
|
||||
if (!token || this.busy()) {
|
||||
return;
|
||||
}
|
||||
this.busy.set(true);
|
||||
this.feedback.set(null);
|
||||
try {
|
||||
// A page started from memory checks the server first: the token
|
||||
// must reach only the one it is meant for.
|
||||
await this.session.ensureVerified();
|
||||
} catch {
|
||||
this.feedback.set('unreachable');
|
||||
this.busy.set(false);
|
||||
return;
|
||||
}
|
||||
// Every request and the reload read the token from storage, and the
|
||||
// next start keeps it only for the server it remembers.
|
||||
if (
|
||||
@@ -64,10 +75,9 @@ export class ServerSignInComponent {
|
||||
) {
|
||||
clearServerAccessToken();
|
||||
this.feedback.set('storage');
|
||||
this.busy.set(false);
|
||||
return;
|
||||
}
|
||||
this.busy.set(true);
|
||||
this.feedback.set(null);
|
||||
try {
|
||||
await this.session.fetchStatus();
|
||||
this.openWorkspace();
|
||||
|
||||
@@ -304,7 +304,9 @@ settings and guards read them in every build.
|
||||
when the backend answers without server storage, or as another server or
|
||||
database (backend URL or instance id) than the one it remembers; sign-in
|
||||
therefore remembers the server along with the token, or reports storage
|
||||
trouble. Both turn
|
||||
trouble. A page started from memory first checks that the instance at the
|
||||
address is the remembered one (`ensureVerified`), and reloads if not,
|
||||
before sign-in sends the token. Both turn
|
||||
the guard's 401 (`isServerAccessChallenge`) into `reportAccessDenied()`,
|
||||
so a token changed on the server sends an open page back to sign-in; a
|
||||
late 401 for a token replaced since (a new sign-in) changes nothing.
|
||||
|
||||
@@ -43,6 +43,11 @@ export class ServerStorageStatusService {
|
||||
this.stateSignal.set('online');
|
||||
}
|
||||
|
||||
/** The instance at the address turned out to be the remembered one. */
|
||||
markVerified(): void {
|
||||
this.verified = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The stored token, only while server mode runs on an instance this
|
||||
* start verified: a token must never reach a server it was not given
|
||||
|
||||
@@ -204,6 +204,38 @@ describe('ServerStorageSession', () => {
|
||||
expect(session.userData.hasUnsentWrites()).toBe(true);
|
||||
});
|
||||
|
||||
it('checks the instance before a token may go out', async () => {
|
||||
const { session, status } = setup('token', true);
|
||||
const reload = jest
|
||||
.spyOn(
|
||||
session as unknown as { reloadPage(): void },
|
||||
'reloadPage'
|
||||
)
|
||||
.mockImplementation(() => undefined);
|
||||
await session.start();
|
||||
localStorage.setItem(SERVER_ACCESS_TOKEN_STORAGE_KEY, 't');
|
||||
expect(status.accessToken()).toBeNull();
|
||||
|
||||
fetchMock.mockResolvedValue({
|
||||
status: 200,
|
||||
ok: true,
|
||||
headers: new Headers(),
|
||||
json: async () => ({ instanceId: 'other' }),
|
||||
});
|
||||
await expect(session.ensureVerified()).rejects.toThrow();
|
||||
expect(reload).toHaveBeenCalled();
|
||||
expect(status.accessToken()).toBeNull();
|
||||
|
||||
fetchMock.mockResolvedValue({
|
||||
status: 200,
|
||||
ok: true,
|
||||
headers: new Headers(),
|
||||
json: async () => ({ instanceId: 'i-1' }),
|
||||
});
|
||||
await session.ensureVerified();
|
||||
expect(status.accessToken()).toBe('t');
|
||||
});
|
||||
|
||||
it('never reloads after a normal start', async () => {
|
||||
const { session } = setup('none');
|
||||
const reload = jest
|
||||
|
||||
@@ -126,6 +126,24 @@ export class ServerStorageSession {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Before a token goes out from a page started from memory: the instance
|
||||
* answering at the address must be the remembered one. Another one
|
||||
* reloads the page, which starts against it.
|
||||
*/
|
||||
async ensureVerified(): Promise<void> {
|
||||
if (!this.config.startedOffline) {
|
||||
return;
|
||||
}
|
||||
const { instanceId } =
|
||||
await this.api.getJson<ServerInstanceInfo>('/instance');
|
||||
if (instanceId !== this.config.instance.instanceId) {
|
||||
this.reloadPage();
|
||||
throw new Error('Another server answers at this address.');
|
||||
}
|
||||
this.status.markVerified();
|
||||
}
|
||||
|
||||
fetchStatus(): Promise<ServerStorageStatusResponse> {
|
||||
return this.api.getJson<ServerStorageStatusResponse>(
|
||||
'/instance/status'
|
||||
|
||||
Reference in new issue
Block a user