mirror of
https://github.com/fosrl/pangolin.git
synced 2025-05-12 21:30:35 +01:00
improve verify email redirect flow
This commit is contained in:
parent
c2cbd7e1a1
commit
5bbf32f6a6
18 changed files with 145 additions and 83 deletions
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
You’ve requested to verify your email. Please use
|
||||
the verification code below:
|
||||
You’ve 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;
|
||||
|
|
|
@ -139,7 +139,7 @@ export async function login(
|
|||
success: true,
|
||||
error: false,
|
||||
message: "Email verification code sent",
|
||||
status: HttpCode.ACCEPTED,
|
||||
status: HttpCode.OK,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>>(
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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("/");
|
||||
|
|
|
@ -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("-");
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue