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
@@ -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 = {
@@ -583,6 +599,61 @@ const firebaseController = {
583
599
  ctx.status = 500;
584
600
  ctx.body = { error: "An error occurred while resetting your password" };
585
601
  }
602
+ },
603
+ /**
604
+ * Send email verification - public endpoint
605
+ * POST /api/firebase-authentication/sendVerificationEmail
606
+ * Public endpoint - no authentication required
607
+ */
608
+ async sendVerificationEmail(ctx) {
609
+ strapi.log.debug("sendVerificationEmail endpoint called");
610
+ try {
611
+ const { email: email2 } = ctx.request.body || {};
612
+ ctx.body = await strapi.plugin(pluginName).service("firebaseService").sendVerificationEmail(email2);
613
+ } catch (error2) {
614
+ strapi.log.error("sendVerificationEmail controller error:", error2);
615
+ if (error2.name === "ValidationError") {
616
+ ctx.status = 400;
617
+ } else {
618
+ ctx.status = 500;
619
+ }
620
+ ctx.body = { error: error2.message };
621
+ }
622
+ },
623
+ /**
624
+ * Verify email using custom JWT token
625
+ * POST /api/firebase-authentication/verifyEmail
626
+ * Public endpoint - token provides authentication
627
+ *
628
+ * @param ctx - Koa context with { token } in body
629
+ * @returns { success: true, message: "Email verified successfully" }
630
+ */
631
+ async verifyEmail(ctx) {
632
+ strapi.log.debug("verifyEmail endpoint called");
633
+ try {
634
+ const { token } = ctx.request.body || {};
635
+ if (!token) {
636
+ ctx.status = 400;
637
+ ctx.body = { error: "Token is required" };
638
+ return;
639
+ }
640
+ const result = await strapi.plugin(pluginName).service("firebaseService").verifyEmail(token);
641
+ ctx.body = result;
642
+ } catch (error2) {
643
+ strapi.log.error("verifyEmail controller error:", error2);
644
+ if (error2.name === "ValidationError") {
645
+ ctx.status = 400;
646
+ ctx.body = { error: error2.message };
647
+ return;
648
+ }
649
+ if (error2.name === "NotFoundError") {
650
+ ctx.status = 404;
651
+ ctx.body = { error: error2.message };
652
+ return;
653
+ }
654
+ ctx.status = 500;
655
+ ctx.body = { error: "An error occurred while verifying your email" };
656
+ }
586
657
  }
587
658
  };
588
659
  var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {};
@@ -28950,6 +29021,17 @@ const userController = {
28950
29021
  } catch (error2) {
28951
29022
  throw new ApplicationError$2(error2.message || "Failed to send password reset email");
28952
29023
  }
29024
+ },
29025
+ sendVerificationEmail: async (ctx) => {
29026
+ const userId = ctx.params.id;
29027
+ if (!userId) {
29028
+ throw new ValidationError$1("User ID is required");
29029
+ }
29030
+ try {
29031
+ ctx.body = await strapi.plugin("firebase-authentication").service("userService").sendVerificationEmail(userId);
29032
+ } catch (error2) {
29033
+ throw new ApplicationError$2(error2.message || "Failed to send verification email");
29034
+ }
28953
29035
  }
28954
29036
  };
28955
29037
  const settingsController = {
@@ -29052,7 +29134,9 @@ const settingsController = {
29052
29134
  enableMagicLink = false,
29053
29135
  magicLinkUrl = "http://localhost:1338/verify-magic-link.html",
29054
29136
  magicLinkEmailSubject = "Sign in to Your Application",
29055
- magicLinkExpiryHours = 1
29137
+ magicLinkExpiryHours = 1,
29138
+ emailVerificationUrl = "http://localhost:3000/verify-email",
29139
+ emailVerificationEmailSubject = "Verify Your Email"
29056
29140
  } = requestBody;
29057
29141
  const existingConfig = await strapi.db.query("plugin::firebase-authentication.firebase-authentication-configuration").findOne({ where: {} });
29058
29142
  let result;
@@ -29066,7 +29150,9 @@ const settingsController = {
29066
29150
  enableMagicLink,
29067
29151
  magicLinkUrl,
29068
29152
  magicLinkEmailSubject,
29069
- magicLinkExpiryHours
29153
+ magicLinkExpiryHours,
29154
+ emailVerificationUrl,
29155
+ emailVerificationEmailSubject
29070
29156
  }
29071
29157
  });
29072
29158
  } else {
@@ -29080,7 +29166,9 @@ const settingsController = {
29080
29166
  enableMagicLink,
29081
29167
  magicLinkUrl,
29082
29168
  magicLinkEmailSubject,
29083
- magicLinkExpiryHours
29169
+ magicLinkExpiryHours,
29170
+ emailVerificationUrl,
29171
+ emailVerificationEmailSubject
29084
29172
  }
29085
29173
  });
29086
29174
  }
