@micha.bigler/ui-core-micha 3.5.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/WORK_ORDERS.md +2 -0
- package/dist/auth/authApi.js +7 -2
- package/dist/i18n/authTranslations.js +42 -0
- package/dist/utils/auth-errors.js +1 -1
- package/package.json +1 -1
- package/src/auth/authApi.jsx +6 -2
- package/src/i18n/authTranslations.ts +46 -0
- package/src/utils/auth-errors.js +2 -2
- package/tests/authApiLogin.test.js +42 -0
- package/tests/authErrors.test.js +43 -0
- package/work-orders/AUTH-7.md +181 -0
- package/work-orders/AUTH-8.md +184 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
Only notable, user-facing changes. Not every version — see `WORK_ORDERS.md` for the full history.
|
|
4
4
|
|
|
5
|
+
## 3.6.0 — AUTH-8
|
|
6
|
+
|
|
7
|
+
**Behavior change, not purely additive — re-check error displays after adopting this version.**
|
|
8
|
+
`extractErrorInfo` (`src/utils/auth-errors.js`) previously returned the hardcoded, untranslated
|
|
9
|
+
literal `'GENERIC'` as the `.code` for ANY backend error shaped `{"detail": "..."}` — Django's
|
|
10
|
+
generic error format, produced by DRF's `Throttled`/`Http404`/`PermissionDenied` and any unhandled
|
|
11
|
+
`APIException`. This unconditionally overrode every caller's own, properly translated
|
|
12
|
+
`defaultCode` (all 39 call sites in this repo already pass one). Fixed: the `{detail}` fallback now
|
|
13
|
+
returns `code: null`, so `normaliseApiError`'s existing `info.code || defaultCode` correctly falls
|
|
14
|
+
back to the caller's default instead. `.message` still carries the raw `detail` text unchanged.
|
|
15
|
+
|
|
16
|
+
Also closes 7 translation gaps found while auditing every `defaultCode` in use (`Auth.RECOVERY_LOGIN_FAILED`,
|
|
17
|
+
`Auth.USER_LIST_FAILED`, `Auth.USER_DELETE_FAILED`, `Auth.USER_ROLE_UPDATE_FAILED`,
|
|
18
|
+
`Auth.USER_SUPPORT_UPDATE_FAILED`, `Auth.PASSKEY_CANCELLED`, `Auth.MFA_CHALLENGE_FAILED`) — these
|
|
19
|
+
existed as fallback codes in the source but had no translation entry in any of the 4 shipped
|
|
20
|
+
locales, so they would have rendered as a raw, untranslated key even with the code-precedence fix
|
|
21
|
+
above. All 39 in-use `defaultCode` values are now confirmed translated in `de`/`en`/`fr`/`sw`.
|
|
22
|
+
|
|
23
|
+
## 3.5.1 — AUTH-7
|
|
24
|
+
|
|
25
|
+
`loginWithPassword`'s "already authenticated" (409) retry — the re-fetch of the current user when
|
|
26
|
+
allauth reports a still-valid session — no longer lets a second failure (e.g. the account's own
|
|
27
|
+
request quota being exhausted elsewhere) escape as a raw, unnormalised error. It now goes through
|
|
28
|
+
the same `normaliseApiError` path every other login failure already uses, so the caller (any app's
|
|
29
|
+
`LoginPage`) gets a `.code` it can translate instead of a bare exception. The success path (409 +
|
|
30
|
+
successful retry) is byte-identical; no prop/behavior contract changed for any existing caller.
|
|
31
|
+
|
|
5
32
|
## 3.5.0 — MSG-19
|
|
6
33
|
|
|
7
34
|
`DirectMessageLauncher`'s recipient picker is now a searchable MUI `Autocomplete` instead of a
|
package/WORK_ORDERS.md
CHANGED
|
@@ -27,6 +27,8 @@ prefixed ID; never reuse a bare flat number across workstreams.
|
|
|
27
27
|
|
|
28
28
|
| ID | Titel | Beschreibung | Datum | Status | Commit(s) | Notiz |
|
|
29
29
|
|---|---|---|---|---|---|---|
|
|
30
|
+
| AUTH-7 | `loginWithPassword`s 409-Retry gegen Zweitfehlschlag haerten | allauth-headless antwortet mit 409 ("already authenticated"), wenn die Session bereits gueltig ist; `loginWithPassword` ruft dann `fetchCurrentUser()` erneut auf. Schlaegt dieser Retry ebenfalls fehl, wurde der Fehler bisher roh durchgereicht statt normalisiert. Spec [`work-orders/AUTH-7.md`](work-orders/AUTH-7.md) | 2026-09-05 | done | ae7aa89 | **Tier 3** (Shared-Core). Ausgeloest durch `kira`: ein erschoepfter globaler Throttle-Bucket (siehe `kira/KIRA-TRACK-3`) liess den 409-Retry selbst mit 429 fehlschlagen, roh statt normalisiert durchgereicht. Rein additiver Bugfix, kein Prop-/Verhaltensvertrag geaendert. **Implementiert (Codex gpt-5.6-luna):** genau wie geplant, `src/auth/authApi.jsx` + neue `tests/authApiLogin.test.js`. review: codex/gpt-5.6-luna (`reviewer` 4 Lenses + `ui_reviewer` + `sec_reviewer`, ein Hintergrund-Batch) 3 raised · 1 accepted · 2 rejected · worst accepted: defect (aber ausserhalb des Scopes dieser WO, siehe unten) — `envelope`/`regression`/`duplication`/`sec_reviewer`: 0 raised. `tests`-Lens: 2 raised, BEIDE empirisch widerlegt (Fix testweise zurueckgerollt, betroffener Test schlug korrekt fehl — sind keine echten Luecken) → rejected. `ui_reviewer`: 1 raised, akzeptiert als real (empirisch bestaetigt: `normaliseApiError('...', 'Auth.LOGIN_FAILED')` liefert bei einem DRF-`{"detail": "..."}`-Fehler `code: 'GENERIC'` statt des übergebenen Default-Codes, weil `extractErrorInfo` in `src/utils/auth-errors.js` `'GENERIC'` bereits als gesetzten Wert zurückgibt, der `info.code || defaultCode` gewinnt) — **PRE-EXISTING in einer anderen Datei, nicht durch diesen Diff verursacht, an JEDER anderen Call-Site derselben Funktion bereits vorhanden** → out-of-scope fuer AUTH-7, nicht hier gefixt, nachverfolgt als `AUTH-8`. Tests: `npx vitest run tests/authApiLogin.test.js tests/AuthContext.test.jsx tests/authApiRegistration.test.js` 7/7 gruen. **PATCH-Bump 3.5.0 -> 3.5.1** (`package.json` + `CHANGELOG.md`), kein Consumer-Pin-Bump noetig (rein additiv). |
|
|
31
|
+
| AUTH-8 | `extractErrorInfo`s `GENERIC`-Code ueberschreibt jeden `defaultCode` | Bei jedem DRF-Fehler mit `{"detail": "..."}`-Form (u.a. Django `Throttled`/`Http404`/`PermissionDenied` — genau die Form, die den urspruenglichen 429 aus `kira` ausloeste) liefert `extractErrorInfo` (`src/utils/auth-errors.js`) `code: 'GENERIC'`; `normaliseApiError`s `info.code \|\| defaultCode` waehlt dann IMMER `'GENERIC'`, nie den vom Aufrufer uebergebenen `defaultCode` (z. B. `'Auth.LOGIN_FAILED'`). `'GENERIC'` ist an keiner Stelle als i18n-Key registriert — die UI zeigt vermutlich den rohen String statt einer Uebersetzung. **Betrifft JEDE bestehende `normaliseApiError(...)`-Call-Site in diesem Repo**, nicht nur `loginWithPassword`. Spec [`work-orders/AUTH-8.md`](work-orders/AUTH-8.md) | 2026-09-05 | done | d3af6ac | Gefunden von `ui_reviewer` waehrend `AUTH-7`s Review, empirisch bestaetigt (`node --input-type=module -e "..."` gegen die echte Funktion). Operator-Entscheidung 2026-09-05: zuerst angehen. **Implementiert (Codex gpt-5.6-luna):** `code: 'GENERIC'` -> `code: null` im `data.detail`-Zweig, die anderen zwei Zweige unveraendert; neue `tests/authErrors.test.js` (4 Tests). review: codex/gpt-5.6-luna (`reviewer` 4 Lenses + `ui_reviewer` + `sec_reviewer`, ein Hintergrund-Batch) 1 raised · 1 accepted · worst accepted: defect — `envelope`/`regression`/`duplication`/`tests`/`sec_reviewer`: 0 raised (regression bestaetigte zusaetzlich per Grep: keine bestehende Test-Assertion haengt am alten `'GENERIC'`-Wert). `ui_reviewer` fand einen ECHTEN, empirisch bestaetigten Zusatzbefund: 7 von 39 im Code verwendeten `defaultCode`-Schluesseln (`Auth.RECOVERY_LOGIN_FAILED`, `Auth.USER_LIST_FAILED`, `Auth.USER_DELETE_FAILED`, `Auth.USER_ROLE_UPDATE_FAILED`, `Auth.USER_SUPPORT_UPDATE_FAILED`, `Auth.PASSKEY_CANCELLED`, `Auth.MFA_CHALLENGE_FAILED`) waren in KEINER der 4 Sprachen registriert — waeren also auch nach dem Code-Precedence-Fix weiterhin roh statt uebersetzt angezeigt worden. Auf Operator-Wunsch ("zuerst das angehen... stoert mich schon lange, auch dass die Meldungen nicht sauber uebersetzt sind") direkt mitgefixt: alle 7 Keys in `src/i18n/authTranslations.ts` ergaenzt (de/fr/en/sw), Stil an bestehende Nachbar-Keys angelehnt. Erneuter Scan aller 39 Aufrufstellen gegen die Uebersetzungsdatei: 0 fehlend. Tests: `npx vitest run tests/authErrors.test.js tests/authErrorCodeRendering.test.jsx tests/authApiLogin.test.js tests/authApiRegistration.test.js tests/authTranslations.test.js tests/i18nAggregate.test.js` 29/29 gruen. **MINOR-Bump 3.5.1 -> 3.6.0** (nicht PATCH) — aendert beobachtbares Verhalten fuer jeden `{detail}`-foermigen Fehler, keine rein additive Aenderung; Changelog warnt Konsumenten explizit, ihre Fehleranzeige nach dem Pin-Bump gegenzupruefen. |
|
|
30
32
|
| MSG-19 | `DirectMessageLauncher` recipient picker: searchable Autocomplete replaces the flat List | Operator request: the new-DM recipient picker had no search, the same friction the fleet's own "Einfache Bedienung schlägt Vollständigkeit" rule already names ("a picker without search stops working long before the list feels long"). The `List`/`ListItemButton` picker is replaced with an MUI `Autocomplete` bound to the same `selectedId`/`selectedCandidate` state; the component's public prop contract (`candidates`, `scope`, `onOpen`) and its `start()`/scope-resolution logic are unchanged — only the picker's own rendering and selection mechanism changed. | 2026-09-05 | done | 7d99d82 | No WO file — operator-requested, fixed directly. Tier 3 (shared-core, `ui-core-micha`). Only current consumer of this component: jg-ferien (confirmed via a fleet-wide grep for `DirectMessageLauncher`), and jg-ferien's own tests fully mock it, so this change has zero blast radius there until jg-ferien separately bumps its `@micha.bigler/ui-core-micha` pin. Implemented directly in Claude (no Codex dispatch — pure component-level UI change). **review: claude/sonnet · 0 raised** (verified null-safety of `isOptionEqualToValue`/`getOptionLabel` against MUI's own `useAutocomplete` source, confirmed the rewritten test file preserves every one of the original 7 cases plus one new filtering case, confirmed i18n completeness) + **ui_reviewer: claude/sonnet · 4 raised · 4 accepted · worst accepted: defect** — (U1: the new "typed query matches nothing" state had no `noOptionsText` and would have shown MUI's hardcoded, untranslated English default — added `MessagingDirect.NO_MATCHES` in all 4 languages plus a test; U2: the Autocomplete was missing `fullWidth`, breaking from this repo's own one other Autocomplete-in-Dialog convention (`SupportRecoveryRequestsTab.jsx`) and regressing from the old List's full-width block layout — added; U3: reused `MessagingDirect.CANDIDATES`'s old plural-noun wording ("Recipients") as a visible single-select field label read awkwardly — reworded to an instruction-style label ("Select recipient") matching the sibling component's own convention, in all 4 languages; U4 (inferred, not rendered/verified): `autoFocus` on the TextField combined with the dialog's `fullScreen` mobile breakpoint could open the virtual keyboard unprompted — removed `autoFocus` entirely, since MUI's own `Dialog` already focuses the first focusable descendant on open by design, making it redundant). Two additional self-found nits fixed post-review: a defensive null-guard added to `candidateLabel` (matching the sibling `getUserOptionLabel` convention, even though confirmed unreachable under MUI's current calling contract) and a one-line update to `docs/messaging-deviations.md` item 18 (stale reference to "displays... candidate list" not mentioning the new search). Tests: `messagingDirectMessageLauncher.test.jsx` rewritten (9/9 green, up from 7 — the rewrite required discovering and documenting a real jsdom/RTL gotcha: MUI's `Autocomplete` only reacts to a typed value once the input is genuinely `.focus()`-ed, not merely `mouseDown`-ed) + `i18nAggregate.test.js` (4/4) both green; `tsc -p tsconfig.build.json` clean. Version bumped 3.4.0 → 3.5.0 (minor — additive UI capability, no prop-contract change) via the repo's `publish.yml` (push-to-`main` + version bump). jg-ferien bumps its pin separately, in its own repo, once this lands on npm. |
|
|
31
33
|
| AUTH-6 | `QrSignupManager` additive `registrationContext`/`defaultMaxRedemptions` props | jg-ferien's `JG-MEM-1` (department QR-join) needs a per-department signup context and a high, poster-scale redemption default — `QrSignupManager` only exposed `enabled`/`expiryDays`. Two new optional props added, both defaulting to today's exact behaviour: `registrationContext` (merged into the generated token's `registration_context`) and `defaultMaxRedemptions` (overrides the low internal default). | 2026-08-28 | done | 1749c09 | No WO file — operator-requested, fixed directly, same convention as AUTH-3. Tier 3 (shared-core). Implemented by `codex exec` (jg-ferien's JG-MEM-1 dispatch touched this repo additively per that WO's own allowance), left uncommitted for the Orchestrator's own review before landing here. **review: claude/sonnet · 2 raised · 2 accepted · worst accepted: defect** — (R1: the new context-change effect also calls `setMaxRedemptions`, and with `maxRedemptions` in that same effect's dependency array, a context change after the admin had typed a custom redemption count fired a second, redundant `createSignupQr` call — fixed by reading the current value through a ref instead of a reactive dependency, so `setMaxRedemptions` no longer re-triggers the effect that just set it; R2: the one new test only covered the happy path on a fresh mount, with no test proving the existing `AccountPage` call (`enabled`/`expiryDays` only) stays byte-for-byte unaffected, and no test that would have caught R1 — two regression tests added: a bare old-prop-shape call asserting the exact legacy payload + single call, and a context-change-after-custom-redemptions call asserting exactly one `createSignupQr` invocation). Confirmed backward compatible against the one existing consumer, `AccountPage.jsx:369-372` (unaffected — it never passes the new props). Tests: `QrSignupManager.registrationContext.test.jsx` (3/3, incl. the two new regression cases) + `AccountPage.test.jsx` (4/4) + `SignUpPage.test.jsx` (10/10) all green — 17/17 total across the three files touching this component. No `ui_reviewer`/`sec_reviewer` (pure prop/effect-timing change, no visual surface, no auth-enforcement logic — the actual token validation stays entirely server-side in `django-core-micha`, already reviewed under `JG-MEM-1`). **Rebased onto a concurrent session's `UCM-CHART-17`/`-18` pushes (3.3.0/3.3.1 already published) — republished as 3.4.0 instead of the originally-committed 3.3.0** (minor — new capability, not a fix), via the repo's `publish.yml` (push-to-`main` + version bump). jg-ferien bumps its pin separately once this lands on npm. |
|
|
32
34
|
| UCM-CHART-18 | The PNG export shows what the chart says, not what the user can click | Two defects, one file, a few lines apart -- both raised by `HRAM-CHT-7`'s independent `ui_reviewer` (2026-08-25) and confirmed, partly by reading and partly live. **(1) Interactive controls are in the image.** Since 3.3.0 the PNG rasterises `ChartFrame`'s whole `chartRef` box, and `chartRef` wraps `children` only -- not `toolbar`, `controls` or `meta` -- so anything a panel renders as children is now in the file: hram's `StructuralReachabilityPanel:468` footer `IconButton` (always visible, so **every** PNG of that panel carries a button), `AllocationPerformancePanel:536,548` and `OptimizationResultsPanel:1120,1141`. **(2) `transform` is not inlined** -- neither property list (`:32-36`, `:44-52`) has it, so `OptimizationResultsPanel:1107`'s rotated legend swatch exports as a **square instead of a diamond**. **Fixed here rather than in the app for two reasons:** it is a property of the element kind, not of each panel -- an exported image should show what the chart communicates, not what can be clicked -- and the alternative (moving controls into `ChartFrame`'s `controls`/`toolbar` slots) would **undo a deliberate design decision**, since `HRAM-RES-30` put that info affordance in the footer beside the caveat line. After this, hram needs **only a pin bump**. Full order [`work-orders/UCM-CHART-18.md`](work-orders/UCM-CHART-18.md) | 2026-08-25 | done | `5f34a2b` | **Tier 3** -- shared-core. `ui_reviewer` (what belongs in an exported image, and the MUI-legend counter-check) + `reviewer` (the ordering trap, the `transform` double-apply question); no `sec_reviewer`. **THE single most likely way to get this wrong:** `inlineComputedStyles` walks source and clone **in lockstep by index** (`:70-77`), so removing controls from the clone **before** it shifts every subsequent index and elements silently receive their neighbours' styles -- an export that looks plausible and is wrong everywhere after the first removal. Removal happens **after** the style walk, and the code comment must say so. **Counter-check that is not optional:** what disappears must be operation, not content -- a consumer whose legend items are `<button>`s (MUI's own legend can be interactive) would lose its legend, and the affected hram panels all pass `hideLegend` so they are **not** the test case; verify against a chart that keeps MUI's own legend, and if it vanishes that changes the approach. Also: `transform` may **double-apply** on SVG nodes that carry it as a presentation attribute the clone already keeps -- check `xLabels="angled"` before assuming it is inert. **Release: patch -> 3.3.1** -- this repairs behaviour 3.3.0 itself introduced rather than adding capability, the same reasoning `UCM-CHART-16` went out on, while `-15`/`-17` were minors because they changed what a consumer gets on purpose. Gate is **opening the files**, as for `UCM-CHART-17`: both defects were invisible to every test and visible immediately in an exported image. **Implementation:** `.claude/models.local.json` has no local override (defaults apply) -- `implementation.runtime` is `claude`/`sonnet`, implemented directly, no Codex dispatch. The lockstep-index ordering was respected from the start (`inlineComputedStyles` before `stripInteractiveControls`), `transform`/`transform-origin` added to the shared property list. **Verification (real Chrome, via the dev harness's module graph, no probe entry ever committed):** all four WO cases confirmed with hard evidence, not visual impression -- (1) a control in `children` (`IconButton` beside static text): captured the PNG path's actual intermediate SVG, confirmed no `<button` tag/text while the sibling and chart survive. (2) MUI's own legend (non-interactive, two series): fully intact, both labels present. (3) a rotated swatch: pixel-sampled the real rasterised PNG on a real `<canvas>` -- bounding-box corner reads background white, centre reads the swatch's own colour, i.e. a genuine diamond. (4) `xLabels="angled"`: the exported tick carries both the original `transform` XML attribute (kept by `cloneNode`) and the newly-inlined computed matrix; the matrix values mathematically match a SINGLE -45° rotation (`cos(-45°)=0.707107`), not a doubled one -- confirmed the inline `style` declaration wins over the presentation attribute rather than composing with it. 89 affected chart tests green (14 in `exportChart.test.js`), `tsc` clean. **Review: codex/gpt-5.6-luna (`reviewer` + `ui_reviewer`, concurrent) · 5 raised · 3 accepted · worst accepted: high (interactive-legend content loss)** (id not divisible by 5, no rotation configured). **Both reviewers independently converged on the SAME defect**, exactly the risk the WO's own Envelope named and required a live counter-check for: the initial fix REMOVED (not unwrapped) MUI's own interactive legend items (`onItemClick`/`toggleVisibilityOnClick` renders each item as `<button role="button" class="...MuiChartsLegend-series...">`), deleting the swatch+label along with the click handler -- the WO's own words, "if it vanishes, that is a finding that changes the approach, not a detail to paper over." FIXED: `MuiChartsLegend-series` matches are now UNWRAPPED (replaced with a plain `<span>` carrying the same class/style/children) instead of removed; re-verified live against an ACTUAL interactive legend (`slotProps={{legend:{toggleVisibilityOnClick:true}}}`, confirmed the live items render as real `<BUTTON>` tags) -- export contains zero `<button` tags, `MuiChartsLegend-series` and both series labels survive intact. `reviewer` additionally found: the interactive-control selector missed `a[href]`/`[role="link"]` (accepted, added -- also corrects the CHANGELOG's own "drill-down links" claim, which wasn't true until this fix); and the original ordering test's button was the LAST descendant, so a wrong call order wouldn't have shifted any node the test actually asserted on -- content-presence checks can't observe a style misattribution, only a wrong STYLE VALUE can (accepted, rewrote with a distinctively-styled trailing sibling, confirmed by temporarily reverting the call order that the new assertion genuinely fails). Two findings rejected as decoration/informational only (transform double-apply already reasoned through correctly by both reviewers; a jsdom-literal vs. browser-matrix distinction in the unit test, already covered by the separate real-browser verification track). |
|
package/dist/auth/authApi.js
CHANGED
|
@@ -107,8 +107,13 @@ export async function loginWithPassword(email, password) {
|
|
|
107
107
|
}
|
|
108
108
|
// 409 = Already logged in
|
|
109
109
|
if (status === 409) {
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
try {
|
|
111
|
+
const user = await fetchCurrentUser();
|
|
112
|
+
return { user, needsMfa: false };
|
|
113
|
+
}
|
|
114
|
+
catch (retryError) {
|
|
115
|
+
throw normaliseApiError(retryError, 'Auth.LOGIN_FAILED');
|
|
116
|
+
}
|
|
112
117
|
}
|
|
113
118
|
throw normaliseApiError(error, 'Auth.LOGIN_FAILED');
|
|
114
119
|
}
|
|
@@ -204,6 +204,12 @@ export const authTranslations = {
|
|
|
204
204
|
"en": "Passkey authentication was cancelled.",
|
|
205
205
|
"sw": "Uthibitishaji wa Passkey umeghairiwa."
|
|
206
206
|
},
|
|
207
|
+
"Auth.PASSKEY_CANCELLED": {
|
|
208
|
+
"de": "Der Passkey-Vorgang wurde abgebrochen.",
|
|
209
|
+
"fr": "L'opération avec le passkey a été annulée.",
|
|
210
|
+
"en": "The passkey operation was cancelled.",
|
|
211
|
+
"sw": "Operesheni ya Passkey imeghairiwa."
|
|
212
|
+
},
|
|
207
213
|
"Auth.PASSKEY_LIST_FAILED": {
|
|
208
214
|
"de": "Die gespeicherten Passkeys konnten nicht geladen werden.",
|
|
209
215
|
"fr": "Les passkeys enregistrés n'ont pas pu être chargés.",
|
|
@@ -228,6 +234,12 @@ export const authTranslations = {
|
|
|
228
234
|
"en": "Two-factor authentication failed.",
|
|
229
235
|
"sw": "Uthibitishaji wa hatua mbili umeshindwa."
|
|
230
236
|
},
|
|
237
|
+
"Auth.MFA_CHALLENGE_FAILED": {
|
|
238
|
+
"de": "Die Zwei-Faktor-Challenge konnte nicht angefordert werden.",
|
|
239
|
+
"fr": "Le défi d'authentification à deux facteurs n'a pas pu être demandé.",
|
|
240
|
+
"en": "The two-factor challenge could not be requested.",
|
|
241
|
+
"sw": "Changamoto ya uthibitishaji wa hatua mbili haikuweza kuombwa."
|
|
242
|
+
},
|
|
231
243
|
"Auth.TOTP_REQUEST_FAILED": {
|
|
232
244
|
"de": "Die TOTP-Einrichtung konnte nicht gestartet werden.",
|
|
233
245
|
"fr": "La configuration TOTP n'a pas pu être démarrée.",
|
|
@@ -258,6 +270,12 @@ export const authTranslations = {
|
|
|
258
270
|
"en": "Recovery codes could not be regenerated.",
|
|
259
271
|
"sw": "Misimbo ya urejesho haikuweza kuundwa upya."
|
|
260
272
|
},
|
|
273
|
+
"Auth.RECOVERY_LOGIN_FAILED": {
|
|
274
|
+
"de": "Die Anmeldung über den Wiederherstellungslink ist fehlgeschlagen.",
|
|
275
|
+
"fr": "La connexion via le lien de récupération a échoué.",
|
|
276
|
+
"en": "Recovery login failed.",
|
|
277
|
+
"sw": "Kuingia kwa njia ya urejeshaji kumeshindwa."
|
|
278
|
+
},
|
|
261
279
|
"Auth.PAGE_CHANGE_PASSWORD_SUBTITLE": {
|
|
262
280
|
"de": "Geben Sie Ihr aktuelles und ein neues Passwort ein.",
|
|
263
281
|
"fr": "Saisissez votre mot de passe actuel et un nouveau mot de passe.",
|
|
@@ -1794,6 +1812,30 @@ export const authTranslations = {
|
|
|
1794
1812
|
"en": "An account already exists for this email address. Please log in.",
|
|
1795
1813
|
"sw": "Akaunti tayari ipo kwa anwani hii ya barua pepe. Tafadhali ingia."
|
|
1796
1814
|
},
|
|
1815
|
+
"Auth.USER_LIST_FAILED": {
|
|
1816
|
+
"de": "Die Benutzerliste konnte nicht geladen werden.",
|
|
1817
|
+
"fr": "La liste des utilisateurs n'a pas pu être chargée.",
|
|
1818
|
+
"en": "The user list could not be loaded.",
|
|
1819
|
+
"sw": "Orodha ya watumiaji haikuweza kupakiwa."
|
|
1820
|
+
},
|
|
1821
|
+
"Auth.USER_DELETE_FAILED": {
|
|
1822
|
+
"de": "Der Benutzer konnte nicht gelöscht werden.",
|
|
1823
|
+
"fr": "L'utilisateur n'a pas pu être supprimé.",
|
|
1824
|
+
"en": "The user could not be deleted.",
|
|
1825
|
+
"sw": "Mtumiaji hakuweza kufutwa."
|
|
1826
|
+
},
|
|
1827
|
+
"Auth.USER_ROLE_UPDATE_FAILED": {
|
|
1828
|
+
"de": "Die Rolle des Benutzers konnte nicht aktualisiert werden.",
|
|
1829
|
+
"fr": "Le rôle de l'utilisateur n'a pas pu être mis à jour.",
|
|
1830
|
+
"en": "The user's role could not be updated.",
|
|
1831
|
+
"sw": "Jukumu la mtumiaji halikuweza kusasishwa."
|
|
1832
|
+
},
|
|
1833
|
+
"Auth.USER_SUPPORT_UPDATE_FAILED": {
|
|
1834
|
+
"de": "Der Support-Status des Benutzers konnte nicht aktualisiert werden.",
|
|
1835
|
+
"fr": "Le statut de support de l'utilisateur n'a pas pu être mis à jour.",
|
|
1836
|
+
"en": "The user's support status could not be updated.",
|
|
1837
|
+
"sw": "Hali ya usaidizi ya mtumiaji haikuweza kusasishwa."
|
|
1838
|
+
},
|
|
1797
1839
|
"Auth.SIGNUP_QR_MAX_REDEMPTIONS_LABEL": {
|
|
1798
1840
|
"de": "Max. Verwendungen",
|
|
1799
1841
|
"fr": "Utilisations max.",
|
|
@@ -17,7 +17,7 @@ export function extractErrorInfo(error) {
|
|
|
17
17
|
}
|
|
18
18
|
// Fallback for generic Django errors
|
|
19
19
|
if (typeof data.detail === 'string') {
|
|
20
|
-
return { status, code:
|
|
20
|
+
return { status, code: null, message: data.detail, raw: data };
|
|
21
21
|
}
|
|
22
22
|
return { status, code: null, message: null, raw: data };
|
|
23
23
|
}
|
package/package.json
CHANGED
package/src/auth/authApi.jsx
CHANGED
|
@@ -114,8 +114,12 @@ export async function loginWithPassword(email, password) {
|
|
|
114
114
|
|
|
115
115
|
// 409 = Already logged in
|
|
116
116
|
if (status === 409) {
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
try {
|
|
118
|
+
const user = await fetchCurrentUser();
|
|
119
|
+
return { user, needsMfa: false };
|
|
120
|
+
} catch (retryError) {
|
|
121
|
+
throw normaliseApiError(retryError, 'Auth.LOGIN_FAILED');
|
|
122
|
+
}
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
throw normaliseApiError(error, 'Auth.LOGIN_FAILED');
|
|
@@ -231,6 +231,13 @@ export const authTranslations = {
|
|
|
231
231
|
"sw": "Uthibitishaji wa Passkey umeghairiwa."
|
|
232
232
|
},
|
|
233
233
|
|
|
234
|
+
"Auth.PASSKEY_CANCELLED": {
|
|
235
|
+
"de": "Der Passkey-Vorgang wurde abgebrochen.",
|
|
236
|
+
"fr": "L'opération avec le passkey a été annulée.",
|
|
237
|
+
"en": "The passkey operation was cancelled.",
|
|
238
|
+
"sw": "Operesheni ya Passkey imeghairiwa."
|
|
239
|
+
},
|
|
240
|
+
|
|
234
241
|
"Auth.PASSKEY_LIST_FAILED": {
|
|
235
242
|
"de": "Die gespeicherten Passkeys konnten nicht geladen werden.",
|
|
236
243
|
"fr": "Les passkeys enregistrés n'ont pas pu être chargés.",
|
|
@@ -259,6 +266,13 @@ export const authTranslations = {
|
|
|
259
266
|
"sw": "Uthibitishaji wa hatua mbili umeshindwa."
|
|
260
267
|
},
|
|
261
268
|
|
|
269
|
+
"Auth.MFA_CHALLENGE_FAILED": {
|
|
270
|
+
"de": "Die Zwei-Faktor-Challenge konnte nicht angefordert werden.",
|
|
271
|
+
"fr": "Le défi d'authentification à deux facteurs n'a pas pu être demandé.",
|
|
272
|
+
"en": "The two-factor challenge could not be requested.",
|
|
273
|
+
"sw": "Changamoto ya uthibitishaji wa hatua mbili haikuweza kuombwa."
|
|
274
|
+
},
|
|
275
|
+
|
|
262
276
|
"Auth.TOTP_REQUEST_FAILED": {
|
|
263
277
|
"de": "Die TOTP-Einrichtung konnte nicht gestartet werden.",
|
|
264
278
|
"fr": "La configuration TOTP n'a pas pu être démarrée.",
|
|
@@ -293,6 +307,13 @@ export const authTranslations = {
|
|
|
293
307
|
"en": "Recovery codes could not be regenerated.",
|
|
294
308
|
"sw": "Misimbo ya urejesho haikuweza kuundwa upya."
|
|
295
309
|
},
|
|
310
|
+
|
|
311
|
+
"Auth.RECOVERY_LOGIN_FAILED": {
|
|
312
|
+
"de": "Die Anmeldung über den Wiederherstellungslink ist fehlgeschlagen.",
|
|
313
|
+
"fr": "La connexion via le lien de récupération a échoué.",
|
|
314
|
+
"en": "Recovery login failed.",
|
|
315
|
+
"sw": "Kuingia kwa njia ya urejeshaji kumeshindwa."
|
|
316
|
+
},
|
|
296
317
|
"Auth.PAGE_CHANGE_PASSWORD_SUBTITLE": {
|
|
297
318
|
"de": "Geben Sie Ihr aktuelles und ein neues Passwort ein.",
|
|
298
319
|
"fr": "Saisissez votre mot de passe actuel et un nouveau mot de passe.",
|
|
@@ -1842,6 +1863,31 @@ export const authTranslations = {
|
|
|
1842
1863
|
"en": "An account already exists for this email address. Please log in.",
|
|
1843
1864
|
"sw": "Akaunti tayari ipo kwa anwani hii ya barua pepe. Tafadhali ingia."
|
|
1844
1865
|
},
|
|
1866
|
+
|
|
1867
|
+
"Auth.USER_LIST_FAILED": {
|
|
1868
|
+
"de": "Die Benutzerliste konnte nicht geladen werden.",
|
|
1869
|
+
"fr": "La liste des utilisateurs n'a pas pu être chargée.",
|
|
1870
|
+
"en": "The user list could not be loaded.",
|
|
1871
|
+
"sw": "Orodha ya watumiaji haikuweza kupakiwa."
|
|
1872
|
+
},
|
|
1873
|
+
"Auth.USER_DELETE_FAILED": {
|
|
1874
|
+
"de": "Der Benutzer konnte nicht gelöscht werden.",
|
|
1875
|
+
"fr": "L'utilisateur n'a pas pu être supprimé.",
|
|
1876
|
+
"en": "The user could not be deleted.",
|
|
1877
|
+
"sw": "Mtumiaji hakuweza kufutwa."
|
|
1878
|
+
},
|
|
1879
|
+
"Auth.USER_ROLE_UPDATE_FAILED": {
|
|
1880
|
+
"de": "Die Rolle des Benutzers konnte nicht aktualisiert werden.",
|
|
1881
|
+
"fr": "Le rôle de l'utilisateur n'a pas pu être mis à jour.",
|
|
1882
|
+
"en": "The user's role could not be updated.",
|
|
1883
|
+
"sw": "Jukumu la mtumiaji halikuweza kusasishwa."
|
|
1884
|
+
},
|
|
1885
|
+
"Auth.USER_SUPPORT_UPDATE_FAILED": {
|
|
1886
|
+
"de": "Der Support-Status des Benutzers konnte nicht aktualisiert werden.",
|
|
1887
|
+
"fr": "Le statut de support de l'utilisateur n'a pas pu être mis à jour.",
|
|
1888
|
+
"en": "The user's support status could not be updated.",
|
|
1889
|
+
"sw": "Hali ya usaidizi ya mtumiaji haikuweza kusasishwa."
|
|
1890
|
+
},
|
|
1845
1891
|
"Auth.SIGNUP_QR_MAX_REDEMPTIONS_LABEL": {
|
|
1846
1892
|
"de": "Max. Verwendungen",
|
|
1847
1893
|
"fr": "Utilisations max.",
|
package/src/utils/auth-errors.js
CHANGED
|
@@ -20,7 +20,7 @@ export function extractErrorInfo(error) {
|
|
|
20
20
|
|
|
21
21
|
// Fallback for generic Django errors
|
|
22
22
|
if (typeof data.detail === 'string') {
|
|
23
|
-
return { status, code:
|
|
23
|
+
return { status, code: null, message: data.detail, raw: data };
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
return { status, code: null, message: null, raw: data };
|
|
@@ -36,4 +36,4 @@ export function normaliseApiError(error, defaultCode = 'Auth.GENERIC_ERROR') {
|
|
|
36
36
|
err.status = info.status;
|
|
37
37
|
err.raw = info.raw;
|
|
38
38
|
return err;
|
|
39
|
-
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const client = vi.hoisted(() => ({
|
|
4
|
+
post: vi.fn(),
|
|
5
|
+
get: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock('../src/auth/apiClient', () => ({ default: client }));
|
|
9
|
+
|
|
10
|
+
import { loginWithPassword } from '../src/auth/authApi';
|
|
11
|
+
|
|
12
|
+
describe('loginWithPassword', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.clearAllMocks();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns the current user when login reports an existing session', async () => {
|
|
18
|
+
const user = { id: 1, email: 'ada@example.com' };
|
|
19
|
+
client.post.mockRejectedValue({ response: { status: 409 } });
|
|
20
|
+
client.get.mockResolvedValue({ data: user });
|
|
21
|
+
|
|
22
|
+
await expect(loginWithPassword('ada@example.com', 'password')).resolves.toEqual({
|
|
23
|
+
user,
|
|
24
|
+
needsMfa: false,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('normalizes a failure while retrying the current user after a 409', async () => {
|
|
29
|
+
const retryError = {
|
|
30
|
+
response: { status: 429, data: { detail: 'Too many requests' } },
|
|
31
|
+
};
|
|
32
|
+
client.post.mockRejectedValue({ response: { status: 409 } });
|
|
33
|
+
client.get.mockRejectedValue(retryError);
|
|
34
|
+
|
|
35
|
+
await expect(loginWithPassword('ada@example.com', 'password')).rejects.toSatisfy((error) => {
|
|
36
|
+
expect(error).toBeInstanceOf(Error);
|
|
37
|
+
expect(error.code).toBeDefined();
|
|
38
|
+
expect(error).not.toBe(retryError);
|
|
39
|
+
return true;
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { extractErrorInfo, normaliseApiError } from '../src/utils/auth-errors.js';
|
|
3
|
+
|
|
4
|
+
describe('auth error normalisation', () => {
|
|
5
|
+
it('uses the caller default for generic detail errors while preserving the detail message', () => {
|
|
6
|
+
const responseError = {
|
|
7
|
+
response: { status: 429, data: { detail: 'Die Anfrage wurde gedrosselt...' } },
|
|
8
|
+
};
|
|
9
|
+
const info = extractErrorInfo(responseError);
|
|
10
|
+
const error = normaliseApiError(responseError, 'Auth.LOGIN_FAILED');
|
|
11
|
+
|
|
12
|
+
expect(info.code).toBeNull();
|
|
13
|
+
expect(error.code).toBe('Auth.LOGIN_FAILED');
|
|
14
|
+
expect(error.message).toBe('Die Anfrage wurde gedrosselt...');
|
|
15
|
+
expect(error.status).toBe(429);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('uses its namespaced default when no caller default is provided', () => {
|
|
19
|
+
const error = normaliseApiError({
|
|
20
|
+
response: { status: 429, data: { detail: 'Die Anfrage wurde gedrosselt...' } },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(error.code).toBe('Auth.GENERIC_ERROR');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('preserves a backend code from the errors array', () => {
|
|
27
|
+
const error = normaliseApiError(
|
|
28
|
+
{ response: { data: { errors: [{ code: 'enter_current_password', message: 'x' }] } } },
|
|
29
|
+
'Auth.PROFILE_UPDATE_FAILED',
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
expect(error.code).toBe('enter_current_password');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('preserves a backend code from the response data', () => {
|
|
36
|
+
const error = normaliseApiError(
|
|
37
|
+
{ response: { data: { code: 'invalid_credentials' } } },
|
|
38
|
+
'Auth.LOGIN_FAILED',
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
expect(error.code).toBe('invalid_credentials');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# AUTH-7 — `loginWithPassword`'s "already authenticated" retry must not crash raw on a second failure
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# A. Envelope — authored by the Expertenchat
|
|
6
|
+
|
|
7
|
+
## Goal & expected outcome
|
|
8
|
+
- Ziel: allauth-headless antwortet auf `POST /auth/login` mit `409` ("Conflict"), wenn bereits
|
|
9
|
+
eine gültige Session existiert (die Session ist real gültig — kein Fehlerfall an sich).
|
|
10
|
+
`loginWithPassword` (`src/auth/authApi.jsx:97-123`) behandelt das korrekt, indem es dann
|
|
11
|
+
`fetchCurrentUser()` erneut aufruft, um den bereits eingeloggten User zurückzugeben. Schlägt
|
|
12
|
+
DIESER Retry-Call aber ebenfalls fehl (beobachtet 2026-09-05: ein anderer, unabhängig
|
|
13
|
+
überlasteter Endpunkt hatte den globalen `user`-Throttle-Bucket geleert, sodass auch
|
|
14
|
+
`/api/users/current/` mit 429 antwortete), wird der Fehler ungefangen durchgereicht — kein
|
|
15
|
+
`.code`, keine Übersetzung, ein rohes Error-Objekt statt eines normalen Login-Fehlers.
|
|
16
|
+
- Expected outcome: schlägt der 409-Retry fehl, bekommt der Aufrufer (`LoginPage.jsx:219-220`
|
|
17
|
+
liest `err.code`) einen normalisierten Fehler mit `.code` — dieselbe Fehlerform wie jeder andere
|
|
18
|
+
Login-Fehlerpfad in dieser Funktion, kein Sonderfall.
|
|
19
|
+
|
|
20
|
+
## Scope + non-goals
|
|
21
|
+
- In scope: ausschliesslich der 409-Zweig in `loginWithPassword`
|
|
22
|
+
(`src/auth/authApi.jsx:116-119`) — den bereits vorhandenen `fetchCurrentUser()`-Retry-Call in
|
|
23
|
+
ein eigenes try/catch nehmen und einen Fehlschlag über `normaliseApiError` weiterwerfen, exakt
|
|
24
|
+
wie jeder andere Fehlerpfad in dieser Funktion (Zeile 121) es bereits tut.
|
|
25
|
+
- Explizite Non-Goals / nicht anfassen: der **Erfolgsfall** dieses Zweigs (409 + erfolgreicher
|
|
26
|
+
Retry → `{ user, needsMfa: false }`) bleibt bytegleich. `AuthContext.jsx`s Bootstrap-Catch-Block
|
|
27
|
+
(Zeile 107-109) wird NICHT geändert — geprüft: `user` ist beim ersten Mount ohnehin bereits
|
|
28
|
+
`null`, eine Fallunterscheidung nach Statuscode hätte dort unter keinem real erreichbaren
|
|
29
|
+
Codepfad einen beobachtbaren Effekt (der Effect läuft genau einmal pro Seitenaufruf), wäre also
|
|
30
|
+
Änderung ohne Wirkung in einer Shared-Core-Datei — bewusst ausgelassen. Keine neue Kontext-Prop,
|
|
31
|
+
keine Änderung an einer konsumierenden App (`kira` o.a.) — der Fix ist rein additiv/intern zu
|
|
32
|
+
`authApi.jsx`. `package.json`s Versionsfeld wird als Teil dieser WO angepasst (PATCH-Bump, siehe
|
|
33
|
+
Teil C) — das ist Prozess/Publish-Mechanik dieses Repos, kein Scope-Zusatz am eigentlichen Fix.
|
|
34
|
+
|
|
35
|
+
## Tier · precondition / gate
|
|
36
|
+
- Tier: 3 — Änderung INNERHALB von Shared-Core (`ui-core-micha`), unabhängig vom Umfang (AGENTS.md
|
|
37
|
+
→ Tiering-Tabelle). Kein Precondition/Gate; die Änderung ist rein additiv (ein neuer catch um
|
|
38
|
+
einen bestehenden Call), kein Breaking Change, kein Versions-Bump nötig, keine konsumierende App
|
|
39
|
+
muss etwas tun.
|
|
40
|
+
|
|
41
|
+
## Risks
|
|
42
|
+
- Der Erfolgspfad (409 + Retry gelingt) darf sich nicht ändern — sonst bricht der bestehende
|
|
43
|
+
"bereits eingeloggt"-Login-Flow für ALLE Apps, die `ui-core-micha` konsumieren.
|
|
44
|
+
- Der neue catch darf den `normaliseApiError`-Fehler nicht verschlucken oder verfälschen — die
|
|
45
|
+
aufrufende `LoginPage.jsx` liest `err.code` für die i18n-Anzeige.
|
|
46
|
+
|
|
47
|
+
## Required tests to WRITE
|
|
48
|
+
- Neue Datei `tests/authApiLogin.test.js` (Muster: `tests/authApiRegistration.test.js` — `apiClient`
|
|
49
|
+
gemockt via `vi.mock`, `client.post`/`client.get` direkt gestubbt, kein React/Testing-Library
|
|
50
|
+
nötig, `loginWithPassword` direkt importiert und aufgerufen):
|
|
51
|
+
1. `post` rejects mit `{ response: { status: 409 } }`, `get` (der `fetchCurrentUser`-Retry)
|
|
52
|
+
resolved mit `{ data: { id: 1, ... } }` → `loginWithPassword` liefert
|
|
53
|
+
`{ user: {...}, needsMfa: false }` (dokumentiert den unveränderten Erfolgspfad).
|
|
54
|
+
2. `post` rejects mit `{ response: { status: 409 } }`, `get` (der Retry) rejects zusätzlich mit
|
|
55
|
+
`{ response: { status: 429, data: { detail: '...' } } }` → `loginWithPassword` wirft einen
|
|
56
|
+
Error mit einem gesetzten `.code` (nicht `undefined`, nicht das rohe Axios-Error-Objekt).
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
# B. Implementation map — filled by the Orchestrator — ADDRESSED TO THE IMPLEMENTER
|
|
61
|
+
|
|
62
|
+
## Context package
|
|
63
|
+
|
|
64
|
+
**Named file to change:** `src/auth/authApi.jsx:97-123` (`loginWithPassword`), konkret nur
|
|
65
|
+
Zeilen 115-119:
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
// 409 = Already logged in
|
|
69
|
+
if (status === 409) {
|
|
70
|
+
const user = await fetchCurrentUser();
|
|
71
|
+
return { user, needsMfa: false };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
throw normaliseApiError(error, 'Auth.LOGIN_FAILED');
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Ändere zu (Retry in eigenes try/catch, Fehlschlag desselben `normaliseApiError`-Wegs wie jeder
|
|
78
|
+
andere Fehlerpfad dieser Funktion):
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
// 409 = Already logged in
|
|
82
|
+
if (status === 409) {
|
|
83
|
+
try {
|
|
84
|
+
const user = await fetchCurrentUser();
|
|
85
|
+
return { user, needsMfa: false };
|
|
86
|
+
} catch (retryError) {
|
|
87
|
+
throw normaliseApiError(retryError, 'Auth.LOGIN_FAILED');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
throw normaliseApiError(error, 'Auth.LOGIN_FAILED');
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`normaliseApiError` ist bereits oben in der Datei importiert (Zeile 3, aus `../utils/auth-errors`)
|
|
95
|
+
— keine neue Abhängigkeit.
|
|
96
|
+
|
|
97
|
+
**Invariants / Pitfalls:**
|
|
98
|
+
- `fetchCurrentUser()` (Zeile 16-24, gleiche Datei) wirft bei jedem Fehler das **rohe** Axios-Error
|
|
99
|
+
weiter (kein eigener catch dort) — das ist bereits der Zustand, den dieser Fix abfängt; nicht
|
|
100
|
+
`fetchCurrentUser()` selbst ändern.
|
|
101
|
+
- Der äussere `catch (error)`-Block (Zeile 103) bleibt unverändert; nur der 409-Zweig innerhalb
|
|
102
|
+
bekommt einen zusätzlichen inneren try/catch. Die MFA-Prüfung (Zeile 108-112) und der finale
|
|
103
|
+
`throw normaliseApiError(error, ...)` (Zeile 121, für den ursprünglichen `error`, NICHT den
|
|
104
|
+
`retryError`) bleiben unverändert.
|
|
105
|
+
- `normaliseApiError`s zweites Argument ist ein Default-Code, kein spezifischer 429-Code — es gibt
|
|
106
|
+
keinen eigenen i18n-Key für "Login gerade nicht möglich, Konto ist gedrosselt"; Wiederverwendung
|
|
107
|
+
von `'Auth.LOGIN_FAILED'` (bereits vorhandener Key) ist beabsichtigt, kein neuer i18n-Key nötig
|
|
108
|
+
in irgendeiner konsumierenden App.
|
|
109
|
+
|
|
110
|
+
Directive: Arbeite ausschliesslich aus diesem Kontext-Paket; öffne nur die genannte Datei plus die
|
|
111
|
+
genannte Testdatei-Vorlage zur Verifikation. Kein breites Erkunden im restlichen `src/auth/`.
|
|
112
|
+
|
|
113
|
+
## Target repo working directory (absolute)
|
|
114
|
+
|
|
115
|
+
`C:\Users\Micha Bigler\Documents\webapps\ui-core-micha`
|
|
116
|
+
|
|
117
|
+
## Preamble — REQUIRED, part of this file
|
|
118
|
+
|
|
119
|
+
> The text above is the COMPLETE spec — the committed WO file's content, not a plan to refine;
|
|
120
|
+
> there is no separate plan file. Read the nearest `AGENTS.md`, the relevant
|
|
121
|
+
> `.codex/skills/<role>/SKILL.md`, and the app `MEMORY.md` ONLY for conventions. Stay in scope; do
|
|
122
|
+
> not touch auth/permissions/deps/schema/CI unless the spec says so; do not update `MEMORY.md`.
|
|
123
|
+
> **Do NOT edit `WORK_ORDERS.md`** — the register row and the review verdicts are the
|
|
124
|
+
> orchestrator's alone. **Your tools are for editing source and test files and for running the
|
|
125
|
+
> tests you wrote — nothing else.** Do NOT install dependencies, touch a lockfile, run a package
|
|
126
|
+
> manager, or tidy up stray files; if something in the repo state blocks you, stop and report it as
|
|
127
|
+
> `RESULT: BLOCKED <reason>` instead of fixing it. Do NOT `git add`/`commit`/`push` — leave every
|
|
128
|
+
> change uncommitted in the working tree for the orchestrator's independent review. WRITE the
|
|
129
|
+
> tests the `Required tests` section calls for AND **RUN the tests you just wrote** to confirm
|
|
130
|
+
> they execute and pass — that is the ONLY test run you do (NOT the app's affected/full suite, NOT
|
|
131
|
+
> any review). The orchestrator re-runs the authoritative set + does the independent review after
|
|
132
|
+
> you finish — those are the gate; your own run does not count as the gate.
|
|
133
|
+
>
|
|
134
|
+
> Narrate continuously: a `PLAN: <step1> | <step2> | …` line up front, then a single-line
|
|
135
|
+
> `PROGRESS: [<n>/<total>] <present-tense action>` before every relevant action (and `… done` on
|
|
136
|
+
> completion), spaced so no gap exceeds ~2 min, stdout unbuffered, plus exactly one final
|
|
137
|
+
> `RESULT: DONE|BLOCKED <reason>`.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
# C. Orchestrator only — NOT ADDRESSED TO THE IMPLEMENTER
|
|
142
|
+
|
|
143
|
+
> **If you are the implementer reading this work order as your own specification: STOP at this
|
|
144
|
+
> line. Everything below describes what the Orchestrator does AFTER you finish. You do none of it
|
|
145
|
+
> — no reviewers, no verification run, no register edit, no commit.** You ARE the invocation
|
|
146
|
+
> described below; do NOT shell out to `codex exec`.
|
|
147
|
+
|
|
148
|
+
## Execution directive
|
|
149
|
+
|
|
150
|
+
Implement through `codex exec` in the background, invoked directly via Bash with both mandatory
|
|
151
|
+
flags. Fallback to direct Claude implementation only on Codex quota/rate-limit/non-zero exit
|
|
152
|
+
(flips authorship — independent reviewer becomes mandatory, already mandatory at Tier 3).
|
|
153
|
+
|
|
154
|
+
## Review routing
|
|
155
|
+
|
|
156
|
+
Tier 3, full set, all concurrent in one background batch: `reviewer` (all 4 lenses — this is
|
|
157
|
+
shared-core, `duplication` and `regression` lenses matter most here), `ui_reviewer` (diff touches
|
|
158
|
+
a `.jsx` file consumed by every app's login page — narrow context: this diff + the relevant
|
|
159
|
+
`AGENTS.md` auth-error-handling convention), `sec_reviewer` (auth/login flow). Orchestrator's own
|
|
160
|
+
targeted security pass runs alongside, additional not substitute.
|
|
161
|
+
|
|
162
|
+
## Verification
|
|
163
|
+
|
|
164
|
+
Authoritative test run: `npx vitest run tests/authApiLogin.test.js tests/AuthContext.test.jsx
|
|
165
|
+
tests/authApiRegistration.test.js` (affected area — everything touching `authApi.jsx` and the
|
|
166
|
+
login flow it feeds). No prototype artifact in scope, no rendered side-by-side needed (no visual
|
|
167
|
+
change).
|
|
168
|
+
|
|
169
|
+
## Register + commit
|
|
170
|
+
|
|
171
|
+
Bump `package.json`'s `version` `3.5.0` -> `3.5.1` (PATCH — bugfix, no interface/behavior change
|
|
172
|
+
for any existing caller) as part of this same commit, matching this repo's established
|
|
173
|
+
push-to-`main` + version-bump publish flow (see `MSG-19`'s register row for precedent). Commit on
|
|
174
|
+
`main` (this repo has no `develop`). `WORK_ORDERS.md` row for `AUTH-7`, named review verdict.
|
|
175
|
+
After push, verify the bumped version actually published (per
|
|
176
|
+
`reference_dcm_ucm_publish_topology` — publish-from-main has no staging gate, check the live
|
|
177
|
+
package version after push). **No consumer pin-bump is required for this to take effect for
|
|
178
|
+
kira** — this is a pure bugfix with no interface change, so record explicitly in the Notiz that no
|
|
179
|
+
app needs to bump its `@micha.bigler/ui-core-micha` pin for this fix to matter (kira's *symptom*
|
|
180
|
+
was caused by the throttle exhaustion fixed in `KIRA-TRACK-3`, not by this bug alone — this fix
|
|
181
|
+
only prevents a raw/uncaught error the NEXT time any app's login flow race is hit).
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# AUTH-8 — `extractErrorInfo`'s hardcoded `'GENERIC'` code must not out-rank the caller's own default
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# A. Envelope — authored by the Expertenchat
|
|
6
|
+
|
|
7
|
+
## Goal & expected outcome
|
|
8
|
+
- Ziel: `extractErrorInfo` (`src/utils/auth-errors.js:2-27`) gibt für jeden Backend-Fehler der
|
|
9
|
+
Form `{"detail": "..."}` (Djangos generisches Fehlerformat — u.a. DRF `Throttled`, `Http404`,
|
|
10
|
+
`PermissionDenied`, jede unbehandelte `APIException`; exakt die Form des 429/409, das die
|
|
11
|
+
gesamte `KIRA-TRACK-3`/`AUTH-7`-Untersuchung ausgelöst hat) den hartcodierten Code `'GENERIC'`
|
|
12
|
+
zurück. `normaliseApiError` (Zeile 29-38) wählt dann `info.code || defaultCode` — da `'GENERIC'`
|
|
13
|
+
ein truthy String ist, GEWINNT er IMMER gegen den von JEDER der 39 bestehenden Aufrufstellen in
|
|
14
|
+
diesem Repo übergebenen, spezifischen `defaultCode` (z. B. `'Auth.LOGIN_FAILED'`,
|
|
15
|
+
`'Auth.PROFILE_UPDATE_FAILED'`, …). `'GENERIC'` ist an KEINER Stelle als i18n-Key registriert
|
|
16
|
+
(verifiziert: kein Treffer in `src/i18n/`, keine Erwähnung ausserhalb dieser einen Zeile) — die
|
|
17
|
+
UI zeigt also für JEDEN `{detail}`-förmigen Backend-Fehler den rohen String `"GENERIC"` statt
|
|
18
|
+
einer übersetzten Meldung, gleich welcher Aufrufer betroffen ist.
|
|
19
|
+
- Expected outcome: ein `{detail}`-förmiger Fehler liefert `code === <übergebener defaultCode>`
|
|
20
|
+
(übersetzt, wie jeder andere fehlende/unbekannte Code auch) — `data.detail`s Text bleibt weiterhin
|
|
21
|
+
in `.message` erhalten (nützlich für Logs/Debugging), beeinflusst aber nicht mehr `.code`.
|
|
22
|
+
|
|
23
|
+
## Scope + non-goals
|
|
24
|
+
- In scope: ausschliesslich `src/utils/auth-errors.js` — die EINE Zeile, die `code: 'GENERIC'`
|
|
25
|
+
zurückgibt, wird zu `code: null` (dann gewinnt in `normaliseApiError` korrekt der übergebene
|
|
26
|
+
`defaultCode`, exakt wie bei jedem anderen Fehler ohne erkennbaren Code).
|
|
27
|
+
- Explizite Non-Goals / nicht anfassen: die ANDEREN beiden Zweige von `extractErrorInfo`
|
|
28
|
+
(`data.errors[0].code`, `data.code`) liefern einen ECHTEN, vom Backend gesetzten Code — dieses
|
|
29
|
+
Verhalten ist korrekt und bleibt unverändert; nicht "verbessern" oder umbauen.
|
|
30
|
+
`normaliseApiError`s Signatur/Rückgabeform (`Error` mit `.code`/`.status`/`.raw`) bleibt gleich.
|
|
31
|
+
Keine neue i18n-Übersetzung für `'GENERIC'` selbst nötig — der Fix macht den String obsolet,
|
|
32
|
+
statt ihn übersetzbar zu machen. Keine Änderung an `LoginPage.jsx`/`SecurityComponent.jsx`/
|
|
33
|
+
anderen Konsumenten — die lesen bereits `err.code || '<eigener Default>'` und profitieren
|
|
34
|
+
automatisch, ohne selbst geändert zu werden.
|
|
35
|
+
|
|
36
|
+
## Tier · precondition / gate
|
|
37
|
+
- Tier: 3 — Shared-Core (`ui-core-micha`), betrifft laut Aufrufstellen-Scan ALLE 39 bestehenden
|
|
38
|
+
`normaliseApiError(...)`-Aufrufe in diesem Repo (jede Funktion in `src/auth/authApi.jsx` und
|
|
39
|
+
`src/utils/authService.js`). Kein Precondition/Gate.
|
|
40
|
+
- **Wichtig fürs Versionsschema:** anders als `AUTH-7` (rein additiv) ändert dieser Fix
|
|
41
|
+
BEOBACHTBARES Verhalten für jeden Aufrufer, der aktuell (fehlerhaft) `.code === 'GENERIC'`
|
|
42
|
+
bekommt — das ist der Bug selbst, aber jede konsumierende App, die diese Version zieht, sieht ab
|
|
43
|
+
sofort eine ANDERE (korrekte) übersetzte Meldung an Stellen, wo bisher der rohe String
|
|
44
|
+
`"GENERIC"` erschien. Deshalb MINOR-Bump (nicht PATCH) vorgesehen, siehe Teil C.
|
|
45
|
+
|
|
46
|
+
## Risks
|
|
47
|
+
- Wird der Fix zu breit gefasst (z. B. `'GENERIC'` komplett aus dem Vokabular entfernt statt nur
|
|
48
|
+
die eine Codepfad-Priorität zu korrigieren), könnte das `.message`-Feld (der rohe `data.detail`-
|
|
49
|
+
Text) verlorengehen — das wird von KEINEM bekannten Aufrufer als Anzeige-Text verwendet (alle
|
|
50
|
+
lesen `.code` fürs Rendering, s.o.), aber sicherheitshalber in den Tests absichern.
|
|
51
|
+
- Die beiden UNVERÄNDERTEN Zweige (`data.errors[].code`, `data.code`) müssen per Regressionstest
|
|
52
|
+
nachweislich unverändert bleiben.
|
|
53
|
+
|
|
54
|
+
## Required tests to WRITE
|
|
55
|
+
Neue Datei `tests/authErrors.test.js` (reiner Unit-Test, kein React/Testing-Library nötig — direkter
|
|
56
|
+
Import von `normaliseApiError`/`extractErrorInfo` aus `../src/utils/auth-errors.js`):
|
|
57
|
+
1. `{ response: { status: 429, data: { detail: 'Die Anfrage wurde gedrosselt...' } } }` mit
|
|
58
|
+
`normaliseApiError(err, 'Auth.LOGIN_FAILED')` aufgerufen → `.code === 'Auth.LOGIN_FAILED'` (NICHT
|
|
59
|
+
`'GENERIC'`), `.message === 'Die Anfrage wurde gedrosselt...'`, `.status === 429`.
|
|
60
|
+
2. Ohne übergebenen `defaultCode` (Default-Parameter) mit demselben `{detail}`-Fehler → `.code ===
|
|
61
|
+
'Auth.GENERIC_ERROR'` (der Funktions-Default, nicht `'GENERIC'`).
|
|
62
|
+
3. Regression: `{ response: { data: { errors: [{ code: 'enter_current_password', message: 'x' }] } } }`
|
|
63
|
+
→ `.code === 'enter_current_password'` (übergebener `defaultCode` NICHT verwendet — echter
|
|
64
|
+
Backend-Code gewinnt weiterhin, unverändert).
|
|
65
|
+
4. Regression: `{ response: { data: { code: 'invalid_credentials' } } }` → `.code ===
|
|
66
|
+
'invalid_credentials'` (unverändert).
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
# B. Implementation map — filled by the Orchestrator — ADDRESSED TO THE IMPLEMENTER
|
|
71
|
+
|
|
72
|
+
## Context package
|
|
73
|
+
|
|
74
|
+
**Named file to change:** `src/utils/auth-errors.js:21-24`:
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
// Fallback for generic Django errors
|
|
78
|
+
if (typeof data.detail === 'string') {
|
|
79
|
+
return { status, code: 'GENERIC', message: data.detail, raw: data };
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Ändere die zurückgegebene `code`-Property von `'GENERIC'` zu `null`:
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
// Fallback for generic Django errors. `code: null` here (not a fake sentinel)
|
|
87
|
+
// lets normaliseApiError's `info.code || defaultCode` correctly prefer the
|
|
88
|
+
// caller's own specific default — `data.detail` is Django's generic
|
|
89
|
+
// human-readable message field, never a machine code, and no caller of
|
|
90
|
+
// normaliseApiError anywhere in this repo expects to see 'GENERIC' as a code
|
|
91
|
+
// (grepped: zero references outside this one line, zero i18n key for it).
|
|
92
|
+
if (typeof data.detail === 'string') {
|
|
93
|
+
return { status, code: null, message: data.detail, raw: data };
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Do NOT touch** the other two branches (lines 11-15 `data.errors[0]`, lines 17-19 `data.code`) or
|
|
98
|
+
`normaliseApiError` itself (lines 29-38) — its `info.code || defaultCode` logic is already correct;
|
|
99
|
+
the bug was entirely in what `extractErrorInfo` fed it.
|
|
100
|
+
|
|
101
|
+
**Invariants / Pitfalls:**
|
|
102
|
+
- `normaliseApiError`'s OWN default parameter is `defaultCode = 'Auth.GENERIC_ERROR'` (line 29) —
|
|
103
|
+
this is a DIFFERENT string (`'Auth.GENERIC_ERROR'`, already namespaced like every other code in
|
|
104
|
+
this file) from the bug's `'GENERIC'` sentinel. Do not confuse the two or "fix" the wrong one.
|
|
105
|
+
- `data.detail` being falsy/absent already falls through correctly to the final `return { status,
|
|
106
|
+
code: null, message: null, raw: data }` (line 26) — that path is already correct and untouched.
|
|
107
|
+
- No i18n file needs a new key — this fix does not introduce any new user-visible string; it makes
|
|
108
|
+
the ALREADY-TRANSLATED caller-specific keys (e.g. `Auth.LOGIN_FAILED`, confirmed translated in
|
|
109
|
+
de/fr/en/sw) reachable again.
|
|
110
|
+
|
|
111
|
+
Directive: Arbeite ausschliesslich aus diesem Kontext-Paket; öffne nur `src/utils/auth-errors.js`
|
|
112
|
+
und die neue Testdatei zur Verifikation. Kein breites Erkunden in `authApi.jsx`/`authService.js` —
|
|
113
|
+
deren 39 Aufrufstellen sind bereits korrekt (sie übergeben schon einen sinnvollen `defaultCode`);
|
|
114
|
+
sie müssen nicht einzeln angeschaut werden.
|
|
115
|
+
|
|
116
|
+
## Target repo working directory (absolute)
|
|
117
|
+
|
|
118
|
+
`C:\Users\Micha Bigler\Documents\webapps\ui-core-micha`
|
|
119
|
+
|
|
120
|
+
## Preamble — REQUIRED, part of this file
|
|
121
|
+
|
|
122
|
+
> The text above is the COMPLETE spec — the committed WO file's content, not a plan to refine;
|
|
123
|
+
> there is no separate plan file. Read the nearest `AGENTS.md`, the relevant
|
|
124
|
+
> `.codex/skills/<role>/SKILL.md`, and the app `MEMORY.md` ONLY for conventions. Stay in scope; do
|
|
125
|
+
> not touch auth/permissions/deps/schema/CI unless the spec says so; do not update `MEMORY.md`.
|
|
126
|
+
> **Do NOT edit `WORK_ORDERS.md`** — the register row and the review verdicts are the
|
|
127
|
+
> orchestrator's alone. **Your tools are for editing source and test files and for running the
|
|
128
|
+
> tests you wrote — nothing else.** Do NOT install dependencies, touch a lockfile, run a package
|
|
129
|
+
> manager, or tidy up stray files; if something in the repo state blocks you, stop and report it as
|
|
130
|
+
> `RESULT: BLOCKED <reason>` instead of fixing it. Do NOT `git add`/`commit`/`push` — leave every
|
|
131
|
+
> change uncommitted in the working tree for the orchestrator's independent review. WRITE the
|
|
132
|
+
> tests the `Required tests` section calls for AND **RUN the tests you just wrote** to confirm
|
|
133
|
+
> they execute and pass — that is the ONLY test run you do (NOT the app's affected/full suite, NOT
|
|
134
|
+
> any review). The orchestrator re-runs the authoritative set + does the independent review after
|
|
135
|
+
> you finish — those are the gate; your own run does not count as the gate.
|
|
136
|
+
>
|
|
137
|
+
> Narrate continuously: a `PLAN: <step1> | <step2> | …` line up front, then a single-line
|
|
138
|
+
> `PROGRESS: [<n>/<total>] <present-tense action>` before every relevant action (and `… done` on
|
|
139
|
+
> completion), spaced so no gap exceeds ~2 min, stdout unbuffered, plus exactly one final
|
|
140
|
+
> `RESULT: DONE|BLOCKED <reason>`.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
# C. Orchestrator only — NOT ADDRESSED TO THE IMPLEMENTER
|
|
145
|
+
|
|
146
|
+
> **If you are the implementer reading this work order as your own specification: STOP at this
|
|
147
|
+
> line. Everything below describes what the Orchestrator does AFTER you finish. You do none of it
|
|
148
|
+
> — no reviewers, no verification run, no register edit, no commit.** You ARE the invocation
|
|
149
|
+
> described below; do NOT shell out to `codex exec`.
|
|
150
|
+
|
|
151
|
+
## Execution directive
|
|
152
|
+
|
|
153
|
+
Implement through `codex exec` in the background, invoked directly via Bash with both mandatory
|
|
154
|
+
flags. Fallback to direct Claude implementation only on Codex quota/rate-limit/non-zero exit
|
|
155
|
+
(flips authorship — independent reviewer becomes mandatory, already mandatory at Tier 3). Codex
|
|
156
|
+
confirmed available today (two successful dispatches already this session, `KIRA-TRACK-3`/
|
|
157
|
+
`AUTH-7`) — no fresh probe needed.
|
|
158
|
+
|
|
159
|
+
## Review routing
|
|
160
|
+
|
|
161
|
+
Tier 3, full set, all concurrent in one background batch: `reviewer` (all 4 lenses — `regression`
|
|
162
|
+
matters most here given 39 call sites; `envelope` needs Part A inline), `ui_reviewer` (this is
|
|
163
|
+
specifically an i18n/error-rendering correctness fix — narrow context: the diff + confirmation that
|
|
164
|
+
every `defaultCode` used across `authApi.jsx`/`authService.js` is actually a translated key in all
|
|
165
|
+
shipped locales), `sec_reviewer` (confirm `.message`/`.raw` still expose no more than before — this
|
|
166
|
+
diff should REDUCE not increase what reaches the UI, since `'GENERIC'` is replaced by `null`).
|
|
167
|
+
Orchestrator's own targeted pass runs alongside.
|
|
168
|
+
|
|
169
|
+
## Verification
|
|
170
|
+
|
|
171
|
+
Authoritative test run: `npx vitest run tests/authErrors.test.js tests/authErrorCodeRendering.test.jsx
|
|
172
|
+
tests/authApiLogin.test.js tests/authApiRegistration.test.js` (affected area — everything touching
|
|
173
|
+
`auth-errors.js` and its consumers). No prototype artifact, no rendered side-by-side (no markup
|
|
174
|
+
changed).
|
|
175
|
+
|
|
176
|
+
## Register + commit
|
|
177
|
+
|
|
178
|
+
Bump `package.json`'s `version` — **MINOR**, not PATCH, per the Envelope's tier note (observable
|
|
179
|
+
behavior change for existing `{detail}`-shaped-error callers, not purely additive) — from whatever
|
|
180
|
+
`AUTH-7` landed at, to the next `3.x.0`. `CHANGELOG.md` entry MUST explicitly warn: existing
|
|
181
|
+
consumers who upgrade will see a DIFFERENT (correct, translated) message wherever they previously
|
|
182
|
+
saw the raw string `"GENERIC"` for a `{detail}`-shaped backend error — re-check that UI surface
|
|
183
|
+
after bumping the pin, don't assume silent compatibility the way `AUTH-7`'s bump was. Commit on
|
|
184
|
+
`main`. `WORK_ORDERS.md` row for `AUTH-8` → `done`, named review verdict, landing SHA.
|