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
@@ -198,7 +198,7 @@ const register = ({ strapi: strapi2 }) => {
198
198
  const config$1 = {
199
199
  default: ({ env: env2 }) => ({
200
200
  firebaseJsonEncryptionKey: env2("FIREBASE_JSON_ENCRYPTION_KEY", "your-key-here"),
201
- emailRequired: true,
201
+ emailRequired: env2.bool("FIREBASE_EMAIL_REQUIRED", false),
202
202
  emailPattern: "{randomString}@phone-user.firebase.local"
203
203
  }),
204
204
  validator(config2) {
@@ -289,6 +289,15 @@ const attributes$1 = {
289
289
  minimum: 1,
290
290
  maximum: 72,
291
291
  description: "How long the magic link remains valid (in hours)"
292
+ },
293
+ emailVerificationUrl: {
294
+ type: "string",
295
+ "default": "http://localhost:3000/verify-email",
296
+ description: "URL where users will be redirected to verify their email"
297
+ },
298
+ emailVerificationEmailSubject: {
299
+ type: "string",
300
+ "default": "Verify Your Email"
292
301
  }
293
302
  };
294
303
  const firebaseAuthenticationConfiguration = {
@@ -339,6 +348,13 @@ const attributes = {
339
348
  },
340
349
  resetTokenExpiresAt: {
341
350
  type: "datetime"
351
+ },
352
+ verificationTokenHash: {
353
+ type: "string",
354
+ "private": true
355
+ },
356
+ verificationTokenExpiresAt: {
357
+ type: "datetime"
342
358
  }
343
359
  };
344
360
  const firebaseUserData = {
@@ -353,238 +369,6 @@ const contentTypes = {
353
369
  "firebase-authentication-configuration": { schema: firebaseAuthenticationConfiguration },
354
370
  "firebase-user-data": { schema: firebaseUserData }
355
371
  };
356
- const pluginName = "firebase-authentication";
357
- const PLUGIN_NAME = "firebase-authentication";
358
- const PLUGIN_UID = `plugin::${PLUGIN_NAME}`;
359
- const CONFIG_CONTENT_TYPE = `${PLUGIN_UID}.firebase-authentication-configuration`;
360
- const DEFAULT_PASSWORD_RESET_URL = "http://localhost:3000/reset-password";
361
- const DEFAULT_PASSWORD_REGEX = "^.{6,}$";
362
- const DEFAULT_PASSWORD_MESSAGE = "Password must be at least 6 characters long";
363
- const DEFAULT_RESET_EMAIL_SUBJECT = "Reset Your Password";
364
- const ERROR_MESSAGES = {
365
- FIREBASE_NOT_INITIALIZED: "Firebase is not initialized. Please upload Firebase service account configuration via Settings → Firebase Authentication.",
366
- INVALID_JSON: "Invalid JSON format. Please ensure you copied the entire JSON content correctly.",
367
- MISSING_DATA: "data is missing",
368
- SOMETHING_WENT_WRONG: "Something went wrong",
369
- AUTHENTICATION_FAILED: "Authentication failed",
370
- TOKEN_MISSING: "idToken is missing!",
371
- EMAIL_PASSWORD_REQUIRED: "Email and password are required",
372
- PASSWORD_REQUIRED: "Password is required",
373
- AUTHORIZATION_REQUIRED: "Authorization token is required",
374
- INVALID_TOKEN: "Invalid or expired token",
375
- USER_NOT_FOUND: "User not found",
376
- USER_NO_EMAIL: "User does not have an email address",
377
- FIREBASE_LINK_FAILED: "Failed to generate Firebase reset link",
378
- CONFIG_NOT_FOUND: "No config found",
379
- INVALID_SERVICE_ACCOUNT: "Invalid service account JSON",
380
- WEB_API_NOT_CONFIGURED: "Email/password authentication is not available. Web API Key is not configured.",
381
- RESET_URL_NOT_CONFIGURED: "Password reset URL is not configured",
382
- RESET_URL_MUST_BE_HTTPS: "Password reset URL must use HTTPS in production",
383
- RESET_URL_INVALID_FORMAT: "Password reset URL is not a valid URL format",
384
- USER_NOT_LINKED_FIREBASE: "User is not linked to Firebase authentication",
385
- OVERRIDE_USER_ID_REQUIRED: "Override user ID is required",
386
- EITHER_EMAIL_OR_PHONE_REQUIRED: "Either email or phoneNumber is required",
387
- DELETION_NO_CONFIG: "No Firebase configs exists for deletion"
388
- };
389
- const SUCCESS_MESSAGES = {
390
- FIREBASE_INITIALIZED: "Firebase successfully initialized",
391
- FIREBASE_CONFIG_DELETED: "Firebase config deleted and reinitialized",
392
- PASSWORD_RESET_EMAIL_SENT: "If an account with that email exists, a password reset link has been sent.",
393
- SERVER_RESTARTING: "SERVER IS RESTARTING"
394
- };
395
- const CONFIG_KEYS = {
396
- ENCRYPTION_KEY: `${PLUGIN_UID}.FIREBASE_JSON_ENCRYPTION_KEY`
397
- };
398
- const REQUIRED_FIELDS = {
399
- SERVICE_ACCOUNT: ["private_key", "client_email", "project_id", "type"],
400
- WEB_CONFIG: ["apiKey", "authDomain"]
401
- // These indicate wrong JSON type
402
- };
403
- const VALIDATION_MESSAGES = {
404
- INVALID_SERVICE_ACCOUNT: "Invalid Service Account JSON. Missing required fields:",
405
- 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.",
406
- 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!"
407
- };
408
- const firebaseController = {
409
- async validateToken(ctx) {
410
- strapi.log.debug("validateToken called");
411
- try {
412
- const { idToken, profileMetaData } = ctx.request.body || {};
413
- const populate2 = ctx.request.query.populate || [];
414
- if (!idToken) {
415
- return ctx.badRequest(ERROR_MESSAGES.TOKEN_MISSING);
416
- }
417
- const result = await strapi.plugin(pluginName).service("firebaseService").validateFirebaseToken(idToken, profileMetaData, populate2);
418
- ctx.body = result;
419
- } catch (error2) {
420
- strapi.log.error(`validateToken controller error: ${error2.message}`);
421
- if (error2.name === "ValidationError") {
422
- return ctx.badRequest(error2.message);
423
- }
424
- if (error2.name === "UnauthorizedError") {
425
- return ctx.unauthorized(error2.message);
426
- }
427
- throw error2;
428
- }
429
- },
430
- async deleteByEmail(email2) {
431
- const user = await strapi.firebase.auth().getUserByEmail(email2);
432
- await strapi.plugin(pluginName).service("firebaseService").delete(user.toJSON().uid);
433
- return user.toJSON();
434
- },
435
- async overrideAccess(ctx) {
436
- try {
437
- const { overrideUserId } = ctx.request.body || {};
438
- const populate2 = ctx.request.query.populate || [];
439
- const result = await strapi.plugin(pluginName).service("firebaseService").overrideFirebaseAccess(overrideUserId, populate2);
440
- ctx.body = result;
441
- } catch (error2) {
442
- if (error2.name === "ValidationError") {
443
- return ctx.badRequest(error2.message);
444
- }
445
- if (error2.name === "NotFoundError") {
446
- return ctx.notFound(error2.message);
447
- }
448
- throw error2;
449
- }
450
- },
451
- /**
452
- * Controller method for email/password authentication
453
- * Handles the `/api/firebase-authentication/emailLogin` endpoint
454
- *
455
- * @param ctx - Koa context object
456
- * @returns Promise that sets ctx.body with user data and JWT or error message
457
- *
458
- * @remarks
459
- * This controller acts as a proxy to Firebase's Identity Toolkit API,
460
- * allowing users to authenticate with email/password and receive a Strapi JWT.
461
- *
462
- * HTTP Status Codes:
463
- * - `400`: Validation errors (missing credentials, invalid email/password)
464
- * - `500`: Server errors (missing configuration, Firebase API issues)
465
- */
466
- async emailLogin(ctx) {
467
- strapi.log.debug("emailLogin controller");
468
- try {
469
- const { email: email2, password } = ctx.request.body || {};
470
- const populate2 = ctx.request.query.populate || [];
471
- const result = await strapi.plugin(pluginName).service("firebaseService").emailLogin(email2, password, populate2);
472
- ctx.body = result;
473
- } catch (error2) {
474
- strapi.log.error("emailLogin controller error:", error2);
475
- if (error2.name === "ValidationError") {
476
- ctx.status = 400;
477
- } else if (error2.name === "ApplicationError") {
478
- ctx.status = 500;
479
- } else {
480
- ctx.status = 500;
481
- }
482
- ctx.body = { error: error2.message };
483
- }
484
- },
485
- /**
486
- * Forgot password - sends reset email
487
- * POST /api/firebase-authentication/forgotPassword
488
- * Public endpoint - no authentication required
489
- */
490
- async forgotPassword(ctx) {
491
- strapi.log.debug("forgotPassword endpoint called");
492
- try {
493
- const { email: email2 } = ctx.request.body || {};
494
- ctx.body = await strapi.plugin(pluginName).service("firebaseService").forgotPassword(email2);
495
- } catch (error2) {
496
- strapi.log.error("forgotPassword controller error:", error2);
497
- if (error2.name === "ValidationError") {
498
- ctx.status = 400;
499
- } else if (error2.name === "NotFoundError") {
500
- ctx.status = 404;
501
- } else {
502
- ctx.status = 500;
503
- }
504
- ctx.body = { error: error2.message };
505
- }
506
- },
507
- /**
508
- * Reset password - authenticated password change
509
- * POST /api/firebase-authentication/resetPassword
510
- * Public endpoint - requires valid JWT in Authorization header
511
- * Used for admin-initiated resets or user self-service password changes
512
- * NOT used for forgot password email flow (which uses Firebase's hosted UI)
513
- */
514
- async resetPassword(ctx) {
515
- strapi.log.debug("resetPassword endpoint called");
516
- try {
517
- const { password } = ctx.request.body || {};
518
- const token = ctx.request.headers.authorization?.replace("Bearer ", "");
519
- const populate2 = ctx.request.query.populate || [];
520
- ctx.body = await strapi.plugin(pluginName).service("firebaseService").resetPassword(password, token, populate2);
521
- } catch (error2) {
522
- strapi.log.error("resetPassword controller error:", error2);
523
- if (error2.name === "ValidationError" || error2.name === "UnauthorizedError") {
524
- ctx.status = 401;
525
- } else {
526
- ctx.status = 500;
527
- }
528
- ctx.body = { error: error2.message };
529
- }
530
- },
531
- async requestMagicLink(ctx) {
532
- try {
533
- const { email: email2 } = ctx.request.body || {};
534
- const result = await strapi.plugin("firebase-authentication").service("firebaseService").requestMagicLink(email2);
535
- ctx.body = result;
536
- } catch (error2) {
537
- if (error2.name === "ValidationError" || error2.name === "ApplicationError") {
538
- throw error2;
539
- }
540
- strapi.log.error("requestMagicLink controller error:", error2);
541
- ctx.body = {
542
- success: false,
543
- message: "An error occurred while processing your request"
544
- };
545
- }
546
- },
547
- /**
548
- * Reset password using custom JWT token
549
- * POST /api/firebase-authentication/resetPasswordWithToken
550
- * Public endpoint - token provides authentication
551
- *
552
- * @param ctx - Koa context with { token, newPassword } in body
553
- * @returns { success: true, message: "Password has been reset successfully" }
554
- */
555
- async resetPasswordWithToken(ctx) {
556
- strapi.log.debug("resetPasswordWithToken endpoint called");
557
- try {
558
- const { token, newPassword } = ctx.request.body || {};
559
- if (!token) {
560
- ctx.status = 400;
561
- ctx.body = { error: "Token is required" };
562
- return;
563
- }
564
- if (!newPassword) {
565
- ctx.status = 400;
566
- ctx.body = { error: "New password is required" };
567
- return;
568
- }
569
- const result = await strapi.plugin(pluginName).service("userService").resetPasswordWithToken(token, newPassword);
570
- ctx.body = result;
571
- } catch (error2) {
572
- strapi.log.error("resetPasswordWithToken controller error:", error2);
573
- if (error2.name === "ValidationError") {
574
- ctx.status = 400;
575
- ctx.body = { error: error2.message };
576
- return;
577
- }
578
- if (error2.name === "NotFoundError") {
579
- ctx.status = 404;
580
- ctx.body = { error: error2.message };
581
- return;
582
- }
583
- ctx.status = 500;
584
- ctx.body = { error: "An error occurred while resetting your password" };
585
- }
586
- }
587
- };
588
372
  var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {};
589
373
  function getDefaultExportFromCjs(x) {
590
374
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
@@ -599,7 +383,7 @@ var lodash = { exports: {} };
599
383
  * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
600
384
  */
601
385
  lodash.exports;
602
- (function(module, exports) {
386
+ (function(module, exports$1) {
603
387
  (function() {
604
388
  var undefined$1;
605
389
  var VERSION = "4.17.21";
@@ -927,7 +711,7 @@ lodash.exports;
927
711
  var freeGlobal2 = typeof commonjsGlobal == "object" && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal;
928
712
  var freeSelf2 = typeof self == "object" && self && self.Object === Object && self;
929
713
  var root2 = freeGlobal2 || freeSelf2 || Function("return this")();
930
- var freeExports = exports && !exports.nodeType && exports;
714
+ var freeExports = exports$1 && !exports$1.nodeType && exports$1;
931
715
  var freeModule = freeExports && true && module && !module.nodeType && module;
932
716
  var moduleExports = freeModule && freeModule.exports === freeExports;
933
717
  var freeProcess = moduleExports && freeGlobal2.process;
@@ -6157,7 +5941,7 @@ var lodash_min = { exports: {} };
6157
5941
  * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
6158
5942
  */
6159
5943
  lodash_min.exports;
6160
- (function(module, exports) {
5944
+ (function(module, exports$1) {
6161
5945
  (function() {
6162
5946
  function n(n2, t3, r2) {
6163
5947
  switch (r2.length) {
@@ -6590,7 +6374,7 @@ lodash_min.exports;
6590
6374
  "œ": "oe",
6591
6375
  "ʼn": "'n",
6592
6376
  "ſ": "s"
6593
- }, 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 && !exports.nodeType && exports, ue = ee && true && module && !module.nodeType && module, ie = ue && ue.exports === ee, oe = ie && ne.process, fe = function() {
6377
+ }, 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 && module && !module.nodeType && module, ie = ue && ue.exports === ee, oe = ie && ne.process, fe = function() {
6594
6378
  try {
6595
6379
  var n2 = ue && ue.require && ue.require("util").types;
6596
6380
  return n2 ? n2 : oe && oe.binding && oe.binding("util");
@@ -9263,8 +9047,8 @@ lodash_min.exports;
9263
9047
  })(lodash_min, lodash_min.exports);
9264
9048
  var lodash_minExports = lodash_min.exports;
9265
9049
  var _mapping = {};
9266
- (function(exports) {
9267
- exports.aliasToReal = {
9050
+ (function(exports$1) {
9051
+ exports$1.aliasToReal = {
9268
9052
  // Lodash aliases.
9269
9053
  "each": "forEach",
9270
9054
  "eachRight": "forEachRight",
@@ -9329,7 +9113,7 @@ var _mapping = {};
9329
9113
  "whereEq": "isMatch",
9330
9114
  "zipObj": "zipObject"
9331
9115
  };
9332
- exports.aryMethod = {
9116
+ exports$1.aryMethod = {
9333
9117
  "1": [
9334
9118
  "assignAll",
9335
9119
  "assignInAll",
@@ -9564,12 +9348,12 @@ var _mapping = {};
9564
9348
  "updateWith"
9565
9349
  ]
9566
9350
  };
9567
- exports.aryRearg = {
9351
+ exports$1.aryRearg = {
9568
9352
  "2": [1, 0],
9569
9353
  "3": [2, 0, 1],
9570
9354
  "4": [3, 2, 0, 1]
9571
9355
  };
9572
- exports.iterateeAry = {
9356
+ exports$1.iterateeAry = {
9573
9357
  "dropRightWhile": 1,
9574
9358
  "dropWhile": 1,
9575
9359
  "every": 1,
@@ -9607,11 +9391,11 @@ var _mapping = {};
9607
9391
  "times": 1,
9608
9392
  "transform": 2
9609
9393
  };
9610
- exports.iterateeRearg = {
9394
+ exports$1.iterateeRearg = {
9611
9395
  "mapKeys": [1],
9612
9396
  "reduceRight": [1, 0]
9613
9397
  };
9614
- exports.methodRearg = {
9398
+ exports$1.methodRearg = {
9615
9399
  "assignInAllWith": [1, 0],
9616
9400
  "assignInWith": [1, 2, 0],
9617
9401
  "assignAllWith": [1, 0],
@@ -9642,7 +9426,7 @@ var _mapping = {};
9642
9426
  "xorWith": [1, 2, 0],
9643
9427
  "zipWith": [1, 2, 0]
9644
9428
  };
9645
- exports.methodSpread = {
9429
+ exports$1.methodSpread = {
9646
9430
  "assignAll": { "start": 0 },
9647
9431
  "assignAllWith": { "start": 0 },
9648
9432
  "assignInAll": { "start": 0 },
@@ -9658,7 +9442,7 @@ var _mapping = {};
9658
9442
  "without": { "start": 1 },
9659
9443
  "zipAll": { "start": 0 }
9660
9444
  };
9661
- exports.mutate = {
9445
+ exports$1.mutate = {
9662
9446
  "array": {
9663
9447
  "fill": true,
9664
9448
  "pull": true,
@@ -9695,8 +9479,8 @@ var _mapping = {};
9695
9479
  "updateWith": true
9696
9480
  }
9697
9481
  };
9698
- exports.realToAlias = function() {
9699
- var hasOwnProperty2 = Object.prototype.hasOwnProperty, object2 = exports.aliasToReal, result = {};
9482
+ exports$1.realToAlias = function() {
9483
+ var hasOwnProperty2 = Object.prototype.hasOwnProperty, object2 = exports$1.aliasToReal, result = {};
9700
9484
  for (var key in object2) {
9701
9485
  var value = object2[key];
9702
9486
  if (hasOwnProperty2.call(result, value)) {
@@ -9707,7 +9491,7 @@ var _mapping = {};
9707
9491
  }
9708
9492
  return result;
9709
9493
  }();
9710
- exports.remap = {
9494
+ exports$1.remap = {
9711
9495
  "assignAll": "assign",
9712
9496
  "assignAllWith": "assignWith",
9713
9497
  "assignInAll": "assignIn",
@@ -9741,7 +9525,7 @@ var _mapping = {};
9741
9525
  "trimCharsStart": "trimStart",
9742
9526
  "zipAll": "zip"
9743
9527
  };
9744
- exports.skipFixed = {
9528
+ exports$1.skipFixed = {
9745
9529
  "castArray": true,
9746
9530
  "flow": true,
9747
9531
  "flowRight": true,
@@ -9750,7 +9534,7 @@ var _mapping = {};
9750
9534
  "rearg": true,
9751
9535
  "runInContext": true
9752
9536
  };
9753
- exports.skipRearg = {
9537
+ exports$1.skipRearg = {
9754
9538
  "add": true,
9755
9539
  "assign": true,
9756
9540
  "assignIn": true,
@@ -10204,13 +9988,13 @@ const traverseEntity = async (visitor2, options2, entity) => {
10204
9988
  if (fp.isNil(value) || fp.isNil(attribute)) {
10205
9989
  continue;
10206
9990
  }
10207
- parent = {
10208
- schema: schema2,
10209
- key,
10210
- attribute,
10211
- path: newPath
10212
- };
10213
9991
  if (isRelationalAttribute(attribute)) {
9992
+ parent = {
9993
+ schema: schema2,
9994
+ key,
9995
+ attribute,
9996
+ path: newPath
9997
+ };
10214
9998
  const isMorphRelation = attribute.relation.toLowerCase().startsWith("morph");
10215
9999
  const method = isMorphRelation ? traverseMorphRelationTarget : traverseRelationTarget(getModel(attribute.target));
10216
10000
  if (fp.isArray(value)) {
@@ -10229,6 +10013,12 @@ const traverseEntity = async (visitor2, options2, entity) => {
10229
10013
  continue;
10230
10014
  }
10231
10015
  if (isMediaAttribute(attribute)) {
10016
+ parent = {
10017
+ schema: schema2,
10018
+ key,
10019
+ attribute,
10020
+ path: newPath
10021
+ };
10232
10022
  if (fp.isArray(value)) {
10233
10023
  const res = new Array(value.length);
10234
10024
  for (let i2 = 0; i2 < value.length; i2 += 1) {
@@ -10245,6 +10035,12 @@ const traverseEntity = async (visitor2, options2, entity) => {
10245
10035
  continue;
10246
10036
  }
10247
10037
  if (attribute.type === "component") {
10038
+ parent = {
10039
+ schema: schema2,
10040
+ key,
10041
+ attribute,
10042
+ path: newPath
10043
+ };
10248
10044
  const targetSchema = getModel(attribute.component);
10249
10045
  if (fp.isArray(value)) {
10250
10046
  const res = new Array(value.length);
@@ -10262,6 +10058,12 @@ const traverseEntity = async (visitor2, options2, entity) => {
10262
10058
  continue;
10263
10059
  }
10264
10060
  if (attribute.type === "dynamiczone" && fp.isArray(value)) {
10061
+ parent = {
10062
+ schema: schema2,
10063
+ key,
10064
+ attribute,
10065
+ path: newPath
10066
+ };
10265
10067
  const res = new Array(value.length);
10266
10068
  for (let i2 = 0; i2 < value.length; i2 += 1) {
10267
10069
  const arrayPath = {
@@ -10286,7 +10088,7 @@ const createVisitorUtils = ({ data }) => ({
10286
10088
  });
10287
10089
  fp.curry(traverseEntity);
10288
10090
  var dist = { exports: {} };
10289
- (function(module, exports) {
10091
+ (function(module, exports$1) {
10290
10092
  !function(t2, n) {
10291
10093
  module.exports = n(require$$0$2, crypto$1);
10292
10094
  }(commonjsGlobal, function(t2, n) {
@@ -11834,9 +11636,9 @@ function stubFalse() {
11834
11636
  }
11835
11637
  var stubFalse_1 = stubFalse;
11836
11638
  isBuffer$2.exports;
11837
- (function(module, exports) {
11639
+ (function(module, exports$1) {
11838
11640
  var root2 = _root, stubFalse2 = stubFalse_1;
11839
- var freeExports = exports && !exports.nodeType && exports;
11641
+ var freeExports = exports$1 && !exports$1.nodeType && exports$1;
11840
11642
  var freeModule = freeExports && true && module && !module.nodeType && module;
11841
11643
  var moduleExports = freeModule && freeModule.exports === freeExports;
11842
11644
  var Buffer2 = moduleExports ? root2.Buffer : void 0;
@@ -11863,9 +11665,9 @@ function baseUnary$1(func) {
11863
11665
  var _baseUnary = baseUnary$1;
11864
11666
  var _nodeUtil = { exports: {} };
11865
11667
  _nodeUtil.exports;
11866
- (function(module, exports) {
11668
+ (function(module, exports$1) {
11867
11669
  var freeGlobal2 = _freeGlobal;
11868
- var freeExports = exports && !exports.nodeType && exports;
11670
+ var freeExports = exports$1 && !exports$1.nodeType && exports$1;
11869
11671
  var freeModule = freeExports && true && module && !module.nodeType && module;
11870
11672
  var moduleExports = freeModule && freeModule.exports === freeExports;
11871
11673
  var freeProcess = moduleExports && freeGlobal2.process;
@@ -14824,7 +14626,7 @@ function toIdentifier(str2) {
14824
14626
  Object.defineProperty(func, "name", desc);
14825
14627
  }
14826
14628
  }
14827
- function populateConstructorExports(exports, codes2, HttpError) {
14629
+ function populateConstructorExports(exports$1, codes2, HttpError) {
14828
14630
  codes2.forEach(function forEachCode(code) {
14829
14631
  var CodeError;
14830
14632
  var name = toIdentifier2(statuses$1.message[code]);
@@ -14837,8 +14639,8 @@ function toIdentifier(str2) {
14837
14639
  break;
14838
14640
  }
14839
14641
  if (CodeError) {
14840
- exports[code] = CodeError;
14841
- exports[name] = CodeError;
14642
+ exports$1[code] = CodeError;
14643
+ exports$1[name] = CodeError;
14842
14644
  }
14843
14645
  });
14844
14646
  }
@@ -14860,7 +14662,7 @@ const formatYupErrors = (yupError) => ({
14860
14662
  message: yupError.message
14861
14663
  });
14862
14664
  let ApplicationError$2 = class ApplicationError extends Error {
14863
- constructor(message = "An application error occured", details = {}) {
14665
+ constructor(message = "An application error occurred", details = {}) {
14864
14666
  super();
14865
14667
  this.name = "ApplicationError";
14866
14668
  this.message = message;
@@ -18056,8 +17858,8 @@ pkgDir$1.exports.sync = (cwd2) => {
18056
17858
  };
18057
17859
  var pkgDirExports = pkgDir$1.exports;
18058
17860
  var utils$8 = {};
18059
- (function(exports) {
18060
- exports.isInteger = (num) => {
17861
+ (function(exports$1) {
17862
+ exports$1.isInteger = (num) => {
18061
17863
  if (typeof num === "number") {
18062
17864
  return Number.isInteger(num);
18063
17865
  }
@@ -18066,13 +17868,13 @@ var utils$8 = {};
18066
17868
  }
18067
17869
  return false;
18068
17870
  };
18069
- exports.find = (node, type2) => node.nodes.find((node2) => node2.type === type2);
18070
- exports.exceedsLimit = (min, max, step = 1, limit) => {
17871
+ exports$1.find = (node, type2) => node.nodes.find((node2) => node2.type === type2);
17872
+ exports$1.exceedsLimit = (min, max, step = 1, limit) => {
18071
17873
  if (limit === false) return false;
18072
- if (!exports.isInteger(min) || !exports.isInteger(max)) return false;
17874
+ if (!exports$1.isInteger(min) || !exports$1.isInteger(max)) return false;
18073
17875
  return (Number(max) - Number(min)) / Number(step) >= limit;
18074
17876
  };
18075
- exports.escapeNode = (block, n = 0, type2) => {
17877
+ exports$1.escapeNode = (block, n = 0, type2) => {
18076
17878
  const node = block.nodes[n];
18077
17879
  if (!node) return;
18078
17880
  if (type2 && node.type === type2 || node.type === "open" || node.type === "close") {
@@ -18082,7 +17884,7 @@ var utils$8 = {};
18082
17884
  }
18083
17885
  }
18084
17886
  };
18085
- exports.encloseBrace = (node) => {
17887
+ exports$1.encloseBrace = (node) => {
18086
17888
  if (node.type !== "brace") return false;
18087
17889
  if (node.commas >> 0 + node.ranges >> 0 === 0) {
18088
17890
  node.invalid = true;
@@ -18090,7 +17892,7 @@ var utils$8 = {};
18090
17892
  }
18091
17893
  return false;
18092
17894
  };
18093
- exports.isInvalidBrace = (block) => {
17895
+ exports$1.isInvalidBrace = (block) => {
18094
17896
  if (block.type !== "brace") return false;
18095
17897
  if (block.invalid === true || block.dollar) return true;
18096
17898
  if (block.commas >> 0 + block.ranges >> 0 === 0) {
@@ -18103,18 +17905,18 @@ var utils$8 = {};
18103
17905
  }
18104
17906
  return false;
18105
17907
  };
18106
- exports.isOpenOrClose = (node) => {
17908
+ exports$1.isOpenOrClose = (node) => {
18107
17909
  if (node.type === "open" || node.type === "close") {
18108
17910
  return true;
18109
17911
  }
18110
17912
  return node.open === true || node.close === true;
18111
17913
  };
18112
- exports.reduce = (nodes) => nodes.reduce((acc, node) => {
17914
+ exports$1.reduce = (nodes) => nodes.reduce((acc, node) => {
18113
17915
  if (node.type === "text") acc.push(node.value);
18114
17916
  if (node.type === "range") node.type = "text";
18115
17917
  return acc;
18116
17918
  }, []);
18117
- exports.flatten = (...args) => {
17919
+ exports$1.flatten = (...args) => {
18118
17920
  const result = [];
18119
17921
  const flat = (arr) => {
18120
17922
  for (let i = 0; i < arr.length; i++) {
@@ -19216,7 +19018,7 @@ var constants$5 = {
19216
19018
  return win32 === true ? WINDOWS_CHARS : POSIX_CHARS;
19217
19019
  }
19218
19020
  };
19219
- (function(exports) {
19021
+ (function(exports$1) {
19220
19022
  const path2 = require$$0__default;
19221
19023
  const win32 = process.platform === "win32";
19222
19024
  const {
@@ -19225,36 +19027,36 @@ var constants$5 = {
19225
19027
  REGEX_SPECIAL_CHARS,
19226
19028
  REGEX_SPECIAL_CHARS_GLOBAL
19227
19029
  } = constants$5;
19228
- exports.isObject = (val) => val !== null && typeof val === "object" && !Array.isArray(val);
19229
- exports.hasRegexChars = (str2) => REGEX_SPECIAL_CHARS.test(str2);
19230
- exports.isRegexChar = (str2) => str2.length === 1 && exports.hasRegexChars(str2);
19231
- exports.escapeRegex = (str2) => str2.replace(REGEX_SPECIAL_CHARS_GLOBAL, "\\$1");
19232
- exports.toPosixSlashes = (str2) => str2.replace(REGEX_BACKSLASH, "/");
19233
- exports.removeBackslashes = (str2) => {
19030
+ exports$1.isObject = (val) => val !== null && typeof val === "object" && !Array.isArray(val);
19031
+ exports$1.hasRegexChars = (str2) => REGEX_SPECIAL_CHARS.test(str2);
19032
+ exports$1.isRegexChar = (str2) => str2.length === 1 && exports$1.hasRegexChars(str2);
19033
+ exports$1.escapeRegex = (str2) => str2.replace(REGEX_SPECIAL_CHARS_GLOBAL, "\\$1");
19034
+ exports$1.toPosixSlashes = (str2) => str2.replace(REGEX_BACKSLASH, "/");
19035
+ exports$1.removeBackslashes = (str2) => {
19234
19036
  return str2.replace(REGEX_REMOVE_BACKSLASH, (match) => {
19235
19037
  return match === "\\" ? "" : match;
19236
19038
  });
19237
19039
  };
19238
- exports.supportsLookbehinds = () => {
19040
+ exports$1.supportsLookbehinds = () => {
19239
19041
  const segs = process.version.slice(1).split(".").map(Number);
19240
19042
  if (segs.length === 3 && segs[0] >= 9 || segs[0] === 8 && segs[1] >= 10) {
19241
19043
  return true;
19242
19044
  }
19243
19045
  return false;
19244
19046
  };
19245
- exports.isWindows = (options2) => {
19047
+ exports$1.isWindows = (options2) => {
19246
19048
  if (options2 && typeof options2.windows === "boolean") {
19247
19049
  return options2.windows;
19248
19050
  }
19249
19051
  return win32 === true || path2.sep === "\\";
19250
19052
  };
19251
- exports.escapeLast = (input, char, lastIdx) => {
19053
+ exports$1.escapeLast = (input, char, lastIdx) => {
19252
19054
  const idx = input.lastIndexOf(char, lastIdx);
19253
19055
  if (idx === -1) return input;
19254
- if (input[idx - 1] === "\\") return exports.escapeLast(input, char, idx - 1);
19056
+ if (input[idx - 1] === "\\") return exports$1.escapeLast(input, char, idx - 1);
19255
19057
  return `${input.slice(0, idx)}\\${input.slice(idx)}`;
19256
19058
  };
19257
- exports.removePrefix = (input, state = {}) => {
19059
+ exports$1.removePrefix = (input, state = {}) => {
19258
19060
  let output = input;
19259
19061
  if (output.startsWith("./")) {
19260
19062
  output = output.slice(2);
@@ -19262,7 +19064,7 @@ var constants$5 = {
19262
19064
  }
19263
19065
  return output;
19264
19066
  };
19265
- exports.wrapOutput = (input, state = {}, options2 = {}) => {
19067
+ exports$1.wrapOutput = (input, state = {}, options2 = {}) => {
19266
19068
  const prepend = options2.contains ? "" : "^";
19267
19069
  const append2 = options2.contains ? "" : "$";
19268
19070
  let output = `${prepend}(?:${input})${append2}`;
@@ -22817,6 +22619,18 @@ function charFromCodepoint(c) {
22817
22619
  (c - 65536 & 1023) + 56320
22818
22620
  );
22819
22621
  }
22622
+ function setProperty(object2, key, value) {
22623
+ if (key === "__proto__") {
22624
+ Object.defineProperty(object2, key, {
22625
+ configurable: true,
22626
+ enumerable: true,
22627
+ writable: true,
22628
+ value
22629
+ });
22630
+ } else {
22631
+ object2[key] = value;
22632
+ }
22633
+ }
22820
22634
  var simpleEscapeCheck = new Array(256);
22821
22635
  var simpleEscapeMap = new Array(256);
22822
22636
  for (var i = 0; i < 256; i++) {
@@ -22923,7 +22737,7 @@ function mergeMappings(state, destination, source, overridableKeys) {
22923
22737
  for (index2 = 0, quantity = sourceKeys.length; index2 < quantity; index2 += 1) {
22924
22738
  key = sourceKeys[index2];
22925
22739
  if (!_hasOwnProperty$1.call(destination, key)) {
22926
- destination[key] = source[key];
22740
+ setProperty(destination, key, source[key]);
22927
22741
  overridableKeys[key] = true;
22928
22742
  }
22929
22743
  }
@@ -22962,7 +22776,7 @@ function storeMappingPair(state, _result, overridableKeys, keyTag, keyNode, valu
22962
22776
  state.position = startPos || state.position;
22963
22777
  throwError(state, "duplicated mapping key");
22964
22778
  }
22965
- _result[keyNode] = valueNode;
22779
+ setProperty(_result, keyNode, valueNode);
22966
22780
  delete overridableKeys[keyNode];
22967
22781
  }
22968
22782
  return _result;
@@ -28865,6 +28679,247 @@ _enum([
28865
28679
  "published"
28866
28680
  ]).describe("Filter by publication status");
28867
28681
  string().describe("Search query string");
28682
+ const pluginName = "firebase-authentication";
28683
+ const PLUGIN_NAME = "firebase-authentication";
28684
+ const PLUGIN_UID = `plugin::${PLUGIN_NAME}`;
28685
+ const CONFIG_CONTENT_TYPE = `${PLUGIN_UID}.firebase-authentication-configuration`;
28686
+ const DEFAULT_PASSWORD_RESET_URL = "http://localhost:3000/reset-password";
28687
+ const DEFAULT_PASSWORD_REGEX = "^.{6,}$";
28688
+ const DEFAULT_PASSWORD_MESSAGE = "Password must be at least 6 characters long";
28689
+ const DEFAULT_RESET_EMAIL_SUBJECT = "Reset Your Password";
28690
+ const ERROR_MESSAGES = {
28691
+ FIREBASE_NOT_INITIALIZED: "Firebase is not initialized. Please upload Firebase service account configuration via Settings → Firebase Authentication.",
28692
+ INVALID_JSON: "Invalid JSON format. Please ensure you copied the entire JSON content correctly.",
28693
+ MISSING_DATA: "data is missing",
28694
+ SOMETHING_WENT_WRONG: "Something went wrong",
28695
+ AUTHENTICATION_FAILED: "Authentication failed",
28696
+ TOKEN_MISSING: "idToken is missing!",
28697
+ EMAIL_PASSWORD_REQUIRED: "Email and password are required",
28698
+ PASSWORD_REQUIRED: "Password is required",
28699
+ AUTHORIZATION_REQUIRED: "Authorization token is required",
28700
+ INVALID_TOKEN: "Invalid or expired token",
28701
+ USER_NOT_FOUND: "User not found",
28702
+ USER_NO_EMAIL: "User does not have an email address",
28703
+ FIREBASE_LINK_FAILED: "Failed to generate Firebase reset link",
28704
+ CONFIG_NOT_FOUND: "No config found",
28705
+ INVALID_SERVICE_ACCOUNT: "Invalid service account JSON",
28706
+ WEB_API_NOT_CONFIGURED: "Email/password authentication is not available. Web API Key is not configured.",
28707
+ RESET_URL_NOT_CONFIGURED: "Password reset URL is not configured",
28708
+ RESET_URL_MUST_BE_HTTPS: "Password reset URL must use HTTPS in production",
28709
+ RESET_URL_INVALID_FORMAT: "Password reset URL is not a valid URL format",
28710
+ USER_NOT_LINKED_FIREBASE: "User is not linked to Firebase authentication",
28711
+ OVERRIDE_USER_ID_REQUIRED: "Override user ID is required",
28712
+ EITHER_EMAIL_OR_PHONE_REQUIRED: "Either email or phoneNumber is required",
28713
+ DELETION_NO_CONFIG: "No Firebase configs exists for deletion"
28714
+ };
28715
+ const SUCCESS_MESSAGES = {
28716
+ FIREBASE_INITIALIZED: "Firebase successfully initialized",
28717
+ FIREBASE_CONFIG_DELETED: "Firebase config deleted and reinitialized",
28718
+ PASSWORD_RESET_EMAIL_SENT: "If an account with that email exists, a password reset link has been sent.",
28719
+ SERVER_RESTARTING: "SERVER IS RESTARTING"
28720
+ };
28721
+ const CONFIG_KEYS = {
28722
+ ENCRYPTION_KEY: `${PLUGIN_UID}.FIREBASE_JSON_ENCRYPTION_KEY`
28723
+ };
28724
+ const REQUIRED_FIELDS = {
28725
+ SERVICE_ACCOUNT: ["private_key", "client_email", "project_id", "type"],
28726
+ WEB_CONFIG: ["apiKey", "authDomain"]
28727
+ // These indicate wrong JSON type
28728
+ };
28729
+ const VALIDATION_MESSAGES = {
28730
+ INVALID_SERVICE_ACCOUNT: "Invalid Service Account JSON. Missing required fields:",
28731
+ 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.",
28732
+ 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!"
28733
+ };
28734
+ const firebaseController = {
28735
+ async validateToken(ctx) {
28736
+ strapi.log.debug("validateToken called");
28737
+ try {
28738
+ const { idToken, profileMetaData } = ctx.request.body || {};
28739
+ const populate2 = ctx.request.query.populate || [];
28740
+ if (!idToken) {
28741
+ return ctx.badRequest(ERROR_MESSAGES.TOKEN_MISSING);
28742
+ }
28743
+ const result = await strapi.plugin(pluginName).service("firebaseService").validateFirebaseToken(idToken, profileMetaData, populate2);
28744
+ ctx.body = result;
28745
+ } catch (error2) {
28746
+ strapi.log.error(`validateToken controller error: ${error2.message}`);
28747
+ if (error2.name === "ValidationError") {
28748
+ return ctx.badRequest(error2.message);
28749
+ }
28750
+ if (error2.name === "UnauthorizedError") {
28751
+ return ctx.unauthorized(error2.message);
28752
+ }
28753
+ throw error2;
28754
+ }
28755
+ },
28756
+ async deleteByEmail(email2) {
28757
+ try {
28758
+ const user = await strapi.firebase.auth().getUserByEmail(email2);
28759
+ await strapi.plugin(pluginName).service("firebaseService").delete(user.toJSON().uid);
28760
+ return user.toJSON();
28761
+ } catch (error2) {
28762
+ strapi.log.error("deleteByEmail error:", error2);
28763
+ throw error2;
28764
+ }
28765
+ },
28766
+ async overrideAccess(ctx) {
28767
+ try {
28768
+ const { overrideUserId } = ctx.request.body || {};
28769
+ const populate2 = ctx.request.query.populate || [];
28770
+ const result = await strapi.plugin(pluginName).service("firebaseService").overrideFirebaseAccess(overrideUserId, populate2);
28771
+ ctx.body = result;
28772
+ } catch (error2) {
28773
+ if (error2.name === "ValidationError") {
28774
+ return ctx.badRequest(error2.message);
28775
+ }
28776
+ if (error2.name === "NotFoundError") {
28777
+ return ctx.notFound(error2.message);
28778
+ }
28779
+ throw error2;
28780
+ }
28781
+ },
28782
+ /**
28783
+ * Controller method for email/password authentication
28784
+ * Handles the `/api/firebase-authentication/emailLogin` endpoint
28785
+ *
28786
+ * @param ctx - Koa context object
28787
+ * @returns Promise that sets ctx.body with user data and JWT or error message
28788
+ *
28789
+ * @remarks
28790
+ * This controller acts as a proxy to Firebase's Identity Toolkit API,
28791
+ * allowing users to authenticate with email/password and receive a Strapi JWT.
28792
+ *
28793
+ * HTTP Status Codes:
28794
+ * - `400`: Validation errors (missing credentials, invalid email/password)
28795
+ * - `500`: Server errors (missing configuration, Firebase API issues)
28796
+ */
28797
+ async emailLogin(ctx) {
28798
+ strapi.log.debug("emailLogin controller");
28799
+ try {
28800
+ const { email: email2, password } = ctx.request.body || {};
28801
+ const populate2 = ctx.request.query.populate || [];
28802
+ const result = await strapi.plugin(pluginName).service("firebaseService").emailLogin(email2, password, populate2);
28803
+ ctx.body = result;
28804
+ } catch (error2) {
28805
+ strapi.log.error("emailLogin controller error:", error2);
28806
+ throw error2;
28807
+ }
28808
+ },
28809
+ /**
28810
+ * Forgot password - sends reset email
28811
+ * POST /api/firebase-authentication/forgotPassword
28812
+ * Public endpoint - no authentication required
28813
+ */
28814
+ async forgotPassword(ctx) {
28815
+ strapi.log.debug("forgotPassword endpoint called");
28816
+ try {
28817
+ const { email: email2 } = ctx.request.body || {};
28818
+ ctx.body = await strapi.plugin(pluginName).service("firebaseService").forgotPassword(email2);
28819
+ } catch (error2) {
28820
+ strapi.log.error("forgotPassword controller error:", error2);
28821
+ throw error2;
28822
+ }
28823
+ },
28824
+ /**
28825
+ * Reset password - authenticated password change
28826
+ * POST /api/firebase-authentication/resetPassword
28827
+ * Authenticated endpoint - requires valid JWT (enforced by is-authenticated policy)
28828
+ * Used for admin-initiated resets or user self-service password changes
28829
+ * NOT used for forgot password email flow (which uses Firebase's hosted UI)
28830
+ */
28831
+ async resetPassword(ctx) {
28832
+ strapi.log.debug("resetPassword endpoint called");
28833
+ try {
28834
+ const { password } = ctx.request.body || {};
28835
+ const user = ctx.state.user;
28836
+ const populate2 = ctx.request.query.populate || [];
28837
+ ctx.body = await strapi.plugin(pluginName).service("firebaseService").resetPassword(password, user, populate2);
28838
+ } catch (error2) {
28839
+ strapi.log.error("resetPassword controller error:", error2);
28840
+ throw error2;
28841
+ }
28842
+ },
28843
+ async requestMagicLink(ctx) {
28844
+ try {
28845
+ const { email: email2 } = ctx.request.body || {};
28846
+ const result = await strapi.plugin("firebase-authentication").service("firebaseService").requestMagicLink(email2);
28847
+ ctx.body = result;
28848
+ } catch (error2) {
28849
+ if (error2.name === "ValidationError" || error2.name === "ApplicationError") {
28850
+ throw error2;
28851
+ }
28852
+ strapi.log.error("requestMagicLink controller error:", error2);
28853
+ ctx.body = {
28854
+ success: false,
28855
+ message: "An error occurred while processing your request"
28856
+ };
28857
+ }
28858
+ },
28859
+ /**
28860
+ * Reset password using custom JWT token
28861
+ * POST /api/firebase-authentication/resetPasswordWithToken
28862
+ * Public endpoint - token provides authentication
28863
+ *
28864
+ * @param ctx - Koa context with { token, newPassword } in body
28865
+ * @returns { success: true, message: "Password has been reset successfully" }
28866
+ */
28867
+ async resetPasswordWithToken(ctx) {
28868
+ strapi.log.debug("resetPasswordWithToken endpoint called");
28869
+ try {
28870
+ const { token, newPassword } = ctx.request.body || {};
28871
+ if (!token) {
28872
+ throw new ValidationError$1("Token is required");
28873
+ }
28874
+ if (!newPassword) {
28875
+ throw new ValidationError$1("New password is required");
28876
+ }
28877
+ const result = await strapi.plugin(pluginName).service("userService").resetPasswordWithToken(token, newPassword);
28878
+ ctx.body = result;
28879
+ } catch (error2) {
28880
+ strapi.log.error("resetPasswordWithToken controller error:", error2);
28881
+ throw error2;
28882
+ }
28883
+ },
28884
+ /**
28885
+ * Send email verification - public endpoint
28886
+ * POST /api/firebase-authentication/sendVerificationEmail
28887
+ * Authenticated endpoint - sends verification email to the logged-in user's email
28888
+ */
28889
+ async sendVerificationEmail(ctx) {
28890
+ strapi.log.debug("sendVerificationEmail endpoint called");
28891
+ try {
28892
+ const user = ctx.state.user;
28893
+ const email2 = user.email;
28894
+ ctx.body = await strapi.plugin(pluginName).service("firebaseService").sendVerificationEmail(email2);
28895
+ } catch (error2) {
28896
+ strapi.log.error("sendVerificationEmail controller error:", error2);
28897
+ throw error2;
28898
+ }
28899
+ },
28900
+ /**
28901
+ * Verify email using custom JWT token
28902
+ * POST /api/firebase-authentication/verifyEmail
28903
+ * Public endpoint - token provides authentication
28904
+ *
28905
+ * @param ctx - Koa context with { token } in body
28906
+ * @returns { success: true, message: "Email verified successfully" }
28907
+ */
28908
+ async verifyEmail(ctx) {
28909
+ strapi.log.debug("verifyEmail endpoint called");
28910
+ try {
28911
+ const { token } = ctx.request.body || {};
28912
+ if (!token) {
28913
+ throw new ValidationError$1("Token is required");
28914
+ }
28915
+ const result = await strapi.plugin(pluginName).service("firebaseService").verifyEmail(token);
28916
+ ctx.body = result;
28917
+ } catch (error2) {
28918
+ strapi.log.error("verifyEmail controller error:", error2);
28919
+ throw error2;
28920
+ }
28921
+ }
28922
+ };
28868
28923
  const STRAPI_DESTINATION = "strapi";
28869
28924
  const FIREBASE_DESTINATION = "firebase";
28870
28925
  const userController = {
@@ -28950,6 +29005,17 @@ const userController = {
28950
29005
  } catch (error2) {
28951
29006
  throw new ApplicationError$2(error2.message || "Failed to send password reset email");
28952
29007
  }
29008
+ },
29009
+ sendVerificationEmail: async (ctx) => {
29010
+ const userId = ctx.params.id;
29011
+ if (!userId) {
29012
+ throw new ValidationError$1("User ID is required");
29013
+ }
29014
+ try {
29015
+ ctx.body = await strapi.plugin("firebase-authentication").service("userService").sendVerificationEmail(userId);
29016
+ } catch (error2) {
29017
+ throw new ApplicationError$2(error2.message || "Failed to send verification email");
29018
+ }
28953
29019
  }
28954
29020
  };
28955
29021
  const settingsController = {
@@ -29052,7 +29118,9 @@ const settingsController = {
29052
29118
  enableMagicLink = false,
29053
29119
  magicLinkUrl = "http://localhost:1338/verify-magic-link.html",
29054
29120
  magicLinkEmailSubject = "Sign in to Your Application",
29055
- magicLinkExpiryHours = 1
29121
+ magicLinkExpiryHours = 1,
29122
+ emailVerificationUrl = "http://localhost:3000/verify-email",
29123
+ emailVerificationEmailSubject = "Verify Your Email"
29056
29124
  } = requestBody;
29057
29125
  const existingConfig = await strapi.db.query("plugin::firebase-authentication.firebase-authentication-configuration").findOne({ where: {} });
29058
29126
  let result;
@@ -29066,7 +29134,9 @@ const settingsController = {
29066
29134
  enableMagicLink,
29067
29135
  magicLinkUrl,
29068
29136
  magicLinkEmailSubject,
29069
- magicLinkExpiryHours
29137
+ magicLinkExpiryHours,
29138
+ emailVerificationUrl,
29139
+ emailVerificationEmailSubject
29070
29140
  }
29071
29141
  });
29072
29142
  } else {
@@ -29080,7 +29150,9 @@ const settingsController = {
29080
29150
  enableMagicLink,
29081
29151
  magicLinkUrl,
29082
29152
  magicLinkEmailSubject,
29083
- magicLinkExpiryHours
29153
+ magicLinkExpiryHours,
29154
+ emailVerificationUrl,
29155
+ emailVerificationEmailSubject
29084
29156
  }
29085
29157
  });
29086
29158
  }
@@ -29092,7 +29164,9 @@ const settingsController = {
29092
29164
  enableMagicLink: result.enableMagicLink,
29093
29165
  magicLinkUrl: result.magicLinkUrl,
29094
29166
  magicLinkEmailSubject: result.magicLinkEmailSubject,
29095
- magicLinkExpiryHours: result.magicLinkExpiryHours
29167
+ magicLinkExpiryHours: result.magicLinkExpiryHours,
29168
+ emailVerificationUrl: result.emailVerificationUrl,
29169
+ emailVerificationEmailSubject: result.emailVerificationEmailSubject
29096
29170
  };
29097
29171
  } catch (error2) {
29098
29172
  throw new ApplicationError$2("Error saving password configuration", {
@@ -29107,7 +29181,16 @@ const controllers = {
29107
29181
  settingsController
29108
29182
  };
29109
29183
  const middlewares = {};
29110
- const policies = {};
29184
+ const isAuthenticated = async (policyContext) => {
29185
+ const user = policyContext.state.user;
29186
+ if (!user) {
29187
+ throw new UnauthorizedError("Authentication required");
29188
+ }
29189
+ return true;
29190
+ };
29191
+ const policies = {
29192
+ "is-authenticated": isAuthenticated
29193
+ };
29111
29194
  const settingsRoute = [
29112
29195
  {
29113
29196
  method: "POST",
@@ -29185,6 +29268,14 @@ const admin = {
29185
29268
  policies: ["admin::isAuthenticatedAdmin"]
29186
29269
  }
29187
29270
  },
29271
+ {
29272
+ method: "PUT",
29273
+ path: "/users/sendVerificationEmail/:id",
29274
+ handler: "userController.sendVerificationEmail",
29275
+ config: {
29276
+ policies: ["admin::isAuthenticatedAdmin"]
29277
+ }
29278
+ },
29188
29279
  {
29189
29280
  method: "GET",
29190
29281
  path: "/users/:id",
@@ -29249,9 +29340,7 @@ const contentApi = {
29249
29340
  path: "/resetPassword",
29250
29341
  handler: "firebaseController.resetPassword",
29251
29342
  config: {
29252
- auth: false,
29253
- // Public endpoint - authenticated password change, requires valid JWT in Authorization header
29254
- policies: []
29343
+ policies: ["plugin::firebase-authentication.is-authenticated"]
29255
29344
  }
29256
29345
  },
29257
29346
  {
@@ -29283,6 +29372,24 @@ const contentApi = {
29283
29372
  // Public endpoint - token provides authentication
29284
29373
  policies: []
29285
29374
  }
29375
+ },
29376
+ {
29377
+ method: "POST",
29378
+ path: "/sendVerificationEmail",
29379
+ handler: "firebaseController.sendVerificationEmail",
29380
+ config: {
29381
+ policies: ["plugin::firebase-authentication.is-authenticated"]
29382
+ }
29383
+ },
29384
+ {
29385
+ method: "POST",
29386
+ path: "/verifyEmail",
29387
+ handler: "firebaseController.verifyEmail",
29388
+ config: {
29389
+ auth: false,
29390
+ // Public endpoint - token provides authentication
29391
+ policies: []
29392
+ }
29286
29393
  }
29287
29394
  ]
29288
29395
  };
@@ -29403,7 +29510,10 @@ const settingsService = ({ strapi: strapi2 }) => {
29403
29510
  enableMagicLink: configObject.enableMagicLink || false,
29404
29511
  magicLinkUrl: configObject.magicLinkUrl || "http://localhost:1338/verify-magic-link.html",
29405
29512
  magicLinkEmailSubject: configObject.magicLinkEmailSubject || "Sign in to Your Application",
29406
- magicLinkExpiryHours: configObject.magicLinkExpiryHours || 1
29513
+ magicLinkExpiryHours: configObject.magicLinkExpiryHours || 1,
29514
+ // Include email verification configuration fields
29515
+ emailVerificationUrl: configObject.emailVerificationUrl || "http://localhost:3000/verify-email",
29516
+ emailVerificationEmailSubject: configObject.emailVerificationEmailSubject || "Verify Your Email"
29407
29517
  };
29408
29518
  } catch (error2) {
29409
29519
  strapi2.log.error(`Firebase config error: ${error2.message}`);
@@ -29446,7 +29556,9 @@ const settingsService = ({ strapi: strapi2 }) => {
29446
29556
  enableMagicLink = false,
29447
29557
  magicLinkUrl = "http://localhost:1338/verify-magic-link.html",
29448
29558
  magicLinkEmailSubject = "Sign in to Your Application",
29449
- magicLinkExpiryHours = 1
29559
+ magicLinkExpiryHours = 1,
29560
+ emailVerificationUrl = "http://localhost:3000/verify-email",
29561
+ emailVerificationEmailSubject = "Verify Your Email"
29450
29562
  } = requestBody;
29451
29563
  if (!requestBody) throw new ValidationError3(ERROR_MESSAGES.MISSING_DATA);
29452
29564
  try {
@@ -29482,7 +29594,9 @@ const settingsService = ({ strapi: strapi2 }) => {
29482
29594
  enableMagicLink,
29483
29595
  magicLinkUrl,
29484
29596
  magicLinkEmailSubject,
29485
- magicLinkExpiryHours
29597
+ magicLinkExpiryHours,
29598
+ emailVerificationUrl,
29599
+ emailVerificationEmailSubject
29486
29600
  }
29487
29601
  });
29488
29602
  } else {
@@ -29498,11 +29612,20 @@ const settingsService = ({ strapi: strapi2 }) => {
29498
29612
  enableMagicLink,
29499
29613
  magicLinkUrl,
29500
29614
  magicLinkEmailSubject,
29501
- magicLinkExpiryHours
29615
+ magicLinkExpiryHours,
29616
+ emailVerificationUrl,
29617
+ emailVerificationEmailSubject
29502
29618
  }
29503
29619
  });
29504
29620
  }
29505
29621
  await strapi2.plugin("firebase-authentication").service("settingsService").init();
29622
+ setImmediate(async () => {
29623
+ try {
29624
+ await strapi2.plugin("firebase-authentication").service("autoLinkService").linkAllUsers(strapi2);
29625
+ } catch (error2) {
29626
+ strapi2.log.error(`Auto-linking after config save failed: ${error2.message}`);
29627
+ }
29628
+ });
29506
29629
  const configData = res.firebaseConfigJson || res.firebase_config_json;
29507
29630
  if (!configData) {
29508
29631
  strapi2.log.error("Firebase config data missing from database response");
@@ -29526,6 +29649,8 @@ const settingsService = ({ strapi: strapi2 }) => {
29526
29649
  res.magicLinkUrl = res.magicLinkUrl || magicLinkUrl;
29527
29650
  res.magicLinkEmailSubject = res.magicLinkEmailSubject || magicLinkEmailSubject;
29528
29651
  res.magicLinkExpiryHours = res.magicLinkExpiryHours || magicLinkExpiryHours;
29652
+ res.emailVerificationUrl = res.emailVerificationUrl || emailVerificationUrl;
29653
+ res.emailVerificationEmailSubject = res.emailVerificationEmailSubject || emailVerificationEmailSubject;
29529
29654
  return res;
29530
29655
  } catch (error2) {
29531
29656
  strapi2.log.error("=== FIREBASE CONFIG SAVE ERROR ===");
@@ -30185,6 +30310,40 @@ const userService = ({ strapi: strapi2 }) => {
30185
30310
  }
30186
30311
  throw new ApplicationError$2(e.message?.toString() || "Failed to reset password");
30187
30312
  }
30313
+ },
30314
+ /**
30315
+ * Send email verification email (admin-initiated)
30316
+ * @param entityId - Firebase UID of the user
30317
+ */
30318
+ sendVerificationEmail: async (entityId) => {
30319
+ try {
30320
+ ensureFirebaseInitialized();
30321
+ const user = await strapi2.firebase.auth().getUser(entityId);
30322
+ if (!user.email) {
30323
+ throw new ApplicationError$2("User does not have an email address");
30324
+ }
30325
+ if (user.emailVerified) {
30326
+ return { success: true, message: "Email is already verified" };
30327
+ }
30328
+ const config2 = await strapi2.db.query("plugin::firebase-authentication.firebase-authentication-configuration").findOne({ where: {} });
30329
+ const emailVerificationUrl = config2?.emailVerificationUrl;
30330
+ if (!emailVerificationUrl) {
30331
+ throw new ApplicationError$2("Email verification URL is not configured");
30332
+ }
30333
+ const firebaseUserData2 = await strapi2.plugin("firebase-authentication").service("firebaseUserDataService").getByFirebaseUID(entityId);
30334
+ if (!firebaseUserData2) {
30335
+ throw new ApplicationError$2("User is not linked to Firebase authentication");
30336
+ }
30337
+ const tokenService2 = strapi2.plugin("firebase-authentication").service("tokenService");
30338
+ const token = await tokenService2.generateVerificationToken(firebaseUserData2.documentId, user.email);
30339
+ const verificationLink = `${emailVerificationUrl}?token=${token}`;
30340
+ strapi2.log.debug(`Generated email verification link for user ${user.email}`);
30341
+ const emailService2 = strapi2.plugin("firebase-authentication").service("emailService");
30342
+ return await emailService2.sendVerificationEmail(user, verificationLink);
30343
+ } catch (e) {
30344
+ strapi2.log.error(`sendVerificationEmail error: ${e.message}`);
30345
+ throw new ApplicationError$2(e.message?.toString() || "Failed to send verification email");
30346
+ }
30188
30347
  }
30189
30348
  };
30190
30349
  };
@@ -30728,20 +30887,17 @@ const firebaseService = ({ strapi: strapi2 }) => ({
30728
30887
  * 2. User-initiated password change (when already authenticated)
30729
30888
  *
30730
30889
  * NOT used for forgot password email flow - that now uses Firebase's hosted UI
30890
+ *
30891
+ * @param password - New password to set
30892
+ * @param user - Authenticated user from ctx.state.user (populated by is-authenticated policy)
30893
+ * @param populate - Fields to populate in response
30731
30894
  */
30732
- resetPassword: async (password, token, populate2) => {
30895
+ resetPassword: async (password, user, populate2) => {
30733
30896
  if (!password) {
30734
30897
  throw new ValidationError$1("Password is required");
30735
30898
  }
30736
- if (!token) {
30737
- throw new UnauthorizedError("Authorization token is required");
30738
- }
30739
- let decoded;
30740
- try {
30741
- const jwtService = strapi2.plugin("users-permissions").service("jwt");
30742
- decoded = await jwtService.verify(token);
30743
- } catch (error2) {
30744
- throw new UnauthorizedError("Invalid or expired token");
30899
+ if (!user || !user.id) {
30900
+ throw new UnauthorizedError("Authentication required");
30745
30901
  }
30746
30902
  const config2 = await strapi2.plugin("firebase-authentication").service("settingsService").getFirebaseConfigJson();
30747
30903
  const passwordRegex = config2?.passwordRequirementsRegex || "^.{6,}$";
@@ -30752,8 +30908,7 @@ const firebaseService = ({ strapi: strapi2 }) => ({
30752
30908
  }
30753
30909
  try {
30754
30910
  const strapiUser = await strapi2.db.query("plugin::users-permissions.user").findOne({
30755
- where: { id: decoded.id }
30756
- // Use numeric id from JWT
30911
+ where: { id: user.id }
30757
30912
  });
30758
30913
  if (!strapiUser) {
30759
30914
  throw new NotFoundError("User not found");
@@ -30864,6 +31019,159 @@ const firebaseService = ({ strapi: strapi2 }) => ({
30864
31019
  verificationUrl: magicLinkUrl
30865
31020
  };
30866
31021
  }
31022
+ },
31023
+ /**
31024
+ * Send email verification - public endpoint
31025
+ * Generates a verification token and sends an email to the user
31026
+ * Security: Always returns generic success message to prevent email enumeration
31027
+ */
31028
+ async sendVerificationEmail(email2) {
31029
+ strapi2.log.info(`[sendVerificationEmail] Starting email verification for: ${email2}`);
31030
+ if (!email2) {
31031
+ throw new ValidationError$1("Email is required");
31032
+ }
31033
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
31034
+ if (!emailRegex.test(email2)) {
31035
+ throw new ValidationError$1("Invalid email format");
31036
+ }
31037
+ const config2 = await strapi2.plugin("firebase-authentication").service("settingsService").getFirebaseConfigJson();
31038
+ const verificationUrl = config2?.emailVerificationUrl;
31039
+ if (!verificationUrl) {
31040
+ throw new ApplicationError$2("Email verification URL is not configured");
31041
+ }
31042
+ if (process.env.NODE_ENV === "production" && !verificationUrl.startsWith("https://")) {
31043
+ throw new ApplicationError$2("Email verification URL must use HTTPS in production");
31044
+ }
31045
+ try {
31046
+ new URL(verificationUrl);
31047
+ } catch (error2) {
31048
+ throw new ApplicationError$2("Email verification URL is not a valid URL format");
31049
+ }
31050
+ try {
31051
+ let firebaseUser;
31052
+ try {
31053
+ firebaseUser = await strapi2.firebase.auth().getUserByEmail(email2);
31054
+ } catch (fbError) {
31055
+ strapi2.log.debug("User not found in Firebase");
31056
+ }
31057
+ if (!firebaseUser) {
31058
+ strapi2.log.warn(`⚠️ [sendVerificationEmail] User not found in Firebase for email: ${email2}`);
31059
+ return { message: "If an account with that email exists, a verification link has been sent." };
31060
+ }
31061
+ if (firebaseUser.emailVerified) {
31062
+ strapi2.log.info(`[sendVerificationEmail] User ${email2} is already verified`);
31063
+ return { message: "Email is already verified." };
31064
+ }
31065
+ const firebaseData = await strapi2.plugin("firebase-authentication").service("firebaseUserDataService").getByFirebaseUID(firebaseUser.uid);
31066
+ if (!firebaseData) {
31067
+ strapi2.log.warn(`⚠️ [sendVerificationEmail] No firebase-user-data record for: ${email2}`);
31068
+ return { message: "If an account with that email exists, a verification link has been sent." };
31069
+ }
31070
+ strapi2.log.info(
31071
+ `✅ [sendVerificationEmail] User found: ${JSON.stringify({
31072
+ firebaseUID: firebaseUser.uid,
31073
+ email: firebaseUser.email,
31074
+ emailVerified: firebaseUser.emailVerified
31075
+ })}`
31076
+ );
31077
+ const tokenService2 = strapi2.plugin("firebase-authentication").service("tokenService");
31078
+ const token = await tokenService2.generateVerificationToken(firebaseData.documentId, email2);
31079
+ const verificationLink = `${verificationUrl}?token=${token}`;
31080
+ strapi2.log.info(`✅ [sendVerificationEmail] Verification link generated for ${email2}`);
31081
+ strapi2.log.info(`[sendVerificationEmail] Attempting to send verification email to: ${email2}`);
31082
+ await strapi2.plugin("firebase-authentication").service("emailService").sendVerificationEmail(firebaseUser, verificationLink);
31083
+ strapi2.log.info(`✅ [sendVerificationEmail] Verification email sent successfully to: ${email2}`);
31084
+ return {
31085
+ message: "If an account with that email exists, a verification link has been sent."
31086
+ };
31087
+ } catch (error2) {
31088
+ strapi2.log.error(
31089
+ `❌ [sendVerificationEmail] ERROR: ${JSON.stringify({
31090
+ email: email2,
31091
+ message: error2.message,
31092
+ code: error2.code,
31093
+ name: error2.name,
31094
+ stack: error2.stack
31095
+ })}`
31096
+ );
31097
+ return {
31098
+ message: "If an account with that email exists, a verification link has been sent."
31099
+ };
31100
+ }
31101
+ },
31102
+ /**
31103
+ * Verify email with token - public endpoint
31104
+ * Validates the token and marks the user's email as verified in Firebase
31105
+ */
31106
+ async verifyEmail(token) {
31107
+ strapi2.log.info(`[verifyEmail] Starting email verification with token`);
31108
+ if (!token) {
31109
+ throw new ValidationError$1("Verification token is required");
31110
+ }
31111
+ const tokenService2 = strapi2.plugin("firebase-authentication").service("tokenService");
31112
+ const validationResult = await tokenService2.validateVerificationToken(token);
31113
+ if (!validationResult.valid) {
31114
+ strapi2.log.warn(`[verifyEmail] Token validation failed: ${validationResult.error}`);
31115
+ throw new ValidationError$1(validationResult.error || "Invalid verification link");
31116
+ }
31117
+ const { firebaseUID, firebaseUserDataDocumentId, email: tokenEmail } = validationResult;
31118
+ try {
31119
+ const firebaseUser = await strapi2.firebase.auth().getUser(firebaseUID);
31120
+ if (tokenEmail && firebaseUser.email !== tokenEmail) {
31121
+ strapi2.log.warn(
31122
+ `[verifyEmail] Email changed: token email ${tokenEmail} != current email ${firebaseUser.email}`
31123
+ );
31124
+ await tokenService2.invalidateVerificationToken(firebaseUserDataDocumentId);
31125
+ throw new ValidationError$1(
31126
+ "Email address has changed since verification was requested. Please request a new verification link."
31127
+ );
31128
+ }
31129
+ if (firebaseUser.emailVerified) {
31130
+ strapi2.log.info(`[verifyEmail] User ${firebaseUser.email} is already verified`);
31131
+ await tokenService2.invalidateVerificationToken(firebaseUserDataDocumentId);
31132
+ return {
31133
+ success: true,
31134
+ message: "Email is already verified."
31135
+ };
31136
+ }
31137
+ await strapi2.firebase.auth().updateUser(firebaseUID, {
31138
+ emailVerified: true
31139
+ });
31140
+ try {
31141
+ const firebaseUserDataService2 = strapi2.plugin("firebase-authentication").service("firebaseUserDataService");
31142
+ const firebaseUserData2 = await firebaseUserDataService2.getByFirebaseUID(firebaseUID);
31143
+ if (firebaseUserData2?.user?.documentId) {
31144
+ await strapi2.db.query("plugin::users-permissions.user").update({
31145
+ where: { documentId: firebaseUserData2.user.documentId },
31146
+ data: { confirmed: true }
31147
+ });
31148
+ strapi2.log.info(`✅ [verifyEmail] Strapi user confirmed for: ${firebaseUserData2.user.documentId}`);
31149
+ }
31150
+ } catch (strapiUpdateError) {
31151
+ strapi2.log.warn(
31152
+ `[verifyEmail] Failed to update Strapi user confirmed status: ${strapiUpdateError.message}`
31153
+ );
31154
+ }
31155
+ strapi2.log.info(`✅ [verifyEmail] Email verified successfully for: ${firebaseUser.email}`);
31156
+ await tokenService2.invalidateVerificationToken(firebaseUserDataDocumentId);
31157
+ return {
31158
+ success: true,
31159
+ message: "Email verified successfully."
31160
+ };
31161
+ } catch (error2) {
31162
+ strapi2.log.error(
31163
+ `❌ [verifyEmail] ERROR: ${JSON.stringify({
31164
+ firebaseUID,
31165
+ message: error2.message,
31166
+ code: error2.code,
31167
+ name: error2.name
31168
+ })}`
31169
+ );
31170
+ if (error2 instanceof ValidationError$1) {
31171
+ throw error2;
31172
+ }
31173
+ throw new ApplicationError$2("Failed to verify email. Please try again.");
31174
+ }
30867
31175
  }
30868
31176
  });
30869
31177
  const passwordResetTemplate = {
@@ -31243,13 +31551,144 @@ The <%= appName %> Team
31243
31551
  Need help? Contact us at <%= supportEmail %>
31244
31552
  <% } %>
31245
31553
 
31554
+ © <%= year %> <%= appName %>. All rights reserved.
31555
+ `.trim()
31556
+ };
31557
+ const emailVerificationTemplate = {
31558
+ subject: "Verify Your Email - <%= appName %>",
31559
+ html: `
31560
+ <!DOCTYPE html>
31561
+ <html lang="en">
31562
+ <head>
31563
+ <meta charset="UTF-8">
31564
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
31565
+ <title>Verify Your Email</title>
31566
+ </head>
31567
+ <body style="margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f4f4;">
31568
+ <table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;">
31569
+ <tr>
31570
+ <td align="center" style="padding: 40px 0;">
31571
+ <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);">
31572
+ <!-- Header -->
31573
+ <tr>
31574
+ <td style="padding: 40px 40px 20px 40px; text-align: center;">
31575
+ <h1 style="margin: 0; font-size: 28px; color: #333333; font-weight: 600;">
31576
+ Verify Your Email Address
31577
+ </h1>
31578
+ </td>
31579
+ </tr>
31580
+
31581
+ <!-- Content -->
31582
+ <tr>
31583
+ <td style="padding: 20px 40px;">
31584
+ <p style="margin: 0 0 20px 0; font-size: 16px; line-height: 1.6; color: #555555;">
31585
+ Hello<% if (user.firstName) { %> <%= user.firstName %><% } %>,
31586
+ </p>
31587
+
31588
+ <p style="margin: 0 0 20px 0; font-size: 16px; line-height: 1.6; color: #555555;">
31589
+ Thank you for signing up with <strong><%= appName %></strong>! Please verify your email address <strong><%= user.email %></strong> to complete your registration.
31590
+ </p>
31591
+
31592
+ <p style="margin: 0 0 30px 0; font-size: 16px; line-height: 1.6; color: #555555;">
31593
+ Click the button below to verify your email:
31594
+ </p>
31595
+
31596
+ <!-- CTA Button -->
31597
+ <table role="presentation" style="width: 100%; border-collapse: collapse;">
31598
+ <tr>
31599
+ <td align="center" style="padding: 0 0 30px 0;">
31600
+ <a href="<%= verificationLink %>"
31601
+ 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;">
31602
+ Verify Email Address
31603
+ </a>
31604
+ </td>
31605
+ </tr>
31606
+ </table>
31607
+
31608
+ <p style="margin: 0 0 20px 0; font-size: 14px; line-height: 1.6; color: #777777;">
31609
+ Or copy and paste this link into your browser:
31610
+ </p>
31611
+
31612
+ <p style="margin: 0 0 30px 0; font-size: 14px; line-height: 1.6; word-break: break-all; color: #28a745;">
31613
+ <%= verificationLink %>
31614
+ </p>
31615
+
31616
+ <!-- Security Notice -->
31617
+ <table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #fff3cd; border-radius: 4px; margin: 0 0 20px 0;">
31618
+ <tr>
31619
+ <td style="padding: 12px 16px;">
31620
+ <p style="margin: 0; font-size: 14px; line-height: 1.6; color: #856404;">
31621
+ <strong>⚠️ Important:</strong> This link will expire in <strong><%= expiresIn %></strong>.
31622
+ If you didn't create an account with us, you can safely ignore this email.
31623
+ </p>
31624
+ </td>
31625
+ </tr>
31626
+ </table>
31627
+
31628
+ <p style="margin: 0 0 20px 0; font-size: 16px; line-height: 1.6; color: #555555;">
31629
+ For security reasons, please do not share this link with anyone.
31630
+ </p>
31631
+ </td>
31632
+ </tr>
31633
+
31634
+ <!-- Footer -->
31635
+ <tr>
31636
+ <td style="padding: 30px 40px 40px 40px; border-top: 1px solid #eeeeee;">
31637
+ <p style="margin: 0 0 10px 0; font-size: 14px; line-height: 1.6; color: #999999; text-align: center;">
31638
+ Best regards,<br>
31639
+ The <%= appName %> Team
31640
+ </p>
31641
+
31642
+ <% if (supportEmail) { %>
31643
+ <p style="margin: 0 0 10px 0; font-size: 12px; line-height: 1.6; color: #999999; text-align: center;">
31644
+ Need help? Contact us at <a href="mailto:<%= supportEmail %>" style="color: #28a745; text-decoration: none;"><%= supportEmail %></a>
31645
+ </p>
31646
+ <% } %>
31647
+
31648
+ <p style="margin: 0; font-size: 12px; line-height: 1.6; color: #999999; text-align: center;">
31649
+ © <%= year %> <%= appName %>. All rights reserved.
31650
+ </p>
31651
+ </td>
31652
+ </tr>
31653
+ </table>
31654
+ </td>
31655
+ </tr>
31656
+ </table>
31657
+ </body>
31658
+ </html>
31659
+ `.trim(),
31660
+ text: `
31661
+ Verify Your Email Address
31662
+
31663
+ Hello<% if (user.firstName) { %> <%= user.firstName %><% } %>,
31664
+
31665
+ Thank you for signing up with <%= appName %>! Please verify your email address <%= user.email %> to complete your registration.
31666
+
31667
+ To verify your email, please visit the following link:
31668
+
31669
+ <%= verificationLink %>
31670
+
31671
+ This link will expire in <%= expiresIn %>.
31672
+
31673
+ If you didn't create an account with us, you can safely ignore this email.
31674
+
31675
+ For security reasons, please do not share this link with anyone.
31676
+
31677
+ Best regards,
31678
+ The <%= appName %> Team
31679
+
31680
+ <% if (supportEmail) { %>
31681
+ Need help? Contact us at <%= supportEmail %>
31682
+ <% } %>
31683
+
31246
31684
  © <%= year %> <%= appName %>. All rights reserved.
31247
31685
  `.trim()
31248
31686
  };
31249
31687
  const defaultTemplates = {
31250
31688
  passwordReset: passwordResetTemplate,
31251
31689
  magicLink: magicLinkTemplate,
31252
- passwordChanged: passwordChangedTemplate
31690
+ passwordChanged: passwordChangedTemplate,
31691
+ emailVerification: emailVerificationTemplate
31253
31692
  };
31254
31693
  class TemplateService {
31255
31694
  /**
@@ -31661,6 +32100,114 @@ class EmailService {
31661
32100
  message: "Password changed but confirmation email could not be sent"
31662
32101
  };
31663
32102
  }
32103
+ /**
32104
+ * Send email verification email with three-tier fallback system
32105
+ * Tier 1: Strapi Email Plugin (if configured)
32106
+ * Tier 2: Custom Hook Function (if provided in config)
32107
+ * Tier 3: Development Console Logging (dev mode only)
32108
+ */
32109
+ async sendVerificationEmail(user, verificationLink) {
32110
+ if (!user.email) {
32111
+ throw new ValidationError$1("User does not have an email address");
32112
+ }
32113
+ const variables = {
32114
+ user: {
32115
+ email: user.email,
32116
+ firstName: user.firstName || user.displayName?.split(" ")[0],
32117
+ lastName: user.lastName,
32118
+ displayName: user.displayName,
32119
+ phoneNumber: user.phoneNumber,
32120
+ uid: user.uid
32121
+ },
32122
+ verificationLink,
32123
+ expiresIn: "1 hour"
32124
+ };
32125
+ const settingsService2 = strapi.plugin("firebase-authentication").service("settingsService");
32126
+ const dbConfig = await settingsService2.getFirebaseConfigJson();
32127
+ const customSubject = dbConfig?.emailVerificationEmailSubject;
32128
+ const pluginConfig = strapi.config.get("plugin::firebase-authentication");
32129
+ const appConfig = pluginConfig?.app || {};
32130
+ const completeVariables = {
32131
+ ...variables,
32132
+ appName: appConfig?.name || "Your Application",
32133
+ appUrl: appConfig?.url || process.env.PUBLIC_URL || "http://localhost:3000",
32134
+ supportEmail: appConfig?.supportEmail,
32135
+ year: (/* @__PURE__ */ new Date()).getFullYear(),
32136
+ expiresIn: variables.expiresIn
32137
+ };
32138
+ const templateService2 = strapi.plugin("firebase-authentication").service("templateService");
32139
+ const template = await templateService2.getTemplate("emailVerification");
32140
+ const subjectTemplate = customSubject || template.subject;
32141
+ const compiledSubject = _$1.template(subjectTemplate)(completeVariables);
32142
+ try {
32143
+ const compiledHtml = template.html ? _$1.template(template.html)(completeVariables) : void 0;
32144
+ const compiledText = template.text ? _$1.template(template.text)(completeVariables) : void 0;
32145
+ const emailPlugin = strapi.plugin("email");
32146
+ if (!emailPlugin) {
32147
+ throw new Error("Email plugin not found");
32148
+ }
32149
+ const emailService2 = emailPlugin.service("email");
32150
+ await emailService2.send({
32151
+ to: user.email,
32152
+ subject: compiledSubject,
32153
+ html: compiledHtml,
32154
+ text: compiledText
32155
+ });
32156
+ strapi.log.info(`✅ Email verification sent via Strapi email plugin to ${user.email}`);
32157
+ return {
32158
+ success: true,
32159
+ message: `Verification email sent to ${user.email}`
32160
+ };
32161
+ } catch (tier1Error) {
32162
+ strapi.log.debug(`Strapi email plugin failed: ${tier1Error.message}. Trying fallback options...`);
32163
+ }
32164
+ const customSender = pluginConfig?.sendVerificationEmail;
32165
+ if (customSender && typeof customSender === "function") {
32166
+ try {
32167
+ const compiledHtml = template.html ? _$1.template(template.html)(completeVariables) : void 0;
32168
+ const compiledText = template.text ? _$1.template(template.text)(completeVariables) : void 0;
32169
+ await customSender({
32170
+ to: user.email,
32171
+ subject: compiledSubject,
32172
+ html: compiledHtml,
32173
+ text: compiledText,
32174
+ verificationLink,
32175
+ variables: completeVariables
32176
+ });
32177
+ strapi.log.info(`✅ Email verification sent via custom hook to ${user.email}`);
32178
+ return {
32179
+ success: true,
32180
+ message: `Verification email sent to ${user.email}`
32181
+ };
32182
+ } catch (tier2Error) {
32183
+ strapi.log.error(`Custom hook failed: ${tier2Error.message}. Continuing to next fallback...`);
32184
+ }
32185
+ }
32186
+ if (process.env.NODE_ENV !== "production") {
32187
+ try {
32188
+ strapi.log.info("\n" + "=".repeat(80));
32189
+ strapi.log.info("EMAIL VERIFICATION (Development Mode)");
32190
+ strapi.log.info("=".repeat(80));
32191
+ strapi.log.info(`To: ${user.email}`);
32192
+ strapi.log.info(`Subject: ${compiledSubject}`);
32193
+ strapi.log.info(`Verification Link: ${verificationLink}`);
32194
+ strapi.log.info(`Expires In: 1 hour`);
32195
+ strapi.log.info("=".repeat(80));
32196
+ strapi.log.info("Note: Email not sent - no email service configured");
32197
+ strapi.log.info("Copy the link above and open in your browser to verify");
32198
+ strapi.log.info("=".repeat(80) + "\n");
32199
+ return {
32200
+ success: true,
32201
+ message: "Verification link logged to console (development mode)"
32202
+ };
32203
+ } catch (tier3Error) {
32204
+ strapi.log.error(`Development fallback failed: ${tier3Error.message}`);
32205
+ }
32206
+ }
32207
+ throw new ApplicationError$2(
32208
+ "Email service is not configured. Please configure Strapi email plugin or provide custom sendVerificationEmail function in plugin config."
32209
+ );
32210
+ }
31664
32211
  }
31665
32212
  const emailService = ({ strapi: strapi2 }) => new EmailService();
31666
32213
  const firebaseUserDataService = ({ strapi: strapi2 }) => ({
@@ -32023,7 +32570,7 @@ var TokenExpiredError_1 = TokenExpiredError$1;
32023
32570
  var jws$3 = {};
32024
32571
  var safeBuffer = { exports: {} };
32025
32572
  /*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
32026
- (function(module, exports) {
32573
+ (function(module, exports$1) {
32027
32574
  var buffer = require$$0$7;
32028
32575
  var Buffer2 = buffer.Buffer;
32029
32576
  function copyProps(src, dst) {
@@ -32034,8 +32581,8 @@ var safeBuffer = { exports: {} };
32034
32581
  if (Buffer2.from && Buffer2.alloc && Buffer2.allocUnsafe && Buffer2.allocUnsafeSlow) {
32035
32582
  module.exports = buffer;
32036
32583
  } else {
32037
- copyProps(buffer, exports);
32038
- exports.Buffer = SafeBuffer;
32584
+ copyProps(buffer, exports$1);
32585
+ exports$1.Buffer = SafeBuffer;
32039
32586
  }
32040
32587
  function SafeBuffer(arg, encodingOrOffset, length) {
32041
32588
  return Buffer2(arg, encodingOrOffset, length);
@@ -32550,7 +33097,12 @@ function jwsSign(opts) {
32550
33097
  return util$1.format("%s.%s", securedInput, signature);
32551
33098
  }
32552
33099
  function SignStream$1(opts) {
32553
- var secret = opts.secret || opts.privateKey || opts.key;
33100
+ var secret = opts.secret;
33101
+ secret = secret == null ? opts.privateKey : secret;
33102
+ secret = secret == null ? opts.key : secret;
33103
+ if (/^hs/i.test(opts.header.alg) === true && secret == null) {
33104
+ throw new TypeError("secret must be a string or buffer or a KeyObject");
33105
+ }
32554
33106
  var secretStream = new DataStream$1(secret);
32555
33107
  this.readable = true;
32556
33108
  this.header = opts.header;
@@ -32656,7 +33208,12 @@ function jwsDecode(jwsSig, opts) {
32656
33208
  }
32657
33209
  function VerifyStream$1(opts) {
32658
33210
  opts = opts || {};
32659
- var secretOrKey = opts.secret || opts.publicKey || opts.key;
33211
+ var secretOrKey = opts.secret;
33212
+ secretOrKey = secretOrKey == null ? opts.publicKey : secretOrKey;
33213
+ secretOrKey = secretOrKey == null ? opts.key : secretOrKey;
33214
+ if (/^hs/i.test(opts.algorithm) === true && secretOrKey == null) {
33215
+ throw new TypeError("secret must be a string or buffer or a KeyObject");
33216
+ }
32660
33217
  var secretStream = new DataStream(secretOrKey);
32661
33218
  this.readable = true;
32662
33219
  this.algorithm = opts.algorithm;
@@ -32899,19 +33456,19 @@ var constants$1 = {
32899
33456
  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) : () => {
32900
33457
  };
32901
33458
  var debug_1 = debug$1;
32902
- (function(module, exports) {
33459
+ (function(module, exports$1) {
32903
33460
  const {
32904
33461
  MAX_SAFE_COMPONENT_LENGTH: MAX_SAFE_COMPONENT_LENGTH2,
32905
33462
  MAX_SAFE_BUILD_LENGTH: MAX_SAFE_BUILD_LENGTH2,
32906
33463
  MAX_LENGTH: MAX_LENGTH2
32907
33464
  } = constants$1;
32908
33465
  const debug2 = debug_1;
32909
- exports = module.exports = {};
32910
- const re2 = exports.re = [];
32911
- const safeRe = exports.safeRe = [];
32912
- const src = exports.src = [];
32913
- const safeSrc = exports.safeSrc = [];
32914
- const t2 = exports.t = {};
33466
+ exports$1 = module.exports = {};
33467
+ const re2 = exports$1.re = [];
33468
+ const safeRe = exports$1.safeRe = [];
33469
+ const src = exports$1.src = [];
33470
+ const safeSrc = exports$1.safeSrc = [];
33471
+ const t2 = exports$1.t = {};
32915
33472
  let R = 0;
32916
33473
  const LETTERDASHNUMBER = "[a-zA-Z0-9-]";
32917
33474
  const safeRegexReplacements = [
@@ -32964,18 +33521,18 @@ var debug_1 = debug$1;
32964
33521
  createToken("COERCERTLFULL", src[t2.COERCEFULL], true);
32965
33522
  createToken("LONETILDE", "(?:~>?)");
32966
33523
  createToken("TILDETRIM", `(\\s*)${src[t2.LONETILDE]}\\s+`, true);
32967
- exports.tildeTrimReplace = "$1~";
33524
+ exports$1.tildeTrimReplace = "$1~";
32968
33525
  createToken("TILDE", `^${src[t2.LONETILDE]}${src[t2.XRANGEPLAIN]}$`);
32969
33526
  createToken("TILDELOOSE", `^${src[t2.LONETILDE]}${src[t2.XRANGEPLAINLOOSE]}$`);
32970
33527
  createToken("LONECARET", "(?:\\^)");
32971
33528
  createToken("CARETTRIM", `(\\s*)${src[t2.LONECARET]}\\s+`, true);
32972
- exports.caretTrimReplace = "$1^";
33529
+ exports$1.caretTrimReplace = "$1^";
32973
33530
  createToken("CARET", `^${src[t2.LONECARET]}${src[t2.XRANGEPLAIN]}$`);
32974
33531
  createToken("CARETLOOSE", `^${src[t2.LONECARET]}${src[t2.XRANGEPLAINLOOSE]}$`);
32975
33532
  createToken("COMPARATORLOOSE", `^${src[t2.GTLT]}\\s*(${src[t2.LOOSEPLAIN]})$|^$`);
32976
33533
  createToken("COMPARATOR", `^${src[t2.GTLT]}\\s*(${src[t2.FULLPLAIN]})$|^$`);
32977
33534
  createToken("COMPARATORTRIM", `(\\s*)${src[t2.GTLT]}\\s*(${src[t2.LOOSEPLAIN]}|${src[t2.XRANGEPLAIN]})`, true);
32978
- exports.comparatorTrimReplace = "$1$2$3";
33535
+ exports$1.comparatorTrimReplace = "$1$2$3";
32979
33536
  createToken("HYPHENRANGE", `^\\s*(${src[t2.XRANGEPLAIN]})\\s+-\\s+(${src[t2.XRANGEPLAIN]})\\s*$`);
32980
33537
  createToken("HYPHENRANGELOOSE", `^\\s*(${src[t2.XRANGEPLAINLOOSE]})\\s+-\\s+(${src[t2.XRANGEPLAINLOOSE]})\\s*$`);
32981
33538
  createToken("STAR", "(<|>)?=?\\s*\\*");
@@ -35114,6 +35671,125 @@ const tokenService = ({ strapi: strapi2 }) => {
35114
35671
  }
35115
35672
  });
35116
35673
  strapi2.log.debug(`Invalidated reset token for user ${firebaseUserDataDocumentId}`);
35674
+ },
35675
+ // ==================== EMAIL VERIFICATION TOKENS ====================
35676
+ /**
35677
+ * Generate an email verification token for a user
35678
+ * @param firebaseUserDataDocumentId - The documentId of the firebase-user-data record
35679
+ * @param email - The email address at time of request (for change detection)
35680
+ * @returns The JWT token to include in the verification URL
35681
+ */
35682
+ async generateVerificationToken(firebaseUserDataDocumentId, email2) {
35683
+ const signingKey = getSigningKey();
35684
+ const jti = crypto$1.randomBytes(32).toString("hex");
35685
+ const payload = {
35686
+ sub: firebaseUserDataDocumentId,
35687
+ purpose: "email-verification",
35688
+ email: email2,
35689
+ jti
35690
+ };
35691
+ const token = jwt.sign(payload, signingKey, {
35692
+ expiresIn: "1h"
35693
+ });
35694
+ const tokenHash = crypto$1.createHash("sha256").update(jti).digest("hex");
35695
+ const expiresAt = new Date(Date.now() + 60 * 60 * 1e3);
35696
+ await strapi2.db.query(FIREBASE_USER_DATA_CONTENT_TYPE).update({
35697
+ where: { documentId: firebaseUserDataDocumentId },
35698
+ data: {
35699
+ verificationTokenHash: tokenHash,
35700
+ verificationTokenExpiresAt: expiresAt.toISOString()
35701
+ }
35702
+ });
35703
+ strapi2.log.debug(`Generated verification token for user ${firebaseUserDataDocumentId}`);
35704
+ return token;
35705
+ },
35706
+ /**
35707
+ * Validate an email verification token
35708
+ * @param token - The JWT token from the verification URL
35709
+ * @returns Validation result with user info and email if valid
35710
+ */
35711
+ async validateVerificationToken(token) {
35712
+ const signingKey = getSigningKey();
35713
+ try {
35714
+ const decoded = jwt.verify(token, signingKey);
35715
+ if (decoded.purpose !== "email-verification") {
35716
+ return {
35717
+ valid: false,
35718
+ firebaseUserDataDocumentId: "",
35719
+ firebaseUID: "",
35720
+ error: "Invalid token purpose"
35721
+ };
35722
+ }
35723
+ const tokenHash = crypto$1.createHash("sha256").update(decoded.jti).digest("hex");
35724
+ const firebaseUserData2 = await strapi2.db.query(FIREBASE_USER_DATA_CONTENT_TYPE).findOne({
35725
+ where: { documentId: decoded.sub }
35726
+ });
35727
+ if (!firebaseUserData2) {
35728
+ return {
35729
+ valid: false,
35730
+ firebaseUserDataDocumentId: "",
35731
+ firebaseUID: "",
35732
+ error: "User not found"
35733
+ };
35734
+ }
35735
+ if (firebaseUserData2.verificationTokenHash !== tokenHash) {
35736
+ return {
35737
+ valid: false,
35738
+ firebaseUserDataDocumentId: "",
35739
+ firebaseUID: "",
35740
+ error: "Verification link has already been used or is invalid"
35741
+ };
35742
+ }
35743
+ if (firebaseUserData2.verificationTokenExpiresAt) {
35744
+ const expiresAt = new Date(firebaseUserData2.verificationTokenExpiresAt);
35745
+ if (expiresAt < /* @__PURE__ */ new Date()) {
35746
+ return {
35747
+ valid: false,
35748
+ firebaseUserDataDocumentId: "",
35749
+ firebaseUID: "",
35750
+ error: "Verification link has expired"
35751
+ };
35752
+ }
35753
+ }
35754
+ return {
35755
+ valid: true,
35756
+ firebaseUserDataDocumentId: firebaseUserData2.documentId,
35757
+ firebaseUID: firebaseUserData2.firebaseUserID,
35758
+ email: decoded.email
35759
+ };
35760
+ } catch (error2) {
35761
+ if (error2.name === "TokenExpiredError") {
35762
+ return {
35763
+ valid: false,
35764
+ firebaseUserDataDocumentId: "",
35765
+ firebaseUID: "",
35766
+ error: "Verification link has expired"
35767
+ };
35768
+ }
35769
+ if (error2.name === "JsonWebTokenError") {
35770
+ return {
35771
+ valid: false,
35772
+ firebaseUserDataDocumentId: "",
35773
+ firebaseUID: "",
35774
+ error: "Invalid verification link"
35775
+ };
35776
+ }
35777
+ throw error2;
35778
+ }
35779
+ },
35780
+ /**
35781
+ * Invalidate a verification token after use
35782
+ * @param firebaseUserDataDocumentId - The documentId of the firebase-user-data record
35783
+ */
35784
+ async invalidateVerificationToken(firebaseUserDataDocumentId) {
35785
+ await strapi2.db.query(FIREBASE_USER_DATA_CONTENT_TYPE).update({
35786
+ where: { documentId: firebaseUserDataDocumentId },
35787
+ data: {
35788
+ verificationTokenHash: null,
35789
+ verificationTokenExpiresAt: null
35790
+ }
35791
+ });
35792
+ strapi2.log.debug(`Invalidated verification token for user ${firebaseUserDataDocumentId}`);
35117
35793
  }
35118
35794
  };
35119
35795
  };