strapi-plugin-firebase-authentication 1.1.12 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/_chunks/{App-BqjE8BHb.js → App-B7d4qS3T.js} +112 -29
  2. package/dist/_chunks/{App-BY1gNGKH.mjs → App-CQ9ehArz.mjs} +112 -29
  3. package/dist/_chunks/{api-D_4cdJU5.mjs → api-BM2UtpvM.mjs} +1 -1
  4. package/dist/_chunks/{api-DQCdqlCd.js → api-DYP-1kdx.js} +1 -1
  5. package/dist/_chunks/{index-D8pv1Q6h.mjs → index-0tTyhxbb.mjs} +148 -34
  6. package/dist/_chunks/{index-DlPxMuSK.js → index-B5EwGI_y.js} +2 -2
  7. package/dist/_chunks/{index-DtGfwf9S.mjs → index-CMFutRyI.mjs} +2 -2
  8. package/dist/_chunks/{index-C4t4JZZ_.js → index-Cwp9xkG4.js} +148 -34
  9. package/dist/admin/index.js +1 -1
  10. package/dist/admin/index.mjs +1 -1
  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 +691 -10
  16. package/dist/server/index.mjs +691 -10
  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 +15 -0
  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 +37 -1
  23. package/dist/server/src/services/emailService.d.ts +10 -0
  24. package/dist/server/src/services/firebaseService.d.ts +16 -0
  25. package/dist/server/src/services/index.d.ts +17 -0
  26. package/dist/server/src/services/settingsService.d.ts +2 -0
  27. package/dist/server/src/services/tokenService.d.ts +21 -0
  28. package/dist/server/src/services/userService.d.ts +5 -0
  29. package/dist/server/src/templates/defaults/email-verification.d.ts +2 -0
  30. package/dist/server/src/templates/defaults/index.d.ts +1 -0
  31. package/dist/server/src/templates/types.d.ts +3 -1
  32. package/package.json +1 -1
@@ -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 = {
@@ -615,6 +631,61 @@ const firebaseController = {
615
631
  ctx.status = 500;
616
632
  ctx.body = { error: "An error occurred while resetting your password" };
617
633
  }
634
+ },
635
+ /**
636
+ * Send email verification - public endpoint
637
+ * POST /api/firebase-authentication/sendVerificationEmail
638
+ * Public endpoint - no authentication required
639
+ */
640
+ async sendVerificationEmail(ctx) {
641
+ strapi.log.debug("sendVerificationEmail endpoint called");
642
+ try {
643
+ const { email: email2 } = ctx.request.body || {};
644
+ ctx.body = await strapi.plugin(pluginName).service("firebaseService").sendVerificationEmail(email2);
645
+ } catch (error2) {
646
+ strapi.log.error("sendVerificationEmail controller error:", error2);
647
+ if (error2.name === "ValidationError") {
648
+ ctx.status = 400;
649
+ } else {
650
+ ctx.status = 500;
651
+ }
652
+ ctx.body = { error: error2.message };
653
+ }
654
+ },
655
+ /**
656
+ * Verify email using custom JWT token
657
+ * POST /api/firebase-authentication/verifyEmail
658
+ * Public endpoint - token provides authentication
659
+ *
660
+ * @param ctx - Koa context with { token } in body
661
+ * @returns { success: true, message: "Email verified successfully" }
662
+ */
663
+ async verifyEmail(ctx) {
664
+ strapi.log.debug("verifyEmail endpoint called");
665
+ try {
666
+ const { token } = ctx.request.body || {};
667
+ if (!token) {
668
+ ctx.status = 400;
669
+ ctx.body = { error: "Token is required" };
670
+ return;
671
+ }
672
+ const result = await strapi.plugin(pluginName).service("firebaseService").verifyEmail(token);
673
+ ctx.body = result;
674
+ } catch (error2) {
675
+ strapi.log.error("verifyEmail controller error:", error2);
676
+ if (error2.name === "ValidationError") {
677
+ ctx.status = 400;
678
+ ctx.body = { error: error2.message };
679
+ return;
680
+ }
681
+ if (error2.name === "NotFoundError") {
682
+ ctx.status = 404;
683
+ ctx.body = { error: error2.message };
684
+ return;
685
+ }
686
+ ctx.status = 500;
687
+ ctx.body = { error: "An error occurred while verifying your email" };
688
+ }
618
689
  }
619
690
  };
620
691
  var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {};
@@ -28982,6 +29053,17 @@ const userController = {
28982
29053
  } catch (error2) {
28983
29054
  throw new ApplicationError$2(error2.message || "Failed to send password reset email");
28984
29055
  }
29056
+ },
29057
+ sendVerificationEmail: async (ctx) => {
29058
+ const userId = ctx.params.id;
29059
+ if (!userId) {
29060
+ throw new ValidationError$1("User ID is required");
29061
+ }
29062
+ try {
29063
+ ctx.body = await strapi.plugin("firebase-authentication").service("userService").sendVerificationEmail(userId);
29064
+ } catch (error2) {
29065
+ throw new ApplicationError$2(error2.message || "Failed to send verification email");
29066
+ }
28985
29067
  }
28986
29068
  };
