email template
This commit is contained in:
parent
ebbb582e99
commit
2451045693
15 changed files with 447 additions and 156 deletions
|
|
@ -43,7 +43,7 @@ function validateForm(form, companyDomain) {
|
|||
} else if (!EMAIL_LOCAL_PATTERN.test(local)) {
|
||||
errors.email_local = "Enter a valid email address before @.";
|
||||
}
|
||||
if (!form.password.trim()) {
|
||||
if (!form.send_invitation && !form.password.trim()) {
|
||||
errors.password = "Please set an initial password.";
|
||||
}
|
||||
if (form.send_invitation) {
|
||||
|
|
@ -178,8 +178,8 @@ export default function AddEmployeeForm({
|
|||
section_id: form.section_id || undefined,
|
||||
org_role: form.org_role,
|
||||
send_invitation: form.send_invitation,
|
||||
force_password_change: form.force_password_change,
|
||||
password: form.password,
|
||||
force_password_change: form.send_invitation ? false : form.force_password_change,
|
||||
password: form.send_invitation ? undefined : form.password,
|
||||
},
|
||||
token,
|
||||
);
|
||||
|
|
@ -327,7 +327,7 @@ export default function AddEmployeeForm({
|
|||
<SetupField
|
||||
label={form.send_invitation ? <RequiredLabel>Invitation email</RequiredLabel> : "Invitation email"}
|
||||
error={fieldErrors.invitation_email}
|
||||
hint={`Personal email where aGentic Email sends the setup invite from @${platformDomain}.`}
|
||||
hint={`Personal email where we send the setup invite from invites@${platformDomain}.`}
|
||||
>
|
||||
<InputWrap icon={Mail}>
|
||||
<input
|
||||
|
|
@ -340,21 +340,6 @@ export default function AddEmployeeForm({
|
|||
</InputWrap>
|
||||
</SetupField>
|
||||
|
||||
<SetupField
|
||||
label={<RequiredLabel>Initial password</RequiredLabel>}
|
||||
error={fieldErrors.password}
|
||||
hint="Must include uppercase, lowercase, and a number."
|
||||
>
|
||||
<InputWrap icon={Lock}>
|
||||
<input
|
||||
type="password"
|
||||
value={form.password}
|
||||
onChange={(event) => updateField("password", event.target.value)}
|
||||
placeholder="Set an initial password"
|
||||
/>
|
||||
</InputWrap>
|
||||
</SetupField>
|
||||
|
||||
<div className="add-user-checkboxes">
|
||||
<label className="add-user-checkbox">
|
||||
<input
|
||||
|
|
@ -364,23 +349,42 @@ export default function AddEmployeeForm({
|
|||
/>
|
||||
<span>
|
||||
<strong>Send invitation email</strong>
|
||||
<small>They will receive an activation link to set their password.</small>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label className="add-user-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.force_password_change}
|
||||
onChange={(event) => updateField("force_password_change", event.target.checked)}
|
||||
/>
|
||||
<span>
|
||||
<strong>Require password change on first login</strong>
|
||||
<small>User must create a new password when they sign in for the first time.</small>
|
||||
<small>They will receive a secure link to set their password (expires in 48 hours).</small>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{!form.send_invitation && (
|
||||
<>
|
||||
<SetupField
|
||||
label={<RequiredLabel>Initial password</RequiredLabel>}
|
||||
error={fieldErrors.password}
|
||||
hint="Must include uppercase, lowercase, and a number."
|
||||
>
|
||||
<InputWrap icon={Lock}>
|
||||
<input
|
||||
type="password"
|
||||
value={form.password}
|
||||
onChange={(event) => updateField("password", event.target.value)}
|
||||
placeholder="Set an initial password"
|
||||
/>
|
||||
</InputWrap>
|
||||
</SetupField>
|
||||
|
||||
<label className="add-user-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.force_password_change}
|
||||
onChange={(event) => updateField("force_password_change", event.target.checked)}
|
||||
/>
|
||||
<span>
|
||||
<strong>Require password change on first login</strong>
|
||||
<small>User must create a new password when they sign in for the first time.</small>
|
||||
</span>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
|
||||
{status.error && <p className="form-error" role="alert">{status.error}</p>}
|
||||
|
||||
<footer className={variant === "inline" ? "org-setup-actions" : "add-user-actions"}>
|
||||
|
|
|
|||
129
frontend/src/components/InviteAcceptPage.jsx
Normal file
129
frontend/src/components/InviteAcceptPage.jsx
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import { ArrowRight, LockKeyhole } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { acceptInvite, previewInvite } from "../api";
|
||||
import AuthLayout from "./AuthLayout";
|
||||
|
||||
function inviteTokenFromPath() {
|
||||
const match = window.location.pathname.match(/^\/invite\/([^/]+)/);
|
||||
return match ? decodeURIComponent(match[1]) : "";
|
||||
}
|
||||
|
||||
export default function InviteAcceptPage() {
|
||||
const token = inviteTokenFromPath();
|
||||
const [invite, setInvite] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadError, setLoadError] = useState("");
|
||||
const [status, setStatus] = useState({ loading: false, error: "" });
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
setLoadError("This invitation link is invalid.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
previewInvite(token)
|
||||
.then((data) => setInvite(data))
|
||||
.catch((error) => setLoadError(error.message))
|
||||
.finally(() => setLoading(false));
|
||||
}, [token]);
|
||||
|
||||
async function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
setStatus({ loading: true, error: "" });
|
||||
const form = new FormData(event.currentTarget);
|
||||
const password = form.get("password");
|
||||
const confirm = form.get("confirm_password");
|
||||
if (password !== confirm) {
|
||||
setStatus({ loading: false, error: "Passwords do not match." });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await acceptInvite(token, { password });
|
||||
sessionStorage.setItem("agmail_access_token", result.access_token);
|
||||
sessionStorage.setItem("agmail_account", JSON.stringify(result.account));
|
||||
window.location.assign("/app");
|
||||
} catch (error) {
|
||||
setStatus({ loading: false, error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<AuthLayout
|
||||
eyebrow="Invitation"
|
||||
title={<>Setting up<br /><em>your account.</em></>}
|
||||
description="Verifying your invitation link…"
|
||||
>
|
||||
<p>Loading invitation…</p>
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
||||
|
||||
if (loadError || !invite) {
|
||||
return (
|
||||
<AuthLayout
|
||||
eyebrow="Invitation"
|
||||
title={<>This link<br /><em>is not valid.</em></>}
|
||||
description="The invitation may have expired or already been used."
|
||||
>
|
||||
<p className="form-error" role="alert">{loadError || "Invitation not found."}</p>
|
||||
<p className="auth-switch"><a href="/login">Go to sign in</a></p>
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthLayout
|
||||
eyebrow="Invitation"
|
||||
title={<>Join <em>{invite.organization}</em></>}
|
||||
description={`${invite.invited_by} invited you to set up your work email.`}
|
||||
>
|
||||
<>
|
||||
<div className="card-heading">
|
||||
<h2>Set up your account</h2>
|
||||
<p>
|
||||
Your work email is <strong>{invite.work_email}</strong>.
|
||||
Choose a password to activate your inbox.
|
||||
</p>
|
||||
</div>
|
||||
<form className="auth-form" onSubmit={handleSubmit}>
|
||||
<label>
|
||||
Password
|
||||
<div className="input-wrap">
|
||||
<LockKeyhole size={16} />
|
||||
<input
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder="Create a password"
|
||||
required
|
||||
minLength={8}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
Confirm password
|
||||
<div className="input-wrap">
|
||||
<LockKeyhole size={16} />
|
||||
<input
|
||||
name="confirm_password"
|
||||
type="password"
|
||||
placeholder="Repeat your password"
|
||||
required
|
||||
minLength={8}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<p className="auth-switch" style={{ marginTop: 0 }}>
|
||||
Must include uppercase, lowercase, and a number.
|
||||
</p>
|
||||
{status.error && <p className="form-error" role="alert">{status.error}</p>}
|
||||
<button className="button button-coral auth-submit" disabled={status.loading}>
|
||||
{status.loading ? "Activating…" : "Set up your account"} <ArrowRight size={16} />
|
||||
</button>
|
||||
</form>
|
||||
</>
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue