mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 17:36:15 -08:00
fix(stalker): keep the portal's words on every login refusal
Only the terminal `blocked` exit carried the portal's own explanation. A login refusal is precisely where a portal says something actionable — wrong password, subscription expired — and every status-2 exit threw without it, leaving the dialog on a generic line while the useful sentence sat unread in the payload. Each exit now reads the response in hand, so the retry's rejection is quoted rather than the request that already succeeded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3b6107335d
commit
a0bfc28dc7
3 files changed
+75
-3
No files matched your search
@@ -345,6 +345,18 @@ dialog shows them in its failure snackbar (with kind-specific i18n headlines,
|
||||
guidance) when category loading failed with a portal refusal
|
||||
(`stalkerCategoryErrorDescription` in `workspace-context-panel.component.ts`).
|
||||
|
||||
Both renderers are kind-agnostic — they append `portalText` whenever it is
|
||||
present — so the obligation sits entirely on the throw sites: **every** exit
|
||||
out of the status-2 branch carries the text, not just the terminal `blocked`
|
||||
one. A login refusal is precisely where the portal says something actionable
|
||||
("wrong password", "subscription expired"), and dropping it leaves the user
|
||||
with a generic line while the useful sentence sits unread in the payload.
|
||||
Each exit reads the response IN HAND: after the `auth_second_step=1` retry the
|
||||
text is the retry's, since quoting the first profile back would describe a
|
||||
request that already succeeded. `do_auth` itself answers a bare `{js: false}`,
|
||||
so a rejection there keeps the profile's text — the one that asked for the
|
||||
login.
|
||||
|
||||
### Abandoning an authentication
|
||||
|
||||
`authenticate()` takes an optional `AbortSignal` and checks it before every
|
||||
|
||||
@@ -140,6 +140,54 @@ describe('StalkerAuthApi', () => {
|
||||
).rejects.toMatchObject({ kind: 'login-rejected' });
|
||||
});
|
||||
|
||||
it('carries the portal explanation out of every login refusal', async () => {
|
||||
// The actionable sentence ("wrong password", "subscription expired")
|
||||
// rides along with the refusal, and the dialog renders it after the
|
||||
// generic line. Each exit must read the response IN HAND: the retry
|
||||
// explains its own rejection, and quoting the first profile back
|
||||
// would describe a request that already succeeded.
|
||||
const handshake = { js: { token: 'TOKEN-1', random: 'r1' } };
|
||||
const credentials = { username: 'user', password: 'secret' };
|
||||
|
||||
sendIpcEvent
|
||||
.mockResolvedValueOnce(handshake)
|
||||
.mockResolvedValueOnce({
|
||||
js: { status: 2, msg: 'Enter your subscriber login' },
|
||||
});
|
||||
await expect(
|
||||
api.authenticate(portalUrl, macAddress)
|
||||
).rejects.toMatchObject({
|
||||
kind: 'login-required',
|
||||
portalText: 'Enter your subscriber login',
|
||||
});
|
||||
|
||||
sendIpcEvent
|
||||
.mockResolvedValueOnce(handshake)
|
||||
.mockResolvedValueOnce({ js: { status: 2, msg: 'Login needed' } })
|
||||
.mockResolvedValueOnce({ js: false });
|
||||
await expect(
|
||||
api.authenticate(portalUrl, macAddress, {}, { credentials })
|
||||
).rejects.toMatchObject({
|
||||
kind: 'login-rejected',
|
||||
portalText: 'Login needed',
|
||||
});
|
||||
|
||||
sendIpcEvent
|
||||
.mockResolvedValueOnce(handshake)
|
||||
.mockResolvedValueOnce({ js: { status: 2, msg: 'Login needed' } })
|
||||
.mockResolvedValueOnce({ js: true })
|
||||
.mockResolvedValueOnce({
|
||||
js: { status: 2, block_msg: 'Subscription expired' },
|
||||
});
|
||||
await expect(
|
||||
api.authenticate(portalUrl, macAddress, {}, { credentials })
|
||||
).rejects.toMatchObject({
|
||||
kind: 'login-rejected',
|
||||
// The retry's text, not the first profile's.
|
||||
portalText: 'Subscription expired',
|
||||
});
|
||||
});
|
||||
|
||||
it('decodes a blocked profile into the portal explanation', async () => {
|
||||
sendIpcEvent
|
||||
.mockResolvedValueOnce({
|
||||
|
||||
@@ -480,10 +480,19 @@ export class StalkerAuthApi {
|
||||
// strict `=== 2` would read `"2"` as a healthy profile and skip the
|
||||
// whole login flow.
|
||||
if (toFiniteNumber(js?.status) === 2) {
|
||||
// The portal's own sentence travels WITH the refusal — "wrong
|
||||
// password", "subscription expired", "contact your provider" —
|
||||
// and reading it is the whole reason these errors carry
|
||||
// `portalText`. It is read at each exit rather than once up
|
||||
// front because `js` advances to the retry's response below: the
|
||||
// second refusal explains itself, and quoting the first one back
|
||||
// would describe a request that already succeeded.
|
||||
const loginText = () =>
|
||||
combineStalkerPortalMessages(js?.msg, js?.block_msg);
|
||||
const username = options.credentials?.username?.trim() ?? '';
|
||||
const password = options.credentials?.password ?? '';
|
||||
if (!username || !password) {
|
||||
throw new StalkerPortalError('login-required');
|
||||
throw new StalkerPortalError('login-required', loginText());
|
||||
}
|
||||
|
||||
assertNotAborted(options.signal);
|
||||
@@ -496,7 +505,10 @@ export class StalkerAuthApi {
|
||||
options.signal
|
||||
);
|
||||
if (!accepted) {
|
||||
throw new StalkerPortalError('login-rejected');
|
||||
// `do_auth` answers a bare `{js: false}`, so the only text
|
||||
// available here is the profile's — which is the one that
|
||||
// asked for the login in the first place.
|
||||
throw new StalkerPortalError('login-rejected', loginText());
|
||||
}
|
||||
|
||||
assertNotAborted(options.signal);
|
||||
@@ -515,7 +527,7 @@ export class StalkerAuthApi {
|
||||
settled = retried;
|
||||
js = retried?.js;
|
||||
if (toFiniteNumber(js?.status) === 2) {
|
||||
throw new StalkerPortalError('login-rejected');
|
||||
throw new StalkerPortalError('login-rejected', loginText());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user