28987
29069
  const settingsController = {
@@ -29084,7 +29166,9 @@ const settingsController = {
29084
29166
  enableMagicLink = false,
29085
29167
  magicLinkUrl = "http://localhost:1338/verify-magic-link.html",
29086
29168
  magicLinkEmailSubject = "Sign in to Your Application",
29087
- magicLinkExpiryHours = 1
29169
+ magicLinkExpiryHours = 1,
29170
+ emailVerificationUrl = "http://localhost:3000/verify-email",
29171
+ emailVerificationEmailSubject = "Verify Your Email"
29088
29172
  } = requestBody;
29089
29173
  const existingConfig = await strapi.db.query("plugin::firebase-authentication.firebase-authentication-configuration").findOne({ where: {} });
29090
29174
  let result;
@@ -29098,7 +29182,9 @@ const settingsController = {
29098
29182
  enableMagicLink,
29099
29183
  magicLinkUrl,
29100
29184
  magicLinkEmailSubject,
29101
- magicLinkExpiryHours
29185
+ magicLinkExpiryHours,
29186
+ emailVerificationUrl,
29187
+ emailVerificationEmailSubject
29102
29188
  }
29103
29189
  });
29104
29190
  } else {
@@ -29112,7 +29198,9 @@ const settingsController = {
29112
29198
  enableMagicLink,
29113
29199
  magicLinkUrl,
29114
29200
  magicLinkEmailSubject,
29115
- magicLinkExpiryHours
29201
+ magicLinkExpiryHours,
29202
+ emailVerificationUrl,
29203
+ emailVerificationEmailSubject
29116
29204
  }
29117
29205
  });
29118
29206
  }
@@ -29124,7 +29212,9 @@ const settingsController = {
29124
29212
  enableMagicLink: result.enableMagicLink,
29125
29213
  magicLinkUrl: result.magicLinkUrl,
29126
29214
  magicLinkEmailSubject: result.magicLinkEmailSubject,
29127
- magicLinkExpiryHours: result.magicLinkExpiryHours
29215
+ magicLinkExpiryHours: result.magicLinkExpiryHours,
29216
+ emailVerificationUrl: result.emailVerificationUrl,
29217
+ emailVerificationEmailSubject: result.emailVerificationEmailSubject
29128
29218
  };
29129
29219
  } catch (error2) {
29130
29220
  throw new ApplicationError$2("Error saving password configuration", {
@@ -29217,6 +29307,14 @@ const admin = {
29217
29307
  policies: ["admin::isAuthenticatedAdmin"]
29218
29308
  }
29219
29309
  },
29310
+ {
29311
+ method: "PUT",
29312
+ path: "/users/sendVerificationEmail/:id",
29313
+ handler: "userController.sendVerificationEmail",
29314
+ config: {
29315
+ policies: ["admin::isAuthenticatedAdmin"]
29316
+ }
29317
+ },
29220
29318
  {
29221
29319
  method: "GET",
29222
29320
  path: "/users/:id",
@@ -29315,6 +29413,26 @@ const contentApi = {
29315
29413
  // Public endpoint - token provides authentication
29316
29414
  policies: []
29317
29415
  }
29416
+ },
29417
+ {
29418
+ method: "POST",
29419
+ path: "/sendVerificationEmail",
29420
+ handler: "firebaseController.sendVerificationEmail",
29421
+ config: {
29422
+ auth: false,
29423
+ // Public endpoint - sends email verification link
29424
+ policies: []
29425
+ }
29426
+ },
29427
+ {
29428
+ method: "POST",
29429
+ path: "/verifyEmail",
29430
+ handler: "firebaseController.verifyEmail",
29431
+ config: {
29432
+ auth: false,
29433
+ // Public endpoint - token provides authentication
29434
+ policies: []
29435
+ }
29318
29436
  }
29319
29437
  ]
29320
29438
  };
@@ -29435,7 +29553,10 @@ const settingsService = ({ strapi: strapi2 }) => {
29435
29553
  enableMagicLink: configObject.enableMagicLink || false,
29436
29554
  magicLinkUrl: configObject.magicLinkUrl || "http://localhost:1338/verify-magic-link.html",
29437
29555
  magicLinkEmailSubject: configObject.magicLinkEmailSubject || "Sign in to Your Application",
29438
- magicLinkExpiryHours: configObject.magicLinkExpiryHours || 1
29556
+ magicLinkExpiryHours: configObject.magicLinkExpiryHours || 1,
29557
+ // Include email verification configuration fields
29558
+ emailVerificationUrl: configObject.emailVerificationUrl || "http://localhost:3000/verify-email",
29559
+ emailVerificationEmailSubject: configObject.emailVerificationEmailSubject || "Verify Your Email"
29439
29560
  };