@@ -29092,7 +29180,9 @@ const settingsController = {
29092
29180
  enableMagicLink: result.enableMagicLink,
29093
29181
  magicLinkUrl: result.magicLinkUrl,
29094
29182
  magicLinkEmailSubject: result.magicLinkEmailSubject,
29095
- magicLinkExpiryHours: result.magicLinkExpiryHours
29183
+ magicLinkExpiryHours: result.magicLinkExpiryHours,
29184
+ emailVerificationUrl: result.emailVerificationUrl,
29185
+ emailVerificationEmailSubject: result.emailVerificationEmailSubject
29096
29186
  };
29097
29187
  } catch (error2) {
29098
29188
  throw new ApplicationError$2("Error saving password configuration", {
@@ -29185,6 +29275,14 @@ const admin = {
29185
29275
  policies: ["admin::isAuthenticatedAdmin"]
29186
29276
  }
29187
29277
  },
29278
+ {
29279
+ method: "PUT",
29280
+ path: "/users/sendVerificationEmail/:id",
29281
+ handler: "userController.sendVerificationEmail",
29282
+ config: {
29283
+ policies: ["admin::isAuthenticatedAdmin"]
29284
+ }
29285
+ },
29188
29286
  {
29189
29287
  method: "GET",
29190
29288
  path: "/users/:id",
@@ -29283,6 +29381,26 @@ const contentApi = {
29283
29381
  // Public endpoint - token provides authentication
29284
29382
  policies: []
29285
29383
  }
29384
+ },
29385
+ {
29386
+ method: "POST",
29387
+ path: "/sendVerificationEmail",
29388
+ handler: "firebaseController.sendVerificationEmail",
29389
+ config: {
29390
+ auth: false,
29391
+ // Public endpoint - sends email verification link
29392
+ policies: []
29393
+ }
29394
+ },
29395
+ {
29396
+ method: "POST",
29397
+ path: "/verifyEmail",
29398
+ handler: "firebaseController.verifyEmail",
29399
+ config: {
29400
+ auth: false,
29401
+ // Public endpoint - token provides authentication
29402
+ policies: []
29403
+ }
29286
29404
  }
29287
29405
  ]
29288
29406
  };
@@ -29403,7 +29521,10 @@ const settingsService = ({ strapi: strapi2 }) => {
29403
29521
  enableMagicLink: configObject.enableMagicLink || false,
29404
29522
  magicLinkUrl: configObject.magicLinkUrl || "http://localhost:1338/verify-magic-link.html",
29405
29523
  magicLinkEmailSubject: configObject.magicLinkEmailSubject || "Sign in to Your Application",
29406
- magicLinkExpiryHours: configObject.magicLinkExpiryHours || 1
29524
+ magicLinkExpiryHours: configObject.magicLinkExpiryHours || 1,
29525
+ // Include email verification configuration fields
29526
+ emailVerificationUrl: configObject.emailVerificationUrl || "http://localhost:3000/verify-email",
29527
+ emailVerificationEmailSubject: configObject.emailVerificationEmailSubject || "Verify Your Email"
29407
29528
  };
29408
29529
  } catch (error2) {
29409
29530
  strapi2.log.error(`Firebase config error: ${error2.message}`);
@@ -29446,7 +29567,9 @@ const settingsService = ({ strapi: strapi2 }) => {
29446
29567
  enableMagicLink = false,
29447
29568
  magicLinkUrl = "http://localhost:1338/verify-magic-link.html",
29448
29569
  magicLinkEmailSubject = "Sign in to Your Application",
29449
- magicLinkExpiryHours = 1
29570
+ magicLinkExpiryHours = 1,
29571
+ emailVerificationUrl = "http://localhost:3000/verify-email",
29572
+ emailVerificationEmailSubject = "Verify Your Email"
29450
29573
  } = requestBody;
29451
29574
  if (!requestBody) throw new ValidationError3(ERROR_MESSAGES.MISSING_DATA);
29452
29575
  try {
@@ -29482,7 +29605,9 @@ const settingsService = ({ strapi: strapi2 }) => {
29482
29605
  enableMagicLink,
29483
29606
  magicLinkUrl,
29484
29607
  magicLinkEmailSubject,
29485
- magicLinkExpiryHours
29608
+ magicLinkExpiryHours,
29609
+ emailVerificationUrl,
29610
+ emailVerificationEmailSubject
29486
29611
  }
29487
29612
  });
29488
29613
  } else {
@@ -29498,11 +29623,20 @@ const settingsService = ({ strapi: strapi2 }) => {
29498
29623
  enableMagicLink,
29499
29624
  magicLinkUrl,
29500
29625
  magicLinkEmailSubject,
29501
- magicLinkExpiryHours
29626
+ magicLinkExpiryHours,
29627
+ emailVerificationUrl,
29628
+ emailVerificationEmailSubject
29502
29629
  }
29503
29630
  });
29504
29631
  }
29505
29632
  await strapi2.plugin("firebase-authentication").service("settingsService").init();
