improve verify email redirect flow

This commit is contained in:
Milo Schwartz 2024-11-28 00:11:13 -05:00
parent c2cbd7e1a1
commit 5bbf32f6a6
No known key found for this signature in database
18 changed files with 145 additions and 83 deletions

View file

@ -21,9 +21,7 @@ export async function sendEmail(
return;
}
logger.debug("Rendering email templatee...")
const emailHtml = await render(template);
logger.debug("Done rendering email templatee")
const options = {
from: opts.from,

View file

@ -33,9 +33,17 @@ export const SendInviteLink = ({
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind>
<Tailwind config={{
theme: {
extend: {
colors: {
primary: "#F97317"
}
}
}
}}>
<Body className="font-sans">
<Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8">
<Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8 rounded-lg">
<Heading className="text-2xl font-semibold text-gray-800 text-center">
You're invited to join a Fossorial organization
</Heading>
@ -58,7 +66,7 @@ export const SendInviteLink = ({
<Section className="text-center my-6">
<Button
href={inviteLink}
className="rounded-md bg-gray-600 px-[12px] py-[12px] text-center font-semibold text-white cursor-pointer"
className="rounded-lg bg-primary px-[12px] py-[9px] text-center font-semibold text-white cursor-pointer"
>
Accept invitation to {orgName}
</Button>

View file

@ -14,11 +14,13 @@ import * as React from "react";
interface VerifyEmailProps {
username?: string;
verificationCode: string;
verifyLink: string;
}
export const VerifyEmail = ({
username,
verificationCode,
verifyLink,
}: VerifyEmailProps) => {
const previewText = `Verify your email, ${username}`;
@ -26,21 +28,34 @@ export const VerifyEmail = ({
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind>
<Tailwind
config={{
theme: {
extend: {
colors: {
primary: "#F97317",
},
},
},
}}
>
<Body className="font-sans">
<Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8">
<Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8 rounded-lg">
<Heading className="text-2xl font-semibold text-gray-800 text-center">
Verify Your Email
Please verify your email
</Heading>
<Text className="text-base text-gray-700 mt-4">
Hi {username || "there"},
</Text>
<Text className="text-base text-gray-700 mt-2">
Youve requested to verify your email. Please use
the verification code below:
Youve requested to verify your email. Please{" "}
<a href={verifyLink} className="text-primary">
click here
</a>{" "}
to verify your email, then enter the following code:
</Text>
<Section className="text-center my-6">
<Text className="inline-block bg-gray-100 text-xl font-bold text-gray-900 py-2 px-4 border border-gray-300 rounded-md">
<Text className="inline-block bg-primary text-xl font-bold text-white py-2 px-4 border border-gray-300 rounded-xl">
{verificationCode}
</Text>
</Section>
@ -59,3 +74,5 @@ export const VerifyEmail = ({
</Html>
);
};
export default VerifyEmail;

View file

@ -139,7 +139,7 @@ export async function login(
success: true,
error: false,
message: "Email verification code sent",
status: HttpCode.ACCEPTED,
status: HttpCode.OK,
});
}

View file

@ -13,11 +13,18 @@ export async function sendEmailVerificationCode(
): Promise<void> {
const code = await generateEmailVerificationCode(userId, email);
await sendEmail(VerifyEmail({ username: email, verificationCode: code }), {
to: email,
from: config.email?.no_reply,
subject: "Verify your email address",
});
await sendEmail(
VerifyEmail({
username: email,
verificationCode: code,
verifyLink: `${config.app.base_url}/auth/verify-email`,
}),
{
to: email,
from: config.email?.no_reply,
subject: "Verify your email address",
},
);
}
async function generateEmailVerificationCode(

View file

@ -1,6 +1,8 @@
import { internal } from "@app/api";
import { authCookieHeader } from "@app/api/cookies";
import { verifySession } from "@app/lib/auth/verifySession";
import { GetOrgResponse } from "@server/routers/org";
import { GetOrgUserResponse } from "@server/routers/user";
import { AxiosResponse } from "axios";
import { redirect } from "next/navigation";
import { cache } from "react";
@ -17,6 +19,25 @@ export default async function OrgLayout(props: {
redirect(`/`);
}
const getUser = cache(verifySession);
const user = await getUser();
if (!user) {
redirect(`/?redirect=/${orgId}`);
}
try {
const getOrgUser = cache(() =>
internal.get<AxiosResponse<GetOrgUserResponse>>(
`/org/${orgId}/user/${user.userId}`,
cookie,
),
);
const orgUser = await getOrgUser();
} catch {
redirect(`/`);
}
try {
const getOrg = cache(() =>
internal.get<AxiosResponse<GetOrgResponse>>(

View file

@ -14,27 +14,6 @@ export default async function OrgPage(props: OrgPageProps) {
const params = await props.params;
const orgId = params.orgId;
const getUser = cache(verifySession);
const user = await getUser();
if (!user) {
redirect("/auth/login");
}
const cookie = await authCookieHeader();
try {
const getOrgUser = cache(() =>
internal.get<AxiosResponse<GetOrgUserResponse>>(
`/org/${orgId}/user/${user.userId}`,
cookie
)
);
const orgUser = await getOrgUser();
} catch {
redirect(`/`);
}
return (
<>
<p>Welcome to {orgId} dashboard</p>

View file

@ -26,7 +26,7 @@ export default async function GeneralSettingsPage({
const user = await getUser();
if (!user) {
redirect("/auth/login");
redirect(`/?redirect=/${orgId}/settings/general`);
}
let orgUser = null;
@ -34,8 +34,8 @@ export default async function GeneralSettingsPage({
const getOrgUser = cache(async () =>
internal.get<AxiosResponse<GetOrgUserResponse>>(
`/org/${orgId}/user/${user.userId}`,
await authCookieHeader()
)
await authCookieHeader(),
),
);
const res = await getOrgUser();
orgUser = res.data.data;
@ -48,8 +48,8 @@ export default async function GeneralSettingsPage({
const getOrg = cache(async () =>
internal.get<AxiosResponse<GetOrgResponse>>(
`/org/${orgId}`,
await authCookieHeader()
)
await authCookieHeader(),
),
);
const res = await getOrg();
org = res.data.data;

View file

@ -55,7 +55,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
const user = await getUser();
if (!user) {
redirect("/auth/login");
redirect(`/?redirect=/${params.orgId}/`);
}
const cookie = await authCookieHeader();
@ -64,8 +64,8 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
const getOrgUser = cache(() =>
internal.get<AxiosResponse<GetOrgUserResponse>>(
`/org/${params.orgId}/user/${user.userId}`,
cookie
)
cookie,
),
);
const orgUser = await getOrgUser();
@ -79,7 +79,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
let orgs: ListOrgsResponse["orgs"] = [];
try {
const getOrgs = cache(() =>
internal.get<AxiosResponse<ListOrgsResponse>>(`/orgs`, cookie)
internal.get<AxiosResponse<ListOrgsResponse>>(`/orgs`, cookie),
);
const res = await getOrgs();
if (res && res.data.data.orgs) {

View file

@ -30,7 +30,15 @@ export default function DashboardLoginForm({
<CardContent>
<LoginForm
redirect={redirect}
onLogin={() => router.push("/")}
onLogin={() => {
if (redirect && redirect.includes("http")) {
window.location.href = redirect;
} else if (redirect) {
router.push(redirect);
} else {
router.push("/");
}
}}
/>
</CardContent>
</Card>

View file

@ -38,6 +38,7 @@ import { LoginResponse } from "@server/routers/auth";
import ResourceAccessDenied from "./ResourceAccessDenied";
import LoginForm from "@app/components/LoginForm";
import { AuthWithPasswordResponse } from "@server/routers/resource";
import { redirect } from "next/dist/server/api-utils";
const pinSchema = z.object({
pin: z
@ -113,11 +114,6 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
},
});
function constructRedirect(redirect: string): string {
const redirectUrl = new URL(redirect);
return redirectUrl.toString();
}
const onPinSubmit = (values: z.infer<typeof pinSchema>) => {
setLoadingLogin(true);
api.post<AxiosResponse<AuthWithPasswordResponse>>(
@ -127,9 +123,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
.then((res) => {
const session = res.data.data.session;
if (session) {
const url = constructRedirect(props.redirect);
console.log(url);
window.location.href = url;
window.location.href = props.redirect;
}
})
.catch((e) => {
@ -152,7 +146,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
.then((res) => {
const session = res.data.data.session;
if (session) {
window.location.href = constructRedirect(props.redirect);
window.location.href = props.redirect;
}
})
.catch((e) => {
@ -172,7 +166,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
}
if (!accessDenied) {
window.location.href = constructRedirect(props.redirect);
window.location.href = props.redirect;
}
}
@ -371,6 +365,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
className={`${numMethods <= 1 ? "mt-0" : ""}`}
>
<LoginForm
redirect={window.location.href}
onLogin={async () =>
await handleSSOAuth()
}

View file

@ -79,6 +79,7 @@ export default function VerifyEmailForm({
.catch((e) => {
setError(formatAxiosError(e, "An error occurred"));
console.error("Failed to verify email:", e);
setIsSubmitting(false);
});
if (res && res.data?.data?.valid) {
@ -125,7 +126,7 @@ export default function VerifyEmailForm({
<div>
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Verify Your Email</CardTitle>
<CardTitle>Verify Email</CardTitle>
<CardDescription>
Enter the verification code sent to your email address.
</CardDescription>
@ -234,7 +235,7 @@ export default function VerifyEmailForm({
</CardContent>
</Card>
<div className="text-center text-muted-foreground mt-4">
<div className="text-center text-muted-foreground mt-2">
<Button
type="button"
variant="link"

View file

@ -8,13 +8,13 @@ export const dynamic = "force-dynamic";
export default async function Page(props: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
if (process.env.PUBLIC_FLAGS_EMAIL_VERIFICATION_REQUIRED !== "true") {
if (process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED !== "true") {
redirect("/");
}
const searchParams = await props.searchParams;
const getUser = cache(verifySession);
const user = await getUser();
const user = await getUser({ skipCheckVerifyEmail: true });
if (!user) {
redirect("/");

View file

@ -21,7 +21,7 @@ export default async function InvitePage(props: {
const user = await verifySession();
if (!user) {
redirect(`/auth/login?redirect=/invite?token=${params.token}`);
redirect(`/?redirect=/invite?token=${params.token}`);
}
const parts = tokenParam.split("-");

View file

@ -12,21 +12,36 @@ import { cache } from "react";
export const dynamic = "force-dynamic";
export default async function Page(props: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
searchParams: Promise<{ redirect: string | undefined }>;
}) {
const params = await props.searchParams; // this is needed to prevent static optimization
const getUser = cache(verifySession);
const user = await getUser();
const user = await getUser({ skipCheckVerifyEmail: true });
if (!user) {
redirect("/auth/login");
if (params.redirect) {
redirect(`/auth/login?redirect=${params.redirect}`);
} else {
redirect(`/auth/login`);
}
}
if (
!user.emailVerified &&
process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED === "true"
) {
if (params.redirect) {
redirect(`/auth/verify-email?redirect=${params.redirect}`);
} else {
redirect(`/auth/verify-email`);
}
}
let orgs: ListOrgsResponse["orgs"] = [];
try {
const res = await internal.get<AxiosResponse<ListOrgsResponse>>(
`/orgs`,
await authCookieHeader()
await authCookieHeader(),
);
if (res && res.data.data.orgs) {

View file

@ -19,7 +19,7 @@ export default async function SetupLayout({
const user = await getUser();
if (!user) {
redirect("/");
redirect("/?redirect=/setup");
}
return <div className="mt-32">{children}</div>;

View file

@ -72,13 +72,14 @@ export default function LoginForm({ redirect, onLogin }: LoginFormProps) {
);
});
console.log(res);
if (res && res.status === 200) {
setError(null);
console.log(res);
if (res.data?.data?.emailVerificationRequired) {
if (redirect) {
console.log("here", redirect)
router.push(`/auth/verify-email?redirect=${redirect}`);
} else {
router.push("/auth/verify-email");
@ -86,14 +87,8 @@ export default function LoginForm({ redirect, onLogin }: LoginFormProps) {
return;
}
if (redirect && redirect.includes("http")) {
window.location.href = redirect;
} else if (redirect) {
router.push(redirect);
} else {
if (onLogin) {
await onLogin();
}
if (onLogin) {
await onLogin();
}
}

View file

@ -3,15 +3,33 @@ import { authCookieHeader } from "@app/api/cookies";
import { GetUserResponse } from "@server/routers/user";
import { AxiosResponse } from "axios";
export async function verifySession(): Promise<GetUserResponse | null> {
export async function verifySession({
skipCheckVerifyEmail,
}: {
skipCheckVerifyEmail?: boolean;
} = {}): Promise<GetUserResponse | null> {
try {
const res = await internal.get<AxiosResponse<GetUserResponse>>(
"/user",
await authCookieHeader()
await authCookieHeader(),
);
return res.data.data;
} catch {
const user = res.data.data;
if (!user) {
return null;
}
if (
!skipCheckVerifyEmail &&
!user.emailVerified &&
process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED == "true"
) {
return null;
}
return user;
} catch (e) {
return null;
}
}
}