feat(cloud): add find-workspace and email verification endpoints (#2020)

* feat: add find-workspace and email verification endpoints
* sync
This commit is contained in:
Philip Okugbe
2026-03-14 13:36:30 +00:00
committed by GitHub
parent d0ed6865cb
commit 97c459be67
25 changed files with 479 additions and 13 deletions
@@ -5,3 +5,15 @@ export async function getJoinedWorkspaces(): Promise<Partial<IWorkspace[]>> {
const req = await api.post<Partial<IWorkspace[]>>("/workspace/joined");
return req.data;
}
export async function findWorkspacesByEmail(email: string): Promise<void> {
await api.post("/workspace/find-by-email", { email });
}
export async function verifyEmail(data: { token: string }): Promise<void> {
await api.post("/workspace/verify-email", data);
}
export async function resendVerificationEmail(data: { email: string; sig: string }): Promise<void> {
await api.post("/workspace/resend-verification", data);
}
@@ -20,14 +20,21 @@ import APP_ROUTE from "@/lib/app-route.ts";
import { useTranslation } from "react-i18next";
import JoinedWorkspaces from "@/ee/components/joined-workspaces.tsx";
import { useJoinedWorkspacesQuery } from "@/ee/cloud/query/cloud-query.ts";
import { findWorkspacesByEmail } from "@/ee/cloud/service/cloud-service.ts";
const formSchema = z.object({
hostname: z.string().min(1, { message: "subdomain is required" }),
});
const findWorkspaceSchema = z.object({
email: z.string().email({ message: "Please enter a valid email" }),
});
export function CloudLoginForm() {
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isFindLoading, setIsFindLoading] = useState<boolean>(false);
const [findEmailSent, setFindEmailSent] = useState<boolean>(false);
const { data: joinedWorkspaces } = useJoinedWorkspacesQuery();
const form = useForm<any>({
@@ -37,6 +44,13 @@ export function CloudLoginForm() {
},
});
const findForm = useForm<any>({
validate: zod4Resolver(findWorkspaceSchema),
initialValues: {
email: "",
},
});
async function onSubmit(data: { hostname: string }) {
setIsLoading(true);
@@ -54,6 +68,19 @@ export function CloudLoginForm() {
setIsLoading(false);
}
async function onFindSubmit(data: { email: string }) {
setIsFindLoading(true);
try {
await findWorkspacesByEmail(data.email);
setFindEmailSent(true);
} catch {
findForm.setFieldError("email", "An error occurred. Please try again.");
}
setIsFindLoading(false);
}
return (
<div>
<Container size={420} className={classes.container}>
@@ -83,6 +110,38 @@ export function CloudLoginForm() {
{t("Continue")}
</Button>
</form>
<Divider my="lg" label="or" labelPosition="center" />
{findEmailSent ? (
<Text ta="center" size="sm" c="dimmed">
{t("We've sent you an email with your associated workspaces.")}
</Text>
) : (
<form onSubmit={findForm.onSubmit(onFindSubmit)}>
<Text fw={600} mb="xs">
{t("Find your workspaces")}
</Text>
<TextInput
type="email"
placeholder="name@company.com"
description={t(
"We'll send a list of your workspaces to this email.",
)}
withErrorStyles={false}
{...findForm.getInputProps("email")}
/>
<Button
type="submit"
fullWidth
mt="md"
variant="light"
loading={isFindLoading}
>
{t("Send")}
</Button>
</form>
)}
</Box>
</Container>
+107
View File
@@ -0,0 +1,107 @@
import { useEffect, useState } from "react";
import { useSearchParams, useNavigate } from "react-router-dom";
import { Container, Title, Text, Button, Box } from "@mantine/core";
import classes from "../../features/auth/components/auth.module.css";
import {
verifyEmail,
resendVerificationEmail,
} from "@/ee/cloud/service/cloud-service.ts";
import { notifications } from "@mantine/notifications";
import APP_ROUTE from "@/lib/app-route.ts";
import { useTranslation } from "react-i18next";
export default function VerifyEmail() {
const { t } = useTranslation();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const token = searchParams.get("token");
const rawEmail = searchParams.get("email");
const email = rawEmail && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(rawEmail) ? rawEmail : null;
const sig = searchParams.get("sig");
const [isResending, setIsResending] = useState(false);
const [resent, setResent] = useState(false);
useEffect(() => {
if (token) {
handleVerify(token);
}
}, [token]);
async function handleVerify(verifyToken: string) {
try {
await verifyEmail({ token: verifyToken });
navigate(APP_ROUTE.HOME);
} catch (err) {
notifications.show({
message: t("Verification failed. The link may have expired."),
color: "red",
});
navigate(APP_ROUTE.AUTH.LOGIN);
}
}
async function handleResend() {
if (!email || !sig) return;
setIsResending(true);
try {
await resendVerificationEmail({ email, sig });
setResent(true);
} catch {
notifications.show({
message: t("Failed to resend verification email. Please try again."),
color: "red",
});
}
setIsResending(false);
}
if (token) {
return (
<Container size={420} className={classes.container}>
<Box p="xl" className={classes.containerBox}>
<Title order={2} ta="center" fw={500} mb="md">
{t("Verifying your email")}
</Title>
<Text ta="center" c="dimmed">
{t("Please wait...")}
</Text>
</Box>
</Container>
);
}
return (
<Container size={420} className={classes.container}>
<Box p="xl" className={classes.containerBox}>
<Title order={2} ta="center" fw={500} mb="md">
{t("Check your email")}
</Title>
<Text ta="center" c="dimmed" mb="md">
{email
? t("We sent a verification link to {{email}}.", { email })
: t("We sent a verification link to your email.")}
</Text>
<Text ta="center" size="sm" c="dimmed" mb="lg">
{t("Click the link to verify your email and access your workspace.")}
</Text>
{email && sig && !resent && (
<Button
fullWidth
variant="light"
onClick={handleResend}
loading={isResending}
>
{t("Resend verification email")}
</Button>
)}
{resent && (
<Text ta="center" size="sm" c="dimmed">
{t("Verification email sent. Please check your inbox.")}
</Text>
)}
</Box>
</Container>
);
}