strapi-identity 0.1.2 → 0.2.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.
Files changed (86) hide show
  1. package/README.md +2 -1
  2. package/dist/admin/{AdminReset-H453s-DE.mjs → AdminReset-B-WGECOX.mjs} +1 -1
  3. package/dist/admin/{AdminReset-C0QibZXW.js → AdminReset-CqHhVBS_.js} +1 -1
  4. package/dist/admin/{ProfileToggle-DQeXCx34.mjs → ProfileToggle-BCtCsOvj.mjs} +298 -38
  5. package/dist/admin/{ProfileToggle-BFmIWCrN.js → ProfileToggle-BRYjt5Lu.js} +298 -38
  6. package/dist/admin/{SettingsPage-OwMik_IK.mjs → SettingsPage-7Ytl01jH.mjs} +343 -101
  7. package/dist/admin/{SettingsPage-DyF7YbsX.js → SettingsPage-DAxGIv_E.js} +342 -100
  8. package/dist/admin/{ar-i2eiMZkz.js → ar-BYnI7Tsa.js} +36 -23
  9. package/dist/admin/{ar-BXaam37U.mjs → ar-DwZqj0qM.mjs} +36 -23
  10. package/dist/admin/{ca-DZ9DbEcQ.mjs → ca-aKVVc8iQ.mjs} +36 -23
  11. package/dist/admin/{ca-BVpGzY8r.js → ca-sBRHuaFU.js} +36 -23
  12. package/dist/admin/{cs-Gok16KLy.mjs → cs--prflMHS.mjs} +36 -23
  13. package/dist/admin/{cs-_PZVkwt0.js → cs-gU7KP3Lx.js} +36 -23
  14. package/dist/admin/de-BT25lv_6.mjs +49 -0
  15. package/dist/admin/de-CrlCAUuf.js +49 -0
  16. package/dist/admin/{dk-B7EOsAdU.js → dk-BNC3WUzY.js} +36 -23
  17. package/dist/admin/{dk-CI64Xmli.mjs → dk-Ck3AQYU7.mjs} +36 -23
  18. package/dist/admin/{en-B_vJwdfS.mjs → en-9qzlpde0.mjs} +36 -23
  19. package/dist/admin/{en-D4KP9t1Y.js → en-DBj0AD5g.js} +36 -23
  20. package/dist/admin/{es-CHwF7YK-.js → es-D5Sn41_H.js} +36 -23
  21. package/dist/admin/{es-CqJcXo4j.mjs → es-lh6XoPb7.mjs} +36 -23
  22. package/dist/admin/{eu-D-snytN8.mjs → eu-Cuz6ijBX.mjs} +36 -23
  23. package/dist/admin/{eu-DvdbwE5E.js → eu-Qr3RvDPW.js} +36 -23
  24. package/dist/admin/fr-C4pmkPYn.js +49 -0
  25. package/dist/admin/fr-ChlDcZsG.mjs +49 -0
  26. package/dist/admin/{gu-3wJbbAmw.mjs → gu-B6zyD1bW.mjs} +36 -23
  27. package/dist/admin/{gu-D2LgVfMp.js → gu-BMZL76zM.js} +36 -23
  28. package/dist/admin/{he-Bjv7eygt.mjs → he-C5V-qZCX.mjs} +36 -23
  29. package/dist/admin/{he-DnhYpbvN.js → he-H6iBa45A.js} +36 -23
  30. package/dist/admin/{hi-DDD2E3A3.js → hi-Be8rPk7I.js} +36 -23
  31. package/dist/admin/{hi-CNiDezU7.mjs → hi-czhOWo6-.mjs} +36 -23
  32. package/dist/admin/{hu-C1_YkZHU.js → hu-DKp6kOmc.js} +36 -23
  33. package/dist/admin/{hu-aLaIWmGw.mjs → hu-NbZ3aiYV.mjs} +36 -23
  34. package/dist/admin/{id-u3wVE6Rv.js → id-DO0bwFgY.js} +36 -23
  35. package/dist/admin/{id-C8WRgGm1.mjs → id-NH9PvcR5.mjs} +36 -23
  36. package/dist/admin/{index-D45I6rWF.mjs → index-BfC6z9N5.mjs} +62 -7
  37. package/dist/admin/{index-BXZI8nMZ.js → index-D03zlFnm.js} +62 -7
  38. package/dist/admin/index.js +1 -1
  39. package/dist/admin/index.mjs +1 -1
  40. package/dist/admin/{it-CjoRoJj1.mjs → it-Cmrey6tg.mjs} +36 -23
  41. package/dist/admin/{it-CDw6dG9Z.js → it-Df6-7-M7.js} +36 -23
  42. package/dist/admin/{ja-CewucIUY.mjs → ja-DH3KMqOL.mjs} +36 -23
  43. package/dist/admin/{ja-CbMXy2ym.js → ja-HuAq9ZwT.js} +36 -23
  44. package/dist/admin/{ko-D-kAxDtd.mjs → ko-DPN28RE8.mjs} +36 -23
  45. package/dist/admin/{ko-BEtJPpfJ.js → ko-S9k8KA8K.js} +36 -23
  46. package/dist/admin/{ml-0fR2_MmA.js → ml-Bh9GGqcW.js} +36 -23
  47. package/dist/admin/{ml-DR3AaofF.mjs → ml-MsHNacm6.mjs} +36 -23
  48. package/dist/admin/{ms-COHLS5e5.mjs → ms-TjHAaxTd.mjs} +36 -23
  49. package/dist/admin/{ms-DLvuGSlk.js → ms-hO5YeEg4.js} +36 -23
  50. package/dist/admin/{nl-wj6kn642.js → nl-BF98NBwL.js} +36 -23
  51. package/dist/admin/{nl-DVtHsM2H.mjs → nl-BLILZU8-.mjs} +36 -23
  52. package/dist/admin/{no-D_0yjyCy.mjs → no-BtVZ-siy.mjs} +36 -23
  53. package/dist/admin/{no-DVBgWt8q.js → no-bl1OXlfa.js} +36 -23
  54. package/dist/admin/{pl-C3GNxjVX.mjs → pl-DCSB6LwZ.mjs} +36 -23
  55. package/dist/admin/{pl-B2ghisbC.js → pl-DCnOWIDw.js} +36 -23
  56. package/dist/admin/{pt-BR-BbKV8YoX.mjs → pt-BR-CeLqmj88.mjs} +36 -23
  57. package/dist/admin/{pt-BR-CfgNaB1-.js → pt-BR-D2_UrxTp.js} +36 -23
  58. package/dist/admin/{pt-DKe8rRWa.js → pt-DIu8RT_X.js} +36 -23
  59. package/dist/admin/{pt-z4K3cCjf.mjs → pt-fgjdOyW5.mjs} +36 -23
  60. package/dist/admin/{ru-C85izLFa.mjs → ru-B_hlpAyP.mjs} +36 -23
  61. package/dist/admin/{ru-BFSm68HC.js → ru-BccMCf0l.js} +36 -23
  62. package/dist/admin/{sa-B1XoTTrE.mjs → sa-BtuJ_I1t.mjs} +36 -23
  63. package/dist/admin/{sa-BOPaqylt.js → sa-D3A-fo85.js} +36 -23
  64. package/dist/admin/{sk-C48lUPuC.mjs → sk-mmuTFlCK.mjs} +36 -23
  65. package/dist/admin/{sk-Dd-S1612.js → sk-uSLC6KhO.js} +36 -23
  66. package/dist/admin/{sv-BLma_kJl.mjs → sv-BlaHc5ax.mjs} +36 -23
  67. package/dist/admin/{sv-lg64Cw78.js → sv-CuKk5tE-.js} +36 -23
  68. package/dist/admin/{th-DPbm5NrX.js → th-Bv3NKkYO.js} +36 -23
  69. package/dist/admin/{th-BJEu5n7q.mjs → th-BwyhFaeE.mjs} +36 -23
  70. package/dist/admin/{tr-DkIUODKq.mjs → tr-BLocNlbZ.mjs} +36 -23
  71. package/dist/admin/{tr-Bm1QZr4v.js → tr-Bmvs-Hx-.js} +36 -23
  72. package/dist/admin/{uk-FARzIGx4.js → uk-BDxn-EZU.js} +36 -23
  73. package/dist/admin/{uk-D7ArtSe3.mjs → uk-CyZ10xtq.mjs} +36 -23
  74. package/dist/admin/{vi-DS0yslPP.mjs → vi-Bx_UJ8up.mjs} +36 -23
  75. package/dist/admin/{vi-Bi9B6eTY.js → vi-F_mqQCme.js} +36 -23
  76. package/dist/admin/{zh-DkEx28ZA.js → zh-CFZJPG5N.js} +36 -23
  77. package/dist/admin/{zh-DwCvIPSz.mjs → zh-CjJdRa3l.mjs} +36 -23
  78. package/dist/admin/{zh-Hans-BwwKCR6_.js → zh-Hans-4BhSwSQw.js} +36 -23
  79. package/dist/admin/{zh-Hans-DP2xZyda.mjs → zh-Hans-s7G2GUHU.mjs} +36 -23
  80. package/dist/server/index.js +487 -50
  81. package/dist/server/index.mjs +487 -50
  82. package/package.json +4 -3
  83. package/dist/admin/de-BuYn1AYX.mjs +0 -26
  84. package/dist/admin/de-GItli7en.js +0 -26
  85. package/dist/admin/fr-Bt6sS5GX.mjs +0 -26
  86. package/dist/admin/fr-CbCW6hVD.js +0 -26
