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

.
docs: update the submodule pointer
.
feat(auth): add signing in through external services
GitHub, Google, VK and Yandex in the box, and any other service as a module: a provider is a class tagged johncms.auth.external_provider, and the contract deliberately describes 'where do we send the visitor' and 'what do we make of them coming back' rather than the OAuth2 flow, so a service like Telegram or Steam can implement it too. AbstractOAuth2Provider covers the usual case in three addresses and a field mapping.

The flow stays in the core: single-use state with PKCE in the session, the routes, matching against accounts and opening the session. A provider never sees the request and cannot decide who it is linked to, so the worst a third-party one can do is break its own button.

Matching an identity to an existing account by email is allowed only when the provider verified the address and the account confirmed it too — anything else is an account takeover. A newcomer finishes the profile before an account exists, since VK may withhold the address and a display name is rarely free as a login. An account signed up this way has no password, and unlinking the only way in is refused.

VK goes through VK ID: the classic oauth.vk.com endpoints answer a bare 'Security Error' for an application registered today, while VK ID requires PKCE, the device_id from the callback and the profile from /oauth2/user_info.
.
feat(auth): add browsing the site as another user
An administrator holding users.impersonate opens a session of the account rather than a swapped cookie: the row carries who opened it and which session to return to, so it is visible in the audit trail and revocable like any other. Their own session waits in a second cookie instead of being closed, and nobody may browse as an account that outranks them.

The session lasts an hour and is never extended, so a forgotten tab resolves itself: the authenticator falls back to the parent cookie and the administrator is themselves again. Signing out during impersonation closes both sessions.

Invisible to the user — the visit fields, the IP history and 'my devices' are left alone — but never to the record: both ends and every changing request are logged, with the impersonator named. ImpersonationVoter refuses the permissions listed in auth.impersonation.denied_permissions whatever the roles say.
.
docs: update the submodule pointer
.
docs: update the submodule pointer
.
feat(admin): add the sign-in log screen
/admin/auth-log lists the audit trail behind the new admin.auth_log.view permission, with filters by account and by kind of event. An account matches both as the subject and as the actor, and an event key from a module the core knows nothing about is shown as it is rather than failing the page.
.
feat(auth): record the sign-in audit trail
Adds auth_events and the logger writing it: sign-ins and refusals, password changes and recoveries, revoked sessions, granted and revoked roles. The logger fills in the address, the browser and the actor itself, so no caller can forget them, and the impersonator is named rather than the account being browsed as.

Attempts refused by the throttle are not recorded: they follow from failures already in the log, and a row per attempt would let anyone fill the table. auth:cleanup drops entries older than --event-retention-days (180 by default).

Fixes a gap left by the session work: completing a password recovery now revokes every session of the owner, without the exception a password change makes for the current one — whoever recovers a password is not signed in.
.
docs: update the submodule pointer
.
refactor(users): the current user is no longer a shared model
The User singleton, its factory and the mirror that refilled it are gone: CurrentUser is the only way to the visitor of the request, and nothing takes the model from the container any more.
.
refactor(login): the sign-out screen asks CurrentUser for the visitor