User management: signing in is required before anything is loaded, the sign-in screen is drawn in the same canvas (user name, password as bullets, Enter moves on and submits) and the editor is built only after it; the API gained auth/session, auth/login and auth/logout while every other route answers 401 without a session, projects carry an owner and a foreign one comes back as 404, accounts live in data/users.json with a password hash and sessions as files in data/sessions with a sliding 30 days, so a build under the open tab does not sign anybody out; Logout sits at the far right of the toolbar and, as asked in the issue, does not reload the page: Editor.dispose() takes the editor down while the canvas stays, which is why the autosave, the focus handlers, the window listeners of the tilemap panel, the shortcuts and the plugins all got a way back; the tools sign in as e2e (tools/lib/auth.mjs, password from GAME_MAKER_E2E_PASSWORD or the git-ignored data/e2e-password.txt) and the browser gets the same session by cookie, new e2e module 45 covers the screen, the wrong pair, the bullets, logout without a reload and signing in again, new unit tests for HttpAuthStore and for what a session leaves behind (#177)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
60ea56a2ce
commit
8e4bb6e1f9
38 files changed
+1530
-139
No files matched your search
@@ -34,6 +34,7 @@ import {
|
||||
Theme,
|
||||
ui,
|
||||
VStack,
|
||||
type Widget,
|
||||
} from '../../ui';
|
||||
import type { EditorState, TilemapFocus, TilemapTool } from '../state';
|
||||
import { TilePalette } from '../tile/TilePalette';
|
||||
@@ -164,6 +165,8 @@ export class TilemapPanel extends Panel {
|
||||
/** As set, before the header upper-cases it – for the check scripts. */
|
||||
title = 'Tilemap';
|
||||
onTargetChange: () => void = () => {};
|
||||
/** What this panel registered outside itself, as the calls that take it away again. */
|
||||
private readonly cleanups: (() => void)[] = [];
|
||||
|
||||
constructor(private readonly state: EditorState) {
|
||||
super({ title: 'Tilemap', padding: 0, gap: 0 });
|
||||
@@ -177,20 +180,36 @@ export class TilemapPanel extends Panel {
|
||||
if (ui().focus.isWithin(this)) this.rebuildWhenLeft = true;
|
||||
else this.sync();
|
||||
});
|
||||
ui().focus.changeHandlers.push(() => {
|
||||
if (this.rebuildWhenLeft && !ui().focus.isWithin(this)) this.sync();
|
||||
});
|
||||
this.cleanups.push(
|
||||
ui().focus.onFocusChange(() => {
|
||||
if (this.rebuildWhenLeft && !ui().focus.isWithin(this)) this.sync();
|
||||
}),
|
||||
);
|
||||
// Holding Alt picks (#133). The focus manager only hears presses, so the release has its own
|
||||
// listener; a handled Alt is kept from the browser, which would otherwise open its menu bar.
|
||||
const onAlt = (held: boolean) => (event: KeyboardEvent) => {
|
||||
if (event.key === 'Alt' && !event.repeat && holdPick(state, held)) event.preventDefault();
|
||||
};
|
||||
window.addEventListener('keydown', onAlt(true));
|
||||
window.addEventListener('keyup', onAlt(false));
|
||||
window.addEventListener('blur', () => holdPick(state, false));
|
||||
const onKeyDown = onAlt(true);
|
||||
const onKeyUp = onAlt(false);
|
||||
const onBlur = () => holdPick(state, false);
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
window.addEventListener('keyup', onKeyUp);
|
||||
window.addEventListener('blur', onBlur);
|
||||
this.cleanups.push(() => {
|
||||
window.removeEventListener('keydown', onKeyDown);
|
||||
window.removeEventListener('keyup', onKeyUp);
|
||||
window.removeEventListener('blur', onBlur);
|
||||
});
|
||||
this.sync();
|
||||
}
|
||||
|
||||
/** Signing out destroys the panel while the page stays up (#177): its listeners go with it. */
|
||||
override destroy(options?: Parameters<Widget['destroy']>[0]): void {
|
||||
for (const cleanup of this.cleanups.splice(0)) cleanup();
|
||||
super.destroy(options);
|
||||
}
|
||||
|
||||
/** Whether there is a map to paint – the editor adds "and the scene tab is showing". */
|
||||
get hasTarget(): boolean {
|
||||
return built !== null;
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface ToolbarActions {
|
||||
togglePlay(): void;
|
||||
/** Hold or resume the running world; only reachable in play mode. */
|
||||
togglePause(): void;
|
||||
/** Ends the session and returns to the sign-in screen. */
|
||||
logOut(): void;
|
||||
}
|
||||
|
||||
const SAVE_LABELS: Record<SaveStatus, string> = {
|
||||
@@ -92,15 +94,18 @@ export class Toolbar extends HStack {
|
||||
this.add(this.pauseButton);
|
||||
this.add(new Spacer(), 1);
|
||||
|
||||
// Flexible, not a fixed strip: it is the one thing here that may shrink, and since the Logout
|
||||
// button (#177) the row no longer fits a narrow window otherwise.
|
||||
this.notice = new Label('', { style: 'small', align: 'right' });
|
||||
this.notice.minWidth = 260;
|
||||
this.add(this.notice);
|
||||
this.add(this.notice, 1);
|
||||
this.status = new Label('', { style: 'small', align: 'right' });
|
||||
this.status.minWidth = 110;
|
||||
this.add(this.status);
|
||||
this.fps = new Label('– fps', { style: 'small', align: 'right' });
|
||||
this.fps.minWidth = 56;
|
||||
this.add(this.fps);
|
||||
// Far right, where the account lives; who is signed in stands in the window title.
|
||||
this.add(new Button('Logout', { onClick: () => actions.logOut() }));
|
||||
|
||||
state.on('project', () => this.refresh());
|
||||
state.on('load', () => this.refresh());
|
||||
|
||||
Reference in new issue
Block a user