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.
Files changed (88) hide show
  1. package/README.md +2 -1
  2. package/dist/admin/{AdminReset-H453s-DE.mjs → AdminReset-CCGgw-0k.mjs} +2 -2
  3. package/dist/admin/{AdminReset-C0QibZXW.js → AdminReset-QClQP2Il.js} +2 -2
  4. package/dist/admin/{ProfileToggle-BFmIWCrN.js → ProfileToggle-H31GHNDA.js} +299 -39
  5. package/dist/admin/{ProfileToggle-DQeXCx34.mjs → ProfileToggle-gPuH6dGP.mjs} +299 -39
  6. package/dist/admin/{SettingsPage-OwMik_IK.mjs → SettingsPage-DgPikS3m.mjs} +344 -102
  7. package/dist/admin/{SettingsPage-DyF7YbsX.js → SettingsPage-DulEjadb.js} +343 -101
  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-BXZI8nMZ.js → index-C9K2h5UC.js} +65 -12
  37. package/dist/admin/{index-D45I6rWF.mjs → index-C_4USMnn.mjs} +65 -12
  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/{tokenHelpers-jtoRu0q5.js → tokenHelpers-B54WRTn1.js} +4 -10
  71. package/dist/admin/{tokenHelpers-DagDzpso.mjs → tokenHelpers-CKVyT1sz.mjs} +4 -10
  72. package/dist/admin/{tr-DkIUODKq.mjs → tr-BLocNlbZ.mjs} +36 -23
  73. package/dist/admin/{tr-Bm1QZr4v.js → tr-Bmvs-Hx-.js} +36 -23
  74. package/dist/admin/{uk-FARzIGx4.js → uk-BDxn-EZU.js} +36 -23
  75. package/dist/admin/{uk-D7ArtSe3.mjs → uk-CyZ10xtq.mjs} +36 -23
  76. package/dist/admin/{vi-DS0yslPP.mjs → vi-Bx_UJ8up.mjs} +36 -23
  77. package/dist/admin/{vi-Bi9B6eTY.js → vi-F_mqQCme.js} +36 -23
  78. package/dist/admin/{zh-DkEx28ZA.js → zh-CFZJPG5N.js} +36 -23
  79. package/dist/admin/{zh-DwCvIPSz.mjs → zh-CjJdRa3l.mjs} +36 -23
  80. package/dist/admin/{zh-Hans-BwwKCR6_.js → zh-Hans-4BhSwSQw.js} +36 -23
  81. package/dist/admin/{zh-Hans-DP2xZyda.mjs → zh-Hans-s7G2GUHU.mjs} +36 -23
  82. package/dist/server/index.js +487 -50
  83. package/dist/server/index.mjs +487 -50
  84. package/package.json +5 -4
  85. package/dist/admin/de-BuYn1AYX.mjs +0 -26
  86. package/dist/admin/de-GItli7en.js +0 -26
  87. package/dist/admin/fr-Bt6sS5GX.mjs +0 -26
  88. package/dist/admin/fr-CbCW6hVD.js +0 -26
@@ -3,12 +3,71 @@ import require$$3 from "stream";
3
3
  import require$$5 from "util";
4
4
  import require$$1 from "crypto";
5
5
  import { TOTP, Secret } from "otpauth";
