Skip to content

Commit 2debd61

Browse files
feat: add resend verification email functionality with cooldown to account verification flow
1 parent d4016f2 commit 2debd61

8 files changed

Lines changed: 115 additions & 9 deletions

File tree

src/public/components/verify-account.js

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
11
import { ConfigurationContext } from "../context/configuration.js";
2-
import { errorTextTimeout, getPlaceholder, useTitle } from "../utils/utils.js";
2+
import { getPlaceholder, useTitle } from "../utils/utils.js";
33
import { get } from "../utils/api.js";
44

5+
const RESEND_COOLDOWN = 30;
6+
57
export default function VerifyAccount() {
68
const submitButtonText = i18next.t("button.verify");
79

810
const configuration = React.useContext(ConfigurationContext);
911

1012
const [errorMessage, setErrorMessage] = React.useState("");
11-
// hasError removed
1213
const [submitting, setSubmitting] = React.useState(false);
14+
const [countdown, setCountdown] = React.useState(RESEND_COOLDOWN);
15+
const [resendStatus, setResendStatus] = React.useState("");
1316

1417
React.useEffect(() => useTitle(configuration["content.app-name"], i18next.t("title.verify-account")), []);
1518

19+
// Start countdown on mount
20+
React.useEffect(() => {
21+
if (countdown <= 0) return;
22+
const timer = setTimeout(() => setCountdown((c) => c - 1), 1000);
23+
return () => clearTimeout(timer);
24+
}, [countdown]);
25+
1626
const onSubmitError = (props) => {
1727
setErrorMessage(props.errorText);
1828
};
@@ -30,13 +40,31 @@ export default function VerifyAccount() {
3040
if (result.ok) {
3141
window.location = `/login${window.location.search}`;
3242
} else {
33-
onSubmitError({ errorText: "Invalid Code" });
43+
onSubmitError({ errorText: i18next.t("error.invalid-code") });
3444
}
3545
} finally {
3646
setSubmitting(false);
3747
}
3848
}
3949

50+
async function resendCode(event) {
51+
event.preventDefault();
52+
setResendStatus("");
53+
const urlParams = new URLSearchParams(window.location.search);
54+
const target = urlParams.get("target");
55+
try {
56+
const result = await get("/user/resend-verification", { target });
57+
if (result.ok) {
58+
setResendStatus(i18next.t("message.verification-resent"));
59+
} else {
60+
setResendStatus(i18next.t("error.resend-failed"));
61+
}
62+
} catch {
63+
setResendStatus(i18next.t("error.resend-failed"));
64+
}
65+
setCountdown(RESEND_COOLDOWN);
66+
}
67+
4068
if (!configuration["user.account-creation.require-email-verification"]) {
4169
return null;
4270
}
@@ -68,8 +96,17 @@ export default function VerifyAccount() {
6896
required
6997
/>
7098
</div>
71-
<div className="page-links"></div>
99+
<div className="page-links">
100+
{countdown > 0 ? (
101+
<span className="resend-countdown">{i18next.t("message.resend-code-in", { seconds: countdown })}</span>
102+
) : (
103+
<a href="#" className="page-link" onClick={resendCode}>
104+
{i18next.t("link.resend-code")}
105+
</a>
106+
)}
107+
</div>
72108
<input type="submit" disabled={submitting} className="button" value={submitButtonText} />
109+
{resendStatus && <div className="form-success-message">{resendStatus}</div>}
73110
<div className="form-error-message">{errorMessage}</div>
74111
</form>
75112
);

src/public/css/form.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@
109109
border-radius: var(--form-input-border-radius);
110110
}
111111

112+
.form-success-message {
113+
color: var(--text-color);
114+
width: 100%;
115+
text-align: center;
116+
margin-top: 1rem;
117+
}
118+
112119
input:-webkit-autofill,
113120
input:-webkit-autofill:focus {
114121
transition:

src/public/languages/en.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
"message.consent": "{{app_name}} wants access to your account. If you consent to this, {{app_name}} will be able to:",
3434
"message.consent-warning": "Before you consent, make sure you trust this application as you might be sharing sensitive information.",
3535
"message.verification-instructions": "A verification code was sent to your email address.",
36+
"message.resend-code-in": "Resend code in {{seconds}}s",
37+
"message.verification-resent": "A new verification code has been sent to your email.",
3638
"message.recover-instructions": "Enter your email address to recover your account.",
3739
"message.reset-password-instructions": "A verification code was sent to your email address.",
3840
"message.page-not-found": "This page could not be found",
@@ -81,5 +83,8 @@
8183
"link.create-account": "Create Account",
8284
"link.forgot-password": "Forgot Password?",
8385
"link.login": "Have an account? Login",
84-
"link.login-minimal": "Login"
86+
"link.login-minimal": "Login",
87+
"link.resend-code": "Resend Code",
88+
89+
"error.resend-failed": "Failed to resend the verification email. Please try again."
8590
}

