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
+93
-16
@@ -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();
|
||||
}
|
||||
Reference in new issue
Block a user