29440
29561
  } catch (error2) {
29441
29562
  strapi2.log.error(`Firebase config error: ${error2.message}`);
@@ -29478,7 +29599,9 @@ const settingsService = ({ strapi: strapi2 }) => {
29478
29599
  enableMagicLink = false,
29479
29600
  magicLinkUrl = "http://localhost:1338/verify-magic-link.html",
29480
29601
  magicLinkEmailSubject = "Sign in to Your Application",
29481
- magicLinkExpiryHours = 1
29602
+ magicLinkExpiryHours = 1,
29603
+ emailVerificationUrl = "http://localhost:3000/verify-email",
29604
+ emailVerificationEmailSubject = "Verify Your Email"
29482
29605
  } = requestBody;
29483
29606
  if (!requestBody) throw new ValidationError3(ERROR_MESSAGES.MISSING_DATA);
29484
29607
  try {
@@ -29514,7 +29637,9 @@ const settingsService = ({ strapi: strapi2 }) => {
29514
29637
  enableMagicLink,
29515
29638
  magicLinkUrl,
29516
29639
  magicLinkEmailSubject,
29517
- magicLinkExpiryHours
29640
+ magicLinkExpiryHours,
29641
+ emailVerificationUrl,
29642
+ emailVerificationEmailSubject
29518
29643
  }
29519
29644
  });
29520
29645
  } else {
@@ -29530,11 +29655,20 @@ const settingsService = ({ strapi: strapi2 }) => {
29530
29655
  enableMagicLink,
29531
29656
  magicLinkUrl,
29532
29657
  magicLinkEmailSubject,
29533
- magicLinkExpiryHours
29658
+ magicLinkExpiryHours,
29659
+ emailVerificationUrl,
29660
+ emailVerificationEmailSubject
29534
29661
  }
29535
29662
  });
29536
29663
  }
29537
29664
  await strapi2.plugin("firebase-authentication").service("settingsService").init();
29665
+ setImmediate(async () => {
29666
+ try {
29667
+ await strapi2.plugin("firebase-authentication").service("autoLinkService").linkAllUsers(strapi2);
29668
+ } catch (error2) {
29669
+ strapi2.log.error(`Auto-linking after config save failed: ${error2.message}`);
29670
+ }
29671
+ });
29538
29672
  const configData = res.firebaseConfigJson || res.firebase_config_json;
29539
29673
  if (!configData) {
29540
29674
  strapi2.log.error("Firebase config data missing from database response");
@@ -29558,6 +29692,8 @@ const settingsService = ({ strapi: strapi2 }) => {
29558
29692
  res.magicLinkUrl = res.magicLinkUrl || magicLinkUrl;
29559
29693
  res.magicLinkEmailSubject = res.magicLinkEmailSubject || magicLinkEmailSubject;
29560
29694
  res.magicLinkExpiryHours = res.magicLinkExpiryHours || magicLinkExpiryHours;
29695
+ res.emailVerificationUrl = res.emailVerificationUrl || emailVerificationUrl;
29696
+ res.emailVerificationEmailSubject = res.emailVerificationEmailSubject || emailVerificationEmailSubject;
29561
29697
  return res;
29562
29698
  } catch (error2) {
29563
29699
  strapi2.log.error("=== FIREBASE CONFIG SAVE ERROR ===");
@@ -30217,6 +30353,40 @@ const userService = ({ strapi: strapi2 }) => {
30217
30353
  }
30218
30354
  throw new ApplicationError$2(e.message?.toString() || "Failed to reset password");
30219
30355
  }
30356
+ },
30357
+ /**
30358
+ * Send email verification email (admin-initiated)
30359
+ * @param entityId - Firebase UID of the user
30360
+ */
30361
+ sendVerificationEmail: async (entityId) => {
30362
+ try {
30363
+ ensureFirebaseInitialized();
30364
+ const user = await strapi2.firebase.auth().getUser(entityId);
30365
+ if (!user.email) {
30366
+ throw new ApplicationError$2("User does not have an email address");
30367
+ }
30368
+ if (user.emailVerified) {
30369
+ return { success: true, message: "Email is already verified" };
30370
+ }
30371
+ const config2 = await strapi2.db.query("plugin::firebase-authentication.firebase-authentication-configuration").findOne({ where: {} });
30372
+ const emailVerificationUrl = config2?.emailVerificationUrl;
30373
+ if (!emailVerificationUrl) {
30374
+ throw new ApplicationError$2("Email verification URL is not configured");
30375
+ }
30376
+ const firebaseUserData2 = await strapi2.plugin("firebase-authentication").service("firebaseUserDataService").getByFirebaseUID(entityId);
30377
+ if (!firebaseUserData2) {
30378
+ throw new ApplicationError$2("User is not linked to Firebase authentication");
30379
+ }
30380
+ const tokenService2 = strapi2.plugin("firebase-authentication").service("tokenService");
30381
+ const token = await tokenService2.generateVerificationToken(firebaseUserData2.documentId, user.email);
30382
+ const verificationLink = `${emailVerificationUrl}?token=${token}`;
30383
+ strapi2.log.debug(`Generated email verification link for user ${user.email}`);
30384
+ const emailService2 = strapi2.plugin("firebase-authentication").service("emailService");
30385
+ return await emailService2.sendVerificationEmail(user, verificationLink);
30386
+ } catch (e) {
30387
+ strapi2.log.error(`sendVerificationEmail error: ${e.message}`);
30388
+ throw new ApplicationError$2(e.message?.toString() || "Failed to send verification email");
30389
+ }
30220
30390
  }
30221
30391
  };
30222
30392
  };
@@ -30896,6 +31066,159 @@ const firebaseService = ({ strapi: strapi2 }) => ({
30896
31066
  verificationUrl: magicLinkUrl
30897
31067
  };
30898
31068
  }
