Files
WeKnora/internal/handler
wizardchen 0cdb922d84 feat(tenant): remember user's last-active workspace across logins
Until now a fresh login (new device, expired refresh token, cleared
browser) always dropped the user into their home tenant, even if they
spend most of their time in a peer workspace. The session-local
X-Tenant-ID override in localStorage gave a "same browser sticky"
effect, but never crossed devices or new sessions.

This adds a small server-side preference, `users.preferences.
last_active_tenant_id`, persisted in the existing jsonb column (no
new migration), and threads it through:

* Backend
  * `UserPreferences` gains `LastActiveTenantID *uint64` with sentinel
    semantics (`*0` from the PATCH endpoint = clear preference).
  * `resolveLoginTenantID` validates the stored id (tenant still
    exists + active membership, or CanAccessAllTenants) and falls
    back to home on any failure, best-effort clearing the stale
    preference so subsequent logins don't pay for it again.
  * `Login` and `LoginWithOIDC` resolve once and use the result for
    both the JWT `tenant_id` claim and the returned `active_tenant`,
    keeping the two in sync. `RefreshToken` rides through
    `GenerateTokens` so refresh rotations also land the user back in
    their preferred tenant instead of bouncing to home.
  * `UpdateUserPreferences` learns to merge the new key.
  * `PUT /auth/me/preferences` accepts the new field.

* Frontend
  * `Login.vue` now writes the user's HOME tenant id into
    `user.tenant_id` (matching the field's documented semantics) and
    expresses any active-vs-home divergence via `setSelectedTenant`,
    so `useHomeTenant` and the "current"/"home" badges stay correct
    after the backend honours a remembered preference. `App.vue`'s
    OIDC sync does the same reconciliation.
  * `TenantSelector`, `UserMenu` and the post-tenant-create handlers
    fire `persistLastActiveTenantPreference` after every successful
    user-initiated switch (switching to home sends `0` to clear).
    The call is raced against the existing reload-grace window so
    most writes finish before the page tears down; lost writes are
    recoverable on the next switch.

No new UI. Users will simply notice that, after re-logging in on
another device, they land back in the workspace they were last
using rather than always in their home tenant.

Note: `make docs` is unrelated-broken on `main` (audit_log.go
references `errors.AppError` which swag can't resolve), so the
Swagger artifacts under docs/ are intentionally not regenerated
in this PR. The handler code is the source of truth.
2026-05-18 20:37:57 +08:00
..
2026-05-18 19:38:23 +08:00