src/public/languages/es.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
"message.consent": "{{app_name}} quiere acceder a tu cuenta. Si aceptas los términos, {{app_name}} podrá:",
3434
"message.consent-warning": "Antes de consentir, asegúrate de que confías en esta aplicación ya que podrías estar compartiendo información sensible.",
3535
"message.verification-instructions": "Un código de verificación ha sido enviado a tu dirección de correo electrónico.",
36+
"message.resend-code-in": "Reenviar código en {{seconds}}s",
37+
"message.verification-resent": "Se ha enviado un nuevo código de verificación a tu correo electrónico.",
3638
"message.recover-instructions": "Introduce tu dirección de correo electrónico para recuperar tu cuenta.",
3739
"message.reset-password-instructions": "Un código de verificación ha sido enviado a tu dirección de correo electrónico.",
3840
"message.page-not-found": "No se pudo encontrar esta página",
@@ -81,5 +83,8 @@
8183
"link.create-account": "Crear cuenta",
8284
"link.forgot-password": "¿Olvidaste tu contraseña?",
8385
"link.login": "¿Tienes una cuenta? Inicia sesión",
84-
"link.login-minimal": "Iniciar sesión"
86+
"link.login-minimal": "Iniciar sesión",
87+
"link.resend-code": "Reenviar código",
88+
89+
"error.resend-failed": "Error al reenviar el correo de verificación. Por favor inténtalo de nuevo."
8590
}

src/public/languages/fr.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
"message.consent": "{{app_name}} veut accéder à votre compte. Si vous y consentez, {{app_name}} pourra:",
3434
"message.consent-warning": "Avant de donner votre consentement, assurez-vous de faire confiance à cette application, car vous pourriez partager des informations sensibles.",
3535
"message.verification-instructions": "Un code de vérification a été envoyé à votre adresse e-mail.",
36+
"message.resend-code-in": "Renvoyer le code dans {{seconds}}s",
37+
"message.verification-resent": "Un nouveau code de vérification a été envoyé à votre e-mail.",
3638
"message.recover-instructions": "Entrez votre adresse e-mail pour récupérer votre compte.",
3739
"message.reset-password-instructions": "Un code de vérification a été envoyé à votre adresse e-mail.",
3840
"message.page-not-found": "Cette page est introuvable",
@@ -81,5 +83,8 @@
8183
"link.create-account": "Créer un compte",
8284
"link.forgot-password": "Mot de passe oublié?",
8385
"link.login": "Avoir un compte? Se connecter",
84-
"link.login-minimal": "Se connecter"
86+
"link.login-minimal": "Se connecter",
87+
"link.resend-code": "Renvoyer le code",
88+
89+
"error.resend-failed": "Impossible de renvoyer l'e-mail de vérification. Veuillez réessayer."
8590
}

src/public/languages/ta.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
"message.consent": "{{app_name}} உங்கள் கணக்கைப் பயன்படுத்த விரும்புகிறது. இதற்கு நீங்கள் ஒப்புக்கொண்டால், {{app_name}} பின்வருவனவற்றைச் செய்ய முடியும்:",
3434
"message.consent-warning": "நீங்கள் ஒப்புக்கொள்வதற்கு முன், முக்கியமான தகவலைப் பகிர்வதால், இந்தப் பயன்பாட்டை நம்புகிறீர்கள் என்பதை உறுதிப்படுத்திக் கொள்ளுங்கள்.",
3535
"message.verification-instructions": "உங்கள் மின்னஞ்சல் முகவரிக்கு சரிபார்ப்புக் குறியீடு அனுப்பப்பட்டது.",
36+
"message.resend-code-in": "{{seconds}} வினாடிகளில் மறுபடியும் அனுப்பவும்",
37+
"message.verification-resent": "புதிய சரிபார்ப்புக் குறியீடு உங்கள் மின்னஞ்சலில் அனுப்பப்பட்டது.",
3638
"message.recover-instructions": "உங்கள் கணக்கை மீட்டெடுக்க உங்கள் மின்னஞ்சல் முகவரியை உள்ளிடவும்.",
3739
"message.reset-password-instructions": "உங்கள் மின்னஞ்சல் முகவரிக்கு சரிபார்ப்புக் குறியீடு அனுப்பப்பட்டது.",
3840
"message.page-not-found": "இந்த பக்கத்தை எங்களால் கண்டுபிடிக்க முடியவில்லை",
@@ -79,8 +81,11 @@
7981
"field.placeholder.new-password": "********",
8082

