Skip to content

plugin-auth: sendMagicLink is the one auth mail to an existing user that still ignores sys_user.locale (#14788 ruling D) #15106

Description

@claude

Found while implementing #14762 (auth OTP SMS + auth mail read sys_user.locale); left out of that PR by the scope fence rather than fixed there.

Observation

#14788 was ruled option D on 2026-09-03: sys_user.locale when set -> the request's Accept-Language -> the deployment default. #14762 layered that stored rung onto AuthManager.emailLocaleArg and threaded it at the three sends that hold a recipient row:

  • sendResetPassword (user.id)
  • sendVerificationEmail (user.id)
  • sendChangeEmailNotice (from.id)

sendMagicLink was left on the two-rung #14319 ladder (request -> deployment). It is the fifth auth send and the only one to an existing account that does not read the column. Measured at origin/main after #14762's branch: auth-manager.ts calls this.emailLocaleArg(ctx) at the magic-link site with no stored argument.

Why it was fenced out rather than fixed

Every other send is handed a user object carrying an id; sendMagicLink is handed only { email, url, token }. Reading the column there needs a lookup keyed by email rather than by id — a query shape the card did not name, on a path the card did not name. Adding it silently would have been scope creep on a p3.

What it costs today

A user who set their language to zh-CN gets Chinese password-reset mail and English magic-link mail from the same deployment, decided by whichever browser happened to send the request. That is the exact inconsistency ruling D exists to remove — the ladder is meant to be one answer per recipient, not one per callback.

Suggested shape (for triage, not a ruling)

One findOne('sys_user', { where: { email }, fields: ['locale'] }) behind the existing placeholder-address refusal, threaded into emailLocaleArg's stored parameter exactly as the other three sends do. The helper (AuthManager.storedRecipientLocale) already exists and takes an arbitrary where, so this is a call site, not a mechanism.

Open question worth deciding rather than assuming: a magic link is requested BY the recipient, so the request rung is legitimate here in a way it is not for an admin-initiated reset. Ruling D still puts the stored column on top, but this send is the weakest case for it and the cheapest to leave alone.

Refs: #14762 (the rungs that did land) - #14788 (the ruled ladder) - #14319 (the request rung) - #14641 (invitations, a separate rung with a separate reason)


Generated by Claude Code

Activity

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

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions