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)
game-maker / gate (push) Successful in 2m5s
game-maker / suite (push) Failing after 6m22s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-20 12:50:52 +02:00
1 parent 60ea56a2ce
commit 8e4bb6e1f9
38 files changed
+1530 -139

No files matched your search

+93 -16
View File
@@ -1,4 +1,6 @@
import type { Application } from 'pixi.js';
import type { AuthStore } from '../core/store/AuthStore';
import { HttpAuthStore } from '../core/store/HttpAuthStore';
import { HttpFileStore } from '../core/store/HttpFileStore';
import { HttpProjectStore } from '../core/store/HttpProjectStore';
import type { StoredTexture, UploadedTexture } from '../core/store/TextureStore';
@@ -7,6 +9,9 @@ import { requestRender, type VStack } from '../ui';
import { fontLibrary } from '../viewport/FontLibrary';
import { soundLibrary } from '../viewport/SoundLibrary';
import { textureLibrary } from '../viewport/TextureLibrary';
import { LoginScreen } from './auth/LoginScreen';
import { installLoginHook } from './auth/loginHook';
import { consoleLog } from './console/log';
import { installDebugHook } from './debugHook';
import { Editor } from './Editor';
import { startPlugins } from './plugins/startPlugins';
@@ -22,27 +27,99 @@ const TEXTURE_BASE = '/game-maker/data/textures';
const FONT_BASE = '/game-maker/data/fonts';
const SOUND_BASE = '/game-maker/data/sounds';
/** Wires state, stores and the editor composition together and opens the first project. */
/**
* Shows the sign-in screen until somebody is signed in, and answers with their name. Returns
* right away when the session from an earlier visit is still good – which is what lets a
* deployment replace the editor under an open tab without asking for the password again.
*/
async function signedInUser(app: Application, root: VStack, auth: AuthStore): Promise<string> {
const existing = await auth.session();
if (existing !== null) return existing;
let done: (user: string) => void = () => {};
const screen = new LoginScreen(auth, (user) => done(user));
const removeHook = installLoginHook(app, screen);
const user = await new Promise<string>((resolve) => {
done = resolve;
root.add(screen, 1);
root.resize(app.screen.width, app.screen.height);
screen.focusFirstField();
});
removeHook();
root.removeChild(screen);
// The editor sizes `root` itself once it is built, but `resize` lays out only when the size
// changed – and the sign-in screen has already set it to the size of the window. Forgetting the
// size here is what makes that call count again; without it the whole editor sits unlaid-out in
// the corner.
root.resize(0, 0);
screen.destroy({ children: true });
return user;
}
/**
* Wires state, stores and the editor composition together and opens the first project.
*
* Signing out does not reload the page (#177): it takes the editor down and puts the sign-in
* screen back, in the same canvas with the same renderer. So this is a loop rather than a straight
* line – every round is one account's session. Only the first round is awaited, because the caller
* has more to set up once the editor is up; the rounds after it run on their own.
*/
export async function startEditor(app: Application, root: VStack): Promise<void> {
const state = new EditorState();
const auth = new HttpAuthStore(API_ENDPOINT);
textureLibrary.configure(TEXTURE_BASE);
fontLibrary.configure(FONT_BASE);
soundLibrary.configure(SOUND_BASE);
const editor = new Editor(
app,
root,
state,
new HttpProjectStore(API_ENDPOINT),
new HttpFileStore<UploadedTexture, StoredTexture>(API_ENDPOINT, 'textures'),
new HttpFileStore(API_ENDPOINT, 'fonts'),
new HttpFileStore(API_ENDPOINT, 'sounds'),
);
// An image or a font arrives long after the click that asked for it, and the widget waiting for
// it repaints without telling anyone: both libraries are a wake-up of their own.
// it repaints without telling anyone: both libraries are a wake-up of their own. Registered once
// for the page, not per session – the files they hold are shared by every account anyway.
textureLibrary.onChange(() => requestRender());
fontLibrary.onChange(() => requestRender());
installDebugHook(app, state, editor);
await editor.boot();
loadPhysics().catch((error: unknown) => console.error('[physics] Rapier could not be loaded:', error));
await startPlugins(API_ENDPOINT, state, editor);
const openSession = async (): Promise<void> => {
const user = await signedInUser(app, root, auth);
// Who is signed in goes into the window title: the toolbar is crowded enough, and the title is
// also what tells two tabs on two accounts apart.
document.title = `Game Maker – ${user}`;
const state = new EditorState();
const editor = new Editor(
app,
root,
state,
new HttpProjectStore(API_ENDPOINT),
new HttpFileStore<UploadedTexture, StoredTexture>(API_ENDPOINT, 'textures'),
new HttpFileStore(API_ENDPOINT, 'fonts'),
new HttpFileStore(API_ENDPOINT, 'sounds'),
{
logOut: () =>
void endSession(editor).catch((error: unknown) => {
console.error('[auth] Signing out failed:', error);
}),
},
);
installDebugHook(app, state, editor);
await editor.boot();
loadPhysics().catch((error: unknown) => console.error('[physics] Rapier could not be loaded:', error));
await startPlugins(API_ENDPOINT, state, editor);
};
/**
* Ends the session the Logout button belongs to: the editor writes what is unsaved and lets go
* of everything, then the server drops the session, and the sign-in screen comes back. The
* editor goes first, so its last save still travels with a session the server accepts.
*/
const endSession = async (editor: Editor): Promise<void> => {
await editor.dispose();
document.title = 'Game Maker';
// The console panel is page-wide, the lines in it are not: what one account's project printed
// is none of the next one's business.
consoleLog.clear();
try {
await auth.logOut();
} catch (error) {
console.error('[auth] Signing out did not reach the server:', error);
}
await openSession();
};
await openSession();
}