8183
"link.create-account": "கணக்கை துவங்குங்கள்",
82-
"link.forgot-password": "கடவுச்சொல்லை மறந்துவிட்டீர்களா?",
84+
"link.forgot-password": "கடவுச்சொல்லை மறந்துவிட்டீர்களா?",
8385
"link.login": "கணக்கு உள்ளதா? உள்நுழைய",
84-
"link.login-minimal": "உள்நுழைய"
86+
"link.login-minimal": "உள்நுழைய",
87+
"link.resend-code": "மறுபடி அனுப்புக்",
88+
89+
"error.resend-failed": "சரிபார்ப்பு மின்னஞ்சல் அனுப்ப முடியவில்லை. மீண்டும் முயலவும்."
8590
}
8691

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Logger } from "../../../singleton/logger.js";
2+
const log = Logger.getLogger().child({ from: "user/resend-verification.get" });
3+
4+
import { Request, Response } from "express";
5+
import { query } from "express-validator";
6+
import { isValidObjectId } from "mongoose";
7+
8+
import UserModel, { UserInterface } from "../../../model/mongo/user.js";
9+
import { hasErrors } from "../../../utils/api.js";
10+
import { errorMessages, statusCodes } from "../../../utils/http-status.js";
11+
import { ErrorResponse, SuccessResponse } from "../../../utils/response.js";
12+
import { Mailer } from "../../../singleton/mailer.js";
13+
import { VerificationCodeType } from "../../../enum/verification-code.js";
14+
15+
export const GET_ResendVerificationValidator = [
16+
query("target").exists().isString().isLength({ max: 64 }).custom(isValidObjectId),
17+
];
18+
19+
const GET_ResendVerification = async (req: Request, res: Response): Promise<void> => {
20+
try {
21+
if (hasErrors(req, res)) return;
22+
const target = req.query.target as string;
23+
const existingUser = (await UserModel.findOne({ _id: target }).exec()) as unknown as UserInterface;
24+
if (!existingUser) {
25+
res.status(statusCodes.clientInputError).json(new ErrorResponse(errorMessages.clientInputError));
26+
return;
27+
}
28+
if (existingUser.emailVerified) {
29+
res.status(statusCodes.clientInputError).json(new ErrorResponse(errorMessages.clientInputError));
30+
return;
31+
}
32+
await Mailer.generateAndSendEmailVerification(existingUser, VerificationCodeType.SIGNUP);
33+
res.status(statusCodes.success).json(new SuccessResponse("Verification email resent successfully."));
34+
} catch (err) {
35+
log.error(err);
36+
res.status(statusCodes.internalError).json(new ErrorResponse(errorMessages.internalError));
37+
}
38+
};
39+
40+
export default GET_ResendVerification;

src/service/api/user/router.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import GET_Following from "./following.get.js";
2424
import GET_UserId from "./_userId.get.js";
2525
import GET_Me from "./me.get.js";
2626
import GET_VerifyEmail, { GET_VerifyEmailValidator } from "./verify-email.get.js";
27+
import GET_ResendVerification, { GET_ResendVerificationValidator } from "./resend-verification.get.js";
2728
import GET_FollowRequests from "./follow-requests.get.js";
2829
import GET_FollowStatus, { GET_FollowStatusValidator } from "./follow-status.get.js";
2930
import GET_InviteCodes from "./invite-codes.get.js";
@@ -48,6 +49,7 @@ UserRouter.get("/session-state", GET_SessionState);
4849
UserRouter.post("/login", ...POST_LoginValidator, POST_Login);
4950
UserRouter.get("/login-history", ...DelegatedAuthFlow, GET_LoginHistory);
5051
UserRouter.get("/verify-email", ...GET_VerifyEmailValidator, GET_VerifyEmail);
52+
UserRouter.get("/resend-verification", ...GET_ResendVerificationValidator, GET_ResendVerification);
5153
UserRouter.post("/private", ...DelegatedAuthFlow, ...POST_PrivateValidator, POST_Private);
5254
UserRouter.get("/code", ...GET_CodeValidator, GET_Code);
5355
UserRouter.post("/reset-password", ...POST_ResetPasswordValidator, POST_ResetPassword);

0 commit comments

Comments
 (0)