package/README.md CHANGED
@@ -7,6 +7,7 @@ Detailed Multi-Factor Authentication (MFA) plugin for Strapi v5+. Secure your St
7
7
  - **MFA Login Interception**: Seamlessly integrates with the default Strapi login flow.
8
8
  - **TOTP Compatibility**: Works with all major authenticator apps (Google Authenticator, Authy, 1Password, etc.).
9
9
  - **Recovery Codes**: Generates secure recovery codes for emergency access.
10
+ - **Email Passcode**: Option to receive a one-time passcode via email as an alternative MFA method.
10
11
  - **Native UI Integration**:
11
12
  - Matches Strapi's design system.
12
13
  - Profile integration for easy setup.
@@ -114,5 +115,5 @@ Below is the implementation status of planned features.
114
115
  - [x] **Custom Issuer**: Configurable app label.
115
116
  - [x] **Multi-language Support**: i18n ready.
116
117
  - [x] **Admin Reset**: Allow super-admins to reset MFA for other users who lost access.
117
- - [ ] **Email Passcode**: Alternative MFA method via Email.
118
+ - [x] **Email Passcode**: Alternative MFA method via Email.
118
119
  - [ ] **Enforced Mode**: Mandatory MFA for specific roles or all users.
@@ -2,7 +2,7 @@ import { jsxs, Fragment, jsx } from "react/jsx-runtime";
2
2
  import { useState, useEffect } from "react";
