История коммитов

.
refactor(auth): the current user service carries the profile
CurrentUser answers who is visiting and, on demand, loads their row: id(), isGuest(), isValid() and a lazy user() behind UserRepositoryInterface::find(). A guest is an empty model, and both the identity and the profile are dropped between requests.

The core reads the visitor through it: the legacy classes, the casts, the validators and the Twig app variable. The shared User instance stays for now, mirrored from the same answer once per request.

Two side effects move out of the way: the ip history becomes IpHistoryRecorder, called by the kernel, and TranslatorServiceFactory stops resolving the visitor locale — at boot there is no database connection yet, and the kernel applies their language anyway.
.
docs: update the submodule pointer
The update path from 9.9 and the route cache.
.
feat(router): cache the route collection
With CACHE_ROUTES on, the routes are dumped once into data/cache/routes.php as a plain nested
array — no objects, so opcache holds it — and the collection is never built again: the twenty-odd
route files are not read, and no Route object exists for the length of the request.

This is what stage 5c bought. The collection could not be cached while it depended on the
visitor: a file that declared its staff routes inside 'if ($user->rights >= 9)' produces a
different set of routes for every visitor, and dumping one of them would have served it to
everybody. Now the route is always declared and its gate is a middleware.

The dump is written to a temporary file and moved into place, so a request arriving mid-write
cannot require half of it. A route whose handler or middleware is a closure cannot be written
out as data: the site is served either way, it just keeps building the collection and says why
in the log once.

Off by default, like the container cache, and cleared by cache:clear.
.
refactor(auth): drop the columns the roles replaced
users.rights, users.failed_login, users.rest_code, users.rest_time and roles.legacy_rights are
gone from a fresh installation, and auth:drop-legacy-columns takes them off an existing one. It
is the only step of this update that deletes data, so it refuses to run before
auth:migrate-rights has turned the numbers into roles: dropping the column first would leave a
site with no staff and no way to work out who they had been.

RightsMirror goes with them. It existed to keep the number in step with the roles while three
hundred checks still compared against it; there are none left, and the role editor stops
writing a column nobody reads.

The other three columns had already been replaced and were only still being written: the
failed sign-in attempts, which the throttle counts per address and per account in the cache
and forgets on its own, and the password recovery code with its deadline, which
password_reset_tokens replaced.

The two schema commands were broken from the command line — Capsule::schema() answers with a
connection of null until something builds the Eloquent capsule, and under HTTP a request always
had. The schema builder is a service now, built by a factory that asks for the connection
first; auth:upgrade-schema works from a terminal for the first time.

Update order for an existing site: auth:upgrade-schema, auth:migrate-rights,
auth:apply-default-permissions, auth:migrate-module-access, then auth:drop-legacy-columns.
.
refactor(users): the caption under a nickname and the staff smilies come from the roles
The last two things that read users.rights: what an account is called next to its nickname,
which was a table of six numbers copied into four classes, and whether the author of an old
post gets the smilies kept for the staff, which was 'the number is above zero'.

Both are one question — the highest role granted to the account — and StaffTitles answers it.
An account holding nothing beyond the roles everybody carries has no caption and is not staff,
which is exactly what a zero used to mean. The caption of a built-in role is translated by its
slug, so it still speaks the visitor's language; a role the site added is shown under the name
the site gave it.

Listings ask about a whole page at once — the visitors of the forum, the files, the voters of
a poll — and the answers are kept for the length of the request, so a topic with ten posts by
three authors costs one query rather than ten.

The joins that dragged users.rights along for the ride are gone from five queries, and the
reply of the shared comments engine records whether its author was staff rather than their
number, the way the guestbook already does; replies written before that still answer with the
number they stored.