31069
+ },
31070
+ /**
31071
+ * Send email verification - public endpoint
31072
+ * Generates a verification token and sends an email to the user
31073
+ * Security: Always returns generic success message to prevent email enumeration
31074
+ */
31075
+ async sendVerificationEmail(email2) {
31076
+ strapi2.log.info(`[sendVerificationEmail] Starting email verification for: ${email2}`);
31077
+ if (!email2) {
31078
+ throw new ValidationError$1("Email is required");
31079
+ }
31080
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
31081
+ if (!emailRegex.test(email2)) {
31082
+ throw new ValidationError$1("Invalid email format");
31083
+ }
31084
+ const config2 = await strapi2.plugin("firebase-authentication").service("settingsService").getFirebaseConfigJson();
31085
+ const verificationUrl = config2?.emailVerificationUrl;
31086
+ if (!verificationUrl) {
31087
+ throw new ApplicationError$2("Email verification URL is not configured");
31088
+ }
31089
+ if (process.env.NODE_ENV === "production" && !verificationUrl.startsWith("https://")) {
31090
+ throw new ApplicationError$2("Email verification URL must use HTTPS in production");
31091
+ }
31092
+ try {
31093
+ new URL(verificationUrl);
31094
+ } catch (error2) {
31095
+ throw new ApplicationError$2("Email verification URL is not a valid URL format");
31096
+ }
31097
+ try {
31098
+ let firebaseUser;
31099
+ try {
31100
+ firebaseUser = await strapi2.firebase.auth().getUserByEmail(email2);
31101
+ } catch (fbError) {
31102
+ strapi2.log.debug("User not found in Firebase");
31103
+ }
31104
+ if (!firebaseUser) {
31105
+ strapi2.log.warn(`⚠️ [sendVerificationEmail] User not found in Firebase for email: ${email2}`);
31106
+ return { message: "If an account with that email exists, a verification link has been sent." };
31107
+ }
31108
+ if (firebaseUser.emailVerified) {
31109
+ strapi2.log.info(`[sendVerificationEmail] User ${email2} is already verified`);
31110
+ return { message: "Email is already verified." };
31111
+ }
31112
+ const firebaseData = await strapi2.plugin("firebase-authentication").service("firebaseUserDataService").getByFirebaseUID(firebaseUser.uid);
31113
+ if (!firebaseData) {
31114
+ strapi2.log.warn(`⚠️ [sendVerificationEmail] No firebase-user-data record for: ${email2}`);
31115
+ return { message: "If an account with that email exists, a verification link has been sent." };
31116
+ }
31117
+ strapi2.log.info(
31118
+ `✅ [sendVerificationEmail] User found: ${JSON.stringify({
31119
+ firebaseUID: firebaseUser.uid,
31120
+ email: firebaseUser.email,
31121
+ emailVerified: firebaseUser.emailVerified
31122
+ })}`
31123
+ );
31124
+ const tokenService2 = strapi2.plugin("firebase-authentication").service("tokenService");
31125
+ const token = await tokenService2.generateVerificationToken(firebaseData.documentId, email2);
31126
+ const verificationLink = `${verificationUrl}?token=${token}`;
31127
+ strapi2.log.info(`✅ [sendVerificationEmail] Verification link generated for ${email2}`);
31128
+ strapi2.log.info(`[sendVerificationEmail] Attempting to send verification email to: ${email2}`);
31129
+ await strapi2.plugin("firebase-authentication").service("emailService").sendVerificationEmail(firebaseUser, verificationLink);
31130
+ strapi2.log.info(`✅ [sendVerificationEmail] Verification email sent successfully to: ${email2}`);
31131
+ return {
31132
+ message: "If an account with that email exists, a verification link has been sent."
31133
+ };
31134
+ } catch (error2) {
31135
+ strapi2.log.error(
31136
+ `❌ [sendVerificationEmail] ERROR: ${JSON.stringify({
31137
+ email: email2,
31138
+ message: error2.message,
31139
+ code: error2.code,
31140
+ name: error2.name,
31141
+ stack: error2.stack
31142
+ })}`
31143
+ );
31144
+ return {
31145
+ message: "If an account with that email exists, a verification link has been sent."
31146
+ };
31147
+ }
31148
+ },
31149
+ /**
31150
+ * Verify email with token - public endpoint
31151
+ * Validates the token and marks the user's email as verified in Firebase
31152
+ */
31153
+ async verifyEmail(token) {
31154
+ strapi2.log.info(`[verifyEmail] Starting email verification with token`);
31155
+ if (!token) {
31156
+ throw new ValidationError$1("Verification token is required");
31157
+ }
31158
+ const tokenService2 = strapi2.plugin("firebase-authentication").service("tokenService");
31159
+ const validationResult = await tokenService2.validateVerificationToken(token);
31160
+ if (!validationResult.valid) {
31161
+ strapi2.log.warn(`[verifyEmail] Token validation failed: ${validationResult.error}`);
31162
+ throw new ValidationError$1(validationResult.error || "Invalid verification link");
31163
+ }
31164
+ const { firebaseUID, firebaseUserDataDocumentId, email: tokenEmail } = validationResult;
31165
+ try {
31166
+ const firebaseUser = await strapi2.firebase.auth().getUser(firebaseUID);
31167
+ if (tokenEmail && firebaseUser.email !== tokenEmail) {
31168
+ strapi2.log.warn(
31169
+ `[verifyEmail] Email changed: token email ${tokenEmail} != current email ${firebaseUser.email}`
31170
+ );
31171
+ await tokenService2.invalidateVerificationToken(firebaseUserDataDocumentId);
31172
+ throw new ValidationError$1(
31173
+ "Email address has changed since verification was requested. Please request a new verification link."
31174
+ );
31175
+ }
31176
+ if (firebaseUser.emailVerified) {
31177
+ strapi2.log.info(`[verifyEmail] User ${firebaseUser.email} is already verified`);
31178
+ await tokenService2.invalidateVerificationToken(firebaseUserDataDocumentId);
31179
+ return {
31180
+ success: true,
31181
+ message: "Email is already verified."
31182
+ };
31183
+ }
31184
+ await strapi2.firebase.auth().updateUser(firebaseUID, {
31185
+ emailVerified: true
31186
+ });
31187
+ try {
31188
+ const firebaseUserDataService2 = strapi2.plugin("firebase-authentication").service("firebaseUserDataService");
31189
+ const firebaseUserData2 = await firebaseUserDataService2.getByFirebaseUID(firebaseUID);
31190
+ if (firebaseUserData2?.user?.documentId) {
31191
+ await strapi2.db.query("plugin::users-permissions.user").update({
31192
+ where: { documentId: firebaseUserData2.user.documentId },
31193
+ data: { confirmed: true }
31194
+ });
31195
+ strapi2.log.info(`✅ [verifyEmail] Strapi user confirmed for: ${firebaseUserData2.user.documentId}`);
31196
+ }
31197
+ } catch (strapiUpdateError) {
31198
+ strapi2.log.warn(
31199
+ `[verifyEmail] Failed to update Strapi user confirmed status: ${strapiUpdateError.message}`
31200
+ );
31201
+ }
31202
+ strapi2.log.info(`✅ [verifyEmail] Email verified successfully for: ${firebaseUser.email}`);
31203
+ await tokenService2.invalidateVerificationToken(firebaseUserDataDocumentId);
31204
+ return {
31205
+ success: true,
31206
+ message: "Email verified successfully."
31207
+ };
31208
+ } catch (error2) {
31209
+ strapi2.log.error(
31210
+ `❌ [verifyEmail] ERROR: ${JSON.stringify({
31211
+ firebaseUID,
31212
+ message: error2.message,
31213
+ code: error2.code,
31214
+ name: error2.name
31215
+ })}`
31216
+ );
31217
+ if (error2 instanceof ValidationError$1) {
31218
+ throw error2;
31219
+ }
31220
+ throw new ApplicationError$2("Failed to verify email. Please try again.");
31221
+ }
30899
31222
  }
