strapi-identity 0.1.2 → 0.3.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 +2 -1
- package/dist/admin/{AdminReset-H453s-DE.mjs → AdminReset-CCGgw-0k.mjs} +2 -2
- package/dist/admin/{AdminReset-C0QibZXW.js → AdminReset-QClQP2Il.js} +2 -2
- package/dist/admin/{ProfileToggle-BFmIWCrN.js → ProfileToggle-H31GHNDA.js} +299 -39
- package/dist/admin/{ProfileToggle-DQeXCx34.mjs → ProfileToggle-gPuH6dGP.mjs} +299 -39
- package/dist/admin/{SettingsPage-OwMik_IK.mjs → SettingsPage-DgPikS3m.mjs} +344 -102
- package/dist/admin/{SettingsPage-DyF7YbsX.js → SettingsPage-DulEjadb.js} +343 -101
- package/dist/admin/{ar-i2eiMZkz.js → ar-BYnI7Tsa.js} +36 -23
- package/dist/admin/{ar-BXaam37U.mjs → ar-DwZqj0qM.mjs} +36 -23
- package/dist/admin/{ca-DZ9DbEcQ.mjs → ca-aKVVc8iQ.mjs} +36 -23
- package/dist/admin/{ca-BVpGzY8r.js → ca-sBRHuaFU.js} +36 -23
- package/dist/admin/{cs-Gok16KLy.mjs → cs--prflMHS.mjs} +36 -23
- package/dist/admin/{cs-_PZVkwt0.js → cs-gU7KP3Lx.js} +36 -23
- package/dist/admin/de-BT25lv_6.mjs +49 -0
- package/dist/admin/de-CrlCAUuf.js +49 -0
- package/dist/admin/{dk-B7EOsAdU.js → dk-BNC3WUzY.js} +36 -23
- package/dist/admin/{dk-CI64Xmli.mjs → dk-Ck3AQYU7.mjs} +36 -23
- package/dist/admin/{en-B_vJwdfS.mjs → en-9qzlpde0.mjs} +36 -23
- package/dist/admin/{en-D4KP9t1Y.js → en-DBj0AD5g.js} +36 -23
- package/dist/admin/{es-CHwF7YK-.js → es-D5Sn41_H.js} +36 -23
- package/dist/admin/{es-CqJcXo4j.mjs → es-lh6XoPb7.mjs} +36 -23
- package/dist/admin/{eu-D-snytN8.mjs → eu-Cuz6ijBX.mjs} +36 -23
- package/dist/admin/{eu-DvdbwE5E.js → eu-Qr3RvDPW.js} +36 -23
- package/dist/admin/fr-C4pmkPYn.js +49 -0
- package/dist/admin/fr-ChlDcZsG.mjs +49 -0
- package/dist/admin/{gu-3wJbbAmw.mjs → gu-B6zyD1bW.mjs} +36 -23
- package/dist/admin/{gu-D2LgVfMp.js → gu-BMZL76zM.js} +36 -23
- package/dist/admin/{he-Bjv7eygt.mjs → he-C5V-qZCX.mjs} +36 -23
- package/dist/admin/{he-DnhYpbvN.js → he-H6iBa45A.js} +36 -23
- package/dist/admin/{hi-DDD2E3A3.js → hi-Be8rPk7I.js} +36 -23
- package/dist/admin/{hi-CNiDezU7.mjs → hi-czhOWo6-.mjs} +36 -23
- package/dist/admin/{hu-C1_YkZHU.js → hu-DKp6kOmc.js} +36 -23
- package/dist/admin/{hu-aLaIWmGw.mjs → hu-NbZ3aiYV.mjs} +36 -23
- package/dist/admin/{id-u3wVE6Rv.js → id-DO0bwFgY.js} +36 -23
- package/dist/admin/{id-C8WRgGm1.mjs → id-NH9PvcR5.mjs} +36 -23
- package/dist/admin/{index-BXZI8nMZ.js → index-C9K2h5UC.js} +65 -12
- package/dist/admin/{index-D45I6rWF.mjs → index-C_4USMnn.mjs} +65 -12
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/{it-CjoRoJj1.mjs → it-Cmrey6tg.mjs} +36 -23
- package/dist/admin/{it-CDw6dG9Z.js → it-Df6-7-M7.js} +36 -23
- package/dist/admin/{ja-CewucIUY.mjs → ja-DH3KMqOL.mjs} +36 -23
- package/dist/admin/{ja-CbMXy2ym.js → ja-HuAq9ZwT.js} +36 -23
- package/dist/admin/{ko-D-kAxDtd.mjs → ko-DPN28RE8.mjs} +36 -23
- package/dist/admin/{ko-BEtJPpfJ.js → ko-S9k8KA8K.js} +36 -23
- package/dist/admin/{ml-0fR2_MmA.js → ml-Bh9GGqcW.js} +36 -23
- package/dist/admin/{ml-DR3AaofF.mjs → ml-MsHNacm6.mjs} +36 -23
- package/dist/admin/{ms-COHLS5e5.mjs → ms-TjHAaxTd.mjs} +36 -23
- package/dist/admin/{ms-DLvuGSlk.js → ms-hO5YeEg4.js} +36 -23
- package/dist/admin/{nl-wj6kn642.js → nl-BF98NBwL.js} +36 -23
- package/dist/admin/{nl-DVtHsM2H.mjs → nl-BLILZU8-.mjs} +36 -23
- package/dist/admin/{no-D_0yjyCy.mjs → no-BtVZ-siy.mjs} +36 -23
- package/dist/admin/{no-DVBgWt8q.js → no-bl1OXlfa.js} +36 -23
- package/dist/admin/{pl-C3GNxjVX.mjs → pl-DCSB6LwZ.mjs} +36 -23
- package/dist/admin/{pl-B2ghisbC.js → pl-DCnOWIDw.js} +36 -23
- package/dist/admin/{pt-BR-BbKV8YoX.mjs → pt-BR-CeLqmj88.mjs} +36 -23
- package/dist/admin/{pt-BR-CfgNaB1-.js → pt-BR-D2_UrxTp.js} +36 -23
- package/dist/admin/{pt-DKe8rRWa.js → pt-DIu8RT_X.js} +36 -23
- package/dist/admin/{pt-z4K3cCjf.mjs → pt-fgjdOyW5.mjs} +36 -23
- package/dist/admin/{ru-C85izLFa.mjs → ru-B_hlpAyP.mjs} +36 -23
- package/dist/admin/{ru-BFSm68HC.js → ru-BccMCf0l.js} +36 -23
- package/dist/admin/{sa-B1XoTTrE.mjs → sa-BtuJ_I1t.mjs} +36 -23
- package/dist/admin/{sa-BOPaqylt.js → sa-D3A-fo85.js} +36 -23
- package/dist/admin/{sk-C48lUPuC.mjs → sk-mmuTFlCK.mjs} +36 -23
- package/dist/admin/{sk-Dd-S1612.js → sk-uSLC6KhO.js} +36 -23
- package/dist/admin/{sv-BLma_kJl.mjs → sv-BlaHc5ax.mjs} +36 -23
- package/dist/admin/{sv-lg64Cw78.js → sv-CuKk5tE-.js} +36 -23
- package/dist/admin/{th-DPbm5NrX.js → th-Bv3NKkYO.js} +36 -23
- package/dist/admin/{th-BJEu5n7q.mjs → th-BwyhFaeE.mjs} +36 -23
- package/dist/admin/{tokenHelpers-jtoRu0q5.js → tokenHelpers-B54WRTn1.js} +4 -10
- package/dist/admin/{tokenHelpers-DagDzpso.mjs → tokenHelpers-CKVyT1sz.mjs} +4 -10
- package/dist/admin/{tr-DkIUODKq.mjs → tr-BLocNlbZ.mjs} +36 -23
- package/dist/admin/{tr-Bm1QZr4v.js → tr-Bmvs-Hx-.js} +36 -23
- package/dist/admin/{uk-FARzIGx4.js → uk-BDxn-EZU.js} +36 -23
- package/dist/admin/{uk-D7ArtSe3.mjs → uk-CyZ10xtq.mjs} +36 -23
- package/dist/admin/{vi-DS0yslPP.mjs → vi-Bx_UJ8up.mjs} +36 -23
- package/dist/admin/{vi-Bi9B6eTY.js → vi-F_mqQCme.js} +36 -23
- package/dist/admin/{zh-DkEx28ZA.js → zh-CFZJPG5N.js} +36 -23
- package/dist/admin/{zh-DwCvIPSz.mjs → zh-CjJdRa3l.mjs} +36 -23
- package/dist/admin/{zh-Hans-BwwKCR6_.js → zh-Hans-4BhSwSQw.js} +36 -23
- package/dist/admin/{zh-Hans-DP2xZyda.mjs → zh-Hans-s7G2GUHU.mjs} +36 -23
- package/dist/server/index.js +487 -50
- package/dist/server/index.mjs +487 -50
- package/package.json +5 -4
- package/dist/admin/de-BuYn1AYX.mjs +0 -26
- package/dist/admin/de-GItli7en.js +0 -26
- package/dist/admin/fr-Bt6sS5GX.mjs +0 -26
- 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
|
-
- [
|
|
118
|
+
- [x] **Email Passcode**: Alternative MFA method via Email.
|
|
118
119
|
- [ ] **Enforced Mode**: Mandatory MFA for specific roles or all users.
|
|
@@ -2,8 +2,8 @@ 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-
|
|
6
|
-
import { g as getToken } from "./tokenHelpers-
|
|
5
|
+
import { g as getTranslation } from "./index-C_4USMnn.mjs";
|
|
6
|
+
import { g as getToken } from "./tokenHelpers-CKVyT1sz.mjs";
|
|
7
7
|
import { useIntl } from "react-intl";
|
|
8
8
|
const AdminReset = ({ id }) => {
|
|
9
9
|
const { formatMessage } = useIntl();
|
|
@@ -4,8 +4,8 @@ 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-
|
|
8
|
-
const tokenHelpers = require("./tokenHelpers-
|
|
7
|
+
const index = require("./index-C9K2h5UC.js");
|
|
8
|
+
const tokenHelpers = require("./tokenHelpers-B54WRTn1.js");
|
|
9
9
|
const reactIntl = require("react-intl");
|
|
10
10
|
const AdminReset = ({ id }) => {
|
|
11
11
|
const { formatMessage } = reactIntl.useIntl();
|
|
@@ -4,10 +4,10 @@ const jsxRuntime = require("react/jsx-runtime");
|
|
|
4
4
|
const React = require("react");
|
|
5
5
|
const designSystem = require("@strapi/design-system");
|
|
6
6
|
const styled = require("styled-components");
|
|
7
|
-
const index = require("./index-
|
|
7
|
+
const index = require("./index-C9K2h5UC.js");
|
|
8
8
|
const qrcode_react = require("qrcode.react");
|
|
9
9
|
const reactIntl = require("react-intl");
|
|
10
|
-
const tokenHelpers = require("./tokenHelpers-
|
|
10
|
+
const tokenHelpers = require("./tokenHelpers-B54WRTn1.js");
|
|
11
11
|
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
12
12
|
const styled__default = /* @__PURE__ */ _interopDefault(styled);
|
|
13
13
|
function ConfirmModal({
|
|
@@ -124,11 +124,155 @@ function RemoveModal({ open, onOpenChange, onSubmit }) {
|
|
|
124
124
|
] })
|
|
125
125
|
] }) }) });
|
|
126
126
|
}
|
|
127
|
+
function EmailOTPModal({
|
|
128
|
+
mode,
|
|
129
|
+
open,
|
|
130
|
+
email,
|
|
131
|
+
onOpenChange,
|
|
132
|
+
onSuccess
|
|
133
|
+
}) {
|
|
134
|
+
const { formatMessage } = reactIntl.useIntl();
|
|
135
|
+
const [step, setStep] = React.useState("send");
|
|
136
|
+
const [loading, setLoading] = React.useState(false);
|
|
137
|
+
const [error, setError] = React.useState(null);
|
|
138
|
+
const handleOpenChange = (nextOpen) => {
|
|
139
|
+
if (!nextOpen) {
|
|
140
|
+
setStep("send");
|
|
141
|
+
setError(null);
|
|
142
|
+
}
|
|
143
|
+
onOpenChange(nextOpen);
|
|
144
|
+
};
|
|
145
|
+
const handleSend = async () => {
|
|
146
|
+
const token = tokenHelpers.getToken();
|
|
147
|
+
setLoading(true);
|
|
148
|
+
setError(null);
|
|
149
|
+
try {
|
|
150
|
+
const endpoint = mode === "setup" ? "/strapi-identity/enable-email" : "/strapi-identity/disable-email/request";
|
|
151
|
+
const response = await fetch(endpoint, {
|
|
152
|
+
method: "POST",
|
|
153
|
+
headers: { "Content-Type": "application/json", authorization: `Bearer ${token}` }
|
|
154
|
+
});
|
|
155
|
+
const body = await response.json();
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
throw new Error(body.error || "Failed to send verification email");
|
|
158
|
+
}
|
|
159
|
+
setStep("confirm");
|
|
160
|
+
} catch (err) {
|
|
161
|
+
setError(err.message);
|
|
162
|
+
} finally {
|
|
163
|
+
setLoading(false);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
const handleConfirm = async (e) => {
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
const token = tokenHelpers.getToken();
|
|
169
|
+
const formData = new FormData(e.target);
|
|
170
|
+
const code = formData.get("otp");
|
|
171
|
+
setLoading(true);
|
|
172
|
+
setError(null);
|
|
173
|
+
try {
|
|
174
|
+
const endpoint = mode === "setup" ? "/strapi-identity/setup-email" : "/strapi-identity/disable";
|
|
175
|
+
const response = await fetch(endpoint, {
|
|
176
|
+
method: "POST",
|
|
177
|
+
headers: { "Content-Type": "application/json", authorization: `Bearer ${token}` },
|
|
178
|
+
body: JSON.stringify({ code })
|
|
179
|
+
});
|
|
180
|
+
const body = await response.json();
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
throw new Error(body.error || "Invalid or expired code");
|
|
183
|
+
}
|
|
184
|
+
handleOpenChange(false);
|
|
185
|
+
onSuccess();
|
|
186
|
+
} catch (err) {
|
|
187
|
+
setError(err.message);
|
|
188
|
+
} finally {
|
|
189
|
+
setLoading(false);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const title = mode === "setup" ? formatMessage({
|
|
193
|
+
id: index.getTranslation("email_otp.setup_title"),
|
|
194
|
+
defaultMessage: "Enable Email OTP"
|
|
195
|
+
}) : formatMessage({
|
|
196
|
+
id: index.getTranslation("email_otp.disable_title"),
|
|
197
|
+
defaultMessage: "Disable Email OTP"
|
|
198
|
+
});
|
|
199
|
+
return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Root, { open, onOpenChange: handleOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Modal.Content, { children: [
|
|
200
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Header, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Title, { children: title }) }),
|
|
201
|
+
step === "send" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
202
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Body, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "center", gap: 4, marginTop: 4, marginBottom: 4, children: [
|
|
203
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textAlign: "center", children: mode === "setup" ? formatMessage(
|
|
204
|
+
{
|
|
205
|
+
id: index.getTranslation("email_otp.setup_description"),
|
|
206
|
+
defaultMessage: "We'll send a 6-digit verification code to {email}. Enter it to enable Email OTP."
|
|
207
|
+
},
|
|
208
|
+
{ email: /* @__PURE__ */ jsxRuntime.jsx("strong", { children: email }) }
|
|
209
|
+
) : formatMessage(
|
|
210
|
+
{
|
|
211
|
+
id: index.getTranslation("email_otp.disable_description"),
|
|
212
|
+
defaultMessage: "We'll send a 6-digit verification code to {email}. Enter it to disable Email OTP."
|
|
213
|
+
},
|
|
214
|
+
{ email: /* @__PURE__ */ jsxRuntime.jsx("strong", { children: email }) }
|
|
215
|
+
) }),
|
|
216
|
+
error ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { role: "alert", textColor: "danger600", textAlign: "center", children: error }) : null
|
|
217
|
+
] }) }),
|
|
218
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Modal.Footer, { children: [
|
|
219
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Close, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", children: formatMessage({ id: "app.components.Button.cancel", defaultMessage: "Cancel" }) }) }),
|
|
220
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { onClick: handleSend, loading, children: formatMessage({
|
|
221
|
+
id: index.getTranslation("email_otp.send_code"),
|
|
222
|
+
defaultMessage: "Send verification email"
|
|
223
|
+
}) })
|
|
224
|
+
] })
|
|
225
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleConfirm, children: [
|
|
226
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Body, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "center", gap: 4, marginTop: 4, marginBottom: 4, children: [
|
|
227
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textAlign: "center", children: formatMessage(
|
|
228
|
+
{
|
|
229
|
+
id: index.getTranslation("email_otp.confirm_description"),
|
|
230
|
+
defaultMessage: "Enter the 6-digit code sent to {email}."
|
|
231
|
+
},
|
|
232
|
+
{ email: /* @__PURE__ */ jsxRuntime.jsx("strong", { children: email }) }
|
|
233
|
+
) }),
|
|
234
|
+
/* @__PURE__ */ jsxRuntime.jsxs(index.InputOTP, { maxLength: 6, name: "otp", id: "otp", autoFocus: true, children: [
|
|
235
|
+
/* @__PURE__ */ jsxRuntime.jsxs(index.InputOTPGroup, { children: [
|
|
236
|
+
/* @__PURE__ */ jsxRuntime.jsx(index.InputOTPSlot, { index: 0 }),
|
|
237
|
+
/* @__PURE__ */ jsxRuntime.jsx(index.InputOTPSlot, { index: 1 }),
|
|
238
|
+
/* @__PURE__ */ jsxRuntime.jsx(index.InputOTPSlot, { index: 2 })
|
|
239
|
+
] }),
|
|
240
|
+
/* @__PURE__ */ jsxRuntime.jsx(index.InputOTPSeparator, {}),
|
|
241
|
+
/* @__PURE__ */ jsxRuntime.jsxs(index.InputOTPGroup, { children: [
|
|
242
|
+
/* @__PURE__ */ jsxRuntime.jsx(index.InputOTPSlot, { index: 3 }),
|
|
243
|
+
/* @__PURE__ */ jsxRuntime.jsx(index.InputOTPSlot, { index: 4 }),
|
|
244
|
+
/* @__PURE__ */ jsxRuntime.jsx(index.InputOTPSlot, { index: 5 })
|
|
245
|
+
] })
|
|
246
|
+
] }),
|
|
247
|
+
error ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { role: "alert", textColor: "danger600", textAlign: "center", children: error }) : null,
|
|
248
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "ghost", type: "button", onClick: () => handleSend(), children: formatMessage({
|
|
249
|
+
id: index.getTranslation("email_otp.resend_code"),
|
|
250
|
+
defaultMessage: "Resend code"
|
|
251
|
+
}) })
|
|
252
|
+
] }) }),
|
|
253
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Modal.Footer, { children: [
|
|
254
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Close, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", children: formatMessage({
|
|
255
|
+
id: "app.components.Button.cancel",
|
|
256
|
+
defaultMessage: "Cancel"
|
|
257
|
+
}) }) }),
|
|
258
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { type: "submit", loading, children: formatMessage({
|
|
259
|
+
id: "app.components.Button.confirm",
|
|
260
|
+
defaultMessage: "Confirm"
|
|
261
|
+
}) })
|
|
262
|
+
] })
|
|
263
|
+
] })
|
|
264
|
+
] }) });
|
|
265
|
+
}
|
|
127
266
|
const ProfileToggle = () => {
|
|
128
267
|
const { formatMessage } = reactIntl.useIntl();
|
|
129
268
|
const [enabled, setEnabled] = React.useState(null);
|
|
269
|
+
const [mfaType, setMfaType] = React.useState(null);
|
|
130
270
|
const [mfaEnabled, setMfaEnabled] = React.useState(false);
|
|
271
|
+
const [emailConfigured, setEmailConfigured] = React.useState(false);
|
|
272
|
+
const [userEmail, setUserEmail] = React.useState("");
|
|
131
273
|
const [disableDialogOpen, setDisableDialogOpen] = React.useState(false);
|
|
274
|
+
const [emailSetupOpen, setEmailSetupOpen] = React.useState(false);
|
|
275
|
+
const [emailDisableOpen, setEmailDisableOpen] = React.useState(false);
|
|
132
276
|
const [modalOpen, setModalOpen] = React.useState(false);
|
|
133
277
|
const [uri, setUri] = React.useState(null);
|
|
134
278
|
const [secret, setSecret] = React.useState(null);
|
|
@@ -140,6 +284,9 @@ const ProfileToggle = () => {
|
|
|
140
284
|
setDisableDialogOpen(true);
|
|
141
285
|
return;
|
|
142
286
|
}
|
|
287
|
+
if (enable && enabled === "full" && mfaType === "email") {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
143
290
|
try {
|
|
144
291
|
const response = await fetch("/strapi-identity/enable", {
|
|
145
292
|
method: "POST",
|
|
@@ -158,11 +305,25 @@ const ProfileToggle = () => {
|
|
|
158
305
|
setUri(data?.uri || null);
|
|
159
306
|
setSecret(data?.secret || null);
|
|
160
307
|
setEnabled(enable ? "temp" : null);
|
|
308
|
+
if (enable) setMfaType("totp");
|
|
161
309
|
} catch (error) {
|
|
162
310
|
console.error(error);
|
|
163
311
|
setEnabled(null);
|
|
164
312
|
}
|
|
165
313
|
};
|
|
314
|
+
const handleEmailToggle = ({ target }) => {
|
|
315
|
+
const enable = target?.checked || false;
|
|
316
|
+
if (!enable && enabled === "full" && mfaType === "email") {
|
|
317
|
+
setEmailDisableOpen(true);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (enable && enabled === "full") {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (enable) {
|
|
324
|
+
setEmailSetupOpen(true);
|
|
325
|
+
}
|
|
326
|
+
};
|
|
166
327
|
const handleConfirm = async (e) => {
|
|
167
328
|
e.preventDefault();
|
|
168
329
|
const form = e.target;
|
|
@@ -187,6 +348,7 @@ const ProfileToggle = () => {
|
|
|
187
348
|
setUri(null);
|
|
188
349
|
setSecret(null);
|
|
189
350
|
setEnabled("full");
|
|
351
|
+
setMfaType("totp");
|
|
190
352
|
} catch (error) {
|
|
191
353
|
console.error(error);
|
|
192
354
|
}
|
|
@@ -218,6 +380,7 @@ const ProfileToggle = () => {
|
|
|
218
380
|
setUri(null);
|
|
219
381
|
setSecret(null);
|
|
220
382
|
setEnabled(null);
|
|
383
|
+
setMfaType(null);
|
|
221
384
|
} catch (error) {
|
|
222
385
|
console.error(error);
|
|
223
386
|
}
|
|
@@ -227,7 +390,7 @@ const ProfileToggle = () => {
|
|
|
227
390
|
(async () => {
|
|
228
391
|
const token = tokenHelpers.getToken();
|
|
229
392
|
try {
|
|
230
|
-
const [
|
|
393
|
+
const [statusRes, enabledRes, meRes] = await Promise.all([
|
|
231
394
|
fetch("/strapi-identity/status", {
|
|
232
395
|
method: "GET",
|
|
233
396
|
headers: { "Content-Type": "application/json", authorization: `Bearer ${token}` },
|
|
@@ -237,18 +400,37 @@ const ProfileToggle = () => {
|
|
|
237
400
|
method: "GET",
|
|
238
401
|
headers: { "Content-Type": "application/json", authorization: `Bearer ${token}` },
|
|
239
402
|
signal: ac.signal
|
|
403
|
+
}),
|
|
404
|
+
fetch("/admin/users/me", {
|
|
405
|
+
method: "GET",
|
|
406
|
+
headers: { "Content-Type": "application/json", authorization: `Bearer ${token}` },
|
|
407
|
+
signal: ac.signal
|
|
240
408
|
})
|
|
241
409
|
]);
|
|
242
|
-
const statusBody = await
|
|
243
|
-
const enabledBody = await
|
|
244
|
-
if (!
|
|
245
|
-
throw new Error(`${
|
|
410
|
+
const statusBody = await statusRes.json();
|
|
411
|
+
const enabledBody = await enabledRes.json();
|
|
412
|
+
if (!statusRes.ok) {
|
|
413
|
+
throw new Error(`${statusRes.status} - ${statusBody.error || "Failed to get MFA status"}`);
|
|
246
414
|
}
|
|
247
|
-
if (!
|
|
248
|
-
throw new Error(`${
|
|
415
|
+
if (!enabledRes.ok) {
|
|
416
|
+
throw new Error(`${enabledRes.status} - ${enabledBody.error || "Failed to get MFA config"}`);
|
|
249
417
|
}
|
|
250
418
|
setMfaEnabled(enabledBody.data);
|
|
251
419
|
setEnabled(statusBody.data?.status || null);
|
|
420
|
+
setMfaType(statusBody.data?.type || null);
|
|
421
|
+
if (meRes.ok) {
|
|
422
|
+
const meBody = await meRes.json();
|
|
423
|
+
setUserEmail(meBody.data?.email || "");
|
|
424
|
+
const configRes = await fetch("/strapi-identity/config", {
|
|
425
|
+
method: "GET",
|
|
426
|
+
headers: { "Content-Type": "application/json", authorization: `Bearer ${token}` },
|
|
427
|
+
signal: ac.signal
|
|
428
|
+
});
|
|
429
|
+
if (configRes.ok) {
|
|
430
|
+
const configBody = await configRes.json();
|
|
431
|
+
setEmailConfigured(!!configBody.data?.email_enabled);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
252
434
|
} catch (error) {
|
|
253
435
|
if (error.name === "AbortError") return;
|
|
254
436
|
console.error("Failed to fetch MFA status:", error);
|
|
@@ -257,6 +439,10 @@ const ProfileToggle = () => {
|
|
|
257
439
|
return () => ac.abort();
|
|
258
440
|
}, []);
|
|
259
441
|
if (!mfaEnabled) return null;
|
|
442
|
+
const totpChecked = enabled !== null && mfaType === "totp";
|
|
443
|
+
const emailChecked = enabled !== null && mfaType === "email";
|
|
444
|
+
const totpDisabled = enabled !== null && mfaType === "email";
|
|
445
|
+
const emailDisabled = enabled !== null && mfaType === "totp";
|
|
260
446
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
261
447
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
262
448
|
designSystem.Box,
|
|
@@ -279,36 +465,78 @@ const ProfileToggle = () => {
|
|
|
279
465
|
defaultMessage: "Add an additional layer of security to your account."
|
|
280
466
|
}) })
|
|
281
467
|
] }),
|
|
282
|
-
/* @__PURE__ */ jsxRuntime.
|
|
283
|
-
designSystem.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
id: index.getTranslation("profile.
|
|
291
|
-
defaultMessage: "
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
designSystem.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
468
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Grid.Root, { tag: "div", gap: 5, children: [
|
|
469
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, s: 12, alignItems: "stretch", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
470
|
+
designSystem.Field.Root,
|
|
471
|
+
{
|
|
472
|
+
width: "100%",
|
|
473
|
+
name: "two-factor-authentication",
|
|
474
|
+
id: "two-factor-authentication",
|
|
475
|
+
hint: totpDisabled ? formatMessage({
|
|
476
|
+
id: index.getTranslation("profile.totp_disabled_hint"),
|
|
477
|
+
defaultMessage: "Disable Email OTP first to enable the authenticator app."
|
|
478
|
+
}) : void 0,
|
|
479
|
+
children: [
|
|
480
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: formatMessage({
|
|
481
|
+
id: index.getTranslation("profile.toggle_label"),
|
|
482
|
+
defaultMessage: "Enable Two-Factor Authentication"
|
|
483
|
+
}) }),
|
|
484
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
485
|
+
designSystem.Toggle,
|
|
486
|
+
{
|
|
487
|
+
offLabel: formatMessage({
|
|
488
|
+
id: "app.components.ToggleCheckbox.off-label",
|
|
489
|
+
defaultMessage: "False"
|
|
490
|
+
}),
|
|
491
|
+
onLabel: formatMessage({
|
|
492
|
+
id: "app.components.ToggleCheckbox.on-label",
|
|
493
|
+
defaultMessage: "True"
|
|
494
|
+
}),
|
|
495
|
+
checked: totpChecked,
|
|
496
|
+
onChange: handleToggle,
|
|
497
|
+
disabled: totpDisabled
|
|
498
|
+
}
|
|
499
|
+
),
|
|
500
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
|
|
501
|
+
]
|
|
502
|
+
}
|
|
503
|
+
) }),
|
|
504
|
+
emailConfigured && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, s: 12, alignItems: "stretch", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
505
|
+
designSystem.Field.Root,
|
|
506
|
+
{
|
|
507
|
+
width: "100%",
|
|
508
|
+
name: "email-otp",
|
|
509
|
+
id: "email-otp",
|
|
510
|
+
hint: emailDisabled ? formatMessage({
|
|
511
|
+
id: index.getTranslation("profile.email_otp_disabled_hint"),
|
|
512
|
+
defaultMessage: "Disable the authenticator app first to enable Email OTP."
|
|
513
|
+
}) : void 0,
|
|
514
|
+
children: [
|
|
515
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: formatMessage({
|
|
516
|
+
id: index.getTranslation("profile.email_otp_label"),
|
|
517
|
+
defaultMessage: "Enable Email OTP"
|
|
518
|
+
}) }),
|
|
519
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
520
|
+
designSystem.Toggle,
|
|
521
|
+
{
|
|
522
|
+
offLabel: formatMessage({
|
|
523
|
+
id: "app.components.ToggleCheckbox.off-label",
|
|
524
|
+
defaultMessage: "False"
|
|
525
|
+
}),
|
|
526
|
+
onLabel: formatMessage({
|
|
527
|
+
id: "app.components.ToggleCheckbox.on-label",
|
|
528
|
+
defaultMessage: "True"
|
|
529
|
+
}),
|
|
530
|
+
checked: emailChecked,
|
|
531
|
+
onChange: handleEmailToggle,
|
|
532
|
+
disabled: emailDisabled
|
|
533
|
+
}
|
|
534
|
+
),
|
|
535
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
|
|
536
|
+
]
|
|
537
|
+
}
|
|
538
|
+
) })
|
|
539
|
+
] })
|
|
312
540
|
] })
|
|
313
541
|
}
|
|
314
542
|
),
|
|
@@ -330,6 +558,38 @@ const ProfileToggle = () => {
|
|
|
330
558
|
onOpenChange: setDisableDialogOpen,
|
|
331
559
|
onSubmit: handleDisable
|
|
332
560
|
}
|
|
561
|
+
),
|
|
562
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
563
|
+
EmailOTPModal,
|
|
564
|
+
{
|
|
565
|
+
mode: "setup",
|
|
566
|
+
open: emailSetupOpen,
|
|
567
|
+
email: userEmail,
|
|
568
|
+
onOpenChange: (open) => {
|
|
569
|
+
if (!open) setEmailSetupOpen(false);
|
|
570
|
+
},
|
|
571
|
+
onSuccess: () => {
|
|
572
|
+
setEnabled("full");
|
|
573
|
+
setMfaType("email");
|
|
574
|
+
setEmailSetupOpen(false);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
),
|
|
578
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
579
|
+
EmailOTPModal,
|
|
580
|
+
{
|
|
581
|
+
mode: "disable",
|
|
582
|
+
open: emailDisableOpen,
|
|
583
|
+
email: userEmail,
|
|
584
|
+
onOpenChange: (open) => {
|
|
585
|
+
if (!open) setEmailDisableOpen(false);
|
|
586
|
+
},
|
|
587
|
+
onSuccess: () => {
|
|
588
|
+
setEnabled(null);
|
|
589
|
+
setMfaType(null);
|
|
590
|
+
setEmailDisableOpen(false);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
333
593
|
)
|
|
334
594
|
] });
|
|
335
595
|
};
|