Skip to content

feat(items): item exchange endpoint for MuEditor (items phase 7, step 3) - #1345

Open
Mosch0512 wants to merge 19 commits into
MUnique:masterfrom
Mosch0512:feat/item-exchange-endpoint
Open

Mosch0512 wants to merge 19 commits into
MUnique:masterfrom
Mosch0512:feat/item-exchange-endpoint

Conversation

@Mosch0512

@Mosch0512 Mosch0512 commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Items phase 7, step 3: the admin panel serves the items as an item exchange file of the MuMain client. MuMain's editor (MuEditor) downloads it with "Update from server" and shows what differs from the client's items before anything changes.

This PR is the endpoint without item fields yet: the fields follow in their own PRs, a group at a time, so each mapping can be reviewed with the differences it shows.

The endpoint

  • GET /api/items/exchange returns the items of the saved configuration as the client's exchange file.
  • It needs an API key with the Viewer role (AdminPolicies.ApiKeyViewer); the login cookie isn't accepted.
  • One export runs at a time: a request in the meantime gets 429 Too Many Requests with Retry-After, which MuEditor already reports. Rate limits per key and a cached export with an ETag follow later.
  • It reads the configuration through ConfigurationReadScope, like the configuration reads of the agents: the configuration which the admin panel has already loaded, or a copy of the stored one while the admin panel has unsaved changes. So unsaved changes aren't part of the file, and the configuration usually isn't loaded again for each request.
  • It isn't exposed to agents ([NotExposedToAgents]).

API keys need a user

The API keys page creates a key only once the admin panel has its first user. While there is no user, anybody who reaches the admin panel can open that page, and a key created then would keep working after the first user exists. This covers every endpoint which takes a key, not only this one.

  • The key service asks the storage for a user (or a bootstrap user) when the dialog of a new key opens and again right before the key is stored, so a key confirmed after the last user is gone isn't stored.
  • Keys which were created before the first user are disabled once, so that an administrator enables only the keys they created:
    • the admin schema migration DisableApiKeysCreatedBeforeTheFirstUser disables every key which is older than the oldest admin user. A migration runs once, so keys enabled again afterwards stay enabled.
    • creating the first user in the admin panel disables every key which exists then, for installations which update while still in setup mode. The new user gets a warning with the number of keys, and the log names them.
  • The API keys page marks the keys created before the first user and says what to do: enable a key only if you created it, better replace it (new key, put it into the application, delete the old one), delete a key you don't know.
  • With only a bootstrap user and no stored user, nothing tells when the panel got its first user, so nothing is disabled; the docs say to check the keys then.

An earlier version of this PR refused only the export while the admin panel has no user. That didn't protect the file: a key created in that setup mode just had to wait for the first user. That check is gone.

The texts are in English, German and Chinese.

