Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added user email verification #522

Merged
merged 8 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 204 additions & 0 deletions src/components/adminPage/mail/mailVerifyEmail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useState, useEffect } from "react";
import { api } from "~/utils/api";
import { toast } from "react-hot-toast";
import cn from "classnames";
import { useTranslations } from "next-intl";
import {
useTrpcApiErrorHandler,
useTrpcApiSuccessHandler,
} from "~/hooks/useTrpcApiHandler";

type InviteUserTemplate = {
subject: string;
body: string;
};

const VerifyEmailTemplate = () => {
const t = useTranslations("admin");

const handleApiError = useTrpcApiErrorHandler();
const handleApiSuccess = useTrpcApiSuccessHandler();

const [changes, setChanges] = useState({
subject: false,
body: false,
});

const [emailTemplate, setEmailTemplate] = useState({
subject: "",
body: "",
});
// get default mail template
const {
data: mailTemplates,
refetch: refetchMailTemplates,
isLoading: loadingTemplates,
} = api.admin.getMailTemplates.useQuery({
template: "verifyEmailTemplate",
});

const changeTemplateHandler = (
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>,
) => {
const modifiedValue = e.target.value.replace(/\n/g, "<br />");
setEmailTemplate({
...emailTemplate,
[e.target.name]: modifiedValue,
});
};

const { mutate: sendTestMail, isLoading: sendingMailLoading } =
api.admin.sendTestMail.useMutation({
onError: handleApiError,
onSuccess: handleApiSuccess({
toastMessage: t("mail.templates.successToastMailSent"),
}),
});

const { mutate: setMailTemplates } = api.admin.setMailTemplates.useMutation();

const { mutate: getDefaultMailTemplate, data: defaultTemplates } =
api.admin.getDefaultMailTemplate.useMutation();

useEffect(() => {
if (!defaultTemplates) return;

setEmailTemplate(defaultTemplates);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultTemplates]);

useEffect(() => {
const verifyEmailTemplate = mailTemplates as InviteUserTemplate;
setEmailTemplate(verifyEmailTemplate);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mailTemplates]);

useEffect(() => {
const keysToCompare = ["subject", "body"]; // Add more keys as needed

const inviteUserTemplate = mailTemplates as InviteUserTemplate;
if (!inviteUserTemplate || !emailTemplate) return;

const newChanges = keysToCompare.reduce(
(acc, key) => {
const val1 = inviteUserTemplate?.[key] as string;
const val2 = emailTemplate[key] as string;

// Here we just compare strings directly, you could add more complex comparison logic if needed
acc[key] = val1 !== val2;

return acc;
},
{ subject: false, body: false },
);

setChanges(newChanges);
}, [mailTemplates, emailTemplate]);

const submitTemplateHandler = () => {
if (!emailTemplate.subject || !emailTemplate.body) {
return toast.error(t("mail.templates.errorFields"));
}

setMailTemplates(
{
template: JSON.stringify(emailTemplate),
type: "verifyEmailTemplate",
},
{
onSuccess: () => {
toast.success(t("mail.templates.successToastTemplateSaved"));
void refetchMailTemplates();
},
},
);
};

if (loadingTemplates) {
return (
<div className="flex flex-col items-center justify-center">
<h1 className="text-center text-2xl font-semibold">
<progress className="progress progress-primary w-56"></progress>
</h1>
</div>
);
}
return (
<div>
<div className="space-y-3">
<p className="font-medium">
{t("mail.templates.availableTags")}
<span className="text-primary flex gap-1">
<kbd className="kbd kbd-sm">toName</kbd>
<kbd className="kbd kbd-sm">verifyLink</kbd>
</span>
</p>
<div className="form-control w-full">
<label className="label">
<span className="label-text">{t("mail.templates.subject")}</span>
</label>
<input
type="text"
placeholder={t("mail.templates.inputPlaceholderSubject")}
value={emailTemplate?.subject || ""}
name="subject"
className={cn("input input-bordered w-full focus:outline-none", {
"border-2 border-red-500": changes?.subject,
})}
onChange={changeTemplateHandler}
/>
</div>
<div className="form-control w-full">
<label className="label">
<span className="label-text">{t("mail.templates.htmlBody")}</span>
</label>
<textarea
value={emailTemplate?.body?.replace(/<br \/>/g, "\n") || ""}
className={cn(
"custom-scrollbar textarea textarea-bordered w-full border-2 font-medium leading-snug focus:outline-none",
{ "border-2 border-red-500": changes.body },
)}
placeholder={t("mail.templates.textareaPlaceholderBody")}
rows={10}
name="body"
onChange={changeTemplateHandler}
></textarea>
</div>
</div>
<div className="flex justify-between p-5">
<div className="space-x-2">
<button
className="btn btn-primary btn-sm"
onClick={() => submitTemplateHandler()}
>
{t("mail.templates.saveTemplateButton")}
</button>
<button
className="btn btn-sm"
onClick={() =>
getDefaultMailTemplate({
template: "verifyEmailTemplate",
})
}
>
{t("mail.templates.resetButton")}
</button>
</div>
<div className="flex justify-end">
<button
className="btn btn-sm"
disabled={changes.subject || changes.body || sendingMailLoading}
onClick={() => sendTestMail({ type: "verifyEmailTemplate" })}
>
{sendingMailLoading
? t("mail.templates.sendTestMailButtonLoading")
: t("mail.templates.sendTestMailButton")}
</button>
</div>
</div>
</div>
);
};