3
3
  import { W as WarningAlert } from "./WarningAlert-VU011LVF.mjs";
4
4
  import { Box, Flex, Typography, Grid, Button } from "@strapi/design-system";
5
- import { g as getTranslation } from "./index-D45I6rWF.mjs";
5
+ import { g as getTranslation } from "./index-BfC6z9N5.mjs";
6
6
  import { g as getToken } from "./tokenHelpers-DagDzpso.mjs";
7
7
  import { useIntl } from "react-intl";
8
8
  const AdminReset = ({ id }) => {
@@ -4,7 +4,7 @@ const jsxRuntime = require("react/jsx-runtime");
4
4
  const React = require("react");
5
5
  const WarningAlert = require("./WarningAlert-DFE5euMk.js");
6
6
  const designSystem = require("@strapi/design-system");
7
- const index = require("./index-BXZI8nMZ.js");
7
+ const index = require("./index-D03zlFnm.js");
8
8
  const tokenHelpers = require("./tokenHelpers-jtoRu0q5.js");
9
9
  const reactIntl = require("react-intl");
10
10
  const AdminReset = ({ id }) => {
@@ -2,7 +2,7 @@ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
2
  import { useState, useEffect } from "react";
3
3
  import { Modal, Flex, Typography, Grid, Button, TextInput, Box, Field, Toggle } from "@strapi/design-system";
4
4
  import styled from "styled-components";
5
- import { g as getTranslation, I as InputOTP, a as InputOTPGroup, b as InputOTPSlot, c as InputOTPSeparator } from "./index-D45I6rWF.mjs";
5
+ import { g as getTranslation, I as InputOTP, a as InputOTPGroup, b as InputOTPSlot, c as InputOTPSeparator } from "./index-BfC6z9N5.mjs";
6
6
  import { QRCodeCanvas } from "qrcode.react";
7
7
  import { useIntl } from "react-intl";
8
8
  import { g as getToken } from "./tokenHelpers-DagDzpso.mjs";
@@ -120,11 +120,155 @@ function RemoveModal({ open, onOpenChange, onSubmit }) {
120
120
  ] })