6
+ const defaultConfig$1 = {
7
+ enabled: false,
8
+ enforce: false,
9
+ issuer: "Strapi",
10
+ from_email: "",
11
+ from_name: "",
12
+ response_email: "",
13
+ subject: "Your One-Time Password",
14
+ text: "Your one-time password is: <%= OTP %>",
15
+ message: `<div style="margin: 0; padding: 0;">
16
+ <table border="0" cellpadding="0" cellspacing="0" width="100%" bgcolor="#f9f9f9">
17
+ <tr>
18
+ <td align="center" style="padding: 40px 10px;">
19
+ <table border="0" cellpadding="0" cellspacing="0" width="600" bgcolor="#ffffff" style="border: 1px solid #dddddd;">
20
+ <tr>
21
+ <td align="center" style="padding: 40px 40px 20px 40px;">
22
+ <font face="Arial, sans-serif" size="5" color="#333333">
23
+ <strong>Verify Your Account</strong>
24
+ </font>
25
+ </td>
26
+ </tr>
27
+ <tr>
28
+ <td align="center" style="padding: 0 40px 20px 40px;">
29
+ <font face="Arial, sans-serif" size="3" color="#555555" style="line-height: 24px;">
30
+ Please use the following one-time password to complete your registration. This code will expire in 10 minutes.
31
+ </font>
32
+ </td>
33
+ </tr>
34
+ <tr>
35
+ <td align="center" style="padding: 20px 40px;">
36
+ <table border="0" cellpadding="0" cellspacing="0">
37
+ <tr>
38
+ <td align="center" bgcolor="#f4f4f4" style="padding: 20px 30px; font-family: Courier, monospace; font-size: 36px; color: #2d3436;">
39
+ <strong><%= OTP %></strong>
40
+ </td>
41
+ </tr>
42
+ </table>
43
+ </td>
44
+ </tr>
45
+ <tr>
46
+ <td align="center" style="padding: 20px 40px 40px 40px;">
47
+ <font face="Arial, sans-serif" size="2" color="#888888">
48
+ If you did not request this code, please ignore this email.
49
+ </font>
50
+ </td>
51
+ </tr>
52
+ </table>
53
+ <table border="0" cellpadding="0" cellspacing="0" width="600">
54
+ <tr>
55
+ <td align="center" style="padding: 20px;">
56
+ <font face="Arial, sans-serif" size="1" color="#aaaaaa">
57
+ &copy; <%= YEAR %> Your Company Name
58
+ </font>
59
+ </td>
60
+ </tr>
61
+ </table>
62
+ </td>
63
+ </tr>
64
+ </table>
65
+ </div>`
66
+ };
6
67
  const bootstrap = async () => {
7
68
  const config2 = strapi.documents("plugin::strapi-identity.strapi-identity-config");
8
69
  const existingConfig = await config2.count({});
9
- if (!existingConfig) {
10
- await config2.create({ data: { enabled: false, enforce: false, issuer: "Strapi" } });
11
- }
70
+ if (!existingConfig) await config2.create({ data: defaultConfig$1 });
12
71
  strapi.admin.services.permission.actionProvider.registerMany([
13
72
  {
14
73
  uid: "settings.read",
@@ -3821,7 +3880,7 @@ function requireLodash() {
3821
3880
  (function(module, exports$1) {
3822
3881
  (function() {
3823
3882
  var undefined$1;
3824
- var VERSION = "4.17.21";
3883
+ var VERSION = "4.17.23";
3825
3884
  var LARGE_ARRAY_SIZE = 200;
3826
3885
  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`";
3827
3886
  var HASH_UNDEFINED = "__lodash_hash_undefined__";
@@ -5749,8 +5808,28 @@ function requireLodash() {
5749
5808
  }
5750
5809
  function baseUnset(object, path) {
5751
5810
  path = castPath(path, object);
5752
- object = parent(object, path);
5753
- return object == null || delete object[toKey(last(path))];
5811
+ var index2 = -1, length = path.length;
5812
+ if (!length) {
5813
+ return true;
5814
+ }
5815
+ var isRootPrimitive = object == null || typeof object !== "object" && typeof object !== "function";
5816
+ while (++index2 < length) {
5817
+ var key = path[index2];
5818
+ if (typeof key !== "string") {
5819
+ continue;
5820
+ }
5821
+ if (key === "__proto__" && !hasOwnProperty.call(object, "__proto__")) {
5822
+ return false;
5823
+ }
5824
+ if (key === "constructor" && index2 + 1 < length && typeof path[index2 + 1] === "string" && path[index2 + 1] === "prototype") {
5825
+ if (isRootPrimitive && index2 === 0) {
5826
+ continue;
5827
+ }
5828
+ return false;
5829
+ }
5830
+ }
5831
+ var obj = parent(object, path);
5832
+ return obj == null || delete obj[toKey(last(path))];
5754
5833
  }
5755
5834
  function baseUpdate(object, path, updater, customizer) {
5756
5835
  return baseSet(object, path, updater(baseGet(object, path)), customizer);
@@ -9533,14 +9612,14 @@ function requireJsonwebtoken() {
9533
9612
  var jsonwebtokenExports = requireJsonwebtoken();
9534
9613
  const jwt = /* @__PURE__ */ getDefaultExportFromCjs(jsonwebtokenExports);
9535
9614
  const register = ({ strapi: strapi2 }) => {
9536
- const adminPlugin = strapi2.admin;
9537
- const secret2 = strapi2.config.get("admin.auth.secret");
9538
- const domain = strapi2.config.get("admin.auth.domain");
9539
- const loginRoute = adminPlugin.routes.admin.routes.find(
9615
+ const { admin: admin2, config: config2, server } = strapi2;
9616
+ const secret2 = config2.get("admin.auth.secret");
9617
+ const domain = config2.get("admin.auth.domain");
9618
+ const loginRoute = admin2.routes.admin.routes.find(
9540
9619
  ({ method, path }) => method === "POST" && path === "/login"
9541
9620
  );
9542
9621
  if (loginRoute) replaceLogin(loginRoute, secret2, domain);
9543
- strapi2.server.use(async (ctx, next) => {
9622
+ server.use(async (ctx, next) => {
9544
9623
  const mfaCookie = ctx.cookies.get("strapi_admin_mfa");
9545
9624
  if (mfaCookie && ctx.path.startsWith("/admin/auth")) {
9546
9625
  ctx.cookies.set("jwtToken", null, { expires: /* @__PURE__ */ new Date(0) });
@@ -9568,8 +9647,25 @@ const replaceLogin = (route2, secret2, domain) => {
9568
9647
  filters: { admin_user: { id: payload.userId }, enabled: true }
9569
9648
  });
9570
9649
  if (!exists) return;
9650
+ if (exists.type === "email") {
9651
+ try {
9652
+ const adminUser = await strapi.db.query("admin::user").findOne({ where: { id: Number(payload.userId) }, select: ["email"] });
9653
+ if (adminUser?.email) {
9654
+ const otp = await strapi.service("plugin::strapi-identity.secret").generateEmailOTP(payload.userId, "login");
9655
+ await strapi.service("plugin::strapi-identity.email").send(adminUser.email, otp);
9656
+ }
9657
+ } catch (err) {
9658
+ console.log("Error sending login email OTP:", err);
9659
+ }
9660
+ }
9571
9661
  ctx.res.removeHeader("set-cookie");
9572
- const newPayload = { userId: payload.userId, deviceId, rememberMe, type: "mfa" };
9662
+ const newPayload = {
9663
+ userId: payload.userId,
9664
+ deviceId,
9665
+ rememberMe,
9666
+ type: "mfa",
9667
+ mfaType: exists.type
9668
+ };
9573
9669
  const newToken = jwt.sign(newPayload, secret2, { expiresIn: "5m" });
9574
9670
  const expires = new Date(Date.now() + 5 * 60 * 1e3);
9575
9671
  const opt = { domain, httpOnly: false, overwrite: true, secure: ctx.request.secure, expires };
@@ -9582,12 +9678,27 @@ const config$4 = {
9582
9678
  validator() {
9583
9679
  }
9584
9680
  };
9681
+ const kind$3 = "collectionType";
9682
+ const collectionName$3 = "mfa-tokens";
9683
+ const info$3 = { "singularName": "mfa-token", "pluralName": "mfa-tokens", "displayName": "MFA Token" };
9684
+ const options$3 = { "draftAndPublish": false };
9685
+ const pluginOptions$3 = { "content-manager": { "visible": false }, "content-type-builder": { "visible": false } };
9686
+ 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 } };
9687
+ const schema$3 = {
9688
+ kind: kind$3,
9689
+ collectionName: collectionName$3,
9690
+ info: info$3,
9691
+ options: options$3,
9692
+ pluginOptions: pluginOptions$3,
9693
+ attributes: attributes$3
9694
+ };
9695
+ const mfaToken = { schema: schema$3 };
9585
9696
  const kind$2 = "collectionType";
9586
- const collectionName$2 = "mfa-tokens";
9587
- const info$2 = { "singularName": "mfa-token", "pluralName": "mfa-tokens", "displayName": "MFA Token" };
9697
+ const collectionName$2 = "mfa-temps";
9698
+ const info$2 = { "singularName": "mfa-temp", "pluralName": "mfa-temps", "displayName": "MFA Temp" };
9588
9699
  const options$2 = { "draftAndPublish": false };
9589
9700
  const pluginOptions$2 = { "content-manager": { "visible": false }, "content-type-builder": { "visible": false } };
9590
- 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 } };
9701
+ const attributes$2 = { "admin_user": { "type": "relation", "relation": "oneToOne", "target": "admin::user" }, "secret": { "type": "string", "required": false, "private": true } };
9591
9702
  const schema$2 = {
9592
9703
  kind: kind$2,
9593
9704
  collectionName: collectionName$2,
@@ -9596,13 +9707,13 @@ const schema$2 = {
9596
9707
  pluginOptions: pluginOptions$2,
9597
9708
  attributes: attributes$2
9598
9709
  };
9599
- const mfaToken = { schema: schema$2 };
9600
- const kind$1 = "collectionType";
9601
- const collectionName$1 = "mfa-temps";
9602
- const info$1 = { "singularName": "mfa-temp", "pluralName": "mfa-temps", "displayName": "MFA Temp" };
9710
+ const mfaTemp = { schema: schema$2 };
9711
+ const kind$1 = "singleType";
9712
+ const collectionName$1 = "strapi-identity-config";
9713
+ const info$1 = { "singularName": "strapi-identity-config", "pluralName": "strapi-identity-configs", "displayName": "Strapi Identity Config" };
9603
9714
  const options$1 = { "draftAndPublish": false };
9604
9715
  const pluginOptions$1 = { "content-manager": { "visible": false }, "content-type-builder": { "visible": false } };
9605
- const attributes$1 = { "admin_user": { "type": "relation", "relation": "oneToOne", "target": "admin::user" }, "secret": { "type": "string", "required": false, "private": true } };
9716
+ 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": "" } };
9606
9717
  const schema$1 = {
9607
9718
  kind: kind$1,
9608
9719
  collectionName: collectionName$1,
@@ -9611,13 +9722,13 @@ const schema$1 = {
9611
9722
  pluginOptions: pluginOptions$1,
9612
9723
  attributes: attributes$1
9613
9724
  };
9614
- const mfaTemp = { schema: schema$1 };
9615
- const kind = "singleType";
9616
- const collectionName = "strapi-identity-config";
9617
- const info = { "singularName": "strapi-identity-config", "pluralName": "strapi-identity-configs", "displayName": "Strapi Identity Config" };
9725
+ const config$3 = { schema: schema$1 };
9726
+ const kind = "collectionType";
9727
+ const collectionName = "email-otps";
9728
+ const info = { "singularName": "email-otp", "pluralName": "email-otps", "displayName": "Email OTP" };
9618
9729
  const options = { "draftAndPublish": false };
9619
9730
  const pluginOptions = { "content-manager": { "visible": false }, "content-type-builder": { "visible": false } };
9620
- const attributes = { "enabled": { "type": "boolean", "default": false }, "enforce": { "type": "boolean", "default": false }, "issuer": { "type": "string", "required": false, "default": "Strapi" } };
9731
+ 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" } };
9621
9732
  const schema = {
9622
9733
  kind,
9623
9734
  collectionName,
@@ -9626,11 +9737,12 @@ const schema = {
9626
9737
  pluginOptions,
9627
9738
  attributes
9628
9739
  };
9629
- const config$3 = { schema };
9740
+ const emailOtp = { schema };
9630
9741
  const contentTypes = {
9631
9742
  "mfa-token": mfaToken,
9632
9743
  "mfa-temp": mfaTemp,
9633
- "strapi-identity-config": config$3
9744
+ "strapi-identity-config": config$3,
9745
+ "email-otp": emailOtp
9634
9746
  };
9635
9747
  const admin$2 = ({ strapi: strapi2 }) => ({
9636
9748
  async isEnabled(ctx) {
@@ -9691,6 +9803,17 @@ const config$2 = ({ strapi: strapi2 }) => ({
9691
9803
  ctx.body = { data: null, error: "Server Error" };
9692
9804
  }
9693
9805
  },
9806
+ async getEmailStatus(ctx) {
9807
+ try {
9808
+ const emailService = strapi2.config.get("plugin::email");
9809
+ ctx.status = 200;
9810
+ ctx.body = { data: emailService, error: null };
9811
+ } catch (error) {
9812
+ console.log("Error getting email status:", error);
9813
+ ctx.status = 500;
9814
+ ctx.body = { data: null, error: "Server Error" };
9815
+ }
9816
+ },
9694
9817
  async updateConfig(ctx) {
9695
9818
  const body = ctx.request.body;
9696
9819
  try {
@@ -9757,7 +9880,13 @@ const controller = ({ strapi: strapi2 }) => ({
9757
9880
  const payload = jwt.verify(token, secret2);
9758
9881
  const body = ctx.request.body;
9759
9882
  try {
9760
- const valid2 = await strapi2.service("plugin::strapi-identity.secret").validateTokenOrRecoveryCode(payload.userId, body.code);
9883
+ const mfaRecord = await strapi2.documents("plugin::strapi-identity.mfa-token").findFirst({ filters: { admin_user: { id: payload.userId }, enabled: true } });
9884
+ let valid2;
9885
+ if (mfaRecord?.type === "email") {
9886
+ valid2 = await strapi2.service("plugin::strapi-identity.secret").validateEmailOTP(payload.userId, body.code, "login");
9887
+ } else {
9888
+ valid2 = await strapi2.service("plugin::strapi-identity.secret").validateTokenOrRecoveryCode(payload.userId, body.code);
9889
+ }
9761
9890
  if (!valid2) {
9762
9891
  ctx.status = 400;
9763
9892
  ctx.body = { data: null, error: "Invalid MFA code" };
@@ -9840,9 +9969,9 @@ const controller = ({ strapi: strapi2 }) => ({
9840
9969
  const user = ctx.state.user;
9841
9970
  const secretService = strapi2.service("plugin::strapi-identity.secret");
9842
9971
  try {
9843
- const isEnabled2 = await secretService.isMFAEnabled(user.id);
9972
+ const info2 = await secretService.getMFAInfo(user.id);
9844
9973
  ctx.status = 200;
9845
- ctx.body = { data: { status: isEnabled2 }, error: null };
9974
+ ctx.body = { data: { status: info2?.status || null, type: info2?.type || null }, error: null };
9846
9975
  } catch (error) {
9847
9976
  ctx.status = 500;
9848
9977
  ctx.body = { data: null, error: "Failed to check MFA status" };
@@ -9861,6 +9990,103 @@ const controller = ({ strapi: strapi2 }) => ({
9861
9990
  ctx.status = 500;
9862
9991
  ctx.body = { data: null, error: "Failed to disable MFA" };
9863
9992
  }
9993
+ },
9994
+ async enableEmail(ctx) {
9995
+ const user = ctx.state.user;
9996
+ const secretService = strapi2.service("plugin::strapi-identity.secret");
9997
+ const emailService = strapi2.service("plugin::strapi-identity.email");
9998
+ const configService = strapi2.service("plugin::strapi-identity.config");
9999
+ try {
10000
+ const config2 = await configService.getConfig();
10001
+ if (!config2.email_enabled) {
10002
+ ctx.status = 400;
10003
+ ctx.body = { data: null, error: "Email OTP is not configured" };
10004
+ return;
10005
+ }
10006
+ const existing = await strapi2.documents("plugin::strapi-identity.mfa-token").findFirst({ filters: { admin_user: { id: user.id }, enabled: true } });
10007
+ if (existing) {
10008
+ ctx.status = 400;
10009
+ ctx.body = { data: null, error: "MFA is already enabled. Disable it first." };
10010
+ return;
10011
+ }
10012
+ const otp = await secretService.generateEmailOTP(user.id, "setup");
10013
+ await emailService.send(user.email, otp);
10014
+ ctx.status = 200;
10015
+ ctx.body = { data: { message: "Verification email sent" }, error: null };
10016
+ } catch (error) {
10017
+ console.log("Error initiating email MFA setup:", error);
10018
+ ctx.status = 500;
10019
+ ctx.body = { data: null, error: "Failed to initiate email MFA setup" };
10020
+ }
10021
+ },
10022
+ async setupEmail(ctx) {
10023
+ const user = ctx.state.user;
10024
+ const body = ctx.request.body;
10025
+ const secretService = strapi2.service("plugin::strapi-identity.secret");
10026
+ try {
10027
+ const valid2 = await secretService.validateEmailOTP(user.id, body.code, "setup");
10028
+ if (!valid2) {
10029
+ ctx.status = 400;
10030
+ ctx.body = { data: null, error: "Invalid or expired verification code" };
10031
+ return;
10032
+ }
10033
+ await secretService.enableEmailMFA(user.id);
10034
+ ctx.status = 200;
10035
+ ctx.body = { data: { message: "Email OTP enabled" }, error: null };
10036
+ } catch (error) {
10037
+ console.log("Error completing email MFA setup:", error);
10038
+ ctx.status = 500;
10039
+ ctx.body = { data: null, error: "Failed to enable email MFA" };
10040
+ }
10041
+ },
10042
+ async requestDisableEmailOTP(ctx) {
10043
+ const user = ctx.state.user;
10044
+ const secretService = strapi2.service("plugin::strapi-identity.secret");
10045
+ const emailService = strapi2.service("plugin::strapi-identity.email");
10046
+ try {
10047
+ const info2 = await secretService.getMFAInfo(user.id);
10048
+ if (!info2 || info2.type !== "email") {
10049
+ ctx.status = 400;
10050
+ ctx.body = { data: null, error: "Email OTP is not enabled" };
10051
+ return;
10052
+ }
10053
+ const otp = await secretService.generateEmailOTP(user.id, "disable");
10054
+ await emailService.send(user.email, otp);
10055
+ ctx.status = 200;
10056
+ ctx.body = { data: { message: "Verification email sent" }, error: null };
10057
+ } catch (error) {
10058
+ console.log("Error sending disable email OTP:", error);
10059
+ ctx.status = 500;
10060
+ ctx.body = { data: null, error: "Failed to send verification email" };
10061
+ }
10062
+ },
10063
+ async resendVerifyOTP(ctx) {
10064
+ const secret2 = strapi2.config.get("admin.auth.secret");
10065
+ const token = ctx.cookies.get("strapi_admin_mfa");
10066
+ try {
10067
+ const payload = jwt.verify(token, secret2);
10068
+ if (payload.mfaType !== "email") {
10069
+ ctx.status = 400;
10070
+ ctx.body = { data: null, error: "Resend is only available for email OTP" };
10071
+ return;
10072
+ }
10073
+ const adminUser = await strapi2.db.query("admin::user").findOne({ where: { id: Number(payload.userId) }, select: ["email"] });
10074
+ if (!adminUser?.email) {
10075
+ ctx.status = 400;
10076
+ ctx.body = { data: null, error: "No email address found for this user" };
10077
+ return;
10078
+ }
10079
+ const secretService = strapi2.service("plugin::strapi-identity.secret");
10080
+ const emailService = strapi2.service("plugin::strapi-identity.email");
10081
+ const otp = await secretService.generateEmailOTP(payload.userId, "login");
10082
+ await emailService.send(adminUser.email, otp);
10083
+ ctx.status = 200;
10084
+ ctx.body = { data: { message: "Verification email resent" }, error: null };
10085
+ } catch (error) {
10086
+ console.log("Error resending login email OTP:", error);
10087
+ ctx.status = 500;
10088
+ ctx.body = { data: null, error: "Failed to resend verification email" };
10089
+ }
9864
10090
  }
9865
10091
  });
9866
10092
  const controllers = {
@@ -9960,6 +10186,21 @@ const config$1 = [
9960
10186
  ]
9961
10187
  }
9962
10188
  },
10189
+ {
10190
+ method: "GET",
10191
+ path: "/config/email",
10192
+ handler: "config.getEmailStatus",
10193
+ info: {
10194
+ apiName: "getEmailStatus",
10195
+ pluginName: "strapi-identity",
10196
+ type: "content-api"
10197
+ },
10198
+ config: {
10199
+ policies: [
10200
+ "admin::isAuthenticatedAdmin"
10201
+ ]
10202
+ }
10203
+ },
9963
10204
  {
9964
10205
  method: "PUT",
9965
10206
  path: "/config",
@@ -10044,6 +10285,55 @@ const mfa = [
10044
10285
  type: "content-api"
10045
10286
  },
10046
10287
  config: {}
10288
+ },
10289
+ {
10290
+ method: "POST",
10291
+ path: "/verify/resend",
10292
+ handler: "controller.resendVerifyOTP",
10293
+ info: {
10294
+ apiName: "resendVerifyOTP",
10295
+ pluginName: "strapi-identity",
10296
+ type: "content-api"
10297
+ },
10298
+ config: {
10299
+ auth: false,
10300
+ policies: [
10301
+ "has-mfa"
10302
+ ]
10303
+ }
10304
+ },
10305
+ {
10306
+ method: "POST",
10307
+ path: "/enable-email",
10308
+ handler: "controller.enableEmail",
10309
+ info: {
10310
+ apiName: "enableEmail",
10311
+ pluginName: "strapi-identity",
10312
+ type: "content-api"
10313
+ },
10314
+ config: {}
10315
+ },
10316
+ {
10317
+ method: "POST",
10318
+ path: "/setup-email",
10319
+ handler: "controller.setupEmail",
10320
+ info: {
10321
+ apiName: "setupEmail",
10322
+ pluginName: "strapi-identity",
10323
+ type: "content-api"
10324
+ },
10325
+ config: {}
10326
+ },
10327
+ {
10328
+ method: "POST",
10329
+ path: "/disable-email/request",
10330
+ handler: "controller.requestDisableEmailOTP",
10331
+ info: {
10332
+ apiName: "requestDisableEmailOTP",
10333
+ pluginName: "strapi-identity",
10334
+ type: "content-api"
10335
+ },
10336
+ config: {}
10047
10337
  }
10048
10338
  ];
10049
10339
  const route = () => ({
@@ -10084,35 +10374,63 @@ const admin = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.definePropert
10084
10374
  isEnabled: isEnabled$1,
10085
10375
  reset
10086
10376
  }, Symbol.toStringTag, { value: "Module" }));
10377
+ const defaultConfig = {
10378
+ email_enabled: false,
10379
+ enabled: false,
10380
+ enforce: false,
10381
+ from_email: "",
10382
+ from_name: "",
10383
+ issuer: "",
10384
+ message: "",
10385
+ response_email: "",
10386
+ subject: "",
10387
+ text: ""
10388
+ };
10087
10389
  const _config = (options2) => {
10088
- return {
10089
- enabled: options2?.enabled || false,
10090
- enforce: options2?.enforce || false,
10091
- issuer: options2?.issuer || ""
10092
- };
10390
+ return Object.assign({}, defaultConfig, options2);
10093
10391
  };
10392
+ const fields = Object.keys(defaultConfig);
10094
10393
  const isEnabled = async () => {
10095
10394
  const config2 = await getConfig();
10096
10395
  return config2?.enabled || false;
10097
10396
  };
10098
10397
  const getConfig = async () => {
10099
10398
  const configDocument = strapi.documents("plugin::strapi-identity.strapi-identity-config");
10100
- return configDocument.findFirst({ fields: ["enabled", "enforce", "issuer"] }).then((config2) => _config(config2));
10399
+ return configDocument.findFirst({ fields }).then((config2) => _config(config2));
10101
10400
  };
10102
10401
  const updateConfig = async (data) => {
10103
10402
  const configDocument = strapi.documents("plugin::strapi-identity.strapi-identity-config");
10104
10403
  const existingConfig = await configDocument.findFirst();
10105
10404
  if (!existingConfig) {
10106
- return configDocument.create({ data, fields: ["enabled", "enforce", "issuer"] }).then((created) => _config(created));
10405
+ return configDocument.create({ data, fields }).then((created) => _config(created));
10107
10406
  }
10108
- if (existingConfig.enabled && !data.enabled) {
10109
- await disableMFAForAllUsers();
10407
+ if (existingConfig.enabled && !data.enabled) await disableMFAForAllUsers();
10408
+ if (existingConfig.email_enabled && data.email_enabled === false)
10409
+ await disableEmailMFAForAllUsers();
10410
+ return configDocument.update({ documentId: existingConfig.documentId, data: { ...existingConfig, ...data }, fields }).then((updated) => _config(updated));
10411
+ };
10412
+ const disableEmailMFAForAllUsers = async () => {
10413
+ const tokenDocument = strapi.documents("plugin::strapi-identity.mfa-token");
10414
+ const otpDocument = strapi.documents("plugin::strapi-identity.email-otp");
10415
+ try {
10416
+ const emailTokens = await tokenDocument.findMany({
10417
+ filters: { type: "email", enabled: true }
10418
+ });
10419
+ await Promise.all([
10420
+ ...emailTokens.map(
10421
+ (token) => tokenDocument.update({
10422
+ documentId: token.documentId,
10423
+ data: { ...token, enabled: false }
10424
+ })
10425
+ ),
10426
+ // Clean up any pending email OTPs
10427
+ otpDocument.findMany({}).then(
10428
+ (otps) => Promise.all(otps.map((otp) => otpDocument.delete({ documentId: otp.documentId })))
10429
+ )
10430
+ ]);
10431
+ } catch (err) {
10432
+ console.log("Error disabling email MFA for all users:", err);
10110
10433
  }
10111
- return configDocument.update({
10112
- documentId: existingConfig.documentId,
10113
- data: { ...existingConfig, ...data },
10114
- fields: ["enabled", "enforce", "issuer"]
10115
- }).then((updated) => _config(updated));
10116
10434
  };
10117
10435
  const disableMFAForAllUsers = async () => {
10118
10436
  const tokenDocument = strapi.documents("plugin::strapi-identity.mfa-token");
@@ -10136,6 +10454,39 @@ const config = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProper
10136
10454
  isEnabled,
10137
10455
  updateConfig
10138
10456
  }, Symbol.toStringTag, { value: "Module" }));
10457
+ const send = async (to, otp) => {
10458
+ const emailService = strapi.plugin("email").service("email");
10459
+ if (!emailService) return;
10460
+ if (!to) return;
10461
+ const config2 = await strapi.service("plugin::strapi-identity.config").getConfig();
10462
+ if (!config2.email_enabled) return;
10463
+ const sendConfig = {
10464
+ to,
10465
+ subject: config2.subject,
10466
+ text: replaceTemplateVariables(config2.text, { OTP: otp }),
10467
+ html: replaceTemplateVariables(config2.message, {
10468
+ OTP: otp,
10469
+ YEAR: (/* @__PURE__ */ new Date()).getFullYear().toString()
10470
+ })
10471
+ };
10472
+ if (config2.from_email) {
10473
+ sendConfig.from = config2.from_name ? `${config2.from_name} <${config2.from_email}>` : config2.from_email;
10474
+ }
10475
+ if (config2.response_email && config2.response_email !== config2.from_email) {
10476
+ sendConfig.replyTo = config2.response_email;
10477
+ }
10478
+ return emailService.send(sendConfig).catch((error) => {
10479
+ console.log("Error sending email:", error);
10480
+ });
10481
+ };
10482
+ const replaceTemplateVariables = (template, variables) => {
10483
+ return template.replace(/<%= (\w+) %>/g, (_, key) => variables[key] || "");
10484
+ };
10485
+ const email = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
10486
+ __proto__: null,
10487
+ replaceTemplateVariables,
10488
+ send
10489
+ }, Symbol.toStringTag, { value: "Module" }));
10139
10490
  function commonjsRequire(path) {
10140
10491
  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.');
10141
10492
  }
@@ -11989,8 +12340,11 @@ const checkRecoveryCode = async (userId, code) => {
11989
12340
  };
11990
12341
  const validateLiveToken = async (userId, token) => {
11991
12342
  const tokenDocument = strapi.documents("plugin::strapi-identity.mfa-token");
11992
- const record = await tokenDocument.findFirst({ filters: { admin_user: { id: userId } } });
11993
- if (!record || !record.enabled) return false;
12343
+ const record = await tokenDocument.findFirst({
12344
+ filters: { admin_user: { id: userId }, enabled: true }
12345
+ });
12346
+ if (!record) return false;
12347
+ if (record.type === "email") return false;
11994
12348
  return validateToken(token, record.secret);
11995
12349
  };
11996
12350
  const validateTempToken = async (userId, token) => {
@@ -12036,12 +12390,13 @@ const setupFullSecret = async (userId) => {
12036
12390
  if (existing) {
12037
12391
  await tokenDocument.update({
12038
12392
  documentId: existing.documentId,
12039
- data: { ...existing, secret: tempField.secret, enabled: true, recovery_codes }
12393
+ data: { ...existing, type: "totp", secret: tempField.secret, enabled: true, recovery_codes }
12040
12394
  });
12041
12395
  } else {
12042
12396
  await tokenDocument.create({
12043
12397
  data: {
12044
12398
  admin_user: { id: userId },
12399
+ type: "totp",
12045
12400
  secret: tempField.secret,
12046
12401
  enabled: true,
12047
12402
  recovery_codes
@@ -12051,14 +12406,92 @@ const setupFullSecret = async (userId) => {
12051
12406
  await tempDocument.delete({ documentId: tempField.documentId });
12052
12407
  return codes;
12053
12408
  };
12409
+ const EMAIL_OTP_EXPIRY_MINUTES = 10;
12410
+ const EMAIL_OTP_MAX_ATTEMPTS = 5;
12411
+ const generateNumericOTP = (length) => {
12412
+ const digits = "0123456789";
12413
+ const max = 256 - 256 % digits.length;
12414
+ const randomValues = new Uint8Array(length * 2);
12415
+ let result = "";
12416
+ while (result.length < length) {
12417
+ crypto.getRandomValues(randomValues);
12418
+ for (let i = 0; i < randomValues.length && result.length < length; i++) {
12419
+ if (randomValues[i] >= max) continue;
12420
+ result += digits[randomValues[i] % digits.length];
12421
+ }
12422
+ }
12423
+ return result;
12424
+ };
12425
+ const generateEmailOTP = async (userId, purpose = "login") => {
12426
+ const otpDocument = strapi.documents("plugin::strapi-identity.email-otp");
12427
+ const otp = generateNumericOTP(6);
12428
+ const code_hash = await bcrypt.hash(otp, 10);
12429
+ const expires_at = new Date(Date.now() + EMAIL_OTP_EXPIRY_MINUTES * 60 * 1e3).toISOString();
12430
+ const existing = await otpDocument.findFirst({
12431
+ filters: { admin_user: { id: userId }, purpose }
12432
+ });
12433
+ if (existing) await otpDocument.delete({ documentId: existing.documentId });
12434
+ await otpDocument.create({
12435
+ data: { admin_user: { id: userId }, code_hash, expires_at, purpose, attempts: 0 }
12436
+ });
12437
+ return otp;
12438
+ };
12439
+ const validateEmailOTP = async (userId, code, purpose = "login") => {
12440
+ const otpDocument = strapi.documents("plugin::strapi-identity.email-otp");
12441
+ const record = await otpDocument.findFirst({ filters: { admin_user: { id: userId }, purpose } });
12442
+ if (!record) return false;
12443
+ if (new Date(record.expires_at) < /* @__PURE__ */ new Date()) {
12444
+ await otpDocument.delete({ documentId: record.documentId });
12445
+ return false;
12446
+ }
12447
+ const attempts = record.attempts || 0;
12448
+ if (attempts >= EMAIL_OTP_MAX_ATTEMPTS) {
12449
+ await otpDocument.delete({ documentId: record.documentId });
12450
+ return false;
12451
+ }
12452
+ await otpDocument.update({
12453
+ documentId: record.documentId,
12454
+ data: { ...record, attempts: attempts + 1 }
12455
+ });
12456
+ const isValid = await bcrypt.compare(code, record.code_hash);
12457
+ if (isValid) await otpDocument.delete({ documentId: record.documentId });
12458
+ return isValid;
12459
+ };
12460
+ const enableEmailMFA = async (userId) => {
12461
+ const tokenDocument = strapi.documents("plugin::strapi-identity.mfa-token");
12462
+ const existing = await tokenDocument.findFirst({ filters: { admin_user: { id: userId } } });
12463
+ if (existing) {
12464
+ await tokenDocument.update({
12465
+ documentId: existing.documentId,
12466
+ data: { ...existing, type: "email", enabled: true, secret: "" }
12467
+ });
12468
+ } else {
12469
+ await tokenDocument.create({
12470
+ data: { admin_user: { id: userId }, type: "email", enabled: true, secret: "" }
12471
+ });
12472
+ }
12473
+ };
12474
+ const getMFAInfo = async (userId) => {
12475
+ const tokenDocument = strapi.documents("plugin::strapi-identity.mfa-token");
12476
+ const record = await tokenDocument.findFirst({
12477
+ filters: { admin_user: { id: userId }, enabled: true }
12478
+ });
12479
+ if (!record) return null;
12480
+ return { status: "full", type: record.type };
12481
+ };
12054
12482
  const disableSecret = async (userId, code) => {
12055
- const validToken = await validateTokenOrRecoveryCode(userId, code);
12056
- if (!validToken) throw new Error("Invalid token or recovery code");
12057
12483
  const tokenDocument = strapi.documents("plugin::strapi-identity.mfa-token");
12058
12484
  const record = await tokenDocument.findFirst({
12059
12485
  filters: { admin_user: { id: userId }, enabled: true }
12060
12486
  });
12061
12487
  if (!record) throw new Error("MFA is not enabled for this user");
12488
+ let valid2;
12489
+ if (record.type === "email") {
12490
+ valid2 = await validateEmailOTP(userId, code, "disable");
12491
+ } else {
12492
+ valid2 = await validateTokenOrRecoveryCode(userId, code);
12493
+ }
12494
+ if (!valid2) throw new Error("Invalid token or recovery code");
12062
12495
  await tokenDocument.update({
12063
12496
  documentId: record.documentId,
12064
12497
  data: { ...record, enabled: false }
@@ -12101,14 +12534,18 @@ const secret = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProper
12101
12534
  __proto__: null,
12102
12535
  disableSecret,
12103
12536
  disableTempSecret,
12537
+ enableEmailMFA,
12538
+ generateEmailOTP,
12104
12539
  generateRecoveryCode,
12540
+ getMFAInfo,
12105
12541
  isMFAEnabled,
12106
12542
  setupFullSecret,
12107
12543
  setupTemporarySecret,
12544
+ validateEmailOTP,
12108
12545
  validateTempToken,
12109
12546
  validateTokenOrRecoveryCode
12110
12547
  }, Symbol.toStringTag, { value: "Module" }));
12111
- const services = { admin, config, secret };
12548
+ const services = { admin, config, email, secret };
12112
12549
  const index = {
12113
12550
  register,
12114
12551
  bootstrap,