login
This commit is contained in:
78
src/auth/session.test.ts
Normal file
78
src/auth/session.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { test, beforeEach } from 'node:test';
|
||||
import { persist, restore, clear } from './session';
|
||||
import { SESSION_STORAGE_KEY } from './token';
|
||||
import type { SessionUser } from './roles';
|
||||
|
||||
/*
|
||||
A session without a token is not a session.
|
||||
|
||||
On 2026-09-25 a PUT to `users/update` came back 401 and the request carried no
|
||||
`authorization` header at all. The console was signed in by every visible
|
||||
measure — name in the corner, nav rendered, pages mounted — and could not make
|
||||
one authenticated request, because `restore()` accepted a stored blob that had a
|
||||
`userid` and a `role` and no token.
|
||||
|
||||
Two ways in. A session stored before the token existed; or a login against a
|
||||
server that could not mint one — `attachWebSession` logs that failure and
|
||||
returns the user record anyway, which was right while WEB_AUTH_REQUIRED was off
|
||||
and is a broken console now that it defaults on.
|
||||
|
||||
Either way the state is the same and it is the worst one available: authorised
|
||||
enough to render, not enough to load anything, and nothing on screen saying so.
|
||||
*/
|
||||
|
||||
// A minimal sessionStorage, since node has none.
|
||||
const store = new Map<string, string>();
|
||||
(globalThis as { sessionStorage?: unknown }).sessionStorage = {
|
||||
getItem: (key: string) => store.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => void store.set(key, value),
|
||||
removeItem: (key: string) => void store.delete(key),
|
||||
};
|
||||
|
||||
const signedIn: SessionUser = {
|
||||
userid: 904,
|
||||
role: 'store-admin',
|
||||
token: 'w1.payload.signature',
|
||||
} as SessionUser;
|
||||
|
||||
beforeEach(() => store.clear());
|
||||
|
||||
test('a stored session with a token comes back', () => {
|
||||
persist(signedIn);
|
||||
assert.equal(restore()?.userid, 904);
|
||||
assert.equal(restore()?.token, 'w1.payload.signature');
|
||||
});
|
||||
|
||||
test('a session with no token is refused', () => {
|
||||
// The bug. Restoring this renders a console that cannot load anything.
|
||||
store.set(SESSION_STORAGE_KEY, JSON.stringify({ userid: 904, role: 'store-admin' }));
|
||||
assert.equal(restore(), null, 'a tokenless session was restored');
|
||||
});
|
||||
|
||||
test('a session with an empty token is refused', () => {
|
||||
// `token: ""` is what a server that failed to mint would produce if the field
|
||||
// were assigned rather than spread. Same broken state, different shape.
|
||||
store.set(SESSION_STORAGE_KEY, JSON.stringify({ userid: 904, role: 'store-admin', token: ' ' }));
|
||||
assert.equal(restore(), null, 'an empty token was accepted');
|
||||
});
|
||||
|
||||
test('the checks that were already there still hold', () => {
|
||||
// A corrupted blob must not crash the shell on boot.
|
||||
for (const bad of [
|
||||
'{"role":"store-admin","token":"t"}',
|
||||
'{"userid":904,"token":"t"}',
|
||||
'{"userid":"904","role":"store-admin","token":"t"}',
|
||||
'not json at all',
|
||||
'',
|
||||
]) {
|
||||
store.set(SESSION_STORAGE_KEY, bad);
|
||||
assert.equal(restore(), null, `restored from ${bad}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('signing out leaves nothing behind', () => {
|
||||
persist(signedIn);
|
||||
clear();
|
||||
assert.equal(restore(), null);
|
||||
});
|
||||
@@ -82,10 +82,25 @@ export async function login(email: string, password: string): Promise<SessionUse
|
||||
}
|
||||
|
||||
// The token rides on the envelope, not on `details` — it is not a fact about
|
||||
// the user, it is what proves a later request is theirs. Absent against a
|
||||
// Fiesta that does not issue one yet, which is why it is spread in rather
|
||||
// than assigned: `exactOptionalPropertyTypes` refuses an explicit undefined.
|
||||
const session = { ...toSessionUser(envelope.details), ...(envelope.token ? { token: envelope.token } : {}) };
|
||||
// the user, it is what proves a later request is theirs.
|
||||
//
|
||||
// Refused when absent, rather than stored and hoped for. `attachWebSession`
|
||||
// on the server logs a minting failure and returns the user record anyway,
|
||||
// which was correct while WEB_AUTH_REQUIRED was off and is a broken console
|
||||
// now that it defaults on: the sign-in succeeds, the shell renders, and every
|
||||
// request after it goes out with no `authorization` header and comes back
|
||||
// 401 with nothing on screen to say why.
|
||||
//
|
||||
// Failing here names the problem at the moment it happens, to the person best
|
||||
// placed to report it, instead of scattering 401s across every page.
|
||||
if (!envelope.token) {
|
||||
throw new Error(
|
||||
'Signed in, but the server did not issue a session. Nothing would load — ' +
|
||||
'tell your administrator the API could not mint a session token.',
|
||||
);
|
||||
}
|
||||
|
||||
const session = { ...toSessionUser(envelope.details), token: envelope.token };
|
||||
persist(session);
|
||||
return session;
|
||||
}
|
||||
@@ -208,6 +223,24 @@ export function restore(): SessionUser | null {
|
||||
// A stored blob is only as trustworthy as the tab it came from; a shape
|
||||
// check keeps a corrupted value from crashing the shell on boot.
|
||||
if (typeof parsed?.userid !== 'number' || typeof parsed?.role !== 'string') return null;
|
||||
// A session without a token is not a session.
|
||||
//
|
||||
// This used to be restored happily, and the result was the worst state the
|
||||
// console can be in: signed in by every visible measure — name in the
|
||||
// corner, nav rendered, pages mounted — and unable to make a single
|
||||
// authenticated request, because `authHeader()` had nothing to send. Every
|
||||
// call came back 401 and nothing on screen explained why. A PUT to
|
||||
// `users/update` on 2026-09-25 went out with no `authorization` header at
|
||||
// all, which is what sent us looking.
|
||||
//
|
||||
// It happens whenever login could not mint one: `attachWebSession` logs the
|
||||
// failure and returns the user record anyway, which was right while
|
||||
// WEB_AUTH_REQUIRED was off and is a broken console now that it defaults on.
|
||||
// A stored session predating the token also lands here.
|
||||
//
|
||||
// Returning null sends them to the sign-in screen, which is a state people
|
||||
// know what to do with.
|
||||
if (typeof parsed.token !== 'string' || parsed.token.trim() === '') return null;
|
||||
return parsed;
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user