strapi-plugin-firebase-authentication 1.1.12 → 1.2.1

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 (40) hide show
  1. package/dist/_chunks/{App-BqjE8BHb.js → App-CAgq2cOo.js} +369 -288
  2. package/dist/_chunks/{App-BY1gNGKH.mjs → App-CyeC5fLT.mjs} +291 -210
  3. package/dist/_chunks/api-BL-wXuSb.js +56776 -0
  4. package/dist/_chunks/api-Bjer83Qp.mjs +56759 -0
  5. package/dist/_chunks/index-Bg-lGlji.mjs +2758 -0
  6. package/dist/_chunks/{index-C4t4JZZ_.js → index-CTenjpqN.js} +285 -172
  7. package/dist/_chunks/{index-D8pv1Q6h.mjs → index-Kidqhaeq.mjs} +268 -155
  8. package/dist/_chunks/index-Rervtuh1.js +2757 -0
  9. package/dist/admin/index.js +1 -3
  10. package/dist/admin/index.mjs +2 -4
  11. package/dist/admin/src/components/user-management/ResendVerification/ResendVerification.d.ts +9 -0
  12. package/dist/admin/src/components/user-management/index.d.ts +1 -0
  13. package/dist/admin/src/pages/Settings/api.d.ts +4 -0
  14. package/dist/admin/src/pages/utils/api.d.ts +2 -1
  15. package/dist/server/index.js +1008 -332
  16. package/dist/server/index.mjs +1008 -332
  17. package/dist/server/src/config/index.d.ts +1 -1
  18. package/dist/server/src/content-types/index.d.ts +16 -0
  19. package/dist/server/src/controllers/firebaseController.d.ts +16 -1
  20. package/dist/server/src/controllers/index.d.ts +3 -0
  21. package/dist/server/src/controllers/userController.d.ts +1 -0
  22. package/dist/server/src/index.d.ts +51 -5
  23. package/dist/server/src/policies/index.d.ts +3 -1
  24. package/dist/server/src/policies/is-authenticated.d.ts +6 -0
  25. package/dist/server/src/routes/content-api.d.ts +10 -2
  26. package/dist/server/src/routes/index.d.ts +10 -2
  27. package/dist/server/src/services/emailService.d.ts +10 -0
  28. package/dist/server/src/services/firebaseService.d.ts +21 -1
  29. package/dist/server/src/services/index.d.ts +18 -1
  30. package/dist/server/src/services/settingsService.d.ts +2 -0
  31. package/dist/server/src/services/tokenService.d.ts +21 -0
  32. package/dist/server/src/services/userService.d.ts +5 -0
  33. package/dist/server/src/templates/defaults/email-verification.d.ts +2 -0
  34. package/dist/server/src/templates/defaults/index.d.ts +1 -0
  35. package/dist/server/src/templates/types.d.ts +3 -1
  36. package/package.json +8 -8
  37. package/dist/_chunks/api-DQCdqlCd.js +0 -35
  38. package/dist/_chunks/api-D_4cdJU5.mjs +0 -36
  39. package/dist/_chunks/index-DlPxMuSK.js +0 -814
  40. package/dist/_chunks/index-DtGfwf9S.mjs +0 -815