export default VerifyEmailTemplate;
48 changes: 30 additions & 18 deletions src/components/elements/inputField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@ interface FormProps {
badge?: {
text: string;
color: string;
onClick?: () => void;
};
headerBadge?: {
text: string;
color: string;
onClick?: () => void;
};
}

Expand Down Expand Up @@ -125,7 +127,7 @@ const InputField = ({
const handleChange = (
e:
| ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>
| { target: { name: string; value: string[] } },
| { target: { name: string; value: string | string[] } },
) => {
const { name, value } = "target" in e ? e.target : e;
setFormValues((prevValues) => ({
Expand Down Expand Up @@ -156,32 +158,42 @@ const InputField = ({
</div>
);

const renderBadge = (badgeProps: FormProps["badge"] | FormProps["headerBadge"]) => {
if (!badgeProps) return null;

return (
<span
className={`badge badge-outline badge-${badgeProps.color} ml-2 ${
badgeProps.onClick ? "cursor-pointer" : ""
}`}
onClick={(e) => {
e.stopPropagation(); // Prevent event from bubbling up
if (badgeProps.onClick) {
badgeProps.onClick();
}
}}
>
{badgeProps.text}
</span>
);
};
return (
<>
{!showInputs ? (
<div className="flex w-full justify-between">
<div onClick={handleEditClick} className={`cursor-pointer ${labelStyle}`}>
<div className="flex font-medium">
<div onClick={handleEditClick} className={`cursor-pointer ${labelStyle}`}>
<div className="flex font-medium items-center">
<span>{label}</span>

{headerBadge && (
<span className={`badge badge-outline badge-${headerBadge.color} ml-2`}>
{headerBadge.text}
</span>
)}
{renderBadge(headerBadge)}
</div>
<div>
{description ? (
<p className="m-0 p-0 text-xs text-gray-500">{description}</p>
) : null}
</div>
<div className="text-gray-500">
{placeholder ?? fields[0].placeholder}
{badge && (
<span className={`badge badge-outline badge-${badge.color} ml-2`}>
{badge.text}
</span>
)}
<div className="flex items-center text-gray-500">
<span>{placeholder ?? fields[0].placeholder}</span>
{renderBadge(badge)}
</div>
</div>
<div>
Expand Down Expand Up @@ -263,9 +275,9 @@ const InputField = ({
formFieldName={field.name}
options={field.selectOptions as string[]}
value={(formValues[field.name] as string[]) || []}
onChange={(e) =>
onChange={(selectedValues: string[]) =>
handleChange({
target: { name: field.name, type: "select", value: e },
target: { name: field.name, value: selectedValues },
})
}
prompt={field.placeholder}
Expand Down
8 changes: 8 additions & 0 deletions src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,13 @@
"mfaRecoveryResetTitle": "2FA Recovery",
"mfaRecoveryResetMessage": "Please enter your credentials and Recovery Code",
"backToLogin": "Back to Login"
},
"emailVerification": {
"emailVerified": "Email Verified",
"errorMessage": "An error occurred while verifying your email.",
"successMessage": "Your email has been successfully verified.",
"redirectMessage": "You will be redirected automatically in {seconds} seconds.",
"goNowButton": "Go Now"
}
},
"networks": {
Expand Down Expand Up @@ -448,6 +455,7 @@
"password": "Password",
"useSSL": "Use SSL",
"inviteUserTemplate": "Invite user template",
"emailVerificationTemplate": "Email verification template",
"forgotPasswordTemplate": "Forgot Password template",
"notificationTemplate": "Notification template",
"organizationInviteTemplate": "Organization Invite template",
Expand Down
8 changes: 8 additions & 0 deletions src/locales/es/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,13 @@
"mfaRecoveryResetTitle": "Recuperación de 2FA",
"mfaRecoveryResetMessage": "Por favor, introduce tus credenciales y el código de recuperación",
"backToLogin": "Volver al inicio de sesión"
},
"emailVerification": {
"emailVerified": "Correo electrónico verificado",
"errorMessage": "Ocurrió un error al verificar tu correo electrónico.",
"successMessage": "Tu correo electrónico ha sido verificado exitosamente.",
"redirectMessage": "Serás redirigido automáticamente en {seconds} segundos.",
"goNowButton": "Ir ahora"
}
},
"networks": {
Expand Down Expand Up @@ -448,6 +455,7 @@
"password": "Contraseña",
"useSSL": "Usar SSL",
"inviteUserTemplate": "Plantilla de invitación de usuario",
"emailVerificationTemplate": "Plantilla de verificación de correo electrónico",
"forgotPasswordTemplate": "Plantilla de contraseña olvidada",
"notificationTemplate": "Plantilla de notificación",
"organizationInviteTemplate": "Plantilla de invitación de organización",
Expand Down
8 changes: 8 additions & 0 deletions src/locales/fr/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,13 @@
"mfaRecoveryResetTitle": "Récupération 2FA",
"mfaRecoveryResetMessage": "Veuillez entrer vos identifiants et le code de récupération",
"backToLogin": "Retour à la connexion"
},
"emailVerification": {
"emailVerified": "E-mail vérifié",
"errorMessage": "Une erreur s'est produite lors de la vérification de votre e-mail.",
"successMessage": "Votre e-mail a été vérifié avec succès.",
"redirectMessage": "Vous serez redirigé automatiquement dans {seconds} secondes.",
"goNowButton": "Aller maintenant"
}
},
"networks": {
Expand Down Expand Up @@ -448,6 +455,7 @@
"password": "Mot de passe",
"useSSL": "Utiliser SSL",
"inviteUserTemplate": "Modèle d'invitation d'utilisateur",
"emailVerificationTemplate": "Modèle de vérification d'e-mail",
"forgotPasswordTemplate": "Modèle de mot de passe oublié",
"notificationTemplate": "Modèle de notification",
"organizationInviteTemplate": "Modèle d'invitation d'organisation",
Expand Down
8 changes: 8 additions & 0 deletions src/locales/no/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,13 @@
"mfaRecoveryResetTitle": "Gjenoppretting av 2FA",
"mfaRecoveryResetMessage": "Vennligst skriv inn dine legitimasjonsbeskrivelser og gjenopprettingskode",
"backToLogin": "Tilbake til innlogging"
},
"emailVerification": {
"emailVerified": "E-post bekreftet",
"errorMessage": "Det oppstod en feil under verifisering av e-posten din.",
"successMessage": "E-posten din har blitt bekreftet.",
"redirectMessage": "Du vil bli videresendt automatisk om {seconds} sekunder.",
"goNowButton": "Gå nå"
}
},
"networks": {
Expand Down Expand Up @@ -448,6 +455,7 @@
"password": "Passord",
"useSSL": "Bruk SSL",
"inviteUserTemplate": "Inviter bruker mal",
"emailVerificationTemplate": "E-post verifikasjons mal",
"forgotPasswordTemplate": "Glemt passord mal",
"notificationTemplate": "Varslings mal",
"organizationInviteTemplate": "Organisasjons invitasjons mal",
Expand Down
Loading
Loading