One accident fixed on the way: the mail lists passed a row of the users table where the
nickname formatter expected the id under another key, so those rows had no caption and no link
to the profile at all.
.
refactor(auth): one current user instead of two
Every request identified its visitor twice: the authenticator chain decided who it was, and
then two shared objects were filled from the database with the same account —
Johncms\System\Users\User, a bag of public properties with the columns written out by hand,
and Johncms\Users\User, the Eloquent model. Two lookups, two IP-history writers racing each
other over the same row, and two answers to 'who is here' that could drift apart.

The old one is gone, with its factory and the property list it inherited. Everything that took
it — the counters, the comments engine, the ads, the locale resolver, the date cast, four use
cases of the mail module, the two authorized-user middlewares — takes the model instead; both
carried the same fields and the same isValid(), so the change is the type and nothing else.

Two things the old factory did on the side go with it: recording the IP history, which the
model's factory already does, and zeroing the rights of a banned visitor in memory, which
BanVoter has been doing properly since the roles arrived.

The columns and the singleton itself are the next steps of this stage; this one is only about
there being one of them.
.
refactor(admin): retire the module access screen, the settings that are left move to the system ones
The screen managed the mod_* numbers, and there is nothing left of them to manage: who may
open a module and write in it is a permission of the guest and the user roles, edited in
/admin/roles. ModulesAccessController, UpdateModulesAccessUseCase, ModulesAccessDTO, the
template, the two routes and the item in the sidebar all go. No redirect from the old address:
it is an internal link of the panel, nobody bookmarks it.

Three switches on that screen were never permissions and would have been left without a home:
whether the library and the downloads have comments at all, and whether a new account waits
for an administrator. They are a 'Modules' fieldset of /admin/settings now, with a line saying
where the access itself is set.

There is a cost, taken knowingly: /admin/modules-access was open to any administrator and
/admin/settings only to whoever may change the site as a whole, so those three switches move
one level up. A screen per module for three checkboxes would have been the alternative.

The mod_* keys stay in the default configuration, marked deprecated and read by nothing but
auth:migrate-module-access, which needs them to see what a site had. docs/ gets the section
for module authors: how to ask a permission, how to declare one, and what the old settings
became.

This finishes 5b of the auth plan: the second access system of the CMS is gone.
.
refactor(registration): split mod_reg into a permission and a workflow setting
The number said two things at once: 2 was 'open', 1 was 'open, and every new account waits
for an administrator', 0 was 'closed'. Whether an account may be registered is
registration.register, held by the guest role — the only role that can act on it. Whether the
account then waits is registration_moderation, a setting, because a workflow is not a right.

The new key starts as null, meaning the site has not been asked yet, and the old number
answers until it is: a site updated with mod_reg = 1 keeps moderating registrations without
anybody having to open the settings first. The bridge is one line in RegistrationSettings and
goes away with the key.

What is left of /admin/modules-access is now only switches that are not permissions of
anybody — the comments of the library and of the downloads, and the moderation of
registrations — which is what the rows of that screen were reduced to one module at a time.

After updating: auth:apply-default-permissions --force, then auth:migrate-module-access.
.
refactor(community): who may open the lists is a permission, not the active setting
community.view, held by the guest and the user roles. Six controllers opened with the same
ten lines — read the config, compare it against isValid(), render a result page — and the
gate is one line in routes.php now, on the group the six routes share. That is what the
route permission of the previous commit was for.

A guest is redirected to sign in instead of being shown a page that says 'for registered
users only' with a 200; the middleware answers the same way everywhere else.

After updating: auth:apply-default-permissions --force, then auth:migrate-module-access.
.
refactor(downloads): who may open them is a permission, not the mod_down setting
downloads.view, held by the guest and the user roles, replacing the two branches of the
access use case; a section closed to everybody still opens for the administrator, and the
migration carries that exception over.

No separate permission for taking a file: nothing ever gated the download itself by the
setting — reaching the page was the whole check — and the file of a folder awaiting
moderation already asks downloads.moderate.

The 'Downloads are closed' banner leaves the three templates that carried it, for the reason
it left the forum and the library: whoever sees the page may open the section.

After updating: auth:apply-default-permissions --force, then auth:migrate-module-access.