@scalio-cloud/ui 0.31.1 → 0.32.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/README.md CHANGED
@@ -319,9 +319,13 @@ sorts the group `system` that way). The reset block talks to scalio-platform `fa
319
319
  import { FactoryReset, factoryResetApi, SettingsField, SettingsRow } from "@scalio-cloud/ui/react";
320
320
 
321
321
  const reset = factoryResetApi(api); // once, outside the component
322
- <FactoryReset client={reset} lang={lang} tokenKey="citadel_token" backupTo="/settings/backup" link={Link} />
322
+ <FactoryReset client={reset} lang={lang} tokenKey="citadel_token" backupTo="/settings/backup" accountTo="/profile" link={Link} />
323
323
  ```
324
324
 
325
+ Since 0.32.0 (N21, security review 2026-10-27) the reset needs the code of the signed-in admin's authenticator app
326
+ or a backup code (`code` in the request, scalio-platform 0.11.0). Without a second factor the block says so and links
327
+ to `accountTo` (the profile page) instead of allowing the reset.
328
+
325
329
  `SettingsField` (label, field, help) and `SettingsRow` (title and help left, a switch or button right; `tone="danger"`)
326
330
  keep the fields inside a `SettingsSection` the same in every app.
327
331
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalio-cloud/ui",
3
- "version": "0.31.1",
3
+ "version": "0.32.0",
4
4
  "description": "Scalio Control Room design tokens, Tailwind preset, self-hosted fonts, the shared app header, sign-in layout, sidebar and page blocks",
5
5
  "license": "SEE LICENSE IN README.md",
6
6
  "private": false,
package/react/labels.js CHANGED
@@ -336,6 +336,9 @@ const LABELS = {
336
336
  removeLicense: "Lizenz ebenfalls entfernen",
337
337
  removeLicenseHint: "Danach läuft die Instanz als freie Edition, bis wieder ein Lizenzschlüssel eingetragen ist. Die Installations-ID bleibt.",
338
338
  password: "Ihr Passwort",
339
+ code: "Code aus der Authenticator-App oder Backup-Code",
340
+ noFactor: "Zum Zurücksetzen braucht Ihr Konto eine Authenticator-App; richten Sie sie zuerst im Profil ein. Ein Konto, das sich nur mit Microsoft anmeldet, kann nicht zurücksetzen: Das macht ein lokaler Admin mit Authenticator-App.",
341
+ setUpFactor: "Zum Profil",
339
342
  run: "Auf Werkseinstellungen zurücksetzen",
340
343
  },
341
344
  license: {
@@ -744,6 +747,9 @@ const LABELS = {
744
747
  removeLicense: "Remove the license as well",
745
748
  removeLicenseHint: "Afterwards the instance runs as the free edition until a license key is entered again. The installation ID stays.",
746
749
  password: "Your password",
750
+ code: "Code from the authenticator app or a backup code",
751
+ noFactor: "To reset, your account needs an authenticator app; set it up in your profile first. An account that signs in only with Microsoft cannot reset: a local admin with an authenticator app does it.",
752
+ setUpFactor: "Go to profile",
747
753
  run: "Reset to factory settings",
748
754
  },
749
755
  license: {
@@ -32,6 +32,7 @@ function FactoryReset(props) {
32
32
  const [info, setInfo] = React.useState(null);
33
33
  const [confirm, setConfirm] = React.useState("");
34
34
  const [password, setPassword] = React.useState("");
35
+ const [code, setCode] = React.useState("");
35
36
  const [removeLicense, setRemoveLicense] = React.useState(false);
36
37
  const [busy, setBusy] = React.useState(false);
37
38
  const [error, setError] = React.useState("");
@@ -42,14 +43,17 @@ function FactoryReset(props) {
42
43
  return () => { alive = false; };
43
44
  }, [client]);
44
45
 
45
- const ready = confirm.trim() === "RESET" && (!info || !info.password || !!password);
46
+ // N21: the backend says second_factor true (code needed) or false (set one up first); an older backend says nothing.
47
+ const factor = info ? info.second_factor : undefined;
48
+ const ready = confirm.trim() === "RESET" && (!info || !info.password || !!password)
49
+ && factor !== false && (factor !== true || !!code.trim());
46
50
  const run = async (e) => {
47
51
  e.preventDefault();
48
52
  if (!ready) return;
49
53
  setBusy(true);
50
54
  setError("");
51
55
  try {
52
- await client.run({ confirm, password, remove_license: removeLicense });
56
+ await client.run({ confirm, password, code, remove_license: removeLicense });
53
57
  try { if (props.tokenKey) window.localStorage.removeItem(props.tokenKey); } catch (x) { /* private mode */ }
54
58
  if (props.onDone) props.onDone();
55
59
  else window.location.href = "/login";
@@ -94,6 +98,19 @@ function FactoryReset(props) {
94
98
  h("input", { id: "factory-reset-password", type: "password", className: "scalio-input", value: password, disabled: busy,
95
99
  autoComplete: "current-password", "data-testid": "factory-reset-password", onChange: (e) => setPassword(e.target.value) }))
96
100
  : null,
101
+ factor === true
102
+ ? h(Field, { id: "factory-reset-code", label: L.code },
103
+ h("input", { id: "factory-reset-code", className: "scalio-input", value: code, disabled: busy, inputMode: "numeric",
104
+ autoComplete: "one-time-code", "data-testid": "factory-reset-code", onChange: (e) => setCode(e.target.value) }))
105
+ : null,
106
+ factor === false
107
+ ? h("div", { className: "scalio-notice is-warn", "data-testid": "factory-reset-no-factor" },
108
+ h("span", null, L.noFactor),
109
+ props.accountTo && info.password
110
+ ? h(props.link || "a", Object.assign({ className: "scalio-btn" },
111
+ props.link ? { to: props.accountTo } : { href: props.accountTo }), L.setUpFactor)
112
+ : null)
113
+ : null,
97
114
  error ? h("div", { className: "scalio-notice is-error", role: "alert", "data-testid": "factory-reset-error" }, error) : null,
98
115
  h("div", { className: "scalio-settings-section-footer" },
99
116
  h("button", { type: "submit", className: "scalio-btn is-danger is-solid", disabled: busy || !ready, "data-testid": "factory-reset-run" },