29633
+ setImmediate(async () => {
29634
+ try {
29635
+ await strapi2.plugin("firebase-authentication").service("autoLinkService").linkAllUsers(strapi2);
29636
+ } catch (error2) {
29637
+ strapi2.log.error(`Auto-linking after config save failed: ${error2.message}`);
29638
+ }
29639
+ });
29506
29640
  const configData = res.firebaseConfigJson || res.firebase_config_json;
29507
29641
  if (!configData) {
29508
29642
  strapi2.log.error("Firebase config data missing from database response");
@@ -29526,6 +29660,8 @@ const settingsService = ({ strapi: strapi2 }) => {
29526
29660
  res.magicLinkUrl = res.magicLinkUrl || magicLinkUrl;
29527
29661
  res.magicLinkEmailSubject = res.magicLinkEmailSubject || magicLinkEmailSubject;
29528
29662
  res.magicLinkExpiryHours = res.magicLinkExpiryHours || magicLinkExpiryHours;
29663
+ res.emailVerificationUrl = res.emailVerificationUrl || emailVerificationUrl;
29664
+ res.emailVerificationEmailSubject = res.emailVerificationEmailSubject || emailVerificationEmailSubject;
29529
29665
  return res;
29530
29666
  } catch (error2) {
29531
29667
  strapi2.log.error("=== FIREBASE CONFIG SAVE ERROR ===");
@@ -30185,6 +30321,40 @@ const userService = ({ strapi: strapi2 }) => {
30185
30321
  }
30186
30322
  throw new ApplicationError$2(e.message?.toString() || "Failed to reset password");
30187
30323
  }
30324
+ },
30325
+ /**
30326
+ * Send email verification email (admin-initiated)
30327
+ * @param entityId - Firebase UID of the user
30328
+ */
30329
+ sendVerificationEmail: async (entityId) => {
30330
+ try {
30331
+ ensureFirebaseInitialized();
30332
+ const user = await strapi2.firebase.auth().getUser(entityId);
30333
+ if (!user.email) {
30334
+ throw new ApplicationError$2("User does not have an email address");
30335
+ }
30336
+ if (user.emailVerified) {
30337
+ return { success: true, message: "Email is already verified" };
30338
+ }
30339
+ const config2 = await strapi2.db.query("plugin::firebase-authentication.firebase-authentication-configuration").findOne({ where: {} });
30340
+ const emailVerificationUrl = config2?.emailVerificationUrl;
30341
+ if (!emailVerificationUrl) {
30342
+ throw new ApplicationError$2("Email verification URL is not configured");
30343
+ }
30344
+ const firebaseUserData2 = await strapi2.plugin("firebase-authentication").service("firebaseUserDataService").getByFirebaseUID(entityId);
30345
+ if (!firebaseUserData2) {
30346
+ throw new ApplicationError$2("User is not linked to Firebase authentication");
30347
+ }
30348
+ const tokenService2 = strapi2.plugin("firebase-authentication").service("tokenService");
30349
+ const token = await tokenService2.generateVerificationToken(firebaseUserData2.documentId, user.email);
30350
+ const verificationLink = `${emailVerificationUrl}?token=${token}`;
30351
+ strapi2.log.debug(`Generated email verification link for user ${user.email}`);
30352
+ const emailService2 = strapi2.plugin("firebase-authentication").service("emailService");
30353
+ return await emailService2.sendVerificationEmail(user, verificationLink);
30354
+ } catch (e) {
30355
+ strapi2.log.error(`sendVerificationEmail error: ${e.message}`);
30356
+ throw new ApplicationError$2(e.message?.toString() || "Failed to send verification email");
30357
+ }
30188
30358
  }
30189
30359
  };
30190
30360
  };
@@ -30864,6 +31034,159 @@ const firebaseService = ({ strapi: strapi2 }) => ({
30864
31034
  verificationUrl: magicLinkUrl
30865
31035
  };
30866
31036
  }