121
121
  ] }) }) });
122
122
  }
123
+ function EmailOTPModal({
124
+ mode,
125
+ open,
126
+ email,
127
+ onOpenChange,
128
+ onSuccess
129
+ }) {
130
+ const { formatMessage } = useIntl();
131
+ const [step, setStep] = useState("send");
132
+ const [loading, setLoading] = useState(false);
133
+ const [error, setError] = useState(null);
134
+ const handleOpenChange = (nextOpen) => {
135
+ if (!nextOpen) {
136
+ setStep("send");
137
+ setError(null);
138
+ }
139
+ onOpenChange(nextOpen);
140
+ };
141
+ const handleSend = async () => {
142
+ const token = getToken();
143
+ setLoading(true);
144
+ setError(null);
145
+ try {
146
+ const endpoint = mode === "setup" ? "/strapi-identity/enable-email" : "/strapi-identity/disable-email/request";
147
+ const response = await fetch(endpoint, {
148
+ method: "POST",
149
+ headers: { "Content-Type": "application/json", authorization: `Bearer ${token}` }
150
+ });
151
+ const body = await response.json();
152
+ if (!response.ok) {
153
+ throw new Error(body.error || "Failed to send verification email");
154
+ }
155
+ setStep("confirm");
156
+ } catch (err) {
157
+ setError(err.message);
158
+ } finally {
159
+ setLoading(false);
160
+ }
161
+ };
162
+ const handleConfirm = async (e) => {
163
+ e.preventDefault();
164
+ const token = getToken();
165
+ const formData = new FormData(e.target);
166
+ const code = formData.get("otp");
167
+ setLoading(true);
168
+ setError(null);
169
+ try {
170
+ const endpoint = mode === "setup" ? "/strapi-identity/setup-email" : "/strapi-identity/disable";
171
+ const response = await fetch(endpoint, {
172
+ method: "POST",
173
+ headers: { "Content-Type": "application/json", authorization: `Bearer ${token}` },
174
+ body: JSON.stringify({ code })
175
+ });
176
+ const body = await response.json();
177
+ if (!response.ok) {
178
+ throw new Error(body.error || "Invalid or expired code");
179
+ }
180
+ handleOpenChange(false);
181
+ onSuccess();
182
+ } catch (err) {
183
+ setError(err.message);
184
+ } finally {
185
+ setLoading(false);
186
+ }
187
+ };
188
+ const title = mode === "setup" ? formatMessage({
189
+ id: getTranslation("email_otp.setup_title"),
190
+ defaultMessage: "Enable Email OTP"
191
+ }) : formatMessage({
192
+ id: getTranslation("email_otp.disable_title"),
193
+ defaultMessage: "Disable Email OTP"
194
+ });
195
+ return /* @__PURE__ */ jsx(Modal.Root, { open, onOpenChange: handleOpenChange, children: /* @__PURE__ */ jsxs(Modal.Content, { children: [
196
+ /* @__PURE__ */ jsx(Modal.Header, { children: /* @__PURE__ */ jsx(Modal.Title, { children: title }) }),
197
+ step === "send" ? /* @__PURE__ */ jsxs(Fragment, { children: [
198
+ /* @__PURE__ */ jsx(Modal.Body, { children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "center", gap: 4, marginTop: 4, marginBottom: 4, children: [
199
+ /* @__PURE__ */ jsx(Typography, { textAlign: "center", children: mode === "setup" ? formatMessage(
200
+ {
201
+ id: getTranslation("email_otp.setup_description"),
202
+ defaultMessage: "We'll send a 6-digit verification code to {email}. Enter it to enable Email OTP."
203
+ },
204
+ { email: /* @__PURE__ */ jsx("strong", { children: email }) }
205
+ ) : formatMessage(
206
+ {
207
+ id: getTranslation("email_otp.disable_description"),
208
+ defaultMessage: "We'll send a 6-digit verification code to {email}. Enter it to disable Email OTP."
209
+ },
210
+ { email: /* @__PURE__ */ jsx("strong", { children: email }) }
211
+ ) }),
212
+ error ? /* @__PURE__ */ jsx(Typography, { role: "alert", textColor: "danger600", textAlign: "center", children: error }) : null
213
+ ] }) }),
214
+ /* @__PURE__ */ jsxs(Modal.Footer, { children: [
215
+ /* @__PURE__ */ jsx(Modal.Close, { children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", children: formatMessage({ id: "app.components.Button.cancel", defaultMessage: "Cancel" }) }) }),
216
+ /* @__PURE__ */ jsx(Button, { onClick: handleSend, loading, children: formatMessage({
217
+ id: getTranslation("email_otp.send_code"),
218
+ defaultMessage: "Send verification email"
219
+ }) })
220
+ ] })
221
+ ] }) : /* @__PURE__ */ jsxs("form", { onSubmit: handleConfirm, children: [
222
+ /* @__PURE__ */ jsx(Modal.Body, { children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "center", gap: 4, marginTop: 4, marginBottom: 4, children: [
223
+ /* @__PURE__ */ jsx(Typography, { textAlign: "center", children: formatMessage(
224
+ {
225
+ id: getTranslation("email_otp.confirm_description"),
226
+ defaultMessage: "Enter the 6-digit code sent to {email}."
227
+ },
228
+ { email: /* @__PURE__ */ jsx("strong", { children: email }) }
229
+ ) }),
230
+ /* @__PURE__ */ jsxs(InputOTP, { maxLength: 6, name: "otp", id: "otp", autoFocus: true, children: [
231
+ /* @__PURE__ */ jsxs(InputOTPGroup, { children: [
232
+ /* @__PURE__ */ jsx(InputOTPSlot, { index: 0 }),
233
+ /* @__PURE__ */ jsx(InputOTPSlot, { index: 1 }),
234
+ /* @__PURE__ */ jsx(InputOTPSlot, { index: 2 })
235
+ ] }),
236
+ /* @__PURE__ */ jsx(InputOTPSeparator, {}),
237
+ /* @__PURE__ */ jsxs(InputOTPGroup, { children: [
238
+ /* @__PURE__ */ jsx(InputOTPSlot, { index: 3 }),
239
+ /* @__PURE__ */ jsx(InputOTPSlot, { index: 4 }),
240
+ /* @__PURE__ */ jsx(InputOTPSlot, { index: 5 })
241
+ ] })
242
+ ] }),
243
+ error ? /* @__PURE__ */ jsx(Typography, { role: "alert", textColor: "danger600", textAlign: "center", children: error }) : null,
244
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", type: "button", onClick: () => handleSend(), children: formatMessage({
245
+ id: getTranslation("email_otp.resend_code"),
246
+ defaultMessage: "Resend code"
247
+ }) })
248
+ ] }) }),
249
+ /* @__PURE__ */ jsxs(Modal.Footer, { children: [
250
+ /* @__PURE__ */ jsx(Modal.Close, { children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", children: formatMessage({
251
+ id: "app.components.Button.cancel",
252
+ defaultMessage: "Cancel"
253
+ }) }) }),
254
+ /* @__PURE__ */ jsx(Button, { type: "submit", loading, children: formatMessage({
255
+ id: "app.components.Button.confirm",
256
+ defaultMessage: "Confirm"
257
+ }) })
258
+ ] })
259
+ ] })
260
+ ] }) });
261
+ }
123
262
  const ProfileToggle = () => {
124
263
  const { formatMessage } = useIntl();
125
264
  const [enabled, setEnabled] = useState(null);
265
+ const [mfaType, setMfaType] = useState(null);
126
266
  const [mfaEnabled, setMfaEnabled] = useState(false);
267
+ const [emailConfigured, setEmailConfigured] = useState(false);
268
+ const [userEmail, setUserEmail] = useState("");
127
269
  const [disableDialogOpen, setDisableDialogOpen] = useState(false);
270
+ const [emailSetupOpen, setEmailSetupOpen] = useState(false);
271
+ const [emailDisableOpen, setEmailDisableOpen] = useState(false);
128
272
  const [modalOpen, setModalOpen] = useState(false);
129
273
  const [uri, setUri] = useState(null);
130
274
  const [secret, setSecret] = useState(null);
@@ -136,6 +280,9 @@ const ProfileToggle = () => {
136
280
  setDisableDialogOpen(true);
137
281
  return;
138
282
  }
283
+ if (enable && enabled === "full" && mfaType === "email") {
284
+ return;
285
+ }
139
286
  try {
140
287
  const response = await fetch("/strapi-identity/enable", {
141
288
  method: "POST",
@@ -154,11 +301,25 @@ const ProfileToggle = () => {
154
301
  setUri(data?.uri || null);
155
302
  setSecret(data?.secret || null);
156
303
  setEnabled(enable ? "temp" : null);
304
+ if (enable) setMfaType("totp");
157
305
  } catch (error) {
158
306
  console.error(error);
159
307
  setEnabled(null);
160
308
  }
161
309
  };
310
+ const handleEmailToggle = ({ target }) => {
311
+ const enable = target?.checked || false;
312
+ if (!enable && enabled === "full" && mfaType === "email") {
313
+ setEmailDisableOpen(true);
314
+ return;
315
+ }
316
+ if (enable && enabled === "full") {
317
+ return;
318
+ }
319
+ if (enable) {
320
+ setEmailSetupOpen(true);
321
+ }
322
+ };
162
323
  const handleConfirm = async (e) => {
163
324
  e.preventDefault();
164
325
  const form = e.target;
@@ -183,6 +344,7 @@ const ProfileToggle = () => {
183
344
  setUri(null);
184
345
  setSecret(null);
185
346
  setEnabled("full");
347
+ setMfaType("totp");
186
348
  } catch (error) {
187
349
  console.error(error);
188
350
  }
@@ -214,6 +376,7 @@ const ProfileToggle = () => {
214
376
  setUri(null);
215
377
  setSecret(null);
216
378
  setEnabled(null);
379
+ setMfaType(null);
217
380
  } catch (error) {
218
381
  console.error(error);
219
382
  }
@@ -223,7 +386,7 @@ const ProfileToggle = () => {
223
386
  (async () => {
224
387
  const token = getToken();
225
388
  try {
226
- const [status, enabled2] = await Promise.all([
389
+ const [statusRes, enabledRes, meRes] = await Promise.all([
227
390
  fetch("/strapi-identity/status", {
228
391
  method: "GET",
229
392
  headers: { "Content-Type": "application/json", authorization: `Bearer ${token}` },
@@ -233,18 +396,37 @@ const ProfileToggle = () => {
233
396
  method: "GET",
234
397
  headers: { "Content-Type": "application/json", authorization: `Bearer ${token}` },
235
398
  signal: ac.signal
399
+ }),
400
+ fetch("/admin/users/me", {
401
+ method: "GET",
402
+ headers: { "Content-Type": "application/json", authorization: `Bearer ${token}` },
403
+ signal: ac.signal
236
404
  })
237
405
  ]);
238
- const statusBody = await status.json();
239
- const enabledBody = await enabled2.json();
240
- if (!status.ok) {
241
- throw new Error(`${status.status} - ${statusBody.error || "Failed to set up MFA"}`);
406
+ const statusBody = await statusRes.json();
407
+ const enabledBody = await enabledRes.json();
408
+ if (!statusRes.ok) {
409
+ throw new Error(`${statusRes.status} - ${statusBody.error || "Failed to get MFA status"}`);
242
410
  }
243
- if (!enabled2.ok) {
244
- throw new Error(`${enabled2.status} - ${enabledBody.error || "Failed to get MFA config"}`);
411
+ if (!enabledRes.ok) {
412
+ throw new Error(`${enabledRes.status} - ${enabledBody.error || "Failed to get MFA config"}`);
245
413
  }
246
414
  setMfaEnabled(enabledBody.data);
247
415
  setEnabled(statusBody.data?.status || null);
416
+ setMfaType(statusBody.data?.type || null);
417
+ if (meRes.ok) {
418
+ const meBody = await meRes.json();
419
+ setUserEmail(meBody.data?.email || "");
420
+ const configRes = await fetch("/strapi-identity/config", {
421
+ method: "GET",
422
+ headers: { "Content-Type": "application/json", authorization: `Bearer ${token}` },
423
+ signal: ac.signal
424
+ });
425
+ if (configRes.ok) {
426
+ const configBody = await configRes.json();
427
+ setEmailConfigured(!!configBody.data?.email_enabled);
428
+ }
429
+ }
248
430
  } catch (error) {
249
431
  if (error.name === "AbortError") return;
250
432
  console.error("Failed to fetch MFA status:", error);
@@ -253,6 +435,10 @@ const ProfileToggle = () => {
253
435
  return () => ac.abort();
254
436
  }, []);
255
437
  if (!mfaEnabled) return null;
438
+ const totpChecked = enabled !== null && mfaType === "totp";
439
+ const emailChecked = enabled !== null && mfaType === "email";
440
+ const totpDisabled = enabled !== null && mfaType === "email";
441
+ const emailDisabled = enabled !== null && mfaType === "totp";
256
442
  return /* @__PURE__ */ jsxs(Fragment, { children: [
257
443
  /* @__PURE__ */ jsx(
258
444
  Box,
@@ -275,36 +461,78 @@ const ProfileToggle = () => {
275
461
  defaultMessage: "Add an additional layer of security to your account."
276
462
  }) })
277
463
  ] }),
278
- /* @__PURE__ */ jsx(Grid.Root, { tag: "div", gap: 5, children: /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, alignItems: "stretch", children: /* @__PURE__ */ jsxs(
279
- Field.Root,
280
- {
281
- width: "100%",
282
- name: "two-factor-authentication",
283
- id: "two-factor-authentication",
284
- children: [
285
- /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({
286
- id: getTranslation("profile.toggle_label"),
287
- defaultMessage: "Enable Two-Factor Authentication"
288
- }) }),
289
- /* @__PURE__ */ jsx(
290
- Toggle,
291
- {
292
- offLabel: formatMessage({
293
- id: "app.components.ToggleCheckbox.off-label",
294
- defaultMessage: "False"
295
- }),
296
- onLabel: formatMessage({
297
- id: "app.components.ToggleCheckbox.on-label",
298
- defaultMessage: "True"
299
- }),
300
- checked: enabled !== null,
301
- onChange: handleToggle
302
- }
303
- ),
304
- /* @__PURE__ */ jsx(Field.Hint, {})
305
- ]
306
- }
307
- ) }) })
464
+ /* @__PURE__ */ jsxs(Grid.Root, { tag: "div", gap: 5, children: [
465
+ /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, alignItems: "stretch", children: /* @__PURE__ */ jsxs(
466
+ Field.Root,
467
+ {
468
+ width: "100%",
469
+ name: "two-factor-authentication",
470
+ id: "two-factor-authentication",
471
+ hint: totpDisabled ? formatMessage({
472
+ id: getTranslation("profile.totp_disabled_hint"),
473
+ defaultMessage: "Disable Email OTP first to enable the authenticator app."
474
+ }) : void 0,
475
+ children: [
476
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({
477
+ id: getTranslation("profile.toggle_label"),
478
+ defaultMessage: "Enable Two-Factor Authentication"
479
+ }) }),
480
+ /* @__PURE__ */ jsx(
481
+ Toggle,
482
+ {
483
+ offLabel: formatMessage({
484
+ id: "app.components.ToggleCheckbox.off-label",
485
+ defaultMessage: "False"
486
+ }),
487
+ onLabel: formatMessage({
488
+ id: "app.components.ToggleCheckbox.on-label",
489
+ defaultMessage: "True"
490
+ }),
491
+ checked: totpChecked,
492
+ onChange: handleToggle,
493
+ disabled: totpDisabled
494
+ }
495
+ ),
496
+ /* @__PURE__ */ jsx(Field.Hint, {})
497
+ ]
498
+ }
499
+ ) }),
500
+ emailConfigured && /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, alignItems: "stretch", children: /* @__PURE__ */ jsxs(
501
+ Field.Root,
502
+ {
503
+ width: "100%",
504
+ name: "email-otp",
505
+ id: "email-otp",
506
+ hint: emailDisabled ? formatMessage({
507
+ id: getTranslation("profile.email_otp_disabled_hint"),
508
+ defaultMessage: "Disable the authenticator app first to enable Email OTP."
509
+ }) : void 0,
510
+ children: [
511
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({
512
+ id: getTranslation("profile.email_otp_label"),
513
+ defaultMessage: "Enable Email OTP"
514
+ }) }),
515
+ /* @__PURE__ */ jsx(
516
+ Toggle,
517
+ {
518
+ offLabel: formatMessage({
519
+ id: "app.components.ToggleCheckbox.off-label",
520
+ defaultMessage: "False"
521
+ }),
522
+ onLabel: formatMessage({
523
+ id: "app.components.ToggleCheckbox.on-label",
524
+ defaultMessage: "True"
525
+ }),
526
+ checked: emailChecked,
527
+ onChange: handleEmailToggle,
528
+ disabled: emailDisabled
529
+ }
530
+ ),
531
+ /* @__PURE__ */ jsx(Field.Hint, {})
532
+ ]
533
+ }
534
+ ) })
535
+ ] })
308
536
  ] })
