- SessionCreated event now carries only domain data (no isFirstSession)
- Mails listener uses ordered guard clauses, deferring the DB query
until cheaper checks pass
- Drop $user Document allocation in favour of direct array access
- Inline FileName validator and $smtpEnabled into their use sites
- Extract $isBranded to eliminate duplicate APP_BRANDED_EMAIL_BASE_TEMPLATE check
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves session alert email side effect out of the account controller
into a dedicated `Mails` listener that reacts to a new `SessionCreated`
bus event. The event is now always dispatched on session creation; the
listener owns all conditional logic (first session, sessionAlerts flag,
email-link sessions, user email presence).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Swap the order of createDocument('sessions') and purgeCachedDocument('users')
in the email/password session creation flow. Previously, the cache was purged
before the session was written, opening a race window in Swoole's async
environment where a concurrent account.get() could re-cache the user with no
sessions, causing sessionVerify to fail with a 401. This matches the correct
ordering already used by the token-based flows (magic URL, OTP, phone).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove team deletion for sole owner+sole member case; let orphan teams
be cleaned up by Cloud's inactive project cleanup (safer, avoids
accidental data loss)
- Add explicit ordering by $createdAt so the most veteran member gets
ownership transfer, with limit(1) for clarity
- Remove confirm filter on primary user transfer in membership deletion
so all members (including unconfirmed) are considered
- Remove redundant ownership transfer from Deletes worker since the API
controller already handles it before queueing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prevent unconfirmed (pending invite) members from being promoted to
owner or set as the team's primary user during membership/account
deletion by adding a Query::equal('confirm', [true]) filter to the
relevant findOne queries.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of blocking account deletion when the user has confirmed team
memberships, handle memberships gracefully during deletion:
- Sole owner + sole member: delete the team and queue project cleanup
- Sole owner + other members: transfer ownership to the next member
- Non-owner / multiple owners: no special handling needed (worker cleans up)
Also update the Deletes worker to transfer the team's primary user
reference when removing a deleted user's memberships.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The users.php and messaging.php legacy controllers were missing the
userType field in their log output, creating an inconsistency with the
new audit user type distinction feature. Also adds missing mode field
to users.php logs endpoint.
https://claude.ai/code/session_01J9gKXwbHoLggsGwJi6KUnM
- Add getDatabaseResourceType() helper to map database types to resource constants
- Use database-specific resourceType for CSV/JSON import/export instead of hardcoded TYPE_DATABASE
- Skip attribute validation for schemaless databases (DocumentsDB/VectorsDB) in exports
- Parse JSON export queries in migration worker
- Restore MigrationsBase from 1.9.x and append VectorsDB/DocumentsDB E2E tests
- general.php: add instanceof guard in error handler to prevent calling
isPrivileged() on a plain Document if getResource('user') returns
an unexpected type
- graphql.php: add setUser() calls on request/response in graphql group
init so sensitive field filtering works correctly for GraphQL routes
- api.php: fix session group init type hint from Document to User for
consistency with all other init blocks
https://claude.ai/code/session_01JLPDurUgyj7qViA8JqQFTH
All call sites now use $user->isApp() and $user->isPrivileged() instance
syntax instead of static User::isApp() / $user::isPrivileged() calls.
Added setUser() to Request class for consistency with Response.
https://claude.ai/code/session_01JLPDurUgyj7qViA8JqQFTH
PHPStan correctly flagged that Document::isPrivileged() doesn't exist.
Changed type hints from Document $user to User $user in all action
signatures where $user::isPrivileged() is called, since the runtime
instance is always a User (or subclass).
https://claude.ai/code/session_01JLPDurUgyj7qViA8JqQFTH
Replace all static User::isPrivileged() calls with $user::isPrivileged()
across the codebase. Since $user is resolved via setDocumentType, this
allows subclasses to override the privilege check without CE needing to
know about downstream-specific roles.
https://claude.ai/code/session_01JLPDurUgyj7qViA8JqQFTH