31037
+ },
31038
+ /**
31039
+ * Send email verification - public endpoint
31040
+ * Generates a verification token and sends an email to the user
31041
+ * Security: Always returns generic success message to prevent email enumeration
31042
+ */
31043
+ async sendVerificationEmail(email2) {
31044
+ strapi2.log.info(`[sendVerificationEmail] Starting email verification for: ${email2}`);
31045
+ if (!email2) {
31046
+ throw new ValidationError$1("Email is required");
31047
+ }
31048
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
31049
+ if (!emailRegex.test(email2)) {
31050
+ throw new ValidationError$1("Invalid email format");
31051
+ }
31052
+ const config2 = await strapi2.plugin("firebase-authentication").service("settingsService").getFirebaseConfigJson();
31053
+ const verificationUrl = config2?.emailVerificationUrl;
31054
+ if (!verificationUrl) {
31055
+ throw new ApplicationError$2("Email verification URL is not configured");
31056
+ }
31057
+ if (process.env.NODE_ENV === "production" && !verificationUrl.startsWith("https://")) {
31058
+ throw new ApplicationError$2("Email verification URL must use HTTPS in production");
31059
+ }
31060
+ try {
31061
+ new URL(verificationUrl);
31062
+ } catch (error2) {
31063
+ throw new ApplicationError$2("Email verification URL is not a valid URL format");
31064
+ }
31065
+ try {
31066
+ let firebaseUser;
31067
+ try {
31068
+ firebaseUser = await strapi2.firebase.auth().getUserByEmail(email2);
31069
+ } catch (fbError) {
31070
+ strapi2.log.debug("User not found in Firebase");
31071
+ }
31072
+ if (!firebaseUser) {
31073
+ strapi2.log.warn(`⚠️ [sendVerificationEmail] User not found in Firebase for email: ${email2}`);
31074
+ return { message: "If an account with that email exists, a verification link has been sent." };
31075
+ }
31076
+ if (firebaseUser.emailVerified) {
31077
+ strapi2.log.info(`[sendVerificationEmail] User ${email2} is already verified`);
31078
+ return { message: "Email is already verified." };
31079
+ }
31080
+ const firebaseData = await strapi2.plugin("firebase-authentication").service("firebaseUserDataService").getByFirebaseUID(firebaseUser.uid);
31081
+ if (!firebaseData) {
31082
+ strapi2.log.warn(`⚠️ [sendVerificationEmail] No firebase-user-data record for: ${email2}`);
31083
+ return { message: "If an account with that email exists, a verification link has been sent." };
31084
+ }
31085
+ strapi2.log.info(
31086
+ `✅ [sendVerificationEmail] User found: ${JSON.stringify({
31087
+ firebaseUID: firebaseUser.uid,
31088
+ email: firebaseUser.email,
31089
+ emailVerified: firebaseUser.emailVerified
31090
+ })}`
31091
+ );
31092
+ const tokenService2 = strapi2.plugin("firebase-authentication").service("tokenService");
31093
+ const token = await tokenService2.generateVerificationToken(firebaseData.documentId, email2);
31094
+ const verificationLink = `${verificationUrl}?token=${token}`;
31095
+ strapi2.log.info(`✅ [sendVerificationEmail] Verification link generated for ${email2}`);
31096
+ strapi2.log.info(`[sendVerificationEmail] Attempting to send verification email to: ${email2}`);
31097
+ await strapi2.plugin("firebase-authentication").service("emailService").sendVerificationEmail(firebaseUser, verificationLink);
31098
+ strapi2.log.info(`✅ [sendVerificationEmail] Verification email sent successfully to: ${email2}`);
31099
+ return {
31100
+ message: "If an account with that email exists, a verification link has been sent."
31101
+ };
31102
+ } catch (error2) {
31103
+ strapi2.log.error(
31104
+ `❌ [sendVerificationEmail] ERROR: ${JSON.stringify({
31105
+ email: email2,
31106
+ message: error2.message,
31107
+ code: error2.code,
31108
+ name: error2.name,
31109
+ stack: error2.stack
31110
+ })}`
31111
+ );
31112
+ return {
31113
+ message: "If an account with that email exists, a verification link has been sent."
31114
+ };
31115
+ }
31116
+ },
31117
+ /**
31118
+ * Verify email with token - public endpoint
31119
+ * Validates the token and marks the user's email as verified in Firebase
31120
+ */
31121
+ async verifyEmail(token) {
31122
+ strapi2.log.info(`[verifyEmail] Starting email verification with token`);
31123
+ if (!token) {
31124
+ throw new ValidationError$1("Verification token is required");
31125
+ }
31126
+ const tokenService2 = strapi2.plugin("firebase-authentication").service("tokenService");
31127
+ const validationResult = await tokenService2.validateVerificationToken(token);
31128
+ if (!validationResult.valid) {
31129
+ strapi2.log.warn(`[verifyEmail] Token validation failed: ${validationResult.error}`);
31130
+ throw new ValidationError$1(validationResult.error || "Invalid verification link");
31131
+ }
31132
+ const { firebaseUID, firebaseUserDataDocumentId, email: tokenEmail } = validationResult;
31133
+ try {
31134
+ const firebaseUser = await strapi2.firebase.auth().getUser(firebaseUID);
31135
+ if (tokenEmail && firebaseUser.email !== tokenEmail) {
31136
+ strapi2.log.warn(
31137
+ `[verifyEmail] Email changed: token email ${tokenEmail} != current email ${firebaseUser.email}`
31138
+ );
31139
+ await tokenService2.invalidateVerificationToken(firebaseUserDataDocumentId);
31140
+ throw new ValidationError$1(
31141
+ "Email address has changed since verification was requested. Please request a new verification link."
31142
+ );
31143
+ }
31144
+ if (firebaseUser.emailVerified) {
31145
+ strapi2.log.info(`[verifyEmail] User ${firebaseUser.email} is already verified`);
31146
+ await tokenService2.invalidateVerificationToken(firebaseUserDataDocumentId);
31147
+ return {
31148
+ success: true,
31149
+ message: "Email is already verified."
31150
+ };
31151
+ }
31152
+ await strapi2.firebase.auth().updateUser(firebaseUID, {
31153
+ emailVerified: true
31154
+ });
31155
+ try {
31156
+ const firebaseUserDataService2 = strapi2.plugin("firebase-authentication").service("firebaseUserDataService");
31157
+ const firebaseUserData2 = await firebaseUserDataService2.getByFirebaseUID(firebaseUID);
31158
+ if (firebaseUserData2?.user?.documentId) {
31159
+ await strapi2.db.query("plugin::users-permissions.user").update({
31160
+ where: { documentId: firebaseUserData2.user.documentId },
31161
+ data: { confirmed: true }
31162
+ });
31163
+ strapi2.log.info(`✅ [verifyEmail] Strapi user confirmed for: ${firebaseUserData2.user.documentId}`);
31164
+ }
31165
+ } catch (strapiUpdateError) {
31166
+ strapi2.log.warn(
31167
+ `[verifyEmail] Failed to update Strapi user confirmed status: ${strapiUpdateError.message}`
31168
+ );
31169
+ }
31170
+ strapi2.log.info(`✅ [verifyEmail] Email verified successfully for: ${firebaseUser.email}`);
31171
+ await tokenService2.invalidateVerificationToken(firebaseUserDataDocumentId);
31172
+ return {
31173
+ success: true,
31174
+ message: "Email verified successfully."
31175
+ };
31176
+ } catch (error2) {
31177
+ strapi2.log.error(
31178
+ `❌ [verifyEmail] ERROR: ${JSON.stringify({
31179
+ firebaseUID,
31180
+ message: error2.message,
31181
+ code: error2.code,
31182
+ name: error2.name
31183
+ })}`
31184
+ );
31185
+ if (error2 instanceof ValidationError$1) {
31186
+ throw error2;
31187
+ }
31188
+ throw new ApplicationError$2("Failed to verify email. Please try again.");
31189
+ }
30867
31190
  }
