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.
- package/README.md +2 -1
- package/dist/admin/{AdminReset-H453s-DE.mjs → AdminReset-B-WGECOX.mjs} +1 -1
- package/dist/admin/{AdminReset-C0QibZXW.js → AdminReset-CqHhVBS_.js} +1 -1
- package/dist/admin/{ProfileToggle-DQeXCx34.mjs → ProfileToggle-BCtCsOvj.mjs} +298 -38
- package/dist/admin/{ProfileToggle-BFmIWCrN.js → ProfileToggle-BRYjt5Lu.js} +298 -38
- package/dist/admin/{SettingsPage-OwMik_IK.mjs → SettingsPage-7Ytl01jH.mjs} +343 -101
- package/dist/admin/{SettingsPage-DyF7YbsX.js → SettingsPage-DAxGIv_E.js} +342 -100
- 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-D45I6rWF.mjs → index-BfC6z9N5.mjs} +62 -7
- package/dist/admin/{index-BXZI8nMZ.js → index-D03zlFnm.js} +62 -7
- 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/{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 +4 -3
- 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,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-
|
|
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-
|
|
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-
|
|
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 [
|
|
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
|
|
239
|
-
const enabledBody = await
|
|
240
|
-
if (!
|
|
241
|
-
throw new Error(`${
|
|
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 (!
|
|
244
|
-
throw new Error(`${
|
|
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__ */
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
id: getTranslation("profile.
|
|
287
|
-
defaultMessage: "
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
};
|