@@ -230,7 +230,7 @@ const register = ({ strapi: strapi2 }) => {
230
230
  const config$1 = {
231
231
  default: ({ env: env2 }) => ({
232
232
  firebaseJsonEncryptionKey: env2("FIREBASE_JSON_ENCRYPTION_KEY", "your-key-here"),
233
- emailRequired: true,
233
+ emailRequired: env2.bool("FIREBASE_EMAIL_REQUIRED", false),
234
234
  emailPattern: "{randomString}@phone-user.firebase.local"
235
235
  }),
236
236
  validator(config2) {
@@ -321,6 +321,15 @@ const attributes$1 = {
321
321
  minimum: 1,
322
322
  maximum: 72,
323
323
  description: "How long the magic link remains valid (in hours)"
324
+ },
325
+ emailVerificationUrl: {
326
+ type: "string",
327
+ "default": "http://localhost:3000/verify-email",
328
+ description: "URL where users will be redirected to verify their email"
329
+ },
330
+ emailVerificationEmailSubject: {
331
+ type: "string",
332
+ "default": "Verify Your Email"
324
333
  }
325
334
  };
326
335
  const firebaseAuthenticationConfiguration = {
@@ -371,6 +380,13 @@ const attributes = {
371
380
  },
372
381
  resetTokenExpiresAt: {
373
382
  type: "datetime"
383
+ },
384
+ verificationTokenHash: {
385
+ type: "string",
386
+ "private": true
387
+ },
388
+ verificationTokenExpiresAt: {
389
+ type: "datetime"
374
390
  }
375
391
  };
376
392
  const firebaseUserData = {
@@ -385,238 +401,6 @@ const contentTypes = {
385
401
  "firebase-authentication-configuration": { schema: firebaseAuthenticationConfiguration },
386
402
  "firebase-user-data": { schema: firebaseUserData }
387
403
  };
388
- const pluginName = "firebase-authentication";
389
- const PLUGIN_NAME = "firebase-authentication";
390
- const PLUGIN_UID = `plugin::${PLUGIN_NAME}`;
391
- const CONFIG_CONTENT_TYPE = `${PLUGIN_UID}.firebase-authentication-configuration`;
392
- const DEFAULT_PASSWORD_RESET_URL = "http://localhost:3000/reset-password";
393
- const DEFAULT_PASSWORD_REGEX = "^.{6,}$";
394
- const DEFAULT_PASSWORD_MESSAGE = "Password must be at least 6 characters long";
395
- const DEFAULT_RESET_EMAIL_SUBJECT = "Reset Your Password";
396
- const ERROR_MESSAGES = {
397
- FIREBASE_NOT_INITIALIZED: "Firebase is not initialized. Please upload Firebase service account configuration via Settings → Firebase Authentication.",
398
- INVALID_JSON: "Invalid JSON format. Please ensure you copied the entire JSON content correctly.",
399
- MISSING_DATA: "data is missing",
400
- SOMETHING_WENT_WRONG: "Something went wrong",
401
- AUTHENTICATION_FAILED: "Authentication failed",
402
- TOKEN_MISSING: "idToken is missing!",
403
- EMAIL_PASSWORD_REQUIRED: "Email and password are required",
404
- PASSWORD_REQUIRED: "Password is required",
405
- AUTHORIZATION_REQUIRED: "Authorization token is required",
406
- INVALID_TOKEN: "Invalid or expired token",
407
- USER_NOT_FOUND: "User not found",
408
- USER_NO_EMAIL: "User does not have an email address",
409
- FIREBASE_LINK_FAILED: "Failed to generate Firebase reset link",
410
- CONFIG_NOT_FOUND: "No config found",
411
- INVALID_SERVICE_ACCOUNT: "Invalid service account JSON",
412
- WEB_API_NOT_CONFIGURED: "Email/password authentication is not available. Web API Key is not configured.",
413
- RESET_URL_NOT_CONFIGURED: "Password reset URL is not configured",
414
- RESET_URL_MUST_BE_HTTPS: "Password reset URL must use HTTPS in production",
415
- RESET_URL_INVALID_FORMAT: "Password reset URL is not a valid URL format",
416
- USER_NOT_LINKED_FIREBASE: "User is not linked to Firebase authentication",
417
- OVERRIDE_USER_ID_REQUIRED: "Override user ID is required",
418
- EITHER_EMAIL_OR_PHONE_REQUIRED: "Either email or phoneNumber is required",
419
- DELETION_NO_CONFIG: "No Firebase configs exists for deletion"
420
- };
421
- const SUCCESS_MESSAGES = {
422
- FIREBASE_INITIALIZED: "Firebase successfully initialized",
423
- FIREBASE_CONFIG_DELETED: "Firebase config deleted and reinitialized",
424
- PASSWORD_RESET_EMAIL_SENT: "If an account with that email exists, a password reset link has been sent.",
425
- SERVER_RESTARTING: "SERVER IS RESTARTING"
426
- };
427
- const CONFIG_KEYS = {
428
- ENCRYPTION_KEY: `${PLUGIN_UID}.FIREBASE_JSON_ENCRYPTION_KEY`
429
- };
430
- const REQUIRED_FIELDS = {
431
- SERVICE_ACCOUNT: ["private_key", "client_email", "project_id", "type"],
432
- WEB_CONFIG: ["apiKey", "authDomain"]
433
- // These indicate wrong JSON type
434
- };
435
- const VALIDATION_MESSAGES = {
436
- INVALID_SERVICE_ACCOUNT: "Invalid Service Account JSON. Missing required fields:",
437
- WRONG_JSON_TYPE: "You uploaded a Web App Config (SDK snippet) instead of a Service Account JSON. Please go to Firebase Console → Service Accounts tab → Generate New Private Key to download the correct file.",
438
- SERVICE_ACCOUNT_HELP: "Please download the correct JSON from Firebase Console → Service Accounts → Generate New Private Key. Do NOT use the Web App Config (SDK snippet) - that is a different file!"
439
- };
440
- const firebaseController = {
441
- async validateToken(ctx) {
442
- strapi.log.debug("validateToken called");
443
- try {
444
- const { idToken, profileMetaData } = ctx.request.body || {};
445
- const populate2 = ctx.request.query.populate || [];
446
- if (!idToken) {
447
- return ctx.badRequest(ERROR_MESSAGES.TOKEN_MISSING);
448
- }
449
- const result = await strapi.plugin(pluginName).service("firebaseService").validateFirebaseToken(idToken, profileMetaData, populate2);
450
- ctx.body = result;
451
- } catch (error2) {
452
- strapi.log.error(`validateToken controller error: ${error2.message}`);
453
- if (error2.name === "ValidationError") {
454
- return ctx.badRequest(error2.message);
455
- }
456
- if (error2.name === "UnauthorizedError") {
457
- return ctx.unauthorized(error2.message);
458
- }
459
- throw error2;
460
- }
461
- },
462
- async deleteByEmail(email2) {
463
- const user = await strapi.firebase.auth().getUserByEmail(email2);
464
- await strapi.plugin(pluginName).service("firebaseService").delete(user.toJSON().uid);
465
- return user.toJSON();
466
- },
467
- async overrideAccess(ctx) {
468
- try {
469
- const { overrideUserId } = ctx.request.body || {};
470
- const populate2 = ctx.request.query.populate || [];
471
- const result = await strapi.plugin(pluginName).service("firebaseService").overrideFirebaseAccess(overrideUserId, populate2);
472
- ctx.body = result;
473
- } catch (error2) {
474
- if (error2.name === "ValidationError") {
475
- return ctx.badRequest(error2.message);
476
- }
477
- if (error2.name === "NotFoundError") {
478
- return ctx.notFound(error2.message);
479
- }
480
- throw error2;
481
- }
482
- },
483
- /**
484
- * Controller method for email/password authentication
485
- * Handles the `/api/firebase-authentication/emailLogin` endpoint
486
- *
487
- * @param ctx - Koa context object
488
- * @returns Promise that sets ctx.body with user data and JWT or error message
489
- *
490
- * @remarks
491
- * This controller acts as a proxy to Firebase's Identity Toolkit API,
492
- * allowing users to authenticate with email/password and receive a Strapi JWT.
493
- *
494
- * HTTP Status Codes:
495
- * - `400`: Validation errors (missing credentials, invalid email/password)
496
- * - `500`: Server errors (missing configuration, Firebase API issues)
497
- */
498
- async emailLogin(ctx) {
499
- strapi.log.debug("emailLogin controller");
500
- try {
501
- const { email: email2, password } = ctx.request.body || {};
502
- const populate2 = ctx.request.query.populate || [];
503
- const result = await strapi.plugin(pluginName).service("firebaseService").emailLogin(email2, password, populate2);
504
- ctx.body = result;
505
- } catch (error2) {
506
- strapi.log.error("emailLogin controller error:", error2);
507
- if (error2.name === "ValidationError") {
508
- ctx.status = 400;
509
- } else if (error2.name === "ApplicationError") {
510
- ctx.status = 500;
511
- } else {
512
- ctx.status = 500;
513
- }
514
- ctx.body = { error: error2.message };
515
- }
516
- },
517
- /**
518
- * Forgot password - sends reset email
519
- * POST /api/firebase-authentication/forgotPassword
520
- * Public endpoint - no authentication required
521
- */
522
- async forgotPassword(ctx) {
523
- strapi.log.debug("forgotPassword endpoint called");
524
- try {
525
- const { email: email2 } = ctx.request.body || {};
526
- ctx.body = await strapi.plugin(pluginName).service("firebaseService").forgotPassword(email2);
527
- } catch (error2) {
528
- strapi.log.error("forgotPassword controller error:", error2);
529
- if (error2.name === "ValidationError") {
530
- ctx.status = 400;
531
- } else if (error2.name === "NotFoundError") {
532
- ctx.status = 404;
533
- } else {
534
- ctx.status = 500;
535
- }
536
- ctx.body = { error: error2.message };
537
- }
538
- },
539
- /**
540
- * Reset password - authenticated password change
541
- * POST /api/firebase-authentication/resetPassword
542
- * Public endpoint - requires valid JWT in Authorization header
543
- * Used for admin-initiated resets or user self-service password changes
544
- * NOT used for forgot password email flow (which uses Firebase's hosted UI)
545
- */
546
- async resetPassword(ctx) {
547
- strapi.log.debug("resetPassword endpoint called");
548
- try {
549
- const { password } = ctx.request.body || {};
550
- const token = ctx.request.headers.authorization?.replace("Bearer ", "");
551
- const populate2 = ctx.request.query.populate || [];
552
- ctx.body = await strapi.plugin(pluginName).service("firebaseService").resetPassword(password, token, populate2);
553
- } catch (error2) {
554
- strapi.log.error("resetPassword controller error:", error2);
555
- if (error2.name === "ValidationError" || error2.name === "UnauthorizedError") {
556
- ctx.status = 401;
557
- } else {
558
- ctx.status = 500;
559
- }
560
- ctx.body = { error: error2.message };
561
- }
562
- },
563
- async requestMagicLink(ctx) {
564
- try {
565
- const { email: email2 } = ctx.request.body || {};
566
- const result = await strapi.plugin("firebase-authentication").service("firebaseService").requestMagicLink(email2);
567
- ctx.body = result;
568
- } catch (error2) {
569
- if (error2.name === "ValidationError" || error2.name === "ApplicationError") {
570
- throw error2;
571
- }
572
- strapi.log.error("requestMagicLink controller error:", error2);
573
- ctx.body = {
574
- success: false,
575
- message: "An error occurred while processing your request"
576
- };
577
- }
578
- },
579
- /**
580
- * Reset password using custom JWT token
581
- * POST /api/firebase-authentication/resetPasswordWithToken
582
- * Public endpoint - token provides authentication
583
- *
584
- * @param ctx - Koa context with { token, newPassword } in body
585
- * @returns { success: true, message: "Password has been reset successfully" }
586
- */
587
- async resetPasswordWithToken(ctx) {
588
- strapi.log.debug("resetPasswordWithToken endpoint called");
589
- try {
590
- const { token, newPassword } = ctx.request.body || {};
591
- if (!token) {
592
- ctx.status = 400;
593
- ctx.body = { error: "Token is required" };
594
- return;
595
- }
596
- if (!newPassword) {
597
- ctx.status = 400;
598
- ctx.body = { error: "New password is required" };
599
- return;
600
- }
601
- const result = await strapi.plugin(pluginName).service("userService").resetPasswordWithToken(token, newPassword);
602
- ctx.body = result;
603
- } catch (error2) {
604
- strapi.log.error("resetPasswordWithToken controller error:", error2);
605
- if (error2.name === "ValidationError") {
606
- ctx.status = 400;
607
- ctx.body = { error: error2.message };
608
- return;
609
- }
610
- if (error2.name === "NotFoundError") {
611
- ctx.status = 404;
612
- ctx.body = { error: error2.message };
613
- return;
614
- }
615
- ctx.status = 500;
616
- ctx.body = { error: "An error occurred while resetting your password" };
617
- }
618
- }
619
- };
620
404
  var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {};
621
405
  function getDefaultExportFromCjs(x) {
622
406
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
@@ -631,7 +415,7 @@ var lodash = { exports: {} };
631
415
  * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
632
416
  */
633
417
  lodash.exports;
634
- (function(module2, exports2) {
418
+ (function(module2, exports$1) {
635
419
  (function() {
636
420
  var undefined$1;
637
421
  var VERSION = "4.17.21";
@@ -959,7 +743,7 @@ lodash.exports;
959
743
  var freeGlobal2 = typeof commonjsGlobal == "object" && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal;
960
744
  var freeSelf2 = typeof self == "object" && self && self.Object === Object && self;
961
745
  var root2 = freeGlobal2 || freeSelf2 || Function("return this")();
962
- var freeExports = exports2 && !exports2.nodeType && exports2;
746
+ var freeExports = exports$1 && !exports$1.nodeType && exports$1;
963
747
  var freeModule = freeExports && true && module2 && !module2.nodeType && module2;
964
748
  var moduleExports = freeModule && freeModule.exports === freeExports;
965
749
  var freeProcess = moduleExports && freeGlobal2.process;
@@ -6189,7 +5973,7 @@ var lodash_min = { exports: {} };
6189
5973
  * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
6190
5974
  */
6191
5975
  lodash_min.exports;
6192
- (function(module2, exports2) {
5976
+ (function(module2, exports$1) {
6193
5977
  (function() {
6194
5978
  function n(n2, t3, r2) {
6195
5979
  switch (r2.length) {
@@ -6622,7 +6406,7 @@ lodash_min.exports;
6622
6406
  "œ": "oe",
6623
6407
  "ʼn": "'n",
6624
6408
  "ſ": "s"
6625
- }, Hr = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }, Jr = { "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": '"', "&#39;": "'" }, Yr = { "\\": "\\", "'": "'", "\n": "n", "\r": "r", "\u2028": "u2028", "\u2029": "u2029" }, Qr = parseFloat, Xr = parseInt, ne = "object" == typeof commonjsGlobal && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal, te = "object" == typeof self && self && self.Object === Object && self, re2 = ne || te || Function("return this")(), ee = exports2 && !exports2.nodeType && exports2, ue = ee && true && module2 && !module2.nodeType && module2, ie = ue && ue.exports === ee, oe = ie && ne.process, fe = function() {
6409
+ }, Hr = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }, Jr = { "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": '"', "&#39;": "'" }, Yr = { "\\": "\\", "'": "'", "\n": "n", "\r": "r", "\u2028": "u2028", "\u2029": "u2029" }, Qr = parseFloat, Xr = parseInt, ne = "object" == typeof commonjsGlobal && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal, te = "object" == typeof self && self && self.Object === Object && self, re2 = ne || te || Function("return this")(), ee = exports$1 && !exports$1.nodeType && exports$1, ue = ee && true && module2 && !module2.nodeType && module2, ie = ue && ue.exports === ee, oe = ie && ne.process, fe = function() {
6626
6410
  try {
6627
6411
  var n2 = ue && ue.require && ue.require("util").types;
6628
6412
  return n2 ? n2 : oe && oe.binding && oe.binding("util");
@@ -9295,8 +9079,8 @@ lodash_min.exports;
9295
9079
  })(lodash_min, lodash_min.exports);
9296
9080
  var lodash_minExports = lodash_min.exports;
9297
9081
  var _mapping = {};
9298
- (function(exports2) {
9299
- exports2.aliasToReal = {
9082
+ (function(exports$1) {
9083
+ exports$1.aliasToReal = {
9300
9084
  // Lodash aliases.
9301
9085
  "each": "forEach",
9302
9086
  "eachRight": "forEachRight",
@@ -9361,7 +9145,7 @@ var _mapping = {};
9361
9145
  "whereEq": "isMatch",
9362
9146
  "zipObj": "zipObject"
9363
9147
  };
9364
- exports2.aryMethod = {
9148
+ exports$1.aryMethod = {
9365
9149
  "1": [
9366
9150
  "assignAll",
9367
9151
  "assignInAll",
@@ -9596,12 +9380,12 @@ var _mapping = {};
9596
9380
  "updateWith"
9597
9381
  ]
9598
9382
  };
9599
- exports2.aryRearg = {
9383
+ exports$1.aryRearg = {
9600
9384
  "2": [1, 0],
9601
9385
  "3": [2, 0, 1],
9602
9386
  "4": [3, 2, 0, 1]
9603
9387
  };
9604
- exports2.iterateeAry = {
9388
+ exports$1.iterateeAry = {
9605
9389
  "dropRightWhile": 1,
9606
9390
  "dropWhile": 1,
9607
9391
  "every": 1,
@@ -9639,11 +9423,11 @@ var _mapping = {};
9639
9423
  "times": 1,
9640
9424
  "transform": 2
9641
9425
  };
9642
- exports2.iterateeRearg = {
9426
+ exports$1.iterateeRearg = {
9643
9427
  "mapKeys": [1],
9644
9428
  "reduceRight": [1, 0]
9645
9429
  };
9646
- exports2.methodRearg = {
9430
+ exports$1.methodRearg = {
9647
9431
  "assignInAllWith": [1, 0],
9648
9432
  "assignInWith": [1, 2, 0],
9649
9433
  "assignAllWith": [1, 0],
@@ -9674,7 +9458,7 @@ var _mapping = {};
9674
9458
  "xorWith": [1, 2, 0],
9675
9459
  "zipWith": [1, 2, 0]
9676
9460
  };
9677
- exports2.methodSpread = {
9461
+ exports$1.methodSpread = {
9678
9462
  "assignAll": { "start": 0 },
9679
9463
  "assignAllWith": { "start": 0 },
9680
9464
  "assignInAll": { "start": 0 },
@@ -9690,7 +9474,7 @@ var _mapping = {};
9690
9474
  "without": { "start": 1 },
9691
9475
  "zipAll": { "start": 0 }
9692
9476
  };
9693
- exports2.mutate = {
9477
+ exports$1.mutate = {
9694
9478
  "array": {
9695
9479
  "fill": true,
9696
9480
  "pull": true,
@@ -9727,8 +9511,8 @@ var _mapping = {};
9727
9511
  "updateWith": true
9728
9512
  }
9729
9513
  };
9730
- exports2.realToAlias = function() {
9731
- var hasOwnProperty2 = Object.prototype.hasOwnProperty, object2 = exports2.aliasToReal, result = {};
9514
+ exports$1.realToAlias = function() {
9515
+ var hasOwnProperty2 = Object.prototype.hasOwnProperty, object2 = exports$1.aliasToReal, result = {};
9732
9516
  for (var key in object2) {
9733
9517
  var value = object2[key];
9734
9518
  if (hasOwnProperty2.call(result, value)) {
@@ -9739,7 +9523,7 @@ var _mapping = {};
9739
9523
  }
9740
9524
  return result;
9741
9525
  }();
9742
- exports2.remap = {
9526
+ exports$1.remap = {
9743
9527
  "assignAll": "assign",
9744
9528
  "assignAllWith": "assignWith",
9745
9529
  "assignInAll": "assignIn",
@@ -9773,7 +9557,7 @@ var _mapping = {};
9773
9557
  "trimCharsStart": "trimStart",
9774
9558
  "zipAll": "zip"
9775
9559
  };
9776
- exports2.skipFixed = {
9560
+ exports$1.skipFixed = {
9777
9561
  "castArray": true,
9778
9562
  "flow": true,
9779
9563
  "flowRight": true,
@@ -9782,7 +9566,7 @@ var _mapping = {};
9782
9566
  "rearg": true,
9783
9567
  "runInContext": true
9784
9568
  };
9785
- exports2.skipRearg = {
9569
+ exports$1.skipRearg = {
9786
9570
  "add": true,
9787
9571
  "assign": true,
9788
9572
  "assignIn": true,
@@ -10236,13 +10020,13 @@ const traverseEntity = async (visitor2, options2, entity) => {
10236
10020
  if (fp.isNil(value) || fp.isNil(attribute)) {
10237
10021
  continue;
10238
10022
  }
10239
- parent = {
10240
- schema: schema2,
10241
- key,
10242
- attribute,
10243
- path: newPath
10244
- };
10245
10023
  if (isRelationalAttribute(attribute)) {
10024
+ parent = {
10025
+ schema: schema2,
10026
+ key,
10027
+ attribute,
10028
+ path: newPath
10029
+ };
10246
10030
  const isMorphRelation = attribute.relation.toLowerCase().startsWith("morph");
10247
10031
  const method = isMorphRelation ? traverseMorphRelationTarget : traverseRelationTarget(getModel(attribute.target));
10248
10032
  if (fp.isArray(value)) {
@@ -10261,6 +10045,12 @@ const traverseEntity = async (visitor2, options2, entity) => {
10261
10045
  continue;
10262
10046
  }
10263
10047
  if (isMediaAttribute(attribute)) {
10048
+ parent = {
10049
+ schema: schema2,
10050
+ key,
10051
+ attribute,
10052
+ path: newPath
10053
+ };
10264
10054
  if (fp.isArray(value)) {
10265
10055
  const res = new Array(value.length);
10266
10056
  for (let i2 = 0; i2 < value.length; i2 += 1) {
@@ -10277,6 +10067,12 @@ const traverseEntity = async (visitor2, options2, entity) => {
10277
10067
  continue;
10278
10068
  }
10279
10069
  if (attribute.type === "component") {
10070
+ parent = {
10071
+ schema: schema2,
10072
+ key,
10073
+ attribute,
10074
+ path: newPath
10075
+ };
10280
10076
  const targetSchema = getModel(attribute.component);
10281
10077
  if (fp.isArray(value)) {
10282
10078
  const res = new Array(value.length);
@@ -10294,6 +10090,12 @@ const traverseEntity = async (visitor2, options2, entity) => {
10294
10090
  continue;
10295
10091
  }
10296
10092
  if (attribute.type === "dynamiczone" && fp.isArray(value)) {
10093
+ parent = {
10094
+ schema: schema2,
10095
+ key,
10096
+ attribute,
10097
+ path: newPath
10098
+ };
10297
10099
  const res = new Array(value.length);
10298
10100
  for (let i2 = 0; i2 < value.length; i2 += 1) {
10299
10101
  const arrayPath = {
@@ -10318,7 +10120,7 @@ const createVisitorUtils = ({ data }) => ({
10318
10120
  });
10319
10121
  fp.curry(traverseEntity);
10320
10122
  var dist = { exports: {} };
10321
- (function(module2, exports2) {
10123
+ (function(module2, exports$1) {
10322
10124
  !function(t2, n) {
10323
10125
  module2.exports = n(require$$0__default.default, crypto__default.default);
10324
10126
  }(commonjsGlobal, function(t2, n) {
@@ -11866,9 +11668,9 @@ function stubFalse() {
11866
11668
  }
11867
11669
  var stubFalse_1 = stubFalse;
11868
11670
  isBuffer$2.exports;
11869
- (function(module2, exports2) {
11671
+ (function(module2, exports$1) {
11870
11672
  var root2 = _root, stubFalse2 = stubFalse_1;
11871
- var freeExports = exports2 && !exports2.nodeType && exports2;
11673
+ var freeExports = exports$1 && !exports$1.nodeType && exports$1;
11872
11674
  var freeModule = freeExports && true && module2 && !module2.nodeType && module2;
11873
11675
  var moduleExports = freeModule && freeModule.exports === freeExports;
11874
11676
  var Buffer2 = moduleExports ? root2.Buffer : void 0;
@@ -11895,9 +11697,9 @@ function baseUnary$1(func) {
11895
11697
  var _baseUnary = baseUnary$1;
11896
11698
  var _nodeUtil = { exports: {} };
11897
11699
  _nodeUtil.exports;
11898
- (function(module2, exports2) {
11700
+ (function(module2, exports$1) {
11899
11701
  var freeGlobal2 = _freeGlobal;
11900
- var freeExports = exports2 && !exports2.nodeType && exports2;
11702
+ var freeExports = exports$1 && !exports$1.nodeType && exports$1;
11901
11703
  var freeModule = freeExports && true && module2 && !module2.nodeType && module2;
11902
11704
  var moduleExports = freeModule && freeModule.exports === freeExports;
11903
11705
  var freeProcess = moduleExports && freeGlobal2.process;
@@ -14856,7 +14658,7 @@ function toIdentifier(str2) {
14856
14658
  Object.defineProperty(func, "name", desc);
14857
14659
  }
14858
14660
  }
14859
- function populateConstructorExports(exports2, codes2, HttpError) {
14661
+ function populateConstructorExports(exports$1, codes2, HttpError) {
14860
14662
  codes2.forEach(function forEachCode(code) {
14861
14663
  var CodeError;
14862
14664
  var name = toIdentifier2(statuses$1.message[code]);
@@ -14869,8 +14671,8 @@ function toIdentifier(str2) {
14869
14671
  break;
14870
14672
  }
14871
14673
  if (CodeError) {
14872
- exports2[code] = CodeError;
14873
- exports2[name] = CodeError;
14674
+ exports$1[code] = CodeError;
14675
+ exports$1[name] = CodeError;
14874
14676
  }
14875
14677
  });
14876
14678
  }
@@ -14892,7 +14694,7 @@ const formatYupErrors = (yupError) => ({
14892
14694
  message: yupError.message
14893
14695
  });
14894
14696
  let ApplicationError$2 = class ApplicationError extends Error {
14895
- constructor(message = "An application error occured", details = {}) {
14697
+ constructor(message = "An application error occurred", details = {}) {
14896
14698
  super();
14897
14699
  this.name = "ApplicationError";
14898
14700
  this.message = message;
@@ -18088,8 +17890,8 @@ pkgDir$1.exports.sync = (cwd2) => {
18088
17890
  };
18089
17891
  var pkgDirExports = pkgDir$1.exports;
18090
17892
  var utils$8 = {};
18091
- (function(exports2) {
18092
- exports2.isInteger = (num) => {
17893
+ (function(exports$1) {
17894
+ exports$1.isInteger = (num) => {
18093
17895
  if (typeof num === "number") {
18094
17896
  return Number.isInteger(num);
18095
17897
  }
@@ -18098,13 +17900,13 @@ var utils$8 = {};
18098
17900
  }
18099
17901
  return false;
18100
17902
  };
18101
- exports2.find = (node, type2) => node.nodes.find((node2) => node2.type === type2);
18102
- exports2.exceedsLimit = (min, max, step = 1, limit) => {
17903
+ exports$1.find = (node, type2) => node.nodes.find((node2) => node2.type === type2);
17904
+ exports$1.exceedsLimit = (min, max, step = 1, limit) => {
18103
17905
  if (limit === false) return false;
18104
- if (!exports2.isInteger(min) || !exports2.isInteger(max)) return false;
17906
+ if (!exports$1.isInteger(min) || !exports$1.isInteger(max)) return false;
18105
17907
  return (Number(max) - Number(min)) / Number(step) >= limit;
18106
17908
  };
18107
- exports2.escapeNode = (block, n = 0, type2) => {
17909
+ exports$1.escapeNode = (block, n = 0, type2) => {
18108
17910
  const node = block.nodes[n];
18109
17911
  if (!node) return;
18110
17912
  if (type2 && node.type === type2 || node.type === "open" || node.type === "close") {
@@ -18114,7 +17916,7 @@ var utils$8 = {};
18114
17916
  }
18115
17917
  }
18116
17918
  };
18117
- exports2.encloseBrace = (node) => {
17919
+ exports$1.encloseBrace = (node) => {
18118
17920
  if (node.type !== "brace") return false;
18119
17921
  if (node.commas >> 0 + node.ranges >> 0 === 0) {
18120
17922
  node.invalid = true;
@@ -18122,7 +17924,7 @@ var utils$8 = {};
18122
17924
  }
18123
17925
  return false;
18124
17926
  };
18125
- exports2.isInvalidBrace = (block) => {
17927
+ exports$1.isInvalidBrace = (block) => {
18126
17928
  if (block.type !== "brace") return false;
18127
17929
  if (block.invalid === true || block.dollar) return true;
18128
17930
  if (block.commas >> 0 + block.ranges >> 0 === 0) {
@@ -18135,18 +17937,18 @@ var utils$8 = {};
18135
17937
  }
18136
17938
  return false;
18137
17939
  };
18138
- exports2.isOpenOrClose = (node) => {
17940
+ exports$1.isOpenOrClose = (node) => {
18139
17941
  if (node.type === "open" || node.type === "close") {
18140
17942
  return true;
18141
17943
  }
18142
17944
  return node.open === true || node.close === true;
18143
17945
  };
18144
- exports2.reduce = (nodes) => nodes.reduce((acc, node) => {
17946
+ exports$1.reduce = (nodes) => nodes.reduce((acc, node) => {
18145
17947
  if (node.type === "text") acc.push(node.value);
18146
17948
  if (node.type === "range") node.type = "text";
18147
17949
  return acc;
18148
17950
  }, []);
18149
- exports2.flatten = (...args) => {
17951
+ exports$1.flatten = (...args) => {
18150
17952
  const result = [];
18151
17953
  const flat = (arr) => {
18152
17954
  for (let i = 0; i < arr.length; i++) {
@@ -19248,7 +19050,7 @@ var constants$5 = {
19248
19050
  return win32 === true ? WINDOWS_CHARS : POSIX_CHARS;
19249
19051
  }
19250
19052
  };
19251
- (function(exports2) {
19053
+ (function(exports$1) {
19252
19054
  const path2 = require$$0__namespace.default;
19253
19055
  const win32 = process.platform === "win32";
19254
19056
  const {
@@ -19257,36 +19059,36 @@ var constants$5 = {
19257
19059
  REGEX_SPECIAL_CHARS,
19258
19060
  REGEX_SPECIAL_CHARS_GLOBAL
19259
19061
  } = constants$5;
19260
- exports2.isObject = (val) => val !== null && typeof val === "object" && !Array.isArray(val);
19261
- exports2.hasRegexChars = (str2) => REGEX_SPECIAL_CHARS.test(str2);
19262
- exports2.isRegexChar = (str2) => str2.length === 1 && exports2.hasRegexChars(str2);
19263
- exports2.escapeRegex = (str2) => str2.replace(REGEX_SPECIAL_CHARS_GLOBAL, "\\$1");
19264
- exports2.toPosixSlashes = (str2) => str2.replace(REGEX_BACKSLASH, "/");
19265
- exports2.removeBackslashes = (str2) => {
19062
+ exports$1.isObject = (val) => val !== null && typeof val === "object" && !Array.isArray(val);
19063
+ exports$1.hasRegexChars = (str2) => REGEX_SPECIAL_CHARS.test(str2);
19064
+ exports$1.isRegexChar = (str2) => str2.length === 1 && exports$1.hasRegexChars(str2);
19065
+ exports$1.escapeRegex = (str2) => str2.replace(REGEX_SPECIAL_CHARS_GLOBAL, "\\$1");
19066
+ exports$1.toPosixSlashes = (str2) => str2.replace(REGEX_BACKSLASH, "/");
19067
+ exports$1.removeBackslashes = (str2) => {
19266
19068
  return str2.replace(REGEX_REMOVE_BACKSLASH, (match) => {
19267
19069
  return match === "\\" ? "" : match;
19268
19070
  });
19269
19071
  };
19270
- exports2.supportsLookbehinds = () => {
19072
+ exports$1.supportsLookbehinds = () => {
19271
19073
  const segs = process.version.slice(1).split(".").map(Number);
19272
19074
  if (segs.length === 3 && segs[0] >= 9 || segs[0] === 8 && segs[1] >= 10) {
19273
19075
  return true;
19274
19076
  }
19275
19077
  return false;
19276
19078
  };
19277
- exports2.isWindows = (options2) => {
19079
+ exports$1.isWindows = (options2) => {
19278
19080
  if (options2 && typeof options2.windows === "boolean") {
19279
19081
  return options2.windows;
19280
19082
  }
19281
19083
  return win32 === true || path2.sep === "\\";
19282
19084
  };
19283
- exports2.escapeLast = (input, char, lastIdx) => {
19085
+ exports$1.escapeLast = (input, char, lastIdx) => {
19284
19086
  const idx = input.lastIndexOf(char, lastIdx);
19285
19087
  if (idx === -1) return input;
19286
- if (input[idx - 1] === "\\") return exports2.escapeLast(input, char, idx - 1);
19088
+ if (input[idx - 1] === "\\") return exports$1.escapeLast(input, char, idx - 1);
19287
19089
  return `${input.slice(0, idx)}\\${input.slice(idx)}`;
19288
19090
  };
19289
- exports2.removePrefix = (input, state = {}) => {
19091
+ exports$1.removePrefix = (input, state = {}) => {
19290
19092
  let output = input;
19291
19093
  if (output.startsWith("./")) {
19292
19094
  output = output.slice(2);
@@ -19294,7 +19096,7 @@ var constants$5 = {
19294
19096
  }
19295
19097
  return output;
19296
19098
  };
19297
- exports2.wrapOutput = (input, state = {}, options2 = {}) => {
19099
+ exports$1.wrapOutput = (input, state = {}, options2 = {}) => {
19298
19100
  const prepend = options2.contains ? "" : "^";
19299
19101
  const append2 = options2.contains ? "" : "$";
19300
19102
  let output = `${prepend}(?:${input})${append2}`;
@@ -22849,6 +22651,18 @@ function charFromCodepoint(c) {
22849
22651
  (c - 65536 & 1023) + 56320
22850
22652
  );
22851
22653
  }
22654
+ function setProperty(object2, key, value) {
22655
+ if (key === "__proto__") {
22656
+ Object.defineProperty(object2, key, {
22657
+ configurable: true,
22658
+ enumerable: true,
22659
+ writable: true,
22660
+ value
22661
+ });
22662
+ } else {
22663
+ object2[key] = value;
22664
+ }
22665
+ }
22852
22666
  var simpleEscapeCheck = new Array(256);
22853
22667
  var simpleEscapeMap = new Array(256);
22854
22668
  for (var i = 0; i < 256; i++) {
@@ -22955,7 +22769,7 @@ function mergeMappings(state, destination, source, overridableKeys) {
22955
22769
  for (index2 = 0, quantity = sourceKeys.length; index2 < quantity; index2 += 1) {
22956
22770
  key = sourceKeys[index2];
22957
22771
  if (!_hasOwnProperty$1.call(destination, key)) {
22958
- destination[key] = source[key];
22772
+ setProperty(destination, key, source[key]);
22959
22773
  overridableKeys[key] = true;
22960
22774
  }
22961
22775
  }
@@ -22994,7 +22808,7 @@ function storeMappingPair(state, _result, overridableKeys, keyTag, keyNode, valu
22994
22808
  state.position = startPos || state.position;
22995
22809
  throwError(state, "duplicated mapping key");
22996
22810
  }
22997
- _result[keyNode] = valueNode;
22811
+ setProperty(_result, keyNode, valueNode);
22998
22812
  delete overridableKeys[keyNode];
22999
22813
  }
23000
22814
  return _result;
@@ -28897,6 +28711,247 @@ _enum([
28897
28711
  "published"
28898
28712
  ]).describe("Filter by publication status");
28899
28713
  string().describe("Search query string");
28714
+ const pluginName = "firebase-authentication";
28715
+ const PLUGIN_NAME = "firebase-authentication";
28716
+ const PLUGIN_UID = `plugin::${PLUGIN_NAME}`;
28717
+ const CONFIG_CONTENT_TYPE = `${PLUGIN_UID}.firebase-authentication-configuration`;
28718
+ const DEFAULT_PASSWORD_RESET_URL = "http://localhost:3000/reset-password";
28719
+ const DEFAULT_PASSWORD_REGEX = "^.{6,}$";
28720
+ const DEFAULT_PASSWORD_MESSAGE = "Password must be at least 6 characters long";
28721
+ const DEFAULT_RESET_EMAIL_SUBJECT = "Reset Your Password";
28722
+ const ERROR_MESSAGES = {
28723
+ FIREBASE_NOT_INITIALIZED: "Firebase is not initialized. Please upload Firebase service account configuration via Settings → Firebase Authentication.",
28724
+ INVALID_JSON: "Invalid JSON format. Please ensure you copied the entire JSON content correctly.",
28725
+ MISSING_DATA: "data is missing",
28726
+ SOMETHING_WENT_WRONG: "Something went wrong",
28727
+ AUTHENTICATION_FAILED: "Authentication failed",
28728
+ TOKEN_MISSING: "idToken is missing!",
28729
+ EMAIL_PASSWORD_REQUIRED: "Email and password are required",
28730
+ PASSWORD_REQUIRED: "Password is required",
28731
+ AUTHORIZATION_REQUIRED: "Authorization token is required",
28732
+ INVALID_TOKEN: "Invalid or expired token",
28733
+ USER_NOT_FOUND: "User not found",
28734
+ USER_NO_EMAIL: "User does not have an email address",
28735
+ FIREBASE_LINK_FAILED: "Failed to generate Firebase reset link",
28736
+ CONFIG_NOT_FOUND: "No config found",
28737
+ INVALID_SERVICE_ACCOUNT: "Invalid service account JSON",
28738
+ WEB_API_NOT_CONFIGURED: "Email/password authentication is not available. Web API Key is not configured.",
28739
+ RESET_URL_NOT_CONFIGURED: "Password reset URL is not configured",
28740
+ RESET_URL_MUST_BE_HTTPS: "Password reset URL must use HTTPS in production",
28741
+ RESET_URL_INVALID_FORMAT: "Password reset URL is not a valid URL format",
28742
+ USER_NOT_LINKED_FIREBASE: "User is not linked to Firebase authentication",
28743
+ OVERRIDE_USER_ID_REQUIRED: "Override user ID is required",
28744
+ EITHER_EMAIL_OR_PHONE_REQUIRED: "Either email or phoneNumber is required",
28745
+ DELETION_NO_CONFIG: "No Firebase configs exists for deletion"
28746
+ };
28747
+ const SUCCESS_MESSAGES = {
28748
+ FIREBASE_INITIALIZED: "Firebase successfully initialized",
28749
+ FIREBASE_CONFIG_DELETED: "Firebase config deleted and reinitialized",
28750
+ PASSWORD_RESET_EMAIL_SENT: "If an account with that email exists, a password reset link has been sent.",
28751
+ SERVER_RESTARTING: "SERVER IS RESTARTING"
28752
+ };
28753
+ const CONFIG_KEYS = {
28754
+ ENCRYPTION_KEY: `${PLUGIN_UID}.FIREBASE_JSON_ENCRYPTION_KEY`
28755
+ };
28756
+ const REQUIRED_FIELDS = {
28757
+ SERVICE_ACCOUNT: ["private_key", "client_email", "project_id", "type"],
28758
+ WEB_CONFIG: ["apiKey", "authDomain"]
28759
+ // These indicate wrong JSON type
28760
+ };
28761
+ const VALIDATION_MESSAGES = {
28762
+ INVALID_SERVICE_ACCOUNT: "Invalid Service Account JSON. Missing required fields:",
28763
+ WRONG_JSON_TYPE: "You uploaded a Web App Config (SDK snippet) instead of a Service Account JSON. Please go to Firebase Console → Service Accounts tab → Generate New Private Key to download the correct file.",
28764
+ SERVICE_ACCOUNT_HELP: "Please download the correct JSON from Firebase Console → Service Accounts → Generate New Private Key. Do NOT use the Web App Config (SDK snippet) - that is a different file!"
28765
+ };
28766
+ const firebaseController = {
28767
+ async validateToken(ctx) {
28768
+ strapi.log.debug("validateToken called");
28769
+ try {
28770
+ const { idToken, profileMetaData } = ctx.request.body || {};
28771
+ const populate2 = ctx.request.query.populate || [];
28772
+ if (!idToken) {
28773
+ return ctx.badRequest(ERROR_MESSAGES.TOKEN_MISSING);
28774
+ }
28775
+ const result = await strapi.plugin(pluginName).service("firebaseService").validateFirebaseToken(idToken, profileMetaData, populate2);
28776
+ ctx.body = result;
28777
+ } catch (error2) {
28778
+ strapi.log.error(`validateToken controller error: ${error2.message}`);
28779
+ if (error2.name === "ValidationError") {
28780
+ return ctx.badRequest(error2.message);
28781
+ }
28782
+ if (error2.name === "UnauthorizedError") {
28783
+ return ctx.unauthorized(error2.message);
28784
+ }
28785
+ throw error2;
28786
+ }
28787
+ },
28788
+ async deleteByEmail(email2) {
28789
+ try {
28790
+ const user = await strapi.firebase.auth().getUserByEmail(email2);
28791
+ await strapi.plugin(pluginName).service("firebaseService").delete(user.toJSON().uid);
28792
+ return user.toJSON();
28793
+ } catch (error2) {
28794
+ strapi.log.error("deleteByEmail error:", error2);
28795
+ throw error2;
28796
+ }
28797
+ },
28798
+ async overrideAccess(ctx) {
28799
+ try {
28800
+ const { overrideUserId } = ctx.request.body || {};
28801
+ const populate2 = ctx.request.query.populate || [];
28802
+ const result = await strapi.plugin(pluginName).service("firebaseService").overrideFirebaseAccess(overrideUserId, populate2);
28803
+ ctx.body = result;
28804
+ } catch (error2) {
28805
+ if (error2.name === "ValidationError") {
28806
+ return ctx.badRequest(error2.message);
28807
+ }
28808
+ if (error2.name === "NotFoundError") {
28809
+ return ctx.notFound(error2.message);
28810
+ }
28811
+ throw error2;
28812
+ }
28813
+ },
28814
+ /**
28815
+ * Controller method for email/password authentication
28816
+ * Handles the `/api/firebase-authentication/emailLogin` endpoint
28817
+ *
28818
+ * @param ctx - Koa context object
28819
+ * @returns Promise that sets ctx.body with user data and JWT or error message
28820
+ *
28821
+ * @remarks
28822
+ * This controller acts as a proxy to Firebase's Identity Toolkit API,
28823
+ * allowing users to authenticate with email/password and receive a Strapi JWT.
28824
+ *
28825
+ * HTTP Status Codes:
28826
+ * - `400`: Validation errors (missing credentials, invalid email/password)
28827
+ * - `500`: Server errors (missing configuration, Firebase API issues)
28828
+ */
28829
+ async emailLogin(ctx) {
28830
+ strapi.log.debug("emailLogin controller");
28831
+ try {
28832
+ const { email: email2, password } = ctx.request.body || {};
28833
+ const populate2 = ctx.request.query.populate || [];
28834
+ const result = await strapi.plugin(pluginName).service("firebaseService").emailLogin(email2, password, populate2);
28835
+ ctx.body = result;
28836
+ } catch (error2) {
28837
+ strapi.log.error("emailLogin controller error:", error2);
28838
+ throw error2;
28839
+ }
28840
+ },
28841
+ /**
28842
+ * Forgot password - sends reset email
28843
+ * POST /api/firebase-authentication/forgotPassword
28844
+ * Public endpoint - no authentication required
28845
+ */
28846
+ async forgotPassword(ctx) {
28847
+ strapi.log.debug("forgotPassword endpoint called");
28848
+ try {
28849
+ const { email: email2 } = ctx.request.body || {};
28850
+ ctx.body = await strapi.plugin(pluginName).service("firebaseService").forgotPassword(email2);
28851
+ } catch (error2) {
28852
+ strapi.log.error("forgotPassword controller error:", error2);
28853
+ throw error2;
28854
+ }
28855
+ },
28856
+ /**
28857
+ * Reset password - authenticated password change
28858
+ * POST /api/firebase-authentication/resetPassword
28859
+ * Authenticated endpoint - requires valid JWT (enforced by is-authenticated policy)
28860
+ * Used for admin-initiated resets or user self-service password changes
28861
+ * NOT used for forgot password email flow (which uses Firebase's hosted UI)
28862
+ */
28863
+ async resetPassword(ctx) {
28864
+ strapi.log.debug("resetPassword endpoint called");
28865
+ try {
28866
+ const { password } = ctx.request.body || {};
28867
+ const user = ctx.state.user;
28868
+ const populate2 = ctx.request.query.populate || [];
28869
+ ctx.body = await strapi.plugin(pluginName).service("firebaseService").resetPassword(password, user, populate2);
28870
+ } catch (error2) {
28871
+ strapi.log.error("resetPassword controller error:", error2);
28872
+ throw error2;
28873
+ }
28874
+ },
28875
+ async requestMagicLink(ctx) {
28876
+ try {
28877
+ const { email: email2 } = ctx.request.body || {};
28878
+ const result = await strapi.plugin("firebase-authentication").service("firebaseService").requestMagicLink(email2);
28879
+ ctx.body = result;
28880
+ } catch (error2) {
28881
+ if (error2.name === "ValidationError" || error2.name === "ApplicationError") {
28882
+ throw error2;
28883
+ }
28884
+ strapi.log.error("requestMagicLink controller error:", error2);
28885
+ ctx.body = {
28886
+ success: false,
28887
+ message: "An error occurred while processing your request"
28888
+ };
28889
+ }
28890
+ },
28891
+ /**
28892
+ * Reset password using custom JWT token
28893
+ * POST /api/firebase-authentication/resetPasswordWithToken
28894
+ * Public endpoint - token provides authentication
28895
+ *
28896
+ * @param ctx - Koa context with { token, newPassword } in body
28897
+ * @returns { success: true, message: "Password has been reset successfully" }
28898
+ */
28899
+ async resetPasswordWithToken(ctx) {
28900
+ strapi.log.debug("resetPasswordWithToken endpoint called");
28901
+ try {
28902
+ const { token, newPassword } = ctx.request.body || {};
28903
+ if (!token) {
28904
+ throw new ValidationError$1("Token is required");
28905
+ }
28906
+ if (!newPassword) {
28907
+ throw new ValidationError$1("New password is required");
28908
+ }
28909
+ const result = await strapi.plugin(pluginName).service("userService").resetPasswordWithToken(token, newPassword);
28910
+ ctx.body = result;
28911
+ } catch (error2) {
28912
+ strapi.log.error("resetPasswordWithToken controller error:", error2);
28913
+ throw error2;
28914
+ }
28915
+ },
28916
+ /**
28917
+ * Send email verification - public endpoint
28918
+ * POST /api/firebase-authentication/sendVerificationEmail
28919
+ * Authenticated endpoint - sends verification email to the logged-in user's email
28920
+ */
28921
+ async sendVerificationEmail(ctx) {
28922
+ strapi.log.debug("sendVerificationEmail endpoint called");
28923
+ try {
28924
+ const user = ctx.state.user;
28925
+ const email2 = user.email;
28926
+ ctx.body = await strapi.plugin(pluginName).service("firebaseService").sendVerificationEmail(email2);
28927
+ } catch (error2) {
28928
+ strapi.log.error("sendVerificationEmail controller error:", error2);
28929
+ throw error2;
28930
+ }
28931
+ },
28932
+ /**
28933
+ * Verify email using custom JWT token
28934
+ * POST /api/firebase-authentication/verifyEmail
28935
+ * Public endpoint - token provides authentication
28936
+ *
28937
+ * @param ctx - Koa context with { token } in body
28938
+ * @returns { success: true, message: "Email verified successfully" }
28939
+ */
28940
+ async verifyEmail(ctx) {
28941
+ strapi.log.debug("verifyEmail endpoint called");
28942
+ try {
28943
+ const { token } = ctx.request.body || {};
28944
+ if (!token) {
28945
+ throw new ValidationError$1("Token is required");
28946
+ }
28947
+ const result = await strapi.plugin(pluginName).service("firebaseService").verifyEmail(token);
28948
+ ctx.body = result;
28949
+ } catch (error2) {
28950
+ strapi.log.error("verifyEmail controller error:", error2);
28951
+ throw error2;
28952
+ }
28953
+ }
28954
+ };
28900
28955
  const STRAPI_DESTINATION = "strapi";
28901
28956
  const FIREBASE_DESTINATION = "firebase";
28902
28957
  const userController = {
@@ -28982,6 +29037,17 @@ const userController = {
28982
29037
  } catch (error2) {
28983
29038
  throw new ApplicationError$2(error2.message || "Failed to send password reset email");
28984
29039
  }
29040
+ },
29041
+ sendVerificationEmail: async (ctx) => {
29042
+ const userId = ctx.params.id;
29043
+ if (!userId) {
29044
+ throw new ValidationError$1("User ID is required");
29045
+ }
29046
+ try {
29047
+ ctx.body = await strapi.plugin("firebase-authentication").service("userService").sendVerificationEmail(userId);
29048
+ } catch (error2) {
29049
+ throw new ApplicationError$2(error2.message || "Failed to send verification email");
29050
+ }
28985
29051
  }
28986
29052
  };
28987
29053
  const settingsController = {
@@ -29084,7 +29150,9 @@ const settingsController = {
29084
29150
  enableMagicLink = false,
29085
29151
  magicLinkUrl = "http://localhost:1338/verify-magic-link.html",
29086
29152
  magicLinkEmailSubject = "Sign in to Your Application",
29087
- magicLinkExpiryHours = 1
29153
+ magicLinkExpiryHours = 1,
29154
+ emailVerificationUrl = "http://localhost:3000/verify-email",
29155
+ emailVerificationEmailSubject = "Verify Your Email"
29088
29156
  } = requestBody;
29089
29157
  const existingConfig = await strapi.db.query("plugin::firebase-authentication.firebase-authentication-configuration").findOne({ where: {} });
29090
29158
  let result;
@@ -29098,7 +29166,9 @@ const settingsController = {
29098
29166
  enableMagicLink,
29099
29167
  magicLinkUrl,
29100
29168
  magicLinkEmailSubject,
29101
- magicLinkExpiryHours
29169
+ magicLinkExpiryHours,
29170
+ emailVerificationUrl,
29171
+ emailVerificationEmailSubject
29102
29172
  }
29103
29173
  });
29104
29174
  } else {
@@ -29112,7 +29182,9 @@ const settingsController = {
29112
29182
  enableMagicLink,
29113
29183
  magicLinkUrl,
29114
29184
  magicLinkEmailSubject,
29115
- magicLinkExpiryHours
29185
+ magicLinkExpiryHours,
29186
+ emailVerificationUrl,
29187
+ emailVerificationEmailSubject
29116
29188
  }
29117
29189
  });
29118
29190
  }
@@ -29124,7 +29196,9 @@ const settingsController = {
29124
29196
  enableMagicLink: result.enableMagicLink,
29125
29197
  magicLinkUrl: result.magicLinkUrl,
29126
29198
  magicLinkEmailSubject: result.magicLinkEmailSubject,
29127
- magicLinkExpiryHours: result.magicLinkExpiryHours
29199
+ magicLinkExpiryHours: result.magicLinkExpiryHours,
29200
+ emailVerificationUrl: result.emailVerificationUrl,
29201
+ emailVerificationEmailSubject: result.emailVerificationEmailSubject
29128
29202
  };
29129
29203
  } catch (error2) {
29130
29204
  throw new ApplicationError$2("Error saving password configuration", {
@@ -29139,7 +29213,16 @@ const controllers = {
29139
29213
  settingsController
29140
29214
  };
29141
29215
  const middlewares = {};
29142
- const policies = {};
29216
+ const isAuthenticated = async (policyContext) => {
29217
+ const user = policyContext.state.user;
29218
+ if (!user) {
29219
+ throw new UnauthorizedError("Authentication required");
29220
+ }
29221
+ return true;
29222
+ };
29223
+ const policies = {
29224
+ "is-authenticated": isAuthenticated
29225
+ };
29143
29226
  const settingsRoute = [
29144
29227
  {
29145
29228
  method: "POST",
@@ -29217,6 +29300,14 @@ const admin = {
29217
29300
  policies: ["admin::isAuthenticatedAdmin"]
29218
29301
  }
29219
29302
  },
29303
+ {
29304
+ method: "PUT",
29305
+ path: "/users/sendVerificationEmail/:id",
29306
+ handler: "userController.sendVerificationEmail",
29307
+ config: {
29308
+ policies: ["admin::isAuthenticatedAdmin"]
29309
+ }
29310
+ },
29220
29311
  {
29221
29312
  method: "GET",
29222
29313
  path: "/users/:id",
@@ -29281,9 +29372,7 @@ const contentApi = {
29281
29372
  path: "/resetPassword",
29282
29373
  handler: "firebaseController.resetPassword",
29283
29374
  config: {
29284
- auth: false,
29285
- // Public endpoint - authenticated password change, requires valid JWT in Authorization header
29286
- policies: []
29375
+ policies: ["plugin::firebase-authentication.is-authenticated"]
29287
29376
  }
29288
29377
  },
29289
29378
  {
@@ -29315,6 +29404,24 @@ const contentApi = {
29315
29404
  // Public endpoint - token provides authentication
29316
29405
  policies: []
29317
29406
  }
29407
+ },
29408
+ {
29409
+ method: "POST",
29410
+ path: "/sendVerificationEmail",
29411
+ handler: "firebaseController.sendVerificationEmail",
29412
+ config: {
29413
+ policies: ["plugin::firebase-authentication.is-authenticated"]
29414
+ }
29415
+ },
29416
+ {
29417
+ method: "POST",
29418
+ path: "/verifyEmail",
29419
+ handler: "firebaseController.verifyEmail",
29420
+ config: {
29421
+ auth: false,
29422
+ // Public endpoint - token provides authentication
29423
+ policies: []
29424
+ }
29318
29425
  }
29319
29426
  ]
29320
29427
  };
@@ -29435,7 +29542,10 @@ const settingsService = ({ strapi: strapi2 }) => {
29435
29542
  enableMagicLink: configObject.enableMagicLink || false,
29436
29543
  magicLinkUrl: configObject.magicLinkUrl || "http://localhost:1338/verify-magic-link.html",
29437
29544
  magicLinkEmailSubject: configObject.magicLinkEmailSubject || "Sign in to Your Application",
29438
- magicLinkExpiryHours: configObject.magicLinkExpiryHours || 1
29545
+ magicLinkExpiryHours: configObject.magicLinkExpiryHours || 1,
29546
+ // Include email verification configuration fields
29547
+ emailVerificationUrl: configObject.emailVerificationUrl || "http://localhost:3000/verify-email",
29548
+ emailVerificationEmailSubject: configObject.emailVerificationEmailSubject || "Verify Your Email"
29439
29549
  };
29440
29550
  } catch (error2) {
29441
29551
  strapi2.log.error(`Firebase config error: ${error2.message}`);
@@ -29478,7 +29588,9 @@ const settingsService = ({ strapi: strapi2 }) => {
29478
29588
  enableMagicLink = false,
29479
29589
  magicLinkUrl = "http://localhost:1338/verify-magic-link.html",
29480
29590
  magicLinkEmailSubject = "Sign in to Your Application",
29481
- magicLinkExpiryHours = 1
29591
+ magicLinkExpiryHours = 1,
29592
+ emailVerificationUrl = "http://localhost:3000/verify-email",
29593
+ emailVerificationEmailSubject = "Verify Your Email"
29482
29594
  } = requestBody;
29483
29595
  if (!requestBody) throw new ValidationError3(ERROR_MESSAGES.MISSING_DATA);
29484
29596
  try {
@@ -29514,7 +29626,9 @@ const settingsService = ({ strapi: strapi2 }) => {
29514
29626
  enableMagicLink,
29515
29627
  magicLinkUrl,
29516
29628
  magicLinkEmailSubject,
29517
- magicLinkExpiryHours
29629
+ magicLinkExpiryHours,
29630
+ emailVerificationUrl,
29631
+ emailVerificationEmailSubject
29518
29632
  }
29519
29633
  });
29520
29634
  } else {
@@ -29530,11 +29644,20 @@ const settingsService = ({ strapi: strapi2 }) => {
29530
29644
  enableMagicLink,
29531
29645
  magicLinkUrl,
29532
29646
  magicLinkEmailSubject,
29533
- magicLinkExpiryHours
29647
+ magicLinkExpiryHours,
29648
+ emailVerificationUrl,
29649
+ emailVerificationEmailSubject
29534
29650
  }
29535
29651
  });
29536
29652
  }
29537
29653
  await strapi2.plugin("firebase-authentication").service("settingsService").init();
29654
+ setImmediate(async () => {
29655
+ try {
29656
+ await strapi2.plugin("firebase-authentication").service("autoLinkService").linkAllUsers(strapi2);
29657
+ } catch (error2) {
29658
+ strapi2.log.error(`Auto-linking after config save failed: ${error2.message}`);
29659
+ }
29660
+ });
29538
29661
  const configData = res.firebaseConfigJson || res.firebase_config_json;
29539
29662
  if (!configData) {
29540
29663
  strapi2.log.error("Firebase config data missing from database response");
@@ -29558,6 +29681,8 @@ const settingsService = ({ strapi: strapi2 }) => {
29558
29681
  res.magicLinkUrl = res.magicLinkUrl || magicLinkUrl;
29559
29682
  res.magicLinkEmailSubject = res.magicLinkEmailSubject || magicLinkEmailSubject;
29560
29683
  res.magicLinkExpiryHours = res.magicLinkExpiryHours || magicLinkExpiryHours;
29684
+ res.emailVerificationUrl = res.emailVerificationUrl || emailVerificationUrl;
29685
+ res.emailVerificationEmailSubject = res.emailVerificationEmailSubject || emailVerificationEmailSubject;
29561
29686
  return res;
29562
29687
  } catch (error2) {
29563
29688
  strapi2.log.error("=== FIREBASE CONFIG SAVE ERROR ===");
@@ -30217,6 +30342,40 @@ const userService = ({ strapi: strapi2 }) => {
30217
30342
  }
30218
30343
  throw new ApplicationError$2(e.message?.toString() || "Failed to reset password");
30219
30344
  }
30345
+ },
30346
+ /**
30347
+ * Send email verification email (admin-initiated)
30348
+ * @param entityId - Firebase UID of the user
30349
+ */
30350
+ sendVerificationEmail: async (entityId) => {
30351
+ try {
30352
+ ensureFirebaseInitialized();
30353
+ const user = await strapi2.firebase.auth().getUser(entityId);
30354
+ if (!user.email) {
30355
+ throw new ApplicationError$2("User does not have an email address");
30356
+ }
30357
+ if (user.emailVerified) {
30358
+ return { success: true, message: "Email is already verified" };
30359
+ }
30360
+ const config2 = await strapi2.db.query("plugin::firebase-authentication.firebase-authentication-configuration").findOne({ where: {} });
30361
+ const emailVerificationUrl = config2?.emailVerificationUrl;
30362
+ if (!emailVerificationUrl) {
30363
+ throw new ApplicationError$2("Email verification URL is not configured");
30364
+ }
30365
+ const firebaseUserData2 = await strapi2.plugin("firebase-authentication").service("firebaseUserDataService").getByFirebaseUID(entityId);
30366
+ if (!firebaseUserData2) {
30367
+ throw new ApplicationError$2("User is not linked to Firebase authentication");
30368
+ }
30369
+ const tokenService2 = strapi2.plugin("firebase-authentication").service("tokenService");
30370
+ const token = await tokenService2.generateVerificationToken(firebaseUserData2.documentId, user.email);
30371
+ const verificationLink = `${emailVerificationUrl}?token=${token}`;
30372
+ strapi2.log.debug(`Generated email verification link for user ${user.email}`);
30373
+ const emailService2 = strapi2.plugin("firebase-authentication").service("emailService");
30374
+ return await emailService2.sendVerificationEmail(user, verificationLink);
30375
+ } catch (e) {
30376
+ strapi2.log.error(`sendVerificationEmail error: ${e.message}`);
30377
+ throw new ApplicationError$2(e.message?.toString() || "Failed to send verification email");
30378
+ }
30220
30379
  }
30221
30380
  };
30222
30381
  };
@@ -30760,20 +30919,17 @@ const firebaseService = ({ strapi: strapi2 }) => ({
30760
30919
  * 2. User-initiated password change (when already authenticated)
30761
30920
  *
30762
30921
  * NOT used for forgot password email flow - that now uses Firebase's hosted UI
30922
+ *
30923
+ * @param password - New password to set
30924
+ * @param user - Authenticated user from ctx.state.user (populated by is-authenticated policy)
30925
+ * @param populate - Fields to populate in response
30763
30926
  */
30764
- resetPassword: async (password, token, populate2) => {
30927
+ resetPassword: async (password, user, populate2) => {
30765
30928
  if (!password) {
30766
30929
  throw new ValidationError$1("Password is required");
30767
30930
  }
30768
- if (!token) {
30769
- throw new UnauthorizedError("Authorization token is required");
30770
- }
30771
- let decoded;
30772
- try {
30773
- const jwtService = strapi2.plugin("users-permissions").service("jwt");
30774
- decoded = await jwtService.verify(token);
30775
- } catch (error2) {
30776
- throw new UnauthorizedError("Invalid or expired token");
30931
+ if (!user || !user.id) {
30932
+ throw new UnauthorizedError("Authentication required");
30777
30933
  }
30778
30934
  const config2 = await strapi2.plugin("firebase-authentication").service("settingsService").getFirebaseConfigJson();
30779
30935
  const passwordRegex = config2?.passwordRequirementsRegex || "^.{6,}$";
@@ -30784,8 +30940,7 @@ const firebaseService = ({ strapi: strapi2 }) => ({
30784
30940
  }
30785
30941
  try {
30786
30942
  const strapiUser = await strapi2.db.query("plugin::users-permissions.user").findOne({
30787
- where: { id: decoded.id }
30788
- // Use numeric id from JWT
30943
+ where: { id: user.id }
30789
30944
  });
30790
30945
  if (!strapiUser) {
30791
30946
  throw new NotFoundError("User not found");
@@ -30896,6 +31051,159 @@ const firebaseService = ({ strapi: strapi2 }) => ({
30896
31051
  verificationUrl: magicLinkUrl
30897
31052
  };
30898
31053
  }
31054
+ },
31055
+ /**
31056
+ * Send email verification - public endpoint
31057
+ * Generates a verification token and sends an email to the user
31058
+ * Security: Always returns generic success message to prevent email enumeration
31059
+ */
31060
+ async sendVerificationEmail(email2) {
31061
+ strapi2.log.info(`[sendVerificationEmail] Starting email verification for: ${email2}`);
31062
+ if (!email2) {
31063
+ throw new ValidationError$1("Email is required");
31064
+ }
31065
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
31066
+ if (!emailRegex.test(email2)) {
31067
+ throw new ValidationError$1("Invalid email format");
31068
+ }
31069
+ const config2 = await strapi2.plugin("firebase-authentication").service("settingsService").getFirebaseConfigJson();
31070
+ const verificationUrl = config2?.emailVerificationUrl;
31071
+ if (!verificationUrl) {
31072
+ throw new ApplicationError$2("Email verification URL is not configured");
31073
+ }
31074
+ if (process.env.NODE_ENV === "production" && !verificationUrl.startsWith("https://")) {
31075
+ throw new ApplicationError$2("Email verification URL must use HTTPS in production");
31076
+ }
31077
+ try {
31078
+ new URL(verificationUrl);
31079
+ } catch (error2) {
31080
+ throw new ApplicationError$2("Email verification URL is not a valid URL format");
31081
+ }
31082
+ try {
31083
+ let firebaseUser;
31084
+ try {
31085
+ firebaseUser = await strapi2.firebase.auth().getUserByEmail(email2);
31086
+ } catch (fbError) {
31087
+ strapi2.log.debug("User not found in Firebase");
31088
+ }
31089
+ if (!firebaseUser) {
31090
+ strapi2.log.warn(`⚠️ [sendVerificationEmail] User not found in Firebase for email: ${email2}`);
31091
+ return { message: "If an account with that email exists, a verification link has been sent." };
31092
+ }
31093
+ if (firebaseUser.emailVerified) {
31094
+ strapi2.log.info(`[sendVerificationEmail] User ${email2} is already verified`);
31095
+ return { message: "Email is already verified." };
31096
+ }
31097
+ const firebaseData = await strapi2.plugin("firebase-authentication").service("firebaseUserDataService").getByFirebaseUID(firebaseUser.uid);
31098
+ if (!firebaseData) {
31099
+ strapi2.log.warn(`⚠️ [sendVerificationEmail] No firebase-user-data record for: ${email2}`);
31100
+ return { message: "If an account with that email exists, a verification link has been sent." };
31101
+ }
31102
+ strapi2.log.info(
31103
+ `✅ [sendVerificationEmail] User found: ${JSON.stringify({
31104
+ firebaseUID: firebaseUser.uid,
31105
+ email: firebaseUser.email,
31106
+ emailVerified: firebaseUser.emailVerified
31107
+ })}`
31108
+ );
31109
+ const tokenService2 = strapi2.plugin("firebase-authentication").service("tokenService");
31110
+ const token = await tokenService2.generateVerificationToken(firebaseData.documentId, email2);
31111
+ const verificationLink = `${verificationUrl}?token=${token}`;
31112
+ strapi2.log.info(`✅ [sendVerificationEmail] Verification link generated for ${email2}`);
31113
+ strapi2.log.info(`[sendVerificationEmail] Attempting to send verification email to: ${email2}`);
31114
+ await strapi2.plugin("firebase-authentication").service("emailService").sendVerificationEmail(firebaseUser, verificationLink);
31115
+ strapi2.log.info(`✅ [sendVerificationEmail] Verification email sent successfully to: ${email2}`);
31116
+ return {
31117
+ message: "If an account with that email exists, a verification link has been sent."
31118
+ };
31119
+ } catch (error2) {
31120
+ strapi2.log.error(
31121
+ `❌ [sendVerificationEmail] ERROR: ${JSON.stringify({
31122
+ email: email2,
31123
+ message: error2.message,
31124
+ code: error2.code,
31125
+ name: error2.name,
31126
+ stack: error2.stack
31127
+ })}`
31128
+ );
31129
+ return {
31130
+ message: "If an account with that email exists, a verification link has been sent."
31131
+ };
31132
+ }
31133
+ },
31134
+ /**
31135
+ * Verify email with token - public endpoint
31136
+ * Validates the token and marks the user's email as verified in Firebase
31137
+ */
31138
+ async verifyEmail(token) {
31139
+ strapi2.log.info(`[verifyEmail] Starting email verification with token`);
31140
+ if (!token) {
31141
+ throw new ValidationError$1("Verification token is required");
31142
+ }
31143
+ const tokenService2 = strapi2.plugin("firebase-authentication").service("tokenService");
31144
+ const validationResult = await tokenService2.validateVerificationToken(token);
31145
+ if (!validationResult.valid) {
31146
+ strapi2.log.warn(`[verifyEmail] Token validation failed: ${validationResult.error}`);
31147
+ throw new ValidationError$1(validationResult.error || "Invalid verification link");
31148
+ }
31149
+ const { firebaseUID, firebaseUserDataDocumentId, email: tokenEmail } = validationResult;
31150
+ try {
31151
+ const firebaseUser = await strapi2.firebase.auth().getUser(firebaseUID);
31152
+ if (tokenEmail && firebaseUser.email !== tokenEmail) {
31153
+ strapi2.log.warn(
31154
+ `[verifyEmail] Email changed: token email ${tokenEmail} != current email ${firebaseUser.email}`
31155
+ );
31156
+ await tokenService2.invalidateVerificationToken(firebaseUserDataDocumentId);
31157
+ throw new ValidationError$1(
31158
+ "Email address has changed since verification was requested. Please request a new verification link."
31159
+ );
31160
+ }
31161
+ if (firebaseUser.emailVerified) {
31162
+ strapi2.log.info(`[verifyEmail] User ${firebaseUser.email} is already verified`);
31163
+ await tokenService2.invalidateVerificationToken(firebaseUserDataDocumentId);
31164
+ return {
31165
+ success: true,
31166
+ message: "Email is already verified."
31167
+ };
31168
+ }
31169
+ await strapi2.firebase.auth().updateUser(firebaseUID, {
31170
+ emailVerified: true
31171
+ });
31172
+ try {
31173
+ const firebaseUserDataService2 = strapi2.plugin("firebase-authentication").service("firebaseUserDataService");
31174
+ const firebaseUserData2 = await firebaseUserDataService2.getByFirebaseUID(firebaseUID);
31175
+ if (firebaseUserData2?.user?.documentId) {
31176
+ await strapi2.db.query("plugin::users-permissions.user").update({
31177
+ where: { documentId: firebaseUserData2.user.documentId },
31178
+ data: { confirmed: true }
31179
+ });
31180
+ strapi2.log.info(`✅ [verifyEmail] Strapi user confirmed for: ${firebaseUserData2.user.documentId}`);
31181
+ }
31182
+ } catch (strapiUpdateError) {
31183
+ strapi2.log.warn(
31184
+ `[verifyEmail] Failed to update Strapi user confirmed status: ${strapiUpdateError.message}`
31185
+ );
31186
+ }
31187
+ strapi2.log.info(`✅ [verifyEmail] Email verified successfully for: ${firebaseUser.email}`);
31188
+ await tokenService2.invalidateVerificationToken(firebaseUserDataDocumentId);
31189
+ return {
31190
+ success: true,
31191
+ message: "Email verified successfully."
31192
+ };
31193
+ } catch (error2) {
31194
+ strapi2.log.error(
31195
+ `❌ [verifyEmail] ERROR: ${JSON.stringify({
31196
+ firebaseUID,
31197
+ message: error2.message,
31198
+ code: error2.code,
31199
+ name: error2.name
31200
+ })}`
31201
+ );
31202
+ if (error2 instanceof ValidationError$1) {
31203
+ throw error2;
31204
+ }
31205
+ throw new ApplicationError$2("Failed to verify email. Please try again.");
31206
+ }
30899
31207
  }
30900
31208
  });
30901
31209
  const passwordResetTemplate = {
@@ -31275,13 +31583,144 @@ The <%= appName %> Team
31275
31583
  Need help? Contact us at <%= supportEmail %>
31276
31584
  <% } %>
31277
31585
 
31586
+ © <%= year %> <%= appName %>. All rights reserved.
31587
+ `.trim()
31588
+ };
31589
+ const emailVerificationTemplate = {
31590
+ subject: "Verify Your Email - <%= appName %>",
31591
+ html: `
31592
+ <!DOCTYPE html>
31593
+ <html lang="en">
31594
+ <head>
31595
+ <meta charset="UTF-8">
31596
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
31597
+ <title>Verify Your Email</title>
31598
+ </head>
31599
+ <body style="margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f4f4;">
31600
+ <table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;">
31601
+ <tr>
31602
+ <td align="center" style="padding: 40px 0;">
31603
+ <table role="presentation" style="width: 600px; max-width: 100%; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);">
31604
+ <!-- Header -->
31605
+ <tr>
31606
+ <td style="padding: 40px 40px 20px 40px; text-align: center;">
31607
+ <h1 style="margin: 0; font-size: 28px; color: #333333; font-weight: 600;">
31608
+ Verify Your Email Address
31609
+ </h1>
31610
+ </td>
31611
+ </tr>
31612
+
31613
+ <!-- Content -->
31614
+ <tr>
31615
+ <td style="padding: 20px 40px;">
31616
+ <p style="margin: 0 0 20px 0; font-size: 16px; line-height: 1.6; color: #555555;">
31617
+ Hello<% if (user.firstName) { %> <%= user.firstName %><% } %>,
31618
+ </p>
31619
+
31620
+ <p style="margin: 0 0 20px 0; font-size: 16px; line-height: 1.6; color: #555555;">
31621
+ Thank you for signing up with <strong><%= appName %></strong>! Please verify your email address <strong><%= user.email %></strong> to complete your registration.
31622
+ </p>
31623
+
31624
+ <p style="margin: 0 0 30px 0; font-size: 16px; line-height: 1.6; color: #555555;">
31625
+ Click the button below to verify your email:
31626
+ </p>
31627
+
31628
+ <!-- CTA Button -->
31629
+ <table role="presentation" style="width: 100%; border-collapse: collapse;">
31630
+ <tr>
31631
+ <td align="center" style="padding: 0 0 30px 0;">
31632
+ <a href="<%= verificationLink %>"
31633
+ style="display: inline-block; padding: 14px 32px; background-color: #28a745; color: #ffffff; text-decoration: none; font-size: 16px; font-weight: 600; border-radius: 5px; transition: background-color 0.3s;">
31634
+ Verify Email Address
31635
+ </a>
31636
+ </td>
31637
+ </tr>
31638
+ </table>
31639
+
31640
+ <p style="margin: 0 0 20px 0; font-size: 14px; line-height: 1.6; color: #777777;">
31641
+ Or copy and paste this link into your browser:
31642
+ </p>
31643
+
31644
+ <p style="margin: 0 0 30px 0; font-size: 14px; line-height: 1.6; word-break: break-all; color: #28a745;">
31645
+ <%= verificationLink %>
31646
+ </p>
31647
+
31648
+ <!-- Security Notice -->
31649
+ <table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #fff3cd; border-radius: 4px; margin: 0 0 20px 0;">
31650
+ <tr>
31651
+ <td style="padding: 12px 16px;">
31652
+ <p style="margin: 0; font-size: 14px; line-height: 1.6; color: #856404;">
31653
+ <strong>⚠️ Important:</strong> This link will expire in <strong><%= expiresIn %></strong>.
31654
+ If you didn't create an account with us, you can safely ignore this email.
31655
+ </p>
31656
+ </td>
31657
+ </tr>
31658
+ </table>
31659
+
31660
+ <p style="margin: 0 0 20px 0; font-size: 16px; line-height: 1.6; color: #555555;">
31661
+ For security reasons, please do not share this link with anyone.
31662
+ </p>
31663
+ </td>
31664
+ </tr>
31665
+
31666
+ <!-- Footer -->
31667
+ <tr>
31668
+ <td style="padding: 30px 40px 40px 40px; border-top: 1px solid #eeeeee;">
31669
+ <p style="margin: 0 0 10px 0; font-size: 14px; line-height: 1.6; color: #999999; text-align: center;">
31670
+ Best regards,<br>
31671
+ The <%= appName %> Team
31672
+ </p>
31673
+
31674
+ <% if (supportEmail) { %>
31675
+ <p style="margin: 0 0 10px 0; font-size: 12px; line-height: 1.6; color: #999999; text-align: center;">
31676
+ Need help? Contact us at <a href="mailto:<%= supportEmail %>" style="color: #28a745; text-decoration: none;"><%= supportEmail %></a>
31677
+ </p>
31678
+ <% } %>
31679
+
31680
+ <p style="margin: 0; font-size: 12px; line-height: 1.6; color: #999999; text-align: center;">
31681
+ © <%= year %> <%= appName %>. All rights reserved.
31682
+ </p>
31683
+ </td>
31684
+ </tr>
31685
+ </table>
31686
+ </td>
31687
+ </tr>
31688
+ </table>
31689
+ </body>
31690
+ </html>
31691
+ `.trim(),
31692
+ text: `
31693
+ Verify Your Email Address
31694
+
31695
+ Hello<% if (user.firstName) { %> <%= user.firstName %><% } %>,
31696
+
31697
+ Thank you for signing up with <%= appName %>! Please verify your email address <%= user.email %> to complete your registration.
31698
+
31699
+ To verify your email, please visit the following link:
31700
+
31701
+ <%= verificationLink %>
31702
+
31703
+ This link will expire in <%= expiresIn %>.
31704
+
31705
+ If you didn't create an account with us, you can safely ignore this email.
31706
+
31707
+ For security reasons, please do not share this link with anyone.
31708
+
31709
+ Best regards,
31710
+ The <%= appName %> Team
31711
+
31712
+ <% if (supportEmail) { %>
31713
+ Need help? Contact us at <%= supportEmail %>
31714
+ <% } %>
31715
+
31278
31716
  © <%= year %> <%= appName %>. All rights reserved.
31279
31717
  `.trim()
31280
31718
  };
31281
31719
  const defaultTemplates = {
31282
31720
  passwordReset: passwordResetTemplate,
31283
31721
  magicLink: magicLinkTemplate,
31284
- passwordChanged: passwordChangedTemplate
31722
+ passwordChanged: passwordChangedTemplate,
31723
+ emailVerification: emailVerificationTemplate
31285
31724
  };
31286
31725
  class TemplateService {
31287
31726
  /**
@@ -31693,6 +32132,114 @@ class EmailService {
31693
32132
  message: "Password changed but confirmation email could not be sent"
31694
32133
  };
31695
32134
  }
32135
+ /**
32136
+ * Send email verification email with three-tier fallback system
32137
+ * Tier 1: Strapi Email Plugin (if configured)
32138
+ * Tier 2: Custom Hook Function (if provided in config)
32139
+ * Tier 3: Development Console Logging (dev mode only)
32140
+ */
32141
+ async sendVerificationEmail(user, verificationLink) {
32142
+ if (!user.email) {
32143
+ throw new ValidationError$1("User does not have an email address");
32144
+ }
32145
+ const variables = {
32146
+ user: {
32147
+ email: user.email,
32148
+ firstName: user.firstName || user.displayName?.split(" ")[0],
32149
+ lastName: user.lastName,
32150
+ displayName: user.displayName,
32151
+ phoneNumber: user.phoneNumber,
32152
+ uid: user.uid
32153
+ },
32154
+ verificationLink,
32155
+ expiresIn: "1 hour"
32156
+ };
32157
+ const settingsService2 = strapi.plugin("firebase-authentication").service("settingsService");
32158
+ const dbConfig = await settingsService2.getFirebaseConfigJson();
32159
+ const customSubject = dbConfig?.emailVerificationEmailSubject;
32160
+ const pluginConfig = strapi.config.get("plugin::firebase-authentication");
32161
+ const appConfig = pluginConfig?.app || {};
32162
+ const completeVariables = {
32163
+ ...variables,
32164
+ appName: appConfig?.name || "Your Application",
32165
+ appUrl: appConfig?.url || process.env.PUBLIC_URL || "http://localhost:3000",
32166
+ supportEmail: appConfig?.supportEmail,
32167
+ year: (/* @__PURE__ */ new Date()).getFullYear(),
32168
+ expiresIn: variables.expiresIn
32169
+ };
32170
+ const templateService2 = strapi.plugin("firebase-authentication").service("templateService");
32171
+ const template = await templateService2.getTemplate("emailVerification");
32172
+ const subjectTemplate = customSubject || template.subject;
32173
+ const compiledSubject = _$1.template(subjectTemplate)(completeVariables);
32174
+ try {
32175
+ const compiledHtml = template.html ? _$1.template(template.html)(completeVariables) : void 0;
32176
+ const compiledText = template.text ? _$1.template(template.text)(completeVariables) : void 0;
32177
+ const emailPlugin = strapi.plugin("email");
32178
+ if (!emailPlugin) {
32179
+ throw new Error("Email plugin not found");
32180
+ }
32181
+ const emailService2 = emailPlugin.service("email");
32182
+ await emailService2.send({
32183
+ to: user.email,
32184
+ subject: compiledSubject,
32185
+ html: compiledHtml,
32186
+ text: compiledText
32187
+ });
32188
+ strapi.log.info(`✅ Email verification sent via Strapi email plugin to ${user.email}`);
32189
+ return {
32190
+ success: true,
32191
+ message: `Verification email sent to ${user.email}`
32192
+ };
32193
+ } catch (tier1Error) {
32194
+ strapi.log.debug(`Strapi email plugin failed: ${tier1Error.message}. Trying fallback options...`);
32195
+ }
32196
+ const customSender = pluginConfig?.sendVerificationEmail;
32197
+ if (customSender && typeof customSender === "function") {
32198
+ try {
32199
+ const compiledHtml = template.html ? _$1.template(template.html)(completeVariables) : void 0;
32200
+ const compiledText = template.text ? _$1.template(template.text)(completeVariables) : void 0;
32201
+ await customSender({
32202
+ to: user.email,
32203
+ subject: compiledSubject,
32204
+ html: compiledHtml,
32205
+ text: compiledText,
32206
+ verificationLink,
32207
+ variables: completeVariables
32208
+ });
32209
+ strapi.log.info(`✅ Email verification sent via custom hook to ${user.email}`);
32210
+ return {
32211
+ success: true,
32212
+ message: `Verification email sent to ${user.email}`
32213
+ };
32214
+ } catch (tier2Error) {
32215
+ strapi.log.error(`Custom hook failed: ${tier2Error.message}. Continuing to next fallback...`);
32216
+ }
32217
+ }
32218
+ if (process.env.NODE_ENV !== "production") {
32219
+ try {
32220
+ strapi.log.info("\n" + "=".repeat(80));
32221
+ strapi.log.info("EMAIL VERIFICATION (Development Mode)");
32222
+ strapi.log.info("=".repeat(80));
32223
+ strapi.log.info(`To: ${user.email}`);
32224
+ strapi.log.info(`Subject: ${compiledSubject}`);
32225
+ strapi.log.info(`Verification Link: ${verificationLink}`);
32226
+ strapi.log.info(`Expires In: 1 hour`);
32227
+ strapi.log.info("=".repeat(80));
32228
+ strapi.log.info("Note: Email not sent - no email service configured");
32229
+ strapi.log.info("Copy the link above and open in your browser to verify");
32230
+ strapi.log.info("=".repeat(80) + "\n");
32231
+ return {
32232
+ success: true,
32233
+ message: "Verification link logged to console (development mode)"
32234
+ };
32235
+ } catch (tier3Error) {
32236
+ strapi.log.error(`Development fallback failed: ${tier3Error.message}`);
32237
+ }
32238
+ }
32239
+ throw new ApplicationError$2(
32240
+ "Email service is not configured. Please configure Strapi email plugin or provide custom sendVerificationEmail function in plugin config."
32241
+ );
32242
+ }
31696
32243
  }
31697
32244
  const emailService = ({ strapi: strapi2 }) => new EmailService();
31698
32245
  const firebaseUserDataService = ({ strapi: strapi2 }) => ({
@@ -32055,7 +32602,7 @@ var TokenExpiredError_1 = TokenExpiredError$1;
32055
32602
  var jws$3 = {};
32056
32603
  var safeBuffer = { exports: {} };
32057
32604
  /*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
32058
- (function(module2, exports2) {
32605
+ (function(module2, exports$1) {
32059
32606
  var buffer = require$$0__default$5.default;
32060
32607
  var Buffer2 = buffer.Buffer;
32061
32608
  function copyProps(src, dst) {
@@ -32066,8 +32613,8 @@ var safeBuffer = { exports: {} };
32066
32613
  if (Buffer2.from && Buffer2.alloc && Buffer2.allocUnsafe && Buffer2.allocUnsafeSlow) {
32067
32614
  module2.exports = buffer;
32068
32615
  } else {
32069
- copyProps(buffer, exports2);
32070
- exports2.Buffer = SafeBuffer;
32616
+ copyProps(buffer, exports$1);
32617
+ exports$1.Buffer = SafeBuffer;
32071
32618
  }
32072
32619
  function SafeBuffer(arg, encodingOrOffset, length) {
32073
32620
  return Buffer2(arg, encodingOrOffset, length);
@@ -32582,7 +33129,12 @@ function jwsSign(opts) {
32582
33129
  return util$1.format("%s.%s", securedInput, signature);
32583
33130
  }
32584
33131
  function SignStream$1(opts) {
32585
- var secret = opts.secret || opts.privateKey || opts.key;
33132
+ var secret = opts.secret;
33133
+ secret = secret == null ? opts.privateKey : secret;
33134
+ secret = secret == null ? opts.key : secret;
33135
+ if (/^hs/i.test(opts.header.alg) === true && secret == null) {
33136
+ throw new TypeError("secret must be a string or buffer or a KeyObject");
33137
+ }
32586
33138
  var secretStream = new DataStream$1(secret);
32587
33139
  this.readable = true;
32588
33140
  this.header = opts.header;
@@ -32688,7 +33240,12 @@ function jwsDecode(jwsSig, opts) {
32688
33240
  }
32689
33241
  function VerifyStream$1(opts) {
32690
33242
  opts = opts || {};
32691
- var secretOrKey = opts.secret || opts.publicKey || opts.key;
33243
+ var secretOrKey = opts.secret;
33244
+ secretOrKey = secretOrKey == null ? opts.publicKey : secretOrKey;
33245
+ secretOrKey = secretOrKey == null ? opts.key : secretOrKey;
33246
+ if (/^hs/i.test(opts.algorithm) === true && secretOrKey == null) {
33247
+ throw new TypeError("secret must be a string or buffer or a KeyObject");
33248
+ }
32692
33249
  var secretStream = new DataStream(secretOrKey);
32693
33250
  this.readable = true;
32694
33251
  this.algorithm = opts.algorithm;
@@ -32931,19 +33488,19 @@ var constants$1 = {
32931
33488
  const debug$1 = typeof process === "object" && process.env && process.env.NODE_DEBUG && /\bsemver\b/i.test(process.env.NODE_DEBUG) ? (...args) => console.error("SEMVER", ...args) : () => {
32932
33489
  };
32933
33490
  var debug_1 = debug$1;
32934
- (function(module2, exports2) {
33491
+ (function(module2, exports$1) {
32935
33492
  const {
32936
33493
  MAX_SAFE_COMPONENT_LENGTH: MAX_SAFE_COMPONENT_LENGTH2,
32937
33494
  MAX_SAFE_BUILD_LENGTH: MAX_SAFE_BUILD_LENGTH2,
32938
33495
  MAX_LENGTH: MAX_LENGTH2
32939
33496
  } = constants$1;
32940
33497
  const debug2 = debug_1;
32941
- exports2 = module2.exports = {};
32942
- const re2 = exports2.re = [];
32943
- const safeRe = exports2.safeRe = [];
32944
- const src = exports2.src = [];
32945
- const safeSrc = exports2.safeSrc = [];
32946
- const t2 = exports2.t = {};
33498
+ exports$1 = module2.exports = {};
33499
+ const re2 = exports$1.re = [];
33500
+ const safeRe = exports$1.safeRe = [];
33501
+ const src = exports$1.src = [];
33502
+ const safeSrc = exports$1.safeSrc = [];
33503
+ const t2 = exports$1.t = {};
32947
33504
  let R = 0;
32948
33505
  const LETTERDASHNUMBER = "[a-zA-Z0-9-]";
32949
33506
  const safeRegexReplacements = [
@@ -32996,18 +33553,18 @@ var debug_1 = debug$1;
32996
33553
  createToken("COERCERTLFULL", src[t2.COERCEFULL], true);
32997
33554
  createToken("LONETILDE", "(?:~>?)");
32998
33555
  createToken("TILDETRIM", `(\\s*)${src[t2.LONETILDE]}\\s+`, true);
32999
- exports2.tildeTrimReplace = "$1~";
33556
+ exports$1.tildeTrimReplace = "$1~";
33000
33557
  createToken("TILDE", `^${src[t2.LONETILDE]}${src[t2.XRANGEPLAIN]}$`);
33001
33558
  createToken("TILDELOOSE", `^${src[t2.LONETILDE]}${src[t2.XRANGEPLAINLOOSE]}$`);
33002
33559
  createToken("LONECARET", "(?:\\^)");
33003
33560
  createToken("CARETTRIM", `(\\s*)${src[t2.LONECARET]}\\s+`, true);
33004
- exports2.caretTrimReplace = "$1^";
33561
+ exports$1.caretTrimReplace = "$1^";
33005
33562
  createToken("CARET", `^${src[t2.LONECARET]}${src[t2.XRANGEPLAIN]}$`);
33006
33563
  createToken("CARETLOOSE", `^${src[t2.LONECARET]}${src[t2.XRANGEPLAINLOOSE]}$`);
33007
33564
  createToken("COMPARATORLOOSE", `^${src[t2.GTLT]}\\s*(${src[t2.LOOSEPLAIN]})$|^$`);
33008
33565
  createToken("COMPARATOR", `^${src[t2.GTLT]}\\s*(${src[t2.FULLPLAIN]})$|^$`);
33009
33566
  createToken("COMPARATORTRIM", `(\\s*)${src[t2.GTLT]}\\s*(${src[t2.LOOSEPLAIN]}|${src[t2.XRANGEPLAIN]})`, true);
33010
- exports2.comparatorTrimReplace = "$1$2$3";
33567
+ exports$1.comparatorTrimReplace = "$1$2$3";
33011
33568
  createToken("HYPHENRANGE", `^\\s*(${src[t2.XRANGEPLAIN]})\\s+-\\s+(${src[t2.XRANGEPLAIN]})\\s*$`);
33012
33569
  createToken("HYPHENRANGELOOSE", `^\\s*(${src[t2.XRANGEPLAINLOOSE]})\\s+-\\s+(${src[t2.XRANGEPLAINLOOSE]})\\s*$`);
33013
33570
  createToken("STAR", "(<|>)?=?\\s*\\*");
@@ -35146,6 +35703,125 @@ const tokenService = ({ strapi: strapi2 }) => {
35146
35703
  }
35147
35704
  });
35148
35705
  strapi2.log.debug(`Invalidated reset token for user ${firebaseUserDataDocumentId}`);
35706
+ },
35707
+ // ==================== EMAIL VERIFICATION TOKENS ====================
35708
+ /**
35709
+ * Generate an email verification token for a user
35710
+ * @param firebaseUserDataDocumentId - The documentId of the firebase-user-data record
35711
+ * @param email - The email address at time of request (for change detection)
35712
+ * @returns The JWT token to include in the verification URL
35713
+ */
35714
+ async generateVerificationToken(firebaseUserDataDocumentId, email2) {
35715
+ const signingKey = getSigningKey();
35716
+ const jti = crypto__default.default.randomBytes(32).toString("hex");
35717
+ const payload = {
35718
+ sub: firebaseUserDataDocumentId,
35719
+ purpose: "email-verification",
35720
+ email: email2,
35721
+ jti
35722
+ };
35723
+ const token = jwt.sign(payload, signingKey, {
35724
+ expiresIn: "1h"
35725
+ });
35726
+ const tokenHash = crypto__default.default.createHash("sha256").update(jti).digest("hex");
35727
+ const expiresAt = new Date(Date.now() + 60 * 60 * 1e3);
35728
+ await strapi2.db.query(FIREBASE_USER_DATA_CONTENT_TYPE).update({
35729
+ where: { documentId: firebaseUserDataDocumentId },
35730
+ data: {
35731
+ verificationTokenHash: tokenHash,
35732
+ verificationTokenExpiresAt: expiresAt.toISOString()
35733
+ }
35734
+ });
35735
+ strapi2.log.debug(`Generated verification token for user ${firebaseUserDataDocumentId}`);
35736
+ return token;
35737
+ },
35738
+ /**
35739
+ * Validate an email verification token
35740
+ * @param token - The JWT token from the verification URL
35741
+ * @returns Validation result with user info and email if valid
35742
+ */
35743
+ async validateVerificationToken(token) {
35744
+ const signingKey = getSigningKey();
35745
+ try {
35746
+ const decoded = jwt.verify(token, signingKey);
35747
+ if (decoded.purpose !== "email-verification") {
35748
+ return {
35749
+ valid: false,
35750
+ firebaseUserDataDocumentId: "",
35751
+ firebaseUID: "",
35752
+ error: "Invalid token purpose"
35753
+ };
35754
+ }
35755
+ const tokenHash = crypto__default.default.createHash("sha256").update(decoded.jti).digest("hex");
35756
+ const firebaseUserData2 = await strapi2.db.query(FIREBASE_USER_DATA_CONTENT_TYPE).findOne({
35757
+ where: { documentId: decoded.sub }
35758
+ });
35759
+ if (!firebaseUserData2) {
35760
+ return {
35761
+ valid: false,
35762
+ firebaseUserDataDocumentId: "",
35763
+ firebaseUID: "",
35764
+ error: "User not found"
35765
+ };
35766
+ }
35767
+ if (firebaseUserData2.verificationTokenHash !== tokenHash) {
35768
+ return {
35769
+ valid: false,
35770
+ firebaseUserDataDocumentId: "",
35771
+ firebaseUID: "",
35772
+ error: "Verification link has already been used or is invalid"
35773
+ };
35774
+ }
35775
+ if (firebaseUserData2.verificationTokenExpiresAt) {
35776
+ const expiresAt = new Date(firebaseUserData2.verificationTokenExpiresAt);
35777
+ if (expiresAt < /* @__PURE__ */ new Date()) {
35778
+ return {
35779
+ valid: false,
35780
+ firebaseUserDataDocumentId: "",
35781
+ firebaseUID: "",
35782
+ error: "Verification link has expired"
35783
+ };
35784
+ }
35785
+ }
35786
+ return {
35787
+ valid: true,
35788
+ firebaseUserDataDocumentId: firebaseUserData2.documentId,
35789
+ firebaseUID: firebaseUserData2.firebaseUserID,
35790
+ email: decoded.email
35791
+ };
35792
+ } catch (error2) {
35793
+ if (error2.name === "TokenExpiredError") {
35794
+ return {
35795
+ valid: false,
35796
+ firebaseUserDataDocumentId: "",
35797
+ firebaseUID: "",
35798
+ error: "Verification link has expired"
35799
+ };
35800
+ }
35801
+ if (error2.name === "JsonWebTokenError") {
35802
+ return {
35803
+ valid: false,
35804
+ firebaseUserDataDocumentId: "",
35805
+ firebaseUID: "",
35806
+ error: "Invalid verification link"
35807
+ };
35808
+ }
35809
+ throw error2;
35810
+ }
35811
+ },
35812
+ /**
35813
+ * Invalidate a verification token after use
35814
+ * @param firebaseUserDataDocumentId - The documentId of the firebase-user-data record
35815
+ */
35816
+ async invalidateVerificationToken(firebaseUserDataDocumentId) {
35817
+ await strapi2.db.query(FIREBASE_USER_DATA_CONTENT_TYPE).update({
35818
+ where: { documentId: firebaseUserDataDocumentId },
35819
+ data: {
35820
+ verificationTokenHash: null,
35821
+ verificationTokenExpiresAt: null
35822
+ }
35823
+ });
35824
+ strapi2.log.debug(`Invalidated verification token for user ${firebaseUserDataDocumentId}`);
35149
35825
  }
35150
35826
  };
35151
35827
  };