30868
31191
  });
30869
31192
  const passwordResetTemplate = {
@@ -31243,13 +31566,144 @@ The <%= appName %> Team
31243
31566
  Need help? Contact us at <%= supportEmail %>
31244
31567
  <% } %>
31245
31568
 
31569
+ © <%= year %> <%= appName %>. All rights reserved.
31570
+ `.trim()
31571
+ };
31572
+ const emailVerificationTemplate = {
31573
+ subject: "Verify Your Email - <%= appName %>",
31574
+ html: `
31575
+ <!DOCTYPE html>
31576
+ <html lang="en">
31577
+ <head>
31578
+ <meta charset="UTF-8">
31579
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
31580
+ <title>Verify Your Email</title>
31581
+ </head>
31582
+ <body style="margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f4f4;">
31583
+ <table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;">
31584
+ <tr>
31585
+ <td align="center" style="padding: 40px 0;">
31586
+ <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);">
31587
+ <!-- Header -->
31588
+ <tr>
31589
+ <td style="padding: 40px 40px 20px 40px; text-align: center;">
31590
+ <h1 style="margin: 0; font-size: 28px; color: #333333; font-weight: 600;">
31591
+ Verify Your Email Address
31592
+ </h1>
31593
+ </td>
31594
+ </tr>
31595
+
31596
+ <!-- Content -->
31597
+ <tr>
31598
+ <td style="padding: 20px 40px;">
31599
+ <p style="margin: 0 0 20px 0; font-size: 16px; line-height: 1.6; color: #555555;">
31600
+ Hello<% if (user.firstName) { %> <%= user.firstName %><% } %>,
31601
+ </p>
31602
+
31603
+ <p style="margin: 0 0 20px 0; font-size: 16px; line-height: 1.6; color: #555555;">
31604
+ Thank you for signing up with <strong><%= appName %></strong>! Please verify your email address <strong><%= user.email %></strong> to complete your registration.
31605
+ </p>
31606
+
31607
+ <p style="margin: 0 0 30px 0; font-size: 16px; line-height: 1.6; color: #555555;">
31608
+ Click the button below to verify your email:
31609
+ </p>
31610
+
31611
+ <!-- CTA Button -->
31612
+ <table role="presentation" style="width: 100%; border-collapse: collapse;">
31613
+ <tr>
31614
+ <td align="center" style="padding: 0 0 30px 0;">
31615
+ <a href="<%= verificationLink %>"
31616
+ 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;">
31617
+ Verify Email Address
31618
+ </a>
31619
+ </td>
31620
+ </tr>
31621
+ </table>
31622
+
31623
+ <p style="margin: 0 0 20px 0; font-size: 14px; line-height: 1.6; color: #777777;">
31624
+ Or copy and paste this link into your browser:
31625
+ </p>
31626
+
31627
+ <p style="margin: 0 0 30px 0; font-size: 14px; line-height: 1.6; word-break: break-all; color: #28a745;">
31628
+ <%= verificationLink %>
31629
+ </p>
31630
+
31631
+ <!-- Security Notice -->
31632
+ <table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #fff3cd; border-radius: 4px; margin: 0 0 20px 0;">
31633
+ <tr>
31634
+ <td style="padding: 12px 16px;">
31635
+ <p style="margin: 0; font-size: 14px; line-height: 1.6; color: #856404;">
31636
+ <strong>⚠️ Important:</strong> This link will expire in <strong><%= expiresIn %></strong>.
31637
+ If you didn't create an account with us, you can safely ignore this email.
31638
+ </p>
31639
+ </td>
31640
+ </tr>
31641
+ </table>
31642
+
31643
+ <p style="margin: 0 0 20px 0; font-size: 16px; line-height: 1.6; color: #555555;">
31644
+ For security reasons, please do not share this link with anyone.
31645
+ </p>
31646
+ </td>
31647
+ </tr>
31648
+
31649
+ <!-- Footer -->
31650
+ <tr>
31651
+ <td style="padding: 30px 40px 40px 40px; border-top: 1px solid #eeeeee;">
31652
+ <p style="margin: 0 0 10px 0; font-size: 14px; line-height: 1.6; color: #999999; text-align: center;">
31653
+ Best regards,<br>
31654
+ The <%= appName %> Team
31655
+ </p>
31656
+
31657
+ <% if (supportEmail) { %>
31658
+ <p style="margin: 0 0 10px 0; font-size: 12px; line-height: 1.6; color: #999999; text-align: center;">
31659
+ Need help? Contact us at <a href="mailto:<%= supportEmail %>" style="color: #28a745; text-decoration: none;"><%= supportEmail %></a>
31660
+ </p>
31661
+ <% } %>
31662
+
31663
+ <p style="margin: 0; font-size: 12px; line-height: 1.6; color: #999999; text-align: center;">
31664
+ © <%= year %> <%= appName %>. All rights reserved.
31665
+ </p>
31666
+ </td>
31667
+ </tr>
31668
+ </table>
31669
+ </td>
31670
+ </tr>
31671
+ </table>
31672
+ </body>
31673
+ </html>
31674
+ `.trim(),
31675
+ text: `
31676
+ Verify Your Email Address
31677
+
31678
+ Hello<% if (user.firstName) { %> <%= user.firstName %><% } %>,
31679
+
31680
+ Thank you for signing up with <%= appName %>! Please verify your email address <%= user.email %> to complete your registration.
31681
+
31682
+ To verify your email, please visit the following link:
31683
+
31684
+ <%= verificationLink %>
31685
+
31686
+ This link will expire in <%= expiresIn %>.
31687
+
31688
+ If you didn't create an account with us, you can safely ignore this email.
31689
+
31690
+ For security reasons, please do not share this link with anyone.
31691
+
31692
+ Best regards,
31693
+ The <%= appName %> Team
31694
+
31695
+ <% if (supportEmail) { %>
31696
+ Need help? Contact us at <%= supportEmail %>
31697
+ <% } %>
31698
+
31246
31699
  © <%= year %> <%= appName %>. All rights reserved.