30900
31223
  });
30901
31224
  const passwordResetTemplate = {
@@ -31275,13 +31598,144 @@ The <%= appName %> Team
31275
31598
  Need help? Contact us at <%= supportEmail %>
31276
31599
  <% } %>
31277
31600
 
31601
+ © <%= year %> <%= appName %>. All rights reserved.
31602
+ `.trim()
31603
+ };
31604
+ const emailVerificationTemplate = {
31605
+ subject: "Verify Your Email - <%= appName %>",
31606
+ html: `
31607
+ <!DOCTYPE html>
31608
+ <html lang="en">
31609
+ <head>
31610
+ <meta charset="UTF-8">
31611
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
31612
+ <title>Verify Your Email</title>
31613
+ </head>
31614
+ <body style="margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f4f4;">
31615
+ <table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;">
31616
+ <tr>
31617
+ <td align="center" style="padding: 40px 0;">
31618
+ <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);">
31619
+ <!-- Header -->
31620
+ <tr>
31621
+ <td style="padding: 40px 40px 20px 40px; text-align: center;">
31622
+ <h1 style="margin: 0; font-size: 28px; color: #333333; font-weight: 600;">
31623
+ Verify Your Email Address
31624
+ </h1>
31625
+ </td>
31626
+ </tr>
31627
+
31628
+ <!-- Content -->
31629
+ <tr>
31630
+ <td style="padding: 20px 40px;">
31631
+ <p style="margin: 0 0 20px 0; font-size: 16px; line-height: 1.6; color: #555555;">
31632
+ Hello<% if (user.firstName) { %> <%= user.firstName %><% } %>,
31633
+ </p>
31634
+
31635
+ <p style="margin: 0 0 20px 0; font-size: 16px; line-height: 1.6; color: #555555;">
31636
+ Thank you for signing up with <strong><%= appName %></strong>! Please verify your email address <strong><%= user.email %></strong> to complete your registration.
31637
+ </p>
31638
+
31639
+ <p style="margin: 0 0 30px 0; font-size: 16px; line-height: 1.6; color: #555555;">
31640
+ Click the button below to verify your email:
31641
+ </p>
31642
+
31643
+ <!-- CTA Button -->
31644
+ <table role="presentation" style="width: 100%; border-collapse: collapse;">
31645
+ <tr>
31646
+ <td align="center" style="padding: 0 0 30px 0;">
31647
+ <a href="<%= verificationLink %>"
31648
+ 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;">
31649
+ Verify Email Address
31650
+ </a>
31651
+ </td>
31652
+ </tr>
31653
+ </table>
31654
+
31655
+ <p style="margin: 0 0 20px 0; font-size: 14px; line-height: 1.6; color: #777777;">
31656
+ Or copy and paste this link into your browser:
31657
+ </p>
31658
+
31659
+ <p style="margin: 0 0 30px 0; font-size: 14px; line-height: 1.6; word-break: break-all; color: #28a745;">
31660
+ <%= verificationLink %>
31661
+ </p>
31662
+
31663
+ <!-- Security Notice -->
31664
+ <table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #fff3cd; border-radius: 4px; margin: 0 0 20px 0;">
31665
+ <tr>
31666
+ <td style="padding: 12px 16px;">
31667
+ <p style="margin: 0; font-size: 14px; line-height: 1.6; color: #856404;">
31668
+ <strong>⚠️ Important:</strong> This link will expire in <strong><%= expiresIn %></strong>.
31669
+ If you didn't create an account with us, you can safely ignore this email.
31670
+ </p>
31671
+ </td>
31672
+ </tr>
31673
+ </table>
31674
+
31675
+ <p style="margin: 0 0 20px 0; font-size: 16px; line-height: 1.6; color: #555555;">
31676
+ For security reasons, please do not share this link with anyone.
31677
+ </p>
31678
+ </td>
31679
+ </tr>
31680
+
31681
+ <!-- Footer -->
31682
+ <tr>
31683
+ <td style="padding: 30px 40px 40px 40px; border-top: 1px solid #eeeeee;">
31684
+ <p style="margin: 0 0 10px 0; font-size: 14px; line-height: 1.6; color: #999999; text-align: center;">
31685
+ Best regards,<br>
31686
+ The <%= appName %> Team
31687
+ </p>
31688
+
31689
+ <% if (supportEmail) { %>
31690
+ <p style="margin: 0 0 10px 0; font-size: 12px; line-height: 1.6; color: #999999; text-align: center;">
31691
+ Need help? Contact us at <a href="mailto:<%= supportEmail %>" style="color: #28a745; text-decoration: none;"><%= supportEmail %></a>
31692
+ </p>
31693
+ <% } %>
31694
+
31695
+ <p style="margin: 0; font-size: 12px; line-height: 1.6; color: #999999; text-align: center;">
31696
+ © <%= year %> <%= appName %>. All rights reserved.
31697
+ </p>
31698
+ </td>
31699
+ </tr>
31700
+ </table>
31701
+ </td>
31702
+ </tr>
31703
+ </table>
31704
+ </body>
31705
+ </html>
31706
+ `.trim(),
31707
+ text: `
31708
+ Verify Your Email Address
31709
+
31710
+ Hello<% if (user.firstName) { %> <%= user.firstName %><% } %>,
31711
+
31712
+ Thank you for signing up with <%= appName %>! Please verify your email address <%= user.email %> to complete your registration.
31713
+
31714
+ To verify your email, please visit the following link:
31715
+
31716
+ <%= verificationLink %>
31717
+
31718
+ This link will expire in <%= expiresIn %>.
31719
+
31720
+ If you didn't create an account with us, you can safely ignore this email.
31721
+
31722
+ For security reasons, please do not share this link with anyone.
31723
+
31724
+ Best regards,
31725
+ The <%= appName %> Team
31726
+
31727
+ <% if (supportEmail) { %>
31728
+ Need help? Contact us at <%= supportEmail %>
31729
+ <% } %>
31730
+
31278
31731
  © <%= year %> <%= appName %>. All rights reserved.
