Let guests try a demo project without an account (#428)
The editor was only open to somebody with an account. A guest now picks
a demo project on the sign-in screen and gets the whole editor on a copy
held in the tab. The server writes nothing for a guest, so there is no
guest session, no guest folder and no quota to keep.
- Demo copies are frozen copies like a release, in data/demos/. An
administrator makes, updates and removes one with "Demo copy..." in
the project's main menu. GET demos and GET demos/{token} need no
session. An empty project is just a demo that is empty.
- Demo material: an administrator offers an uploaded file with "Offer
as demo" in the pickers (PATCH {demo}, data/demo-files.json). Every
list carries it with demo: true, for accounts and guests alike.
Signed out, a list holds the demo material alone, without the owner.
- An offered file is only an administrator's to delete. A file a demo
copy names counts as used, like one a release names.
- The guest's project lives in MemoryProjectStore. Every store that
would write refuses in the tab before a request is made, and the
autosave is not attached at all.
- The main menu drops projects, snapshots and release for a guest. The
pickers drop Upload and Delete, a dropped or pasted file is refused,
and a recording is saved as a download.
- The toolbar says the work is not saved. The Account menu holds
"Sign in..." instead of Logout and the password.
- A guest's demo never goes into the address: a reload brings the
sign-in screen back, not the copy. Found by e2e 70.
- GET plugins needs no session any more, since a demo may switch one
on. It only reads the server's own modules.
Tests:
- e2e 70 (atx): ALL PASSED, 13 checks. Counter-test with the guest
autosave sent to the server: 70f and 70g red on the PUT.
- tests/core/guest.test.ts: 9 tests. PHP: demos 20, files 46,
rights 80 checks (the matrix asks every new route).
- Gate (atx): CHECK EXIT 0, 314 files, 4852 tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ce1f25f2eb
commit
925a39a276
30 files changed
+1397
-128
No files matched your search
+42
-14
@@ -4,6 +4,7 @@ import { findAsset, findObjectHolder, ROUTING_ID } from '../core/model/Project';
|
||||
import { assetTitle } from '../core/music/embedded';
|
||||
import { mixOf, videoBusesOf } from '../core/music/mixer';
|
||||
import type { Role } from '../core/store/AuthStore';
|
||||
import type { DemoStore } from '../core/store/DemoStore';
|
||||
import type { ProjectStore } from '../core/store/ProjectStore';
|
||||
import { pluginsOf } from '../core/store/projectPlugins';
|
||||
import type { FontStore } from '../core/store/FontStore';
|
||||
@@ -158,7 +159,11 @@ export interface EditorAccount {
|
||||
user: string;
|
||||
/** What the editor offers: an administrator gets the user management (#180, #179). */
|
||||
role: Role;
|
||||
/** Somebody trying a demo copy without an account (#428): nothing they do is kept. */
|
||||
guest?: boolean;
|
||||
users: UsersStore;
|
||||
/** An administrator's: making the demo copy of the open project (#428). */
|
||||
demos?: DemoStore;
|
||||
logOut: () => void;
|
||||
/** Rejects with `WrongCredentialsError` when the current password does not match (#182). */
|
||||
changePassword: (current: string, next: string) => Promise<void>;
|
||||
@@ -365,6 +370,7 @@ export class Editor implements PlayHost {
|
||||
private readonly cleanups: (() => void)[] = [];
|
||||
private readonly autosave: { flush: () => Promise<void>; stop: () => void };
|
||||
private disposed = false;
|
||||
private readonly guest: boolean;
|
||||
// Each starts where its divider was left in this browser (`panelSizes.ts`), else at the number here.
|
||||
private leftWidth = panelSize('left', 250, MIN_LEFT_WIDTH, MAX_LEFT_WIDTH);
|
||||
private rightWidth = panelSize('right', 350, MIN_RIGHT_WIDTH, MAX_RIGHT_WIDTH);
|
||||
@@ -448,11 +454,13 @@ export class Editor implements PlayHost {
|
||||
() => {
|
||||
if (this.play.isPlaying) this.play.stop();
|
||||
},
|
||||
{ snapshots, releases, flush: () => this.autosave.flush() },
|
||||
{ snapshots, releases, demos: account.demos, flush: () => this.autosave.flush() },
|
||||
{ guest: account.guest === true, role: account.role },
|
||||
// Every project brings its own plugins (#119); until `startPlugins` has run there is no
|
||||
// manager, and a project with plugins simply has none of them.
|
||||
async (raw) => void (await this.plugins?.syncTo(pluginsOf(raw))),
|
||||
);
|
||||
this.guest = account.guest === true;
|
||||
|
||||
this.graphEditor.canvas.traceProvider = (assetId) => this.play.traceFor(assetId);
|
||||
this.graphEditor.canvas.timeProvider = () => this.play.time;
|
||||
@@ -476,16 +484,20 @@ export class Editor implements PlayHost {
|
||||
: null;
|
||||
this.managementPane = new ManagementPane(this.usersPane, this.pluginsPane);
|
||||
|
||||
this.toolbar = new Toolbar(state, {
|
||||
openMainMenu: (anchor) =>
|
||||
this.projects.openMainMenu(anchor, this.managementEntries(), [
|
||||
{ label: 'Master routing', onSelect: () => this.openDocument('routing') },
|
||||
]),
|
||||
togglePlay: () => (state.mode === 'edit' ? this.play.play() : this.play.stop()),
|
||||
togglePause: () => this.play.togglePause(),
|
||||
step: () => this.play.step(),
|
||||
openAccountMenu: (anchor) => this.openAccountMenu(anchor, account),
|
||||
});
|
||||
this.toolbar = new Toolbar(
|
||||
state,
|
||||
{
|
||||
openMainMenu: (anchor) =>
|
||||
this.projects.openMainMenu(anchor, this.managementEntries(), [
|
||||
{ label: 'Master routing', onSelect: () => this.openDocument('routing') },
|
||||
]),
|
||||
togglePlay: () => (state.mode === 'edit' ? this.play.play() : this.play.stop()),
|
||||
togglePause: () => this.play.togglePause(),
|
||||
step: () => this.play.step(),
|
||||
openAccountMenu: (anchor) => this.openAccountMenu(anchor, account),
|
||||
},
|
||||
account.guest === true,
|
||||
);
|
||||
root.add(this.toolbar);
|
||||
|
||||
this.workspace = new HStack();
|
||||
@@ -614,7 +626,10 @@ export class Editor implements PlayHost {
|
||||
// A press in the editor takes the keys back from a window (#371).
|
||||
this.workspace.on('pointerdowncapture', () => (this.pressedWindow = null));
|
||||
|
||||
this.autosave = attachAutosave(state, store);
|
||||
// A guest's project lives in the tab alone (#428): there is nowhere to save it to.
|
||||
this.autosave = this.guest
|
||||
? { flush: () => Promise.resolve(), stop: () => {} }
|
||||
: attachAutosave(state, store);
|
||||
this.cleanups.push(
|
||||
installShortcuts(state, {
|
||||
play: () => this.play.play(),
|
||||
@@ -863,6 +878,14 @@ export class Editor implements PlayHost {
|
||||
* picked: it says who is signed in without costing the crowded toolbar another label.
|
||||
*/
|
||||
private openAccountMenu(anchor: Widget, account: EditorAccount): void {
|
||||
if (this.guest) {
|
||||
new Menu([
|
||||
{ label: 'Guest – nothing is saved', disabled: true, onSelect: () => {} },
|
||||
{ separator: true },
|
||||
{ label: 'Sign in…', onSelect: () => account.logOut() },
|
||||
]).openBelow(anchor);
|
||||
return;
|
||||
}
|
||||
new Menu([
|
||||
{ label: `Signed in as ${account.user}`, disabled: true, onSelect: () => {} },
|
||||
{ separator: true },
|
||||
@@ -1348,8 +1371,13 @@ export class Editor implements PlayHost {
|
||||
};
|
||||
}
|
||||
|
||||
boot(): Promise<void> {
|
||||
return this.projects.boot();
|
||||
async boot(): Promise<void> {
|
||||
await this.projects.boot();
|
||||
if (this.guest) {
|
||||
this.state.setNotice(
|
||||
'You are trying a demo as a guest: change anything, play it, record it – nothing is saved. Sign in from the menu at the far right to keep your work.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user