- Accounts: roles, the User management tab, the account table and the role argument of make-user.php. - API: the users routes and how admins reach other users' projects. - Concepts, Plugins, Architecture: the shared closable tab. - Development: check sum 1333. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent
314c09ef50
commit
3af598d40e
6 files changed
+113
-36
No files matched your search
+9
-2
@@ -2,8 +2,8 @@ All routes go through `api/index.php?r=…` (the site's nginx does not pass `PAT
|
||||
|
||||
| Method | Route | Body → Result |
|
||||
|---|---|---|
|
||||
| GET | `auth/session` | `{user}` — the name of whoever is signed in, `null` when nobody is. Always 200, and the only route that needs no session |
|
||||
| POST | `auth/login` | `{username,password}` → `{user}` and the session cookie; **401** `{error}` for a wrong pair |
|
||||
| GET | `auth/session` | `{user, role}` — the name and role (`admin` or `user`) of whoever is signed in, both `null` when nobody is. Always 200, and the only route that needs no session |
|
||||
| POST | `auth/login` | `{username,password}` → `{user, role}` and the session cookie; **401** `{error}` for a wrong pair |
|
||||
| POST | `auth/logout` | 204, and the cookie is cleared |
|
||||
| POST | `auth/password` | `{currentPassword,newPassword}` → 204. Changes the password of whoever is signed in — the only `auth` route that needs a session. **401** for a wrong current password, **422** when the new one is shorter than 8 characters. Every other session of the account ends; the one asking keeps its own |
|
||||
| GET | `projects` | `[{id,name,revision,updatedAt}]` |
|
||||
@@ -23,10 +23,17 @@ All routes go through `api/index.php?r=…` (the site's nginx does not pass `PAT
|
||||
| POST | `pixels` | **raw cel bytes** → `{file,bytes}` (201) — the pixels of one layer on one frame of [pixel art](Pixel-Art); **422** for anything that is not a cel. No listing and no delete: a cel is found by the name in the project |
|
||||
| GET | `plugins` | `[{file,bytes,updatedAt}]` — every `.js` in `data/plugins/`, sorted by file name |
|
||||
| GET | `plugins/{file}` | the module source as `text/javascript` (`nosniff`, `no-cache`); 404 for anything but a bare file name there — see [Plugins](Plugins) |
|
||||
| GET | `users` | `[{username,role,createdAt,projects:[{id,name,revision,updatedAt}]}]` — every account with its projects, never a password. Administrators only |
|
||||
| POST | `users` | `{username,password,role}` → `{username,role,createdAt}` (201). **409** for a taken name, **422** for a bad name, a password under 8 characters or an unknown role. Administrators only |
|
||||
| PATCH | `users/{name}` | `{username?,role?,password?}` → `{username,role,createdAt}`. Only what is sent changes, and everything is checked before anything is written. A new name moves the account's projects and signs it out everywhere; a new password ends its sessions (the caller's own stays). **404** unknown account, **409** taken name, **422** for the caller's own name or role and for demoting the last administrator. Administrators only |
|
||||
| DELETE | `users/{name}` | 204, and the account is signed out everywhere. **409** while it owns projects, **422** for the caller's own account and the last administrator. Administrators only |
|
||||
|
||||
**Everything but `auth/*` needs a session** and answers **401** `{"error":"Sign in first"}` without
|
||||
one. Projects belong to an account: `projects` lists only the caller's own, and somebody else's
|
||||
project answers **404**, not 403 — that an id exists is already more than a stranger should learn.
|
||||
An **administrator** (#180, #179) reads, saves and deletes every project with the same routes, and
|
||||
a save keeps the project's `owner`; their `projects` list stays their own. The `users` routes
|
||||
answer anybody but an administrator **403** `{"error":"Administrators only"}`.
|
||||
Images, fonts, sounds, cels and plugins are shared between accounts; being signed in is still the price
|
||||
of admission. See [Accounts](Accounts).
|
||||
|
||||
|
||||
+73
-18
@@ -45,37 +45,92 @@ without asking for the password again, and so does closing the browser overnight
|
||||
PHP's own session handler would not do that: it throws sessions away after
|
||||
`session.gc_maxlifetime`, 24 minutes by default.
|
||||
|
||||
## Roles
|
||||
|
||||
Every account has one of two roles (#180): **admin** or **user**. An account without a role in
|
||||
`data/users.json` is a user — the weaker reading is the one a missing field gets, so a file
|
||||
written by hand never hands out rights by accident. The same goes for a role the server does not
|
||||
know.
|
||||
|
||||
What an administrator may do on top is decided **on the server**: every such route asks
|
||||
`Auth::requireAdmin()` and answers anybody else **403**. The editor only leaves out what it may
|
||||
not offer; it shows the role nowhere else, there is no badge and no word in the title.
|
||||
|
||||
An administrator
|
||||
|
||||
- manages the accounts in the **User management** tab (below), and
|
||||
- opens, edits and deletes **every** project as if it were their own (#179). A save keeps the
|
||||
project's owner: an administrator who fixes somebody's project does not take it over. Their own
|
||||
project list (*Open project…*) still shows only their own; the others are reached through the
|
||||
user management.
|
||||
|
||||
## User management
|
||||
|
||||
*Menu → User management* (administrators only) opens a tab with every account and, below it, the
|
||||
[plugins](Plugins). *Menu → Plugins* opens the same tab — for a user it holds the plugins alone and
|
||||
is called **Plugins**. It closes like a document and is not part of the fixed tabs.
|
||||
|
||||
Each account is one row: name, role, creation date (empty for the accounts from before #179) and
|
||||
how many projects it owns, the projects listed under it with **Open**. On the right:
|
||||
|
||||
| Button | What it does |
|
||||
|---|---|
|
||||
| **Rename…** | A new name. The account's projects move with it — the name is their `owner` — and the account is signed out everywhere, since its sessions carry the old name |
|
||||
| **Role** | `user` or `admin`, picked from a menu |
|
||||
| **Password…** | Sets a new password without the old one. The account's sessions end; on the administrator's own row their own session stays |
|
||||
| **Delete** | Asks first, then removes the account and signs it out. **Refused while it owns projects** — they would belong to nobody and vanish from every list; delete them first |
|
||||
| **New user…** | In the tab's header: name, password (twice) and role |
|
||||
|
||||
On their **own row** an administrator may only set a password: renaming themselves would end the
|
||||
session they are working in, and demoting or deleting themselves would lock them out of the page
|
||||
they are on. The server refuses the same, and it refuses to demote or delete the **last**
|
||||
administrator.
|
||||
|
||||
A refused change comes back in the server's words: in the open form (*The name e2e is taken*),
|
||||
or on the status line for Role and Delete (*anna still owns 2 projects*).
|
||||
|
||||
## The accounts
|
||||
|
||||
There are three, and for now they are made on the server, not through the editor:
|
||||
|
||||
| Account | What it holds |
|
||||
|---|---|
|
||||
| `admin` | no projects; the account that will get the admin role |
|
||||
| `e2e` | the four projects the check tools write: fixture, Jump and Run, Tetris, Actor demo |
|
||||
| `user` | every other project |
|
||||
| Account | Role | What it holds |
|
||||
|---|---|---|
|
||||
| `admin` | admin | no projects |
|
||||
| `e2e` | user | the four projects the check tools write: fixture, Jump and Run, Tetris, Actor demo |
|
||||
| `e2e-admin` | admin | nothing; the account the user management step signs in as |
|
||||
| `user` | user | every other project |
|
||||
|
||||
A project file carries its owner (`owner` in `data/projects/{id}.json`). A file without one
|
||||
belongs to nobody and is invisible to everybody — the safe way round, and the one that shows up
|
||||
straight away when a new project file is put there by hand without an owner.
|
||||
belongs to nobody and shows up in no list, not even the user management's — the safe way round,
|
||||
and the one that shows up straight away when a new project file is put there by hand without an
|
||||
owner. Only an administrator who knows its id can still open it (`?project=<id>`).
|
||||
|
||||
## Making an account
|
||||
## Making an account on the server
|
||||
|
||||
php tools/make-user.php <username> <password> # creates it, or sets the password
|
||||
php tools/make-user.php --list
|
||||
php tools/make-user.php <username> <password> [admin|user] # creates it, or sets the password
|
||||
php tools/make-user.php --list # name and role per line
|
||||
|
||||
This is how the first administrator comes about; after that, accounts are managed in the editor.
|
||||
Without a role a new account is a user, and an existing one keeps the role it has — resetting a
|
||||
password on the command line never demotes an administrator. The CLI does not check the password's
|
||||
length.
|
||||
|
||||
Passwords are stored as hashes (`password_hash`) in `data/users.json`, which is git-ignored, as
|
||||
is `data/sessions/`. The CLI is the only way to *create* an account and the only way to set a
|
||||
password without knowing the old one; changing one's own works in the editor (#182). Registration
|
||||
(#178), administering users (#179), roles (#180) and shared projects (#181) are follow-ups of
|
||||
their own.
|
||||
is `data/sessions/`. Registration (#178) and shared projects (#181) are follow-ups of their own.
|
||||
|
||||
## What the check tools do
|
||||
|
||||
Every seed, smoke and e2e run signs in as `e2e` first and sends the same session cookie a browser
|
||||
would (`tools/lib/auth.mjs`). The password comes from `GAME_MAKER_E2E_PASSWORD` or from
|
||||
`data/e2e-password.txt`, which is git-ignored — CI writes its own on a fresh checkout and makes
|
||||
the account with it (`tools/ci/serve.sh`).
|
||||
the account with it (`tools/ci/serve.sh`). `e2e-admin` works the same way, with
|
||||
`GAME_MAKER_E2E_ADMIN_PASSWORD` or `data/e2e-admin-password.txt`.
|
||||
|
||||
The browser in an e2e run is handed that session as a cookie, so it starts on the editor. The
|
||||
sign-in screen itself is driven by hand in step 45, the one step that sees it.
|
||||
sign-in screen itself is driven by hand in step 45, the one step that sees it. Step 49 signs the
|
||||
browser in as `e2e-admin`, manages a throwaway account `e2e-tmp` through the tab and hands the
|
||||
browser back to `e2e` at the end; the account and its project are gone again even when a check on
|
||||
the way throws.
|
||||
|
||||
The API's own rules — roles, who reaches which project, what may not be done to an account — are
|
||||
tested without a server in `tests/php/accounts.test.php`, a plain PHP file CI runs in the gate job
|
||||
(`php tests/php/accounts.test.php`). The machine the editor is built on has no PHP; there it runs
|
||||
in the web server's container.
|
||||
+12
-3
@@ -150,7 +150,7 @@ src/editor/ App → Editor (layout + ticker), PlaySession (the run),
|
||||
#129, #195),
|
||||
fileImport (drop/paste) → assetImport (one upload pipeline, an ImportKind per sort) ← textureImport / fontImport / soundImport,
|
||||
FilePicker (the shared "+ → Texture / Font / Sound" dialog; ImagePicker/FontPicker/SoundPicker only describe their row),
|
||||
framing (fit/marquee/snap maths), FormDialog (the small modal forms: label column, Cancel + confirm, one open at a time; `dialogButtons` is the closing row every dialog shares, the pickers' included), AssetTabEditor (the shared shell of the asset tabs), TabHeader (the bar across the top of a tab — asset tabs, Plugins tab, NodeBoard; drawn by `drawHeaderBar` like the toolbar and the breadcrumb),
|
||||
framing (fit/marquee/snap maths), FormDialog (the small modal forms: label column, Cancel + confirm, one open at a time; `dialogButtons` is the closing row every dialog shares, the pickers' included), AssetTabEditor (the shared shell of the asset tabs), TabHeader (the bar across the top of a tab — asset tabs, the plugins and accounts lists, NodeBoard; drawn by `drawHeaderBar` like the toolbar and the breadcrumb),
|
||||
TabStrip + tabRow (the tab row and what it does when the tabs outgrow it; plugin tabs are inserted at run time),
|
||||
assetTabs (which asset kind opens into which tab, and so which kind has an editor at all),
|
||||
TabButton (how a tab is drawn: the card of the one showing, plain weight, no pressed fill, #176),
|
||||
@@ -160,7 +160,16 @@ src/editor/ App → Editor (layout + ticker), PlaySession (the run),
|
||||
screen (`AssetTabEditor.showAsset`), and it keeps each document's view through `saveView`/`restoreView`
|
||||
src/editor/plugins/ startPlugins (list, import, host; runs before the first project) → loader (PluginManager: syncTo makes the
|
||||
running plugins match what the project asks for, order, checks, what became of each file), pluginApi
|
||||
(API v1, the per-plugin scope), PluginsPane (the Plugins tab), NodeBoard (api.ui)
|
||||
(API v1, the per-plugin scope), PluginsPane (the list of plugins), NodeBoard (api.ui)
|
||||
src/editor/users/ the user management (#179): ManagementPane (the tab behind Menu → User management / Plugins,
|
||||
a document named `management` rather than a fixed tab; accounts above, PluginsPane below),
|
||||
UsersPane (one row per account, its projects with Open, the actions; reads the list again
|
||||
after every change), AccountDialog (the one form class for new user, rename and password),
|
||||
accountRules (what the forms say before sending, which buttons a row offers — pure).
|
||||
The store is `core/store/UsersStore` over `?r=users`; a refusal arrives as the
|
||||
server's own sentence. The server decides everything: `api/lib/AccountRules.php`
|
||||
(not the own account, not the last administrator, not an account with projects),
|
||||
`Auth::requireAdmin()` (403), `Store` with `seesAll` for an administrator
|
||||
src/editor/viewport/ WorldViewport base → FreeViewport (a camera of its own: fit, pan, wheel zoom) → SceneViewport, PreviewCanvas;
|
||||
GameViewport follows the game's camera instead — editing tools, hence not in src/viewport/.
|
||||
A pan only moves the world and redraws the screen-space overlays (`moveCamera`); a full
|
||||
@@ -178,7 +187,7 @@ src/editor/inspector/ Inspector panel, host (what a section builds into), field
|
||||
objectQueries/vectorRows/parentRow
|
||||
src/editor/panels/ AssetPanel, assetGroups (groups by type and their order, #176), assetIcons (one SVG per
|
||||
asset type in icons/, shared by the panel, the + menu, the tabs and Scene objects,
|
||||
plus the List, Game and Plugins tabs' own three, which stand for no asset),
|
||||
plus the List, Game, Plugins and User management tabs' own four, which stand for no asset),
|
||||
assetMenu (the two groups of the + menu), HierarchyPanel,
|
||||
Toolbar + toolbarIcons (five icon buttons, no captions: menu, undo, redo, play, pause, #183),
|
||||
headerIcons (the pictures of the tab headers' switches: the magnet, #194),
|
||||
|
||||
+7
-5
@@ -74,17 +74,19 @@ The asset panel and **Scene objects** share the left column; the divider between
|
||||
the height from one to the other, and dragged all the way either keeps its header. The split is a
|
||||
share of the column, so a taller or shorter window keeps it.
|
||||
|
||||
**The tab row.** A tab is an **open document**, as in an IDE (#171). **Scene**, **Game** and
|
||||
**Plugins** are fixed and cannot be closed; a plugin's own tab follows them for as long as the
|
||||
plugin is on. Everything else opens as a document: double-click (or `Enter`) an asset in the
|
||||
**The tab row.** A tab is an **open document**, as in an IDE (#171). **Scene** and **Game** are
|
||||
fixed and cannot be closed; a plugin's own tab follows them for as long as the plugin is on. The
|
||||
plugins themselves, and for an administrator the accounts, are in a tab that opens from the menu
|
||||
and closes like a document (*Menu → Plugins* or *User management*, #179, see [Accounts](Accounts)). Everything else opens as a document: double-click (or `Enter`) an asset in the
|
||||
asset panel, create one, or open a list from its row. An open document only comes up again, a new
|
||||
one gets its tab right of the document on screen (or last, from a fixed tab). Several assets of
|
||||
one kind can be open at once — two textures, three behaviors — each with its own tab.
|
||||
|
||||
- The tab shows the **icon** of its kind, then the name, so a behavior and a texture both called
|
||||
`Player` stay apart; the `»` menu lists hidden documents the same way. **Scene** and **Game**
|
||||
carry an icon too (#176) — the scene one from the asset panel, a play sign for the Game tab,
|
||||
a puzzle piece for **Plugins** (#119). Only a plugin's own tab goes without one, as it brings
|
||||
carry an icon too (#176) — the scene one from the asset panel, a play sign for the Game tab.
|
||||
The plugins' tab shows a puzzle piece (#119), or two people when it holds the user management
|
||||
(#179). Only a plugin's own tab goes without one, as it brings
|
||||
no picture of its own. A long name is cut with an
|
||||
ellipsis. Renaming an asset renames its tab.
|
||||
- A document tab always shows its own asset. Selecting something elsewhere never changes it; there
|
||||
|
||||
+5
-3
@@ -12,8 +12,9 @@ npm run check # typecheck + lint + format:check + tests, the gate CI runs
|
||||
# code is lost (tail's is reported), so a red run reads as green
|
||||
npm run build # writes dist/ and regenerates index.html
|
||||
./build.sh # the same under `set -o pipefail`, for callers that pipe the output
|
||||
php tools/make-user.php <name> <password> # an account; every tool signs in (see Accounts)
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1314 editor checks, screenshots in /tmp/gm-e2e-*.png
|
||||
php tools/make-user.php <name> <password> [admin|user] # an account; every tool signs in (see Accounts)
|
||||
php tests/php/accounts.test.php # the API's account rules, no server needed (CI's gate job)
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1333 editor checks, screenshots in /tmp/gm-e2e-*.png
|
||||
GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step
|
||||
./e2e.sh start [GM_STEPS] # the same run detached, see below
|
||||
./e2e.sh status # live progress, or the last run's result
|
||||
@@ -271,7 +272,8 @@ Counter-check: dropping generated assets from resolvePicture fails 5.
|
||||
## Continuous integration
|
||||
|
||||
Every push runs the whole proof on the Gitea runner: `.gitea/workflows/ci.yml` installs, runs
|
||||
`./check.sh`, builds, then `tools/ci/run-e2e.sh` (the editor suite) and `tools/ci/run-smokes.sh`
|
||||
`./check.sh` and the PHP tests (`tests/php/`, #179 — this machine has no PHP, so they are not in
|
||||
`check.sh`; here they run in the web server's container), builds, then `tools/ci/run-e2e.sh` (the editor suite) and `tools/ci/run-smokes.sh`
|
||||
(the three games). Both scripts source `tools/ci/serve.sh`, which puts the checkout behind
|
||||
`http://127.0.0.1:8080/game-maker/` with `php -S` — the build wires its assets absolutely under
|
||||
`/game-maker/dist/`, so the path segment is not optional — and writes the empty project files the
|
||||
|
||||
+7
-5
@@ -3,8 +3,8 @@ server, and the editor loads the ones the open project asks for. v1 is deliberat
|
||||
the demo plugin needs (read the assets, add a tab, show nodes on a board) and nothing it does not.
|
||||
|
||||
**Where plugins come from.** A file is put into `data/plugins/` by hand; there is no upload yet
|
||||
(#116). A file put there is **off** in every project until someone switches it on in the **Plugins**
|
||||
tab (#114, #119). The plugins that ship with the repository
|
||||
(#116). A file put there is **off** in every project until someone switches it on in the list of
|
||||
plugins (#114, #119), which *Menu → Plugins* opens. The plugins that ship with the repository
|
||||
live in `plugins/` and are copied to `data/plugins/` by `npm run build` and by `tools/seed-e2e.mjs`;
|
||||
a file of the same name is replaced, anything else in the folder is left alone.
|
||||
`data/plugins/` is not in the repository. Which plugins run is decided **by the project** (#119):
|
||||
@@ -61,8 +61,10 @@ no tab behind.
|
||||
|
||||
## Switching a plugin on and off
|
||||
|
||||
The **Plugins** tab lists every file in `data/plugins/` with its name, version, file and state, each
|
||||
with a switch. The switch belongs to the **open project**: it is kept in the project file as
|
||||
*Menu → Plugins* opens a tab that lists every file in `data/plugins/` with its name, version, file
|
||||
and state, each with a switch. It closes like a document. For an administrator it is the lower half
|
||||
of the **User management** tab, which *Menu → User management* opens as well (#179, see
|
||||
[Accounts](Accounts)). The switch belongs to the **open project**: it is kept in the project file as
|
||||
`"plugins": ["demo-plugin.js"]`, so it travels with the project and says nothing about any other
|
||||
one. A new project switches on nothing.
|
||||
|
||||
@@ -91,7 +93,7 @@ of no consequence while nothing secret lives in a plugin; #115 and #116 have to
|
||||
| `api.version` | `1` |
|
||||
| `api.project.assets()` | The assets of the open project in list order, as `{ id, name, type }` — copies, so writing to them changes nothing. |
|
||||
| `api.project.on('change', fn)` | Calls `fn` whenever the asset list changes: an asset created, renamed or deleted, by hand, by undo or by loading another project. A change that leaves ids, names and types alone (moving an object, editing a property) does not call it. Returns a function that unsubscribes. A listener that throws is reported and stays subscribed. Any event but `'change'` throws. |
|
||||
| `api.tabs.add({ id, label, create })` | Adds a tab after the fixed ones (Scene, Game, Plugins), ahead of the open documents; it stays for as long as the plugin is on. `create()` is called once and must return a widget from `api.ui`. Returns `false`, with a console warning, when the id is taken — by a built-in tab (`scene`, `graph`, …) or a plugin that came earlier; `create` is not called then. A missing `id` or `label`, or a `create` that is not a function, throws. |
|
||||
| `api.tabs.add({ id, label, create })` | Adds a tab after the fixed ones (Scene, Game), ahead of the open documents; it stays for as long as the plugin is on. `create()` is called once and must return a widget from `api.ui`. Returns `false`, with a console warning, when the id is taken — by a built-in tab (`scene`, `graph`, …) or a plugin that came earlier; `create` is not called then. A missing `id` or `label`, or a `create` that is not a function, throws. |
|
||||
| `new api.ui.NodeBoard()` | A read-only node graph: `setNodes([{ id, title, x, y }])` replaces what it shows, `fit()` frames it. It has a **Fit** button and a node count above the canvas; a drag with any button pans, the wheel zooms about the pointer. The nodes are drawn by the graph editor's own node view, empty but for their title — no ports, no editing, no selection. The first `setNodes` with anything in it frames the nodes once the tab has a size; after that the view stays where the user put it. A title that is not text is empty, a position that is not a number is 0, and something that is not a list throws. |
|
||||
|
||||
## The demo plugin
|
||||
|
||||
Reference in new issue
Block a user