31279
31732
  `.trim()
31280
31733
  };
31281
31734
  const defaultTemplates = {
31282
31735
  passwordReset: passwordResetTemplate,
31283
31736
  magicLink: magicLinkTemplate,
31284
- passwordChanged: passwordChangedTemplate
31737
+ passwordChanged: passwordChangedTemplate,
31738
+ emailVerification: emailVerificationTemplate
31285
31739
  };
31286
31740
  class TemplateService {
31287
31741
  /**
@@ -31693,6 +32147,114 @@ class EmailService {
31693
32147
  message: "Password changed but confirmation email could not be sent"
31694
32148
  };
31695
32149
  }
32150
+ /**
32151
+ * Send email verification email with three-tier fallback system
32152
+ * Tier 1: Strapi Email Plugin (if configured)
32153
+ * Tier 2: Custom Hook Function (if provided in config)
32154
+ * Tier 3: Development Console Logging (dev mode only)
32155
+ */
32156
+ async sendVerificationEmail(user, verificationLink) {
32157
+ if (!user.email) {
32158
+ throw new ValidationError$1("User does not have an email address");
32159
+ }
32160
+ const variables = {
32161
+ user: {
32162
+ email: user.email,
32163
+ firstName: user.firstName || user.displayName?.split(" ")[0],
32164
+ lastName: user.lastName,
32165
+ displayName: user.displayName,
32166
+ phoneNumber: user.phoneNumber,
32167
+ uid: user.uid
32168
+ },
32169
+ verificationLink,
32170
+ expiresIn: "1 hour"
32171
+ };
32172
+ const settingsService2 = strapi.plugin("firebase-authentication").service("settingsService");
32173
+ const dbConfig = await settingsService2.getFirebaseConfigJson();
32174
+ const customSubject = dbConfig?.emailVerificationEmailSubject;
32175
+ const pluginConfig = strapi.config.get("plugin::firebase-authentication");
32176
+ const appConfig = pluginConfig?.app || {};
32177
+ const completeVariables = {
32178
+ ...variables,
32179
+ appName: appConfig?.name || "Your Application",
32180
+ appUrl: appConfig?.url || process.env.PUBLIC_URL || "http://localhost:3000",
32181
+ supportEmail: appConfig?.supportEmail,
32182
+ year: (/* @__PURE__ */ new Date()).getFullYear(),
32183
+ expiresIn: variables.expiresIn
32184
+ };
32185
+ const templateService2 = strapi.plugin("firebase-authentication").service("templateService");
32186
+ const template = await templateService2.getTemplate("emailVerification");
32187
+ const subjectTemplate = customSubject || template.subject;
32188
+ const compiledSubject = _$1.template(subjectTemplate)(completeVariables);
32189
+ try {
32190
+ const compiledHtml = template.html ? _$1.template(template.html)(completeVariables) : void 0;
32191
+ const compiledText = template.text ? _$1.template(template.text)(completeVariables) : void 0;
32192
+ const emailPlugin = strapi.plugin("email");
32193
+ if (!emailPlugin) {
32194
+ throw new Error("Email plugin not found");
32195
+ }
32196
+ const emailService2 = emailPlugin.service("email");
32197
+ await emailService2.send({
32198
+ to: user.email,
32199
+ subject: compiledSubject,
32200
+ html: compiledHtml,
32201
+ text: compiledText
32202
+ });
32203
+ strapi.log.info(`✅ Email verification sent via Strapi email plugin to ${user.email}`);
32204
+ return {
32205
+ success: true,
32206
+ message: `Verification email sent to ${user.email}`
32207
+ };
32208
+ } catch (tier1Error) {
32209
+ strapi.log.debug(`Strapi email plugin failed: ${tier1Error.message}. Trying fallback options...`);
32210
+ }
32211
+ const customSender = pluginConfig?.sendVerificationEmail;
32212
+ if (customSender && typeof customSender === "function") {
32213
+ try {
32214
+ const compiledHtml = template.html ? _$1.template(template.html)(completeVariables) : void 0;
32215
+ const compiledText = template.text ? _$1.template(template.text)(completeVariables) : void 0;
32216
+ await customSender({
32217
+ to: user.email,
32218
+ subject: compiledSubject,
32219
+ html: compiledHtml,
32220
+ text: compiledText,
32221
+ verificationLink,
32222
+ variables: completeVariables
32223
+ });
32224
+ strapi.log.info(`✅ Email verification sent via custom hook to ${user.email}`);
32225
+ return {
32226
+ success: true,
32227
+ message: `Verification email sent to ${user.email}`
32228
+ };
32229
+ } catch (tier2Error) {
32230
+ strapi.log.error(`Custom hook failed: ${tier2Error.message}. Continuing to next fallback...`);
32231
+ }
32232
+ }
32233
+ if (process.env.NODE_ENV !== "production") {
32234
+ try {
32235
+ strapi.log.info("\n" + "=".repeat(80));
32236
+ strapi.log.info("EMAIL VERIFICATION (Development Mode)");
32237
+ strapi.log.info("=".repeat(80));
32238
+ strapi.log.info(`To: ${user.email}`);
32239
+ strapi.log.info(`Subject: ${compiledSubject}`);
32240
+ strapi.log.info(`Verification Link: ${verificationLink}`);
32241
+ strapi.log.info(`Expires In: 1 hour`);
32242
+ strapi.log.info("=".repeat(80));
32243
+ strapi.log.info("Note: Email not sent - no email service configured");
32244
+ strapi.log.info("Copy the link above and open in your browser to verify");
32245
+ strapi.log.info("=".repeat(80) + "\n");
32246
+ return {
32247
+ success: true,
32248
+ message: "Verification link logged to console (development mode)"
32249
+ };
32250
+ } catch (tier3Error) {
32251
+ strapi.log.error(`Development fallback failed: ${tier3Error.message}`);
32252
+ }
32253
+ }
32254
+ throw new ApplicationError$2(
32255
+ "Email service is not configured. Please configure Strapi email plugin or provide custom sendVerificationEmail function in plugin config."
32256
+ );
32257
+ }
31696
32258
  }
31697
32259
  const emailService = ({ strapi: strapi2 }) => new EmailService();
31698
32260
  const firebaseUserDataService = ({ strapi: strapi2 }) => ({
@@ -35146,6 +35708,125 @@ const tokenService = ({ strapi: strapi2 }) => {
35146
35708
  }
35147
35709
  });
35148
35710
  strapi2.log.debug(`Invalidated reset token for user ${firebaseUserDataDocumentId}`);
35711
+ },
35712
+ // ==================== EMAIL VERIFICATION TOKENS ====================
35713
+ /**
35714
+ * Generate an email verification token for a user
35715
+ * @param firebaseUserDataDocumentId - The documentId of the firebase-user-data record
35716
+ * @param email - The email address at time of request (for change detection)
35717
+ * @returns The JWT token to include in the verification URL
35718
+ */
35719
+ async generateVerificationToken(firebaseUserDataDocumentId, email2) {
35720
+ const signingKey = getSigningKey();
35721
+ const jti = crypto__default.default.randomBytes(32).toString("hex");
35722
+ const payload = {
35723
+ sub: firebaseUserDataDocumentId,
35724
+ purpose: "email-verification",
35725
+ email: email2,
35726
+ jti
35727
+ };
35728
+ const token = jwt.sign(payload, signingKey, {
35729
+ expiresIn: "1h"
35730
+ });
35731
+ const tokenHash = crypto__default.default.createHash("sha256").update(jti).digest("hex");
35732
+ const expiresAt = new Date(Date.now() + 60 * 60 * 1e3);
35733
+ await strapi2.db.query(FIREBASE_USER_DATA_CONTENT_TYPE).update({
35734
+ where: { documentId: firebaseUserDataDocumentId },
35735
+ data: {
35736
+ verificationTokenHash: tokenHash,
35737
+ verificationTokenExpiresAt: expiresAt.toISOString()
35738
+ }
35739
+ });
35740
+ strapi2.log.debug(`Generated verification token for user ${firebaseUserDataDocumentId}`);
35741
+ return token;
35742
+ },
35743
+ /**
35744
+ * Validate an email verification token
35745
+ * @param token - The JWT token from the verification URL
35746
+ * @returns Validation result with user info and email if valid
35747
+ */
35748
+ async validateVerificationToken(token) {
35749
+ const signingKey = getSigningKey();
35750
+ try {
35751
+ const decoded = jwt.verify(token, signingKey);
35752
+ if (decoded.purpose !== "email-verification") {
35753
+ return {
35754
+ valid: false,
35755
+ firebaseUserDataDocumentId: "",
35756
+ firebaseUID: "",
35757
+ error: "Invalid token purpose"
35758
+ };
35759
+ }
35760
+ const tokenHash = crypto__default.default.createHash("sha256").update(decoded.jti).digest("hex");
35761
+ const firebaseUserData2 = await strapi2.db.query(FIREBASE_USER_DATA_CONTENT_TYPE).findOne({
35762
+ where: { documentId: decoded.sub }
35763
+ });
35764
+ if (!firebaseUserData2) {
35765
+ return {
35766
+ valid: false,
35767
+ firebaseUserDataDocumentId: "",
35768
+ firebaseUID: "",
35769
+ error: "User not found"
35770
+ };
35771
+ }
35772
+ if (firebaseUserData2.verificationTokenHash !== tokenHash) {
35773
+ return {
35774
+ valid: false,
35775
+ firebaseUserDataDocumentId: "",
35776
+ firebaseUID: "",
35777
+ error: "Verification link has already been used or is invalid"
35778
+ };
35779
+ }
35780
+ if (firebaseUserData2.verificationTokenExpiresAt) {
35781
+ const expiresAt = new Date(firebaseUserData2.verificationTokenExpiresAt);
35782
+ if (expiresAt < /* @__PURE__ */ new Date()) {
35783
+ return {
35784
+ valid: false,
35785
+ firebaseUserDataDocumentId: "",
35786
+ firebaseUID: "",
35787
+ error: "Verification link has expired"
35788
+ };
35789
+ }
35790
+ }
35791
+ return {
35792
+ valid: true,
35793
+ firebaseUserDataDocumentId: firebaseUserData2.documentId,
35794
+ firebaseUID: firebaseUserData2.firebaseUserID,
35795
+ email: decoded.email
35796
+ };
35797
+ } catch (error2) {
35798
+ if (error2.name === "TokenExpiredError") {
35799
+ return {
35800
+ valid: false,
35801
+ firebaseUserDataDocumentId: "",
35802
+ firebaseUID: "",
35803
+ error: "Verification link has expired"
35804
+ };
35805
+ }
35806
+ if (error2.name === "JsonWebTokenError") {
35807
+ return {
35808
+ valid: false,
35809
+ firebaseUserDataDocumentId: "",
35810
+ firebaseUID: "",
35811
+ error: "Invalid verification link"
35812
+ };
35813
+ }
35814
+ throw error2;
35815
+ }
35816
+ },
35817
+ /**
35818
+ * Invalidate a verification token after use
35819
+ * @param firebaseUserDataDocumentId - The documentId of the firebase-user-data record
35820
+ */
35821
+ async invalidateVerificationToken(firebaseUserDataDocumentId) {
35822
+ await strapi2.db.query(FIREBASE_USER_DATA_CONTENT_TYPE).update({
35823
+ where: { documentId: firebaseUserDataDocumentId },
35824
+ data: {
35825
+ verificationTokenHash: null,
35826
+ verificationTokenExpiresAt: null
35827
+ }
35828
+ });
35829
+ strapi2.log.debug(`Invalidated verification token for user ${firebaseUserDataDocumentId}`);
35149
35830
  }
35150
35831
  };
35151
35832
  };