309
537
  }
310
538
  ),
@@ -326,6 +554,38 @@ const ProfileToggle = () => {
326
554
  onOpenChange: setDisableDialogOpen,
327
555
  onSubmit: handleDisable
328
556
  }
557
+ ),
558
+ /* @__PURE__ */ jsx(
559
+ EmailOTPModal,
560
+ {
561
+ mode: "setup",
562
+ open: emailSetupOpen,
563
+ email: userEmail,
564
+ onOpenChange: (open) => {
565
+ if (!open) setEmailSetupOpen(false);
566
+ },
567
+ onSuccess: () => {
568
+ setEnabled("full");
569
+ setMfaType("email");
570
+ setEmailSetupOpen(false);
571
+ }
572
+ }
573
+ ),
574
+ /* @__PURE__ */ jsx(
575
+ EmailOTPModal,
576
+ {
577
+ mode: "disable",
578
+ open: emailDisableOpen,
579
+ email: userEmail,
580
+ onOpenChange: (open) => {
581
+ if (!open) setEmailDisableOpen(false);
582
+ },
583
+ onSuccess: () => {
584
+ setEnabled(null);
585
+ setMfaType(null);
586
+ setEmailDisableOpen(false);
587
+ }
588
+ }
329
589
  )
330
590
  ] });
331
591
  };