31247
31700
  `.trim()
31248
31701
  };
31249
31702
  const defaultTemplates = {
31250
31703
  passwordReset: passwordResetTemplate,
31251
31704
  magicLink: magicLinkTemplate,
31252
- passwordChanged: passwordChangedTemplate
31705
+ passwordChanged: passwordChangedTemplate,
31706
+ emailVerification: emailVerificationTemplate
31253
31707
  };
31254
31708
  class TemplateService {
31255
31709
  /**
@@ -31661,6 +32115,114 @@ class EmailService {
31661
32115
  message: "Password changed but confirmation email could not be sent"
31662
32116
  };
31663
32117
  }
32118
+ /**
32119
+ * Send email verification email with three-tier fallback system
32120
+ * Tier 1: Strapi Email Plugin (if configured)
32121
+ * Tier 2: Custom Hook Function (if provided in config)
32122
+ * Tier 3: Development Console Logging (dev mode only)
32123
+ */
32124
+ async sendVerificationEmail(user, verificationLink) {
32125
+ if (!user.email) {
32126
+ throw new ValidationError$1("User does not have an email address");
32127
+ }
32128
+ const variables = {
32129
+ user: {
32130
+ email: user.email,
32131
+ firstName: user.firstName || user.displayName?.split(" ")[0],
32132
+ lastName: user.lastName,
32133
+ displayName: user.displayName,
32134
+ phoneNumber: user.phoneNumber,
32135
+ uid: user.uid
32136
+ },
32137
+ verificationLink,
32138
+ expiresIn: "1 hour"
32139
+ };
32140
+ const settingsService2 = strapi.plugin("firebase-authentication").service("settingsService");
32141
+ const dbConfig = await settingsService2.getFirebaseConfigJson();
32142
+ const customSubject = dbConfig?.emailVerificationEmailSubject;
32143
+ const pluginConfig = strapi.config.get("plugin::firebase-authentication");
32144
+ const appConfig = pluginConfig?.app || {};
32145
+ const completeVariables = {
32146
+ ...variables,
32147
+ appName: appConfig?.name || "Your Application",
32148
+ appUrl: appConfig?.url || process.env.PUBLIC_URL || "http://localhost:3000",
32149
+ supportEmail: appConfig?.supportEmail,
32150
+ year: (/* @__PURE__ */ new Date()).getFullYear(),
32151
+ expiresIn: variables.expiresIn
32152
+ };
32153
+ const templateService2 = strapi.plugin("firebase-authentication").service("templateService");
32154
+ const template = await templateService2.getTemplate("emailVerification");
32155
+ const subjectTemplate = customSubject || template.subject;
32156
+ const compiledSubject = _$1.template(subjectTemplate)(completeVariables);
32157
+ try {
32158
+ const compiledHtml = template.html ? _$1.template(template.html)(completeVariables) : void 0;
32159
+ const compiledText = template.text ? _$1.template(template.text)(completeVariables) : void 0;
32160
+ const emailPlugin = strapi.plugin("email");
32161
+ if (!emailPlugin) {
32162
+ throw new Error("Email plugin not found");
32163
+ }
32164
+ const emailService2 = emailPlugin.service("email");
32165
+ await emailService2.send({
32166
+ to: user.email,
32167
+ subject: compiledSubject,
32168
+ html: compiledHtml,
32169
+ text: compiledText
32170
+ });
32171
+ strapi.log.info(`✅ Email verification sent via Strapi email plugin to ${user.email}`);
32172
+ return {
32173
+ success: true,
32174
+ message: `Verification email sent to ${user.email}`
32175
+ };
32176
+ } catch (tier1Error) {
32177
+ strapi.log.debug(`Strapi email plugin failed: ${tier1Error.message}. Trying fallback options...`);
32178
+ }
32179
+ const customSender = pluginConfig?.sendVerificationEmail;
32180
+ if (customSender && typeof customSender === "function") {
32181
+ try {
32182
+ const compiledHtml = template.html ? _$1.template(template.html)(completeVariables) : void 0;
32183
+ const compiledText = template.text ? _$1.template(template.text)(completeVariables) : void 0;
32184
+ await customSender({
32185
+ to: user.email,
32186
+ subject: compiledSubject,
32187
+ html: compiledHtml,
32188
+ text: compiledText,
32189
+ verificationLink,
32190
+ variables: completeVariables
32191
+ });
32192
+ strapi.log.info(`✅ Email verification sent via custom hook to ${user.email}`);
32193
+ return {
32194
+ success: true,
32195
+ message: `Verification email sent to ${user.email}`
32196
+ };
32197
+ } catch (tier2Error) {
32198
+ strapi.log.error(`Custom hook failed: ${tier2Error.message}. Continuing to next fallback...`);
32199
+ }
32200
+ }
32201
+ if (process.env.NODE_ENV !== "production") {
32202
+ try {
32203
+ strapi.log.info("\n" + "=".repeat(80));
32204
+ strapi.log.info("EMAIL VERIFICATION (Development Mode)");
32205
+ strapi.log.info("=".repeat(80));
32206
+ strapi.log.info(`To: ${user.email}`);
32207
+ strapi.log.info(`Subject: ${compiledSubject}`);
32208
+ strapi.log.info(`Verification Link: ${verificationLink}`);
32209
+ strapi.log.info(`Expires In: 1 hour`);
32210
+ strapi.log.info("=".repeat(80));
32211
+ strapi.log.info("Note: Email not sent - no email service configured");
32212
+ strapi.log.info("Copy the link above and open in your browser to verify");
32213
+ strapi.log.info("=".repeat(80) + "\n");
32214
+ return {
32215
+ success: true,
32216
+ message: "Verification link logged to console (development mode)"
32217
+ };
32218
+ } catch (tier3Error) {
32219
+ strapi.log.error(`Development fallback failed: ${tier3Error.message}`);
32220
+ }
32221
+ }
32222
+ throw new ApplicationError$2(
32223
+ "Email service is not configured. Please configure Strapi email plugin or provide custom sendVerificationEmail function in plugin config."
32224
+ );
32225
+ }
31664
32226
  }
31665
32227
  const emailService = ({ strapi: strapi2 }) => new EmailService();
31666
32228
  const firebaseUserDataService = ({ strapi: strapi2 }) => ({
@@ -35114,6 +35676,125 @@ const tokenService = ({ strapi: strapi2 }) => {
35114
35676
  }
35115
35677
  });
35116
35678
  strapi2.log.debug(`Invalidated reset token for user ${firebaseUserDataDocumentId}`);
35679
+ },
35680
+ // ==================== EMAIL VERIFICATION TOKENS ====================
35681
+ /**
35682
+ * Generate an email verification token for a user
35683
+ * @param firebaseUserDataDocumentId - The documentId of the firebase-user-data record
35684
+ * @param email - The email address at time of request (for change detection)
35685
+ * @returns The JWT token to include in the verification URL
35686
+ */
35687
+ async generateVerificationToken(firebaseUserDataDocumentId, email2) {
35688
+ const signingKey = getSigningKey();
35689
+ const jti = crypto$1.randomBytes(32).toString("hex");
35690
+ const payload = {
35691
+ sub: firebaseUserDataDocumentId,
35692
+ purpose: "email-verification",
35693
+ email: email2,
35694
+ jti
35695
+ };
35696
+ const token = jwt.sign(payload, signingKey, {
35697
+ expiresIn: "1h"
35698
+ });
35699
+ const tokenHash = crypto$1.createHash("sha256").update(jti).digest("hex");
35700
+ const expiresAt = new Date(Date.now() + 60 * 60 * 1e3);
35701
+ await strapi2.db.query(FIREBASE_USER_DATA_CONTENT_TYPE).update({
35702
+ where: { documentId: firebaseUserDataDocumentId },
35703
+ data: {
35704
+ verificationTokenHash: tokenHash,
35705
+ verificationTokenExpiresAt: expiresAt.toISOString()
35706
+ }
35707
+ });
35708
+ strapi2.log.debug(`Generated verification token for user ${firebaseUserDataDocumentId}`);
35709
+ return token;
35710
+ },
35711
+ /**
35712
+ * Validate an email verification token
35713
+ * @param token - The JWT token from the verification URL
35714
+ * @returns Validation result with user info and email if valid
35715
+ */
35716
+ async validateVerificationToken(token) {
35717
+ const signingKey = getSigningKey();
35718
+ try {
35719
+ const decoded = jwt.verify(token, signingKey);
35720
+ if (decoded.purpose !== "email-verification") {
35721
+ return {
35722
+ valid: false,
35723
+ firebaseUserDataDocumentId: "",
35724
+ firebaseUID: "",
35725
+ error: "Invalid token purpose"
35726
+ };
35727
+ }
35728
+ const tokenHash = crypto$1.createHash("sha256").update(decoded.jti).digest("hex");
35729
+ const firebaseUserData2 = await strapi2.db.query(FIREBASE_USER_DATA_CONTENT_TYPE).findOne({
35730
+ where: { documentId: decoded.sub }
35731
+ });
35732
+ if (!firebaseUserData2) {
35733
+ return {
35734
+ valid: false,
35735
+ firebaseUserDataDocumentId: "",
35736
+ firebaseUID: "",
35737
+ error: "User not found"
35738
+ };
35739
+ }
35740
+ if (firebaseUserData2.verificationTokenHash !== tokenHash) {
35741
+ return {
35742
+ valid: false,
35743
+ firebaseUserDataDocumentId: "",
35744
+ firebaseUID: "",
35745
+ error: "Verification link has already been used or is invalid"
35746
+ };
35747
+ }
35748
+ if (firebaseUserData2.verificationTokenExpiresAt) {
35749
+ const expiresAt = new Date(firebaseUserData2.verificationTokenExpiresAt);
35750
+ if (expiresAt < /* @__PURE__ */ new Date()) {
35751
+ return {
35752
+ valid: false,
35753
+ firebaseUserDataDocumentId: "",
35754
+ firebaseUID: "",
35755
+ error: "Verification link has expired"
35756
+ };
35757
+ }
35758
+ }
35759
+ return {
35760
+ valid: true,
35761
+ firebaseUserDataDocumentId: firebaseUserData2.documentId,
35762
+ firebaseUID: firebaseUserData2.firebaseUserID,
35763
+ email: decoded.email
35764
+ };
35765
+ } catch (error2) {
35766
+ if (error2.name === "TokenExpiredError") {
35767
+ return {
35768
+ valid: false,
35769
+ firebaseUserDataDocumentId: "",
35770
+ firebaseUID: "",
35771
+ error: "Verification link has expired"
35772
+ };
35773
+ }
35774
+ if (error2.name === "JsonWebTokenError") {
35775
+ return {
35776
+ valid: false,
35777
+ firebaseUserDataDocumentId: "",
35778
+ firebaseUID: "",
35779
+ error: "Invalid verification link"
35780
+ };
35781
+ }
35782
+ throw error2;
35783
+ }
35784
+ },
35785
+ /**
35786
+ * Invalidate a verification token after use
35787
+ * @param firebaseUserDataDocumentId - The documentId of the firebase-user-data record
35788
+ */
35789
+ async invalidateVerificationToken(firebaseUserDataDocumentId) {
35790
+ await strapi2.db.query(FIREBASE_USER_DATA_CONTENT_TYPE).update({
35791
+ where: { documentId: firebaseUserDataDocumentId },
35792
+ data: {
35793
+ verificationTokenHash: null,
35794
+ verificationTokenExpiresAt: null
35795
+ }
35796
+ });
35797
+ strapi2.log.debug(`Invalidated verification token for user ${firebaseUserDataDocumentId}`);
35117
35798
  }
35118
35799
  };
35119
35800
  };