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
@@ -10,12 +10,71 @@ const require$$0__default = /* @__PURE__ */ _interopDefault(require$$0);
10
10
  const require$$3__default = /* @__PURE__ */ _interopDefault(require$$3);
11
11
  const require$$5__default = /* @__PURE__ */ _interopDefault(require$$5);
12
12
  const require$$1__default = /* @__PURE__ */ _interopDefault(require$$1);
13
+ const defaultConfig$1 = {
14
+ enabled: false,
15
+ enforce: false,
16
+ issuer: "Strapi",
17
+ from_email: "",
18
+ from_name: "",
19
+ response_email: "",
20
+ subject: "Your One-Time Password",
21
+ text: "Your one-time password is: <%= OTP %>",
22
+ message: `<div style="margin: 0; padding: 0;">
23
+ <table border="0" cellpadding="0" cellspacing="0" width="100%" bgcolor="#f9f9f9">
24
+ <tr>
25
+ <td align="center" style="padding: 40px 10px;">
26
+ <table border="0" cellpadding="0" cellspacing="0" width="600" bgcolor="#ffffff" style="border: 1px solid #dddddd;">
27
+ <tr>
28
+ <td align="center" style="padding: 40px 40px 20px 40px;">
29
+ <font face="Arial, sans-serif" size="5" color="#333333">
30
+ <strong>Verify Your Account</strong>
31
+ </font>
32
+ </td>
33
+ </tr>
34
+ <tr>
35
+ <td align="center" style="padding: 0 40px 20px 40px;">
36
+ <font face="Arial, sans-serif" size="3" color="#555555" style="line-height: 24px;">
37
+ Please use the following one-time password to complete your registration. This code will expire in 10 minutes.
38
+ </font>
39
+ </td>
40
+ </tr>
41
+ <tr>
42
+ <td align="center" style="padding: 20px 40px;">
43
+ <table border="0" cellpadding="0" cellspacing="0">
44
+ <tr>
45
+ <td align="center" bgcolor="#f4f4f4" style="padding: 20px 30px; font-family: Courier, monospace; font-size: 36px; color: #2d3436;">
46
+ <strong><%= OTP %></strong>
47
+ </td>
48
+ </tr>
49
+ </table>
50
+ </td>
51
+ </tr>
52
+ <tr>
53
+ <td align="center" style="padding: 20px 40px 40px 40px;">
54
+ <font face="Arial, sans-serif" size="2" color="#888888">
55
+ If you did not request this code, please ignore this email.
56
+ </font>
57
+ </td>
58
+ </tr>
59
+ </table>
60
+ <table border="0" cellpadding="0" cellspacing="0" width="600">
61
+ <tr>
62
+ <td align="center" style="padding: 20px;">
63
+ <font face="Arial, sans-serif" size="1" color="#aaaaaa">
64
+ &copy; <%= YEAR %> Your Company Name
65
+ </font>
66
+ </td>
67
+ </tr>
68
+ </table>
69
+ </td>
70
+ </tr>
71
+ </table>
72
+ </div>`
73
+ };
13
74
  const bootstrap = async () => {
14
75
  const config2 = strapi.documents("plugin::strapi-identity.strapi-identity-config");
15
76
  const existingConfig = await config2.count({});
16
- if (!existingConfig) {
17
- await config2.create({ data: { enabled: false, enforce: false, issuer: "Strapi" } });
18
- }
77
+ if (!existingConfig) await config2.create({ data: defaultConfig$1 });
19
78
  strapi.admin.services.permission.actionProvider.registerMany([
20
79
  {
21
80
  uid: "settings.read",
@@ -3828,7 +3887,7 @@ function requireLodash() {
3828
3887
  (function(module2, exports$1) {
3829
3888
  (function() {
3830
3889
  var undefined$1;
3831
- var VERSION = "4.17.21";
3890
+ var VERSION = "4.17.23";
3832
3891
  var LARGE_ARRAY_SIZE = 200;
3833
3892
  var CORE_ERROR_TEXT = "Unsupported core-js use. Try https://npms.io/search?q=ponyfill.", FUNC_ERROR_TEXT = "Expected a function", INVALID_TEMPL_VAR_ERROR_TEXT = "Invalid `variable` option passed into `_.template`";
3834
3893
  var HASH_UNDEFINED = "__lodash_hash_undefined__";
@@ -5756,8 +5815,28 @@ function requireLodash() {
5756
5815
  }
5757
5816
  function baseUnset(object, path) {
5758
5817
  path = castPath(path, object);
5759
- object = parent(object, path);
5760
- return object == null || delete object[toKey(last(path))];
5818
+ var index2 = -1, length = path.length;
5819
+ if (!length) {
5820
+ return true;
5821
+ }
5822
+ var isRootPrimitive = object == null || typeof object !== "object" && typeof object !== "function";
5823
+ while (++index2 < length) {
5824
+ var key = path[index2];
5825
+ if (typeof key !== "string") {
5826
+ continue;
5827
+ }
5828
+ if (key === "__proto__" && !hasOwnProperty.call(object, "__proto__")) {
5829
+ return false;
5830
+ }
5831
+ if (key === "constructor" && index2 + 1 < length && typeof path[index2 + 1] === "string" && path[index2 + 1] === "prototype") {
5832
+ if (isRootPrimitive && index2 === 0) {
5833
+ continue;
5834
+ }
5835
+ return false;
5836
+ }
5837
+ }
5838
+ var obj = parent(object, path);
5839
+ return obj == null || delete obj[toKey(last(path))];
5761
5840
  }
5762
5841
  function baseUpdate(object, path, updater, customizer) {
5763
5842
  return baseSet(object, path, updater(baseGet(object, path)), customizer);
@@ -9540,14 +9619,14 @@ function requireJsonwebtoken() {
9540
9619
  var jsonwebtokenExports = requireJsonwebtoken();
9541
9620
  const jwt = /* @__PURE__ */ getDefaultExportFromCjs(jsonwebtokenExports);
9542
9621
  const register = ({ strapi: strapi2 }) => {
9543
- const adminPlugin = strapi2.admin;
9544
- const secret2 = strapi2.config.get("admin.auth.secret");
9545
- const domain = strapi2.config.get("admin.auth.domain");
9546
- const loginRoute = adminPlugin.routes.admin.routes.find(
9622
+ const { admin: admin2, config: config2, server } = strapi2;
9623
+ const secret2 = config2.get("admin.auth.secret");
9624
+ const domain = config2.get("admin.auth.domain");
9625
+ const loginRoute = admin2.routes.admin.routes.find(
9547
9626
  ({ method, path }) => method === "POST" && path === "/login"
9548
9627
  );
9549
9628
  if (loginRoute) replaceLogin(loginRoute, secret2, domain);
9550
- strapi2.server.use(async (ctx, next) => {
9629
+ server.use(async (ctx, next) => {
9551
9630
  const mfaCookie = ctx.cookies.get("strapi_admin_mfa");
9552
9631
  if (mfaCookie && ctx.path.startsWith("/admin/auth")) {
9553
9632
  ctx.cookies.set("jwtToken", null, { expires: /* @__PURE__ */ new Date(0) });
@@ -9575,8 +9654,25 @@ const replaceLogin = (route2, secret2, domain) => {
9575
9654
  filters: { admin_user: { id: payload.userId }, enabled: true }
9576
9655
  });
9577
9656
  if (!exists) return;
9657
+ if (exists.type === "email") {
9658
+ try {
9659
+ const adminUser = await strapi.db.query("admin::user").findOne({ where: { id: Number(payload.userId) }, select: ["email"] });
9660
+ if (adminUser?.email) {
9661
+ const otp = await strapi.service("plugin::strapi-identity.secret").generateEmailOTP(payload.userId, "login");
9662
+ await strapi.service("plugin::strapi-identity.email").send(adminUser.email, otp);
9663
+ }
9664
+ } catch (err) {
9665
+ console.log("Error sending login email OTP:", err);
9666
+ }
9667
+ }
9578
9668
  ctx.res.removeHeader("set-cookie");
9579
- const newPayload = { userId: payload.userId, deviceId, rememberMe, type: "mfa" };
9669
+ const newPayload = {
9670
+ userId: payload.userId,
9671
+ deviceId,
9672
+ rememberMe,
9673
+ type: "mfa",
9674
+ mfaType: exists.type
9675
+ };
9580
9676
  const newToken = jwt.sign(newPayload, secret2, { expiresIn: "5m" });
9581
9677
  const expires = new Date(Date.now() + 5 * 60 * 1e3);
9582
9678
  const opt = { domain, httpOnly: false, overwrite: true, secure: ctx.request.secure, expires };
@@ -9589,12 +9685,27 @@ const config$4 = {
9589
9685
  validator() {
9590
9686
  }
9591
9687
  };
9688
+ const kind$3 = "collectionType";
9689
+ const collectionName$3 = "mfa-tokens";
9690
+ const info$3 = { "singularName": "mfa-token", "pluralName": "mfa-tokens", "displayName": "MFA Token" };
9691
+ const options$3 = { "draftAndPublish": false };
9692
+ const pluginOptions$3 = { "content-manager": { "visible": false }, "content-type-builder": { "visible": false } };
9693
+ const attributes$3 = { "admin_user": { "type": "relation", "relation": "oneToOne", "target": "admin::user" }, "enabled": { "type": "boolean", "default": false }, "type": { "type": "enumeration", "enum": ["totp", "hotp", "email"], "default": "totp" }, "secret": { "type": "string", "private": true }, "counter": { "type": "biginteger", "default": "0" }, "digits": { "type": "integer", "default": 6 }, "recovery_codes": { "type": "json", "private": true } };
9694
+ const schema$3 = {
9695
+ kind: kind$3,
9696
+ collectionName: collectionName$3,
9697
+ info: info$3,
9698
+ options: options$3,
9699
+ pluginOptions: pluginOptions$3,
9700
+ attributes: attributes$3
9701
+ };
9702
+ const mfaToken = { schema: schema$3 };
9592
9703
  const kind$2 = "collectionType";
9593
- const collectionName$2 = "mfa-tokens";
9594
- const info$2 = { "singularName": "mfa-token", "pluralName": "mfa-tokens", "displayName": "MFA Token" };
9704
+ const collectionName$2 = "mfa-temps";
9705
+ const info$2 = { "singularName": "mfa-temp", "pluralName": "mfa-temps", "displayName": "MFA Temp" };
9595
9706
  const options$2 = { "draftAndPublish": false };
9596
9707
  const pluginOptions$2 = { "content-manager": { "visible": false }, "content-type-builder": { "visible": false } };
9597
- const attributes$2 = { "admin_user": { "type": "relation", "relation": "oneToOne", "target": "admin::user" }, "enabled": { "type": "boolean", "default": false }, "type": { "type": "enumeration", "enum": ["totp", "hotp"], "default": "totp" }, "secret": { "type": "string", "required": true, "private": true }, "counter": { "type": "biginteger", "default": "0" }, "digits": { "type": "integer", "default": 6 }, "recovery_codes": { "type": "json", "private": true } };
9708
+ const attributes$2 = { "admin_user": { "type": "relation", "relation": "oneToOne", "target": "admin::user" }, "secret": { "type": "string", "required": false, "private": true } };
9598
9709
  const schema$2 = {
9599
9710
  kind: kind$2,
9600
9711
  collectionName: collectionName$2,
@@ -9603,13 +9714,13 @@ const schema$2 = {
9603
9714
  pluginOptions: pluginOptions$2,
9604
9715
  attributes: attributes$2
9605
9716
  };
9606
- const mfaToken = { schema: schema$2 };
9607
- const kind$1 = "collectionType";
9608
- const collectionName$1 = "mfa-temps";
9609
- const info$1 = { "singularName": "mfa-temp", "pluralName": "mfa-temps", "displayName": "MFA Temp" };
9717
+ const mfaTemp = { schema: schema$2 };
9718
+ const kind$1 = "singleType";
9719
+ const collectionName$1 = "strapi-identity-config";
9720
+ const info$1 = { "singularName": "strapi-identity-config", "pluralName": "strapi-identity-configs", "displayName": "Strapi Identity Config" };
9610
9721
  const options$1 = { "draftAndPublish": false };
9611
9722
  const pluginOptions$1 = { "content-manager": { "visible": false }, "content-type-builder": { "visible": false } };
9612
- const attributes$1 = { "admin_user": { "type": "relation", "relation": "oneToOne", "target": "admin::user" }, "secret": { "type": "string", "required": false, "private": true } };
9723
+ const attributes$1 = { "enabled": { "type": "boolean", "default": false }, "enforce": { "type": "boolean", "default": false }, "issuer": { "type": "string", "required": false, "default": "Strapi" }, "email_enabled": { "type": "boolean", "default": false }, "from_email": { "type": "string", "required": false, "default": "" }, "from_name": { "type": "string", "required": false, "default": "" }, "response_email": { "type": "string", "required": false, "default": "" }, "subject": { "type": "string", "required": false, "default": "" }, "text": { "type": "text", "required": false, "default": "" }, "message": { "type": "text", "required": false, "default": "" } };
9613
9724
  const schema$1 = {
9614
9725
  kind: kind$1,
9615
9726
  collectionName: collectionName$1,
@@ -9618,13 +9729,13 @@ const schema$1 = {
9618
9729
  pluginOptions: pluginOptions$1,
9619
9730
  attributes: attributes$1
9620
9731
  };
9621
- const mfaTemp = { schema: schema$1 };
9622
- const kind = "singleType";
9623
- const collectionName = "strapi-identity-config";
9624
- const info = { "singularName": "strapi-identity-config", "pluralName": "strapi-identity-configs", "displayName": "Strapi Identity Config" };
9732
+ const config$3 = { schema: schema$1 };
9733
+ const kind = "collectionType";
9734
+ const collectionName = "email-otps";
9735
+ const info = { "singularName": "email-otp", "pluralName": "email-otps", "displayName": "Email OTP" };
9625
9736
  const options = { "draftAndPublish": false };
9626
9737
  const pluginOptions = { "content-manager": { "visible": false }, "content-type-builder": { "visible": false } };
9627
- const attributes = { "enabled": { "type": "boolean", "default": false }, "enforce": { "type": "boolean", "default": false }, "issuer": { "type": "string", "required": false, "default": "Strapi" } };
9738
+ const attributes = { "admin_user": { "type": "relation", "relation": "oneToOne", "target": "admin::user" }, "code_hash": { "type": "string", "required": true, "private": true }, "expires_at": { "type": "string", "required": true }, "attempts": { "type": "integer", "default": 0 }, "purpose": { "type": "enumeration", "enum": ["login", "setup", "disable"], "default": "login" } };
9628
9739
  const schema = {
9629
9740
  kind,
9630
9741
  collectionName,
@@ -9633,11 +9744,12 @@ const schema = {
9633
9744
  pluginOptions,
9634
9745
  attributes
9635
9746
  };
9636
- const config$3 = { schema };
9747
+ const emailOtp = { schema };
9637
9748
  const contentTypes = {
9638
9749
  "mfa-token": mfaToken,
9639
9750
  "mfa-temp": mfaTemp,
9640
- "strapi-identity-config": config$3
9751
+ "strapi-identity-config": config$3,
9752
+ "email-otp": emailOtp
9641
9753
  };
9642
9754
  const admin$2 = ({ strapi: strapi2 }) => ({
9643
9755
  async isEnabled(ctx) {
@@ -9698,6 +9810,17 @@ const config$2 = ({ strapi: strapi2 }) => ({
9698
9810
  ctx.body = { data: null, error: "Server Error" };
9699
9811
  }
9700
9812
  },
9813
+ async getEmailStatus(ctx) {
9814
+ try {
9815
+ const emailService = strapi2.config.get("plugin::email");
9816
+ ctx.status = 200;
9817
+ ctx.body = { data: emailService, error: null };
9818
+ } catch (error) {
9819
+ console.log("Error getting email status:", error);
9820
+ ctx.status = 500;
9821
+ ctx.body = { data: null, error: "Server Error" };
9822
+ }
9823
+ },
9701
9824
  async updateConfig(ctx) {
9702
9825
  const body = ctx.request.body;
9703
9826
  try {
@@ -9764,7 +9887,13 @@ const controller = ({ strapi: strapi2 }) => ({
9764
9887
  const payload = jwt.verify(token, secret2);
9765
9888
  const body = ctx.request.body;
9766
9889
  try {
9767
- const valid2 = await strapi2.service("plugin::strapi-identity.secret").validateTokenOrRecoveryCode(payload.userId, body.code);
9890
+ const mfaRecord = await strapi2.documents("plugin::strapi-identity.mfa-token").findFirst({ filters: { admin_user: { id: payload.userId }, enabled: true } });
9891
+ let valid2;
9892
+ if (mfaRecord?.type === "email") {
9893
+ valid2 = await strapi2.service("plugin::strapi-identity.secret").validateEmailOTP(payload.userId, body.code, "login");
9894
+ } else {
9895
+ valid2 = await strapi2.service("plugin::strapi-identity.secret").validateTokenOrRecoveryCode(payload.userId, body.code);
9896
+ }
9768
9897
  if (!valid2) {
9769
9898
  ctx.status = 400;
9770
9899
  ctx.body = { data: null, error: "Invalid MFA code" };
@@ -9847,9 +9976,9 @@ const controller = ({ strapi: strapi2 }) => ({
9847
9976
  const user = ctx.state.user;
9848
9977
  const secretService = strapi2.service("plugin::strapi-identity.secret");
9849
9978
  try {
9850
- const isEnabled2 = await secretService.isMFAEnabled(user.id);
9979
+ const info2 = await secretService.getMFAInfo(user.id);
9851
9980
  ctx.status = 200;
9852
- ctx.body = { data: { status: isEnabled2 }, error: null };
9981
+ ctx.body = { data: { status: info2?.status || null, type: info2?.type || null }, error: null };
9853
9982
  } catch (error) {
9854
9983
  ctx.status = 500;
9855
9984
  ctx.body = { data: null, error: "Failed to check MFA status" };
@@ -9868,6 +9997,103 @@ const controller = ({ strapi: strapi2 }) => ({
9868
9997
  ctx.status = 500;
9869
9998
  ctx.body = { data: null, error: "Failed to disable MFA" };
9870
9999
  }
10000
+ },
10001
+ async enableEmail(ctx) {
10002
+ const user = ctx.state.user;
10003
+ const secretService = strapi2.service("plugin::strapi-identity.secret");
10004
+ const emailService = strapi2.service("plugin::strapi-identity.email");
10005
+ const configService = strapi2.service("plugin::strapi-identity.config");
10006
+ try {
10007
+ const config2 = await configService.getConfig();
10008
+ if (!config2.email_enabled) {
10009
+ ctx.status = 400;
10010
+ ctx.body = { data: null, error: "Email OTP is not configured" };
10011
+ return;
10012
+ }
10013
+ const existing = await strapi2.documents("plugin::strapi-identity.mfa-token").findFirst({ filters: { admin_user: { id: user.id }, enabled: true } });
10014
+ if (existing) {
10015
+ ctx.status = 400;
10016
+ ctx.body = { data: null, error: "MFA is already enabled. Disable it first." };
10017
+ return;
10018
+ }
10019
+ const otp = await secretService.generateEmailOTP(user.id, "setup");
10020
+ await emailService.send(user.email, otp);
10021
+ ctx.status = 200;
10022
+ ctx.body = { data: { message: "Verification email sent" }, error: null };
10023
+ } catch (error) {
10024
+ console.log("Error initiating email MFA setup:", error);
10025
+ ctx.status = 500;
10026
+ ctx.body = { data: null, error: "Failed to initiate email MFA setup" };
10027
+ }
10028
+ },
10029
+ async setupEmail(ctx) {
10030
+ const user = ctx.state.user;
10031
+ const body = ctx.request.body;
10032
+ const secretService = strapi2.service("plugin::strapi-identity.secret");
10033
+ try {
10034
+ const valid2 = await secretService.validateEmailOTP(user.id, body.code, "setup");
10035
+ if (!valid2) {
10036
+ ctx.status = 400;
10037
+ ctx.body = { data: null, error: "Invalid or expired verification code" };
10038
+ return;
10039
+ }
10040
+ await secretService.enableEmailMFA(user.id);
10041
+ ctx.status = 200;
10042
+ ctx.body = { data: { message: "Email OTP enabled" }, error: null };
10043
+ } catch (error) {
10044
+ console.log("Error completing email MFA setup:", error);
10045
+ ctx.status = 500;
10046
+ ctx.body = { data: null, error: "Failed to enable email MFA" };
10047
+ }
10048
+ },
10049
+ async requestDisableEmailOTP(ctx) {
10050
+ const user = ctx.state.user;
10051
+ const secretService = strapi2.service("plugin::strapi-identity.secret");
10052
+ const emailService = strapi2.service("plugin::strapi-identity.email");
10053
+ try {
10054
+ const info2 = await secretService.getMFAInfo(user.id);
10055
+ if (!info2 || info2.type !== "email") {
10056
+ ctx.status = 400;
10057
+ ctx.body = { data: null, error: "Email OTP is not enabled" };
10058
+ return;
10059
+ }
10060
+ const otp = await secretService.generateEmailOTP(user.id, "disable");
10061
+ await emailService.send(user.email, otp);
10062
+ ctx.status = 200;
10063
+ ctx.body = { data: { message: "Verification email sent" }, error: null };
10064
+ } catch (error) {
10065
+ console.log("Error sending disable email OTP:", error);
10066
+ ctx.status = 500;
10067
+ ctx.body = { data: null, error: "Failed to send verification email" };
10068
+ }
10069
+ },
10070
+ async resendVerifyOTP(ctx) {
10071
+ const secret2 = strapi2.config.get("admin.auth.secret");
10072
+ const token = ctx.cookies.get("strapi_admin_mfa");
10073
+ try {
10074
+ const payload = jwt.verify(token, secret2);
10075
+ if (payload.mfaType !== "email") {
10076
+ ctx.status = 400;
10077
+ ctx.body = { data: null, error: "Resend is only available for email OTP" };
10078
+ return;
10079
+ }
10080
+ const adminUser = await strapi2.db.query("admin::user").findOne({ where: { id: Number(payload.userId) }, select: ["email"] });
10081
+ if (!adminUser?.email) {
10082
+ ctx.status = 400;
10083
+ ctx.body = { data: null, error: "No email address found for this user" };
10084
+ return;
10085
+ }
10086
+ const secretService = strapi2.service("plugin::strapi-identity.secret");
10087
+ const emailService = strapi2.service("plugin::strapi-identity.email");
10088
+ const otp = await secretService.generateEmailOTP(payload.userId, "login");
10089
+ await emailService.send(adminUser.email, otp);
10090
+ ctx.status = 200;
10091
+ ctx.body = { data: { message: "Verification email resent" }, error: null };
10092
+ } catch (error) {
10093
+ console.log("Error resending login email OTP:", error);
10094
+ ctx.status = 500;
10095
+ ctx.body = { data: null, error: "Failed to resend verification email" };
10096
+ }
9871
10097
  }
9872
10098
  });
9873
10099
  const controllers = {
@@ -9967,6 +10193,21 @@ const config$1 = [
9967
10193
  ]
9968
10194
  }
9969
10195
  },
10196
+ {
10197
+ method: "GET",
10198
+ path: "/config/email",
10199
+ handler: "config.getEmailStatus",
10200
+ info: {
10201
+ apiName: "getEmailStatus",
10202
+ pluginName: "strapi-identity",
10203
+ type: "content-api"
10204
+ },
10205
+ config: {
10206
+ policies: [
10207
+ "admin::isAuthenticatedAdmin"
10208
+ ]
10209
+ }
10210
+ },
9970
10211
  {
9971
10212
  method: "PUT",
9972
10213
  path: "/config",
@@ -10051,6 +10292,55 @@ const mfa = [
10051
10292
  type: "content-api"
10052
10293
  },
10053
10294
  config: {}
10295
+ },
10296
+ {
10297
+ method: "POST",
10298
+ path: "/verify/resend",
10299
+ handler: "controller.resendVerifyOTP",
10300
+ info: {
10301
+ apiName: "resendVerifyOTP",
10302
+ pluginName: "strapi-identity",
10303
+ type: "content-api"
10304
+ },
10305
+ config: {
10306
+ auth: false,
10307
+ policies: [
10308
+ "has-mfa"
10309
+ ]
10310
+ }
10311
+ },
10312
+ {
10313
+ method: "POST",
10314
+ path: "/enable-email",
10315
+ handler: "controller.enableEmail",
10316
+ info: {
10317
+ apiName: "enableEmail",
10318
+ pluginName: "strapi-identity",
10319
+ type: "content-api"
10320
+ },
10321
+ config: {}
10322
+ },
10323
+ {
10324
+ method: "POST",
10325
+ path: "/setup-email",
10326
+ handler: "controller.setupEmail",
10327
+ info: {
10328
+ apiName: "setupEmail",
10329
+ pluginName: "strapi-identity",
10330
+ type: "content-api"
10331
+ },
10332
+ config: {}
10333
+ },
10334
+ {
10335
+ method: "POST",
10336
+ path: "/disable-email/request",
10337
+ handler: "controller.requestDisableEmailOTP",
10338
+ info: {
10339
+ apiName: "requestDisableEmailOTP",
10340
+ pluginName: "strapi-identity",
10341
+ type: "content-api"
10342
+ },
10343
+ config: {}
10054
10344
  }
10055
10345
  ];
10056
10346
  const route = () => ({
@@ -10091,35 +10381,63 @@ const admin = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.definePropert
10091
10381
  isEnabled: isEnabled$1,
10092
10382
  reset
10093
10383
  }, Symbol.toStringTag, { value: "Module" }));
10384
+ const defaultConfig = {
10385
+ email_enabled: false,
10386
+ enabled: false,
10387
+ enforce: false,
10388
+ from_email: "",
10389
+ from_name: "",
10390
+ issuer: "",
10391
+ message: "",
10392
+ response_email: "",
10393
+ subject: "",
10394
+ text: ""
10395
+ };
10094
10396
  const _config = (options2) => {
10095
- return {
10096
- enabled: options2?.enabled || false,
10097
- enforce: options2?.enforce || false,
10098
- issuer: options2?.issuer || ""
10099
- };
10397
+ return Object.assign({}, defaultConfig, options2);
10100
10398
  };
10399
+ const fields = Object.keys(defaultConfig);
10101
10400
  const isEnabled = async () => {
10102
10401
  const config2 = await getConfig();
10103
10402
  return config2?.enabled || false;
10104
10403
  };
10105
10404
  const getConfig = async () => {
10106
10405
  const configDocument = strapi.documents("plugin::strapi-identity.strapi-identity-config");
10107
- return configDocument.findFirst({ fields: ["enabled", "enforce", "issuer"] }).then((config2) => _config(config2));
10406
+ return configDocument.findFirst({ fields }).then((config2) => _config(config2));
10108
10407
  };
10109
10408
  const updateConfig = async (data) => {
10110
10409
  const configDocument = strapi.documents("plugin::strapi-identity.strapi-identity-config");
10111
10410
  const existingConfig = await configDocument.findFirst();
10112
10411
  if (!existingConfig) {
10113
- return configDocument.create({ data, fields: ["enabled", "enforce", "issuer"] }).then((created) => _config(created));
10412
+ return configDocument.create({ data, fields }).then((created) => _config(created));
10114
10413
  }
10115
- if (existingConfig.enabled && !data.enabled) {
10116
- await disableMFAForAllUsers();
10414
+ if (existingConfig.enabled && !data.enabled) await disableMFAForAllUsers();
10415
+ if (existingConfig.email_enabled && data.email_enabled === false)
10416
+ await disableEmailMFAForAllUsers();
10417
+ return configDocument.update({ documentId: existingConfig.documentId, data: { ...existingConfig, ...data }, fields }).then((updated) => _config(updated));
10418
+ };
10419
+ const disableEmailMFAForAllUsers = async () => {
10420
+ const tokenDocument = strapi.documents("plugin::strapi-identity.mfa-token");
10421
+ const otpDocument = strapi.documents("plugin::strapi-identity.email-otp");
10422
+ try {
10423
+ const emailTokens = await tokenDocument.findMany({
10424
+ filters: { type: "email", enabled: true }
10425
+ });
10426
+ await Promise.all([
10427
+ ...emailTokens.map(
10428
+ (token) => tokenDocument.update({
10429
+ documentId: token.documentId,
10430
+ data: { ...token, enabled: false }
10431
+ })
10432
+ ),
10433
+ // Clean up any pending email OTPs
10434
+ otpDocument.findMany({}).then(
10435
+ (otps) => Promise.all(otps.map((otp) => otpDocument.delete({ documentId: otp.documentId })))
10436
+ )
10437
+ ]);
10438
+ } catch (err) {
10439
+ console.log("Error disabling email MFA for all users:", err);
10117
10440
  }
10118
- return configDocument.update({
10119
- documentId: existingConfig.documentId,
10120
- data: { ...existingConfig, ...data },
10121
- fields: ["enabled", "enforce", "issuer"]
10122
- }).then((updated) => _config(updated));
10123
10441
  };
10124
10442
  const disableMFAForAllUsers = async () => {
10125
10443
  const tokenDocument = strapi.documents("plugin::strapi-identity.mfa-token");
@@ -10143,6 +10461,39 @@ const config = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProper
10143
10461
  isEnabled,
10144
10462
  updateConfig
10145
10463
  }, Symbol.toStringTag, { value: "Module" }));
10464
+ const send = async (to, otp) => {
10465
+ const emailService = strapi.plugin("email").service("email");
10466
+ if (!emailService) return;
10467
+ if (!to) return;
10468
+ const config2 = await strapi.service("plugin::strapi-identity.config").getConfig();
10469
+ if (!config2.email_enabled) return;
10470
+ const sendConfig = {
10471
+ to,
10472
+ subject: config2.subject,
10473
+ text: replaceTemplateVariables(config2.text, { OTP: otp }),
10474
+ html: replaceTemplateVariables(config2.message, {
10475
+ OTP: otp,
10476
+ YEAR: (/* @__PURE__ */ new Date()).getFullYear().toString()
10477
+ })
10478
+ };
10479
+ if (config2.from_email) {
10480
+ sendConfig.from = config2.from_name ? `${config2.from_name} <${config2.from_email}>` : config2.from_email;
10481
+ }
10482
+ if (config2.response_email && config2.response_email !== config2.from_email) {
10483
+ sendConfig.replyTo = config2.response_email;
10484
+ }
10485
+ return emailService.send(sendConfig).catch((error) => {
10486
+ console.log("Error sending email:", error);
10487
+ });
10488
+ };
10489
+ const replaceTemplateVariables = (template, variables) => {
10490
+ return template.replace(/<%= (\w+) %>/g, (_, key) => variables[key] || "");
10491
+ };
10492
+ const email = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
10493
+ __proto__: null,
10494
+ replaceTemplateVariables,
10495
+ send
10496
+ }, Symbol.toStringTag, { value: "Module" }));
10146
10497
  function commonjsRequire(path) {
10147
10498
  throw new Error('Could not dynamically require "' + path + '". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.');
10148
10499
  }
@@ -11996,8 +12347,11 @@ const checkRecoveryCode = async (userId, code) => {
11996
12347
  };
11997
12348
  const validateLiveToken = async (userId, token) => {
11998
12349
  const tokenDocument = strapi.documents("plugin::strapi-identity.mfa-token");
11999
- const record = await tokenDocument.findFirst({ filters: { admin_user: { id: userId } } });
12000
- if (!record || !record.enabled) return false;
12350
+ const record = await tokenDocument.findFirst({
12351
+ filters: { admin_user: { id: userId }, enabled: true }
12352
+ });
12353
+ if (!record) return false;
12354
+ if (record.type === "email") return false;
12001
12355
  return validateToken(token, record.secret);
12002
12356
  };
12003
12357
  const validateTempToken = async (userId, token) => {
@@ -12043,12 +12397,13 @@ const setupFullSecret = async (userId) => {
12043
12397
  if (existing) {
12044
12398
  await tokenDocument.update({
12045
12399
  documentId: existing.documentId,
12046
- data: { ...existing, secret: tempField.secret, enabled: true, recovery_codes }
12400
+ data: { ...existing, type: "totp", secret: tempField.secret, enabled: true, recovery_codes }
12047
12401
  });
12048
12402
  } else {
12049
12403
  await tokenDocument.create({
12050
12404
  data: {
12051
12405
  admin_user: { id: userId },
12406
+ type: "totp",
12052
12407
  secret: tempField.secret,
12053
12408
  enabled: true,
12054
12409
  recovery_codes
@@ -12058,14 +12413,92 @@ const setupFullSecret = async (userId) => {
12058
12413
  await tempDocument.delete({ documentId: tempField.documentId });
12059
12414
  return codes;
12060
12415
  };
12416
+ const EMAIL_OTP_EXPIRY_MINUTES = 10;
12417
+ const EMAIL_OTP_MAX_ATTEMPTS = 5;
12418
+ const generateNumericOTP = (length) => {
12419
+ const digits = "0123456789";
12420
+ const max = 256 - 256 % digits.length;
12421
+ const randomValues = new Uint8Array(length * 2);
12422
+ let result = "";
12423
+ while (result.length < length) {
12424
+ crypto.getRandomValues(randomValues);
12425
+ for (let i = 0; i < randomValues.length && result.length < length; i++) {
12426
+ if (randomValues[i] >= max) continue;
12427
+ result += digits[randomValues[i] % digits.length];
12428
+ }
12429
+ }
12430
+ return result;
12431
+ };
12432
+ const generateEmailOTP = async (userId, purpose = "login") => {
12433
+ const otpDocument = strapi.documents("plugin::strapi-identity.email-otp");
12434
+ const otp = generateNumericOTP(6);
12435
+ const code_hash = await bcrypt.hash(otp, 10);
12436
+ const expires_at = new Date(Date.now() + EMAIL_OTP_EXPIRY_MINUTES * 60 * 1e3).toISOString();
12437
+ const existing = await otpDocument.findFirst({
12438
+ filters: { admin_user: { id: userId }, purpose }
12439
+ });
12440
+ if (existing) await otpDocument.delete({ documentId: existing.documentId });
12441
+ await otpDocument.create({
12442
+ data: { admin_user: { id: userId }, code_hash, expires_at, purpose, attempts: 0 }
12443
+ });
12444
+ return otp;
12445
+ };
12446
+ const validateEmailOTP = async (userId, code, purpose = "login") => {
12447
+ const otpDocument = strapi.documents("plugin::strapi-identity.email-otp");
12448
+ const record = await otpDocument.findFirst({ filters: { admin_user: { id: userId }, purpose } });
12449
+ if (!record) return false;
12450
+ if (new Date(record.expires_at) < /* @__PURE__ */ new Date()) {
12451
+ await otpDocument.delete({ documentId: record.documentId });
12452
+ return false;
12453
+ }
12454
+ const attempts = record.attempts || 0;
12455
+ if (attempts >= EMAIL_OTP_MAX_ATTEMPTS) {
12456
+ await otpDocument.delete({ documentId: record.documentId });
12457
+ return false;
12458
+ }
12459
+ await otpDocument.update({
12460
+ documentId: record.documentId,
12461
+ data: { ...record, attempts: attempts + 1 }
12462
+ });
12463
+ const isValid = await bcrypt.compare(code, record.code_hash);
12464
+ if (isValid) await otpDocument.delete({ documentId: record.documentId });
12465
+ return isValid;
12466
+ };
12467
+ const enableEmailMFA = async (userId) => {
12468
+ const tokenDocument = strapi.documents("plugin::strapi-identity.mfa-token");
12469
+ const existing = await tokenDocument.findFirst({ filters: { admin_user: { id: userId } } });
12470
+ if (existing) {
12471
+ await tokenDocument.update({
12472
+ documentId: existing.documentId,
12473
+ data: { ...existing, type: "email", enabled: true, secret: "" }
12474
+ });
12475
+ } else {
12476
+ await tokenDocument.create({
12477
+ data: { admin_user: { id: userId }, type: "email", enabled: true, secret: "" }
12478
+ });
12479
+ }
12480
+ };
12481
+ const getMFAInfo = async (userId) => {
12482
+ const tokenDocument = strapi.documents("plugin::strapi-identity.mfa-token");
12483
+ const record = await tokenDocument.findFirst({
12484
+ filters: { admin_user: { id: userId }, enabled: true }
12485
+ });
12486
+ if (!record) return null;
12487
+ return { status: "full", type: record.type };
12488
+ };
12061
12489
  const disableSecret = async (userId, code) => {
12062
- const validToken = await validateTokenOrRecoveryCode(userId, code);
12063
- if (!validToken) throw new Error("Invalid token or recovery code");
12064
12490
  const tokenDocument = strapi.documents("plugin::strapi-identity.mfa-token");
12065
12491
  const record = await tokenDocument.findFirst({
12066
12492
  filters: { admin_user: { id: userId }, enabled: true }
12067
12493
  });
12068
12494
  if (!record) throw new Error("MFA is not enabled for this user");
12495
+ let valid2;
12496
+ if (record.type === "email") {
12497
+ valid2 = await validateEmailOTP(userId, code, "disable");
12498
+ } else {
12499
+ valid2 = await validateTokenOrRecoveryCode(userId, code);
12500
+ }
12501
+ if (!valid2) throw new Error("Invalid token or recovery code");
12069
12502
  await tokenDocument.update({
12070
12503
  documentId: record.documentId,
12071
12504
  data: { ...record, enabled: false }
@@ -12108,14 +12541,18 @@ const secret = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProper
12108
12541
  __proto__: null,
12109
12542
  disableSecret,
12110
12543
  disableTempSecret,
12544
+ enableEmailMFA,
12545
+ generateEmailOTP,
12111
12546
  generateRecoveryCode,
12547
+ getMFAInfo,
12112
12548
  isMFAEnabled,
12113
12549
  setupFullSecret,
12114
12550
  setupTemporarySecret,
12551
+ validateEmailOTP,
12115
12552
  validateTempToken,
12116
12553
  validateTokenOrRecoveryCode
12117
12554
  }, Symbol.toStringTag, { value: "Module" }));
12118
- const services = { admin, config, secret };
12555
+ const services = { admin, config, email, secret };
12119
12556
  const index = {
12120
12557
  register,
12121
12558
  bootstrap,