The file

  • Format version 2 with a fields list: the item fields the file carries. The client compares and applies only these and keeps its own values of every other field, and an older client refuses the file instead of reading the missing fields as zero.
  • gameVersion is the key of the data initialization which created the configuration (season6, 0.75 or 0.95d), found like the configuration updates find it: the stored update state, or for an older configuration its Season 6 client. The client only reads season6, so it refuses the file of a 0.75 or 0.95d server. When neither tells the version, the export is refused (503).
  • Items are sorted by group and number. Items which share them (OpenMU doesn't keep them unique) are sorted by their id, so the same configuration always gives the same file.
  • Items outside the client's groups (0 to 15) and numbers (0 to 511) are left out, because the client refuses the whole file because of them. The log names them in a warning.
  • No field is listed yet: each item has its group and number.

Docs

docs-website/docs/admin-panel/authentication.md: the item exchange file, keys only after the first user, what happens to the keys from before the first user and what to do with them, and the endpoints which accept only a key are marked "key only" in the table (changing an account's password or state and disconnecting a player already were).

Testing

Built with -p:ci=true. MUnique.OpenMU.Web.Tests: 531 pass. The 16 failures are the network analyzer tests, which expect English texts and get German ones on this machine; they pass with DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1.

  • ItemExchangeApiTests (own host): no key, the login cookie (with a check that it does sign in), a cash shop key, a viewer key, one export at a time.
  • ItemExchangeExportTests: the header, every Season 6 item sorted, repeated items by id, only listed fields, UTF-8 with LF, the operation on a stored configuration, unsaved changes left out, the loaded configuration reused, the registration in AddAdminReadOperations, the game version (update state, Season 6 client, unknown refused), the left-out items and their warning.
  • ApiKeyManagementServiceTests: no key without a user, a key with one, no key when the last user is gone while the dialog is open (it fails without the second check).
  • AdminPanelSetupModeTests: the setup mode without a user, with a stored user and with a bootstrap user; the oldest user; disabling the enabled keys; the first user created in the admin panel disables the keys, a later one doesn't.
  • The SQL of the migration was run on PostgreSQL 16 with users (only the key older than the oldest user is disabled) and without users (nothing is disabled).

GET /api/items/exchange returns the items as an item exchange file of the
MuMain client, which its editor downloads with "Update from server".

- The file has the format version 2 and a "fields" list: the item fields
  it carries. The client compares and applies only these and keeps its own
  values of the others, so the fields can be added one at a time. Items are
  sorted by group and number; items outside the client's groups and
  numbers are left out. No field is listed yet.
- The endpoint needs an API key with the Viewer role (the login cookie
  isn't accepted) and refuses requests while the admin panel has no user,
  because anybody can create a key in that setup mode.
- It reads the saved configuration in a context of its own.
The page said that only the item exchange file needs a key, but changing
an account's password or state and disconnecting a player don't accept
the login cookie either. These rows now say "key only" as well.
The controller repeated the scheme which the policy already names. With
two places, changing the attribute to the schemes of the other
controllers would quietly accept the login cookie again.
OpenMU doesn't keep the group and number of an item unique, and the
order in which the configuration is loaded can differ. Items which share
them now follow their id, so the same configuration always gives the
same file (which a later ETag depends on).
The export loaded the whole configuration for each request in a new
context, and the repository attached all of it to a change tracker.
It now reads through ConfigurationReadScope, like the configuration
reads of the agents: the configuration which the admin panel has
already loaded, or a copy of the stored one while the admin panel has
unsaved changes. Unsaved edits still aren't part of the file.

When an edit changes the items while they are read, the file is
written again, at most three times.
The tests read a configuration which only had stored data, so nothing
showed that edits the admin panel hasn't saved are left out. Now:
- with unsaved changes, the stored configuration is written;
- without, the configuration the admin panel has loaded is written;
- AddAdminReadOperations registers the export, which the controller
  needs (the API tests register it by hand, because their host doesn't
  have the services of the other operations).
…e file

The file always said "season6", so the client accepted the items of a
0.75 or 0.95d server as Season 6 data. The file now names the key of the
data initialization which created the configuration, as the
configuration updates find it: the stored update state, or for an older
configuration its Season 6 client. The client then refuses the file of
another version. When neither tells the version, the export is refused
(503).
Items outside the client's groups and numbers were left out without a
word, so neither the server operator nor the user of the client editor
learned that the file is missing them. The export now logs a warning
with their group, number and name.
While the admin panel has no user, anybody who reaches it can open the
API keys page. A key created then kept working after the first user
existed, so refusing the item exchange file in that setup mode didn't
protect it: the key only had to wait. The same held for every other key
endpoint.

The API keys page now refuses to create a key until the first user
exists, which covers all endpoints. The item exchange file's own
setup-mode requirement is gone with it. In setup mode the whole panel is
open anyway, and the requirement also refused valid keys while its
cached "no user" answer was stale, with a 403 which the client editor
reads as a missing role.
An export reads the whole item configuration, and loads it again while
the admin panel has unsaved changes. Any Viewer key could start as many
of them at once as it liked. A request while an export is running now
gets 429 Too Many Requests with Retry-After, which the client editor
already reports.

Rate limits per key and a cached export with an ETag are still to come.
The test refused the item exchange file to a signed-in browser, but
never showed that its cookie signed in at all, so it would also pass
when the cookie got lost. It now checks the sign-in on an endpoint
which accepts the cookie first.
@codacy-production

codacy-production Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 92 complexity · 17 duplication

Metric Results
Complexity 92
Duplication 17

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

…operties

Codacy (Sonar S2339) asks for static read-only properties instead of
public constants, as other public values of OpenMU are written. The check
of the client's numbers compares the number instead of matching it in a
pattern, which needs a constant.
…ation reads

The retry loop had no stop condition of its own (Sonar S1994): it ended
through the exception filter. It now tries as ConfigurationReadOperations
does: the attempts but the last catch a change during the read, and the
last one lets it through.
…I tests

Codacy's Sonar doesn't see the global usings and took the fixture for a
class which doesn't dispose its HTTP client (S2931), as it did for the
account administration API tests (e813622).
markdownlint (MD004) wants one list marker per page; the page's other
lists use asterisks.

@sven-n sven-n left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested: src/Web/AdminPanel/Services/ApiKeyManagementService.cs:63-69 blocks creation of new API keys before initial administrator setup, but previously issued setup-mode keys remain valid and can access endpoints including src/Web/AdminPanel/API/ItemExchangeController.cs:17-21 if sufficiently privileged. Please provide a way to identify/revoke or rotate potentially exposed legacy keys, with administrator guidance. Also, the check in ApiKeyManagementService.cs:65-100 occurs before an asynchronous modal interaction; recheck at persistence time to prevent creating a key after the last administrator is deleted. Add a concurrency regression test.

The API keys page checked for a user only when the dialog of a new key
opened, from the cached answer of the request authorization. The dialog
can stay open for long; a key confirmed after the last user was gone was
still stored.

AdminPanelSetupMode asks the storage whether there is a user (or a
bootstrap user). The key service asks it when the dialog opens and again
right before the key is stored. A test deletes the last user while the
dialog is open and confirms it afterwards: no key, an error. It fails
without the second check.
Keys created while the admin panel had no user kept working: anybody who
reached the panel then could have created one. They are disabled once,
so that an administrator enables only the keys they created:

- the migration DisableApiKeysCreatedBeforeTheFirstUser disables every
  key which is older than the oldest admin user; a migration runs once,
  so keys enabled again afterwards stay enabled,
- creating the first user in the admin panel disables every key which
  exists then, for installations which update while still in setup mode.
  The new user gets a warning with the number of keys.

The API keys page marks the keys created before the first user and says
what to do with them; the docs explain it, also why nothing is disabled
with only a bootstrap user. The texts are in English, German and
Chinese.
Codacy's Sonar can't tell that the test dialogs implement IModalService
and took a parameter of each for unused (S1172). The user dialog checks
that it fills in a dialog which creates something, and the pending
dialog keeps the parameters it was opened with, which the test checks
instead of a flag.
Codacy's Sonar names one unused parameter per method at a time (S1172),
so using one parameter of each test dialog only moved the warning to the
next. Both test dialogs now keep each dialog they show, with all its
parameters, and the tests check that exactly one dialog was shown. The
check of the user dialog's component type is gone again.
@Mosch0512

Copy link
Copy Markdown
Contributor Author

Thanks, both points are addressed:

  1. Keys from before the first user (47e0bb8): they are disabled once. The admin schema migration DisableApiKeysCreatedBeforeTheFirstUser disables every key which is older than the oldest admin user, and creating the first user in the admin panel disables every key which exists then (installations which update while still in setup mode). The API keys page marks such keys and tells the administrator to enable only the keys they created, or better to replace them; authentication.md has a section on it. With only a bootstrap user, nothing tells when the panel got its first user, so nothing is disabled there; the docs say to check the keys in that case.
  2. Check at persistence time (262fcbf): the key service asks the storage again right before the key is stored, not only when the dialog opens. NoKeyWhenTheLastUserIsGoneWhileTheDialogIsOpenAsync deletes the last user while the dialog is open and confirms it afterwards; it fails without the second check.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants