mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: authentication workflow changes
This commit is contained in:
parent
c480ea34de
commit
49fe79367a
@ -29,8 +29,8 @@ class SignInAuthEndpoint(View):
|
|||||||
if instance is None or not instance.is_setup_done:
|
if instance is None or not instance.is_setup_done:
|
||||||
# Redirection params
|
# Redirection params
|
||||||
params = {
|
params = {
|
||||||
"error_code": "REQUIRED_EMAIL_PASSWORD",
|
"error_code": "INSTANCE_NOT_CONFIGURED",
|
||||||
"error_message": "Both email and password are required",
|
"error_message": "Instance is not configured",
|
||||||
}
|
}
|
||||||
if next_path:
|
if next_path:
|
||||||
params["next_path"] = str(next_path)
|
params["next_path"] = str(next_path)
|
||||||
|
@ -26,7 +26,7 @@ from plane.authentication.utils.workspace_project_join import (
|
|||||||
from plane.bgtasks.magic_link_code_task import magic_link
|
from plane.bgtasks.magic_link_code_task import magic_link
|
||||||
from plane.license.models import Instance
|
from plane.license.models import Instance
|
||||||
from plane.authentication.utils.host import base_host
|
from plane.authentication.utils.host import base_host
|
||||||
from plane.db.models import User
|
from plane.db.models import User, Profile
|
||||||
|
|
||||||
|
|
||||||
class MagicGenerateEndpoint(APIView):
|
class MagicGenerateEndpoint(APIView):
|
||||||
@ -123,11 +123,12 @@ class MagicSignInEndpoint(View):
|
|||||||
request=request, key=f"magic_{email}", code=code
|
request=request, key=f"magic_{email}", code=code
|
||||||
)
|
)
|
||||||
user = provider.authenticate()
|
user = provider.authenticate()
|
||||||
|
profile = Profile.objects.get(user=user)
|
||||||
# Login the user and record his device info
|
# Login the user and record his device info
|
||||||
user_login(request=request, user=user)
|
user_login(request=request, user=user)
|
||||||
# Process workspace and project invitations
|
# Process workspace and project invitations
|
||||||
process_workspace_project_invitations(user=user)
|
process_workspace_project_invitations(user=user)
|
||||||
if user.is_password_autoset:
|
if user.is_password_autoset and profile.is_onboarded:
|
||||||
path = "accounts/set-password"
|
path = "accounts/set-password"
|
||||||
else:
|
else:
|
||||||
# Get the redirection path
|
# Get the redirection path
|
||||||
|
@ -22,7 +22,7 @@ from plane.authentication.utils.login import user_login
|
|||||||
from plane.bgtasks.magic_link_code_task import magic_link
|
from plane.bgtasks.magic_link_code_task import magic_link
|
||||||
from plane.license.models import Instance
|
from plane.license.models import Instance
|
||||||
from plane.authentication.utils.host import base_host
|
from plane.authentication.utils.host import base_host
|
||||||
from plane.db.models import User
|
from plane.db.models import User, Profile
|
||||||
|
|
||||||
|
|
||||||
class MagicGenerateSpaceEndpoint(APIView):
|
class MagicGenerateSpaceEndpoint(APIView):
|
||||||
|
4
packages/types/src/auth.d.ts
vendored
4
packages/types/src/auth.d.ts
vendored
@ -26,6 +26,6 @@ export interface IPasswordSignInData {
|
|||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICsrfTokenData {
|
export interface ICsrfTokenData {
|
||||||
csrf_token: string;
|
csrf_token: string;
|
||||||
};
|
}
|
||||||
|
27
web/components/account/auth-forms/auth-banner.tsx
Normal file
27
web/components/account/auth-forms/auth-banner.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { Info, X } from "lucide-react";
|
||||||
|
// helpers
|
||||||
|
import { TAuthErrorInfo } from "@/helpers/authentication.helper";
|
||||||
|
|
||||||
|
type TAuthBanner = {
|
||||||
|
bannerData: TAuthErrorInfo | undefined;
|
||||||
|
handleBannerData?: (bannerData: TAuthErrorInfo | undefined) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AuthBanner: FC<TAuthBanner> = (props) => {
|
||||||
|
const { bannerData, handleBannerData } = props;
|
||||||
|
|
||||||
|
if (!bannerData) return <></>;
|
||||||
|
return (
|
||||||
|
<div className="relative inline-flex items-center p-3 rounded-md gap-3 border border-custom-primary-100/50 bg-custom-primary-100/10">
|
||||||
|
<Info className="w-5 h-5 flex-shrink-0 text-custom-primary-100" />
|
||||||
|
<div className="w-full text-sm font-medium text-custom-primary-100">{bannerData?.message}</div>
|
||||||
|
<div
|
||||||
|
className="relative ml-auto w-6 h-6 rounded-sm flex justify-center items-center transition-all cursor-pointer hover:bg-custom-primary-100/20 text-custom-primary-100/80"
|
||||||
|
onClick={() => handleBannerData && handleBannerData(undefined)}
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4 flex-shrink-0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
99
web/components/account/auth-forms/auth-header.tsx
Normal file
99
web/components/account/auth-forms/auth-header.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { FC, useEffect, useState } from "react";
|
||||||
|
import { IWorkspaceMemberInvitation } from "@plane/types";
|
||||||
|
// components
|
||||||
|
import { WorkspaceLogo } from "@/components/workspace/logo";
|
||||||
|
// helpers
|
||||||
|
import { EAuthModes, EAuthSteps } from "@/helpers/authentication.helper";
|
||||||
|
// services
|
||||||
|
import { WorkspaceService } from "@/services/workspace.service";
|
||||||
|
|
||||||
|
type TAuthHeader = {
|
||||||
|
workspaceSlug: string | undefined;
|
||||||
|
invitationId: string | undefined;
|
||||||
|
invitationEmail: string | undefined;
|
||||||
|
authMode: EAuthModes;
|
||||||
|
currentAuthStep: EAuthSteps;
|
||||||
|
handleLoader: (isLoading: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Titles = {
|
||||||
|
[EAuthModes.SIGN_IN]: {
|
||||||
|
[EAuthSteps.EMAIL]: {
|
||||||
|
header: "Sign in to Plane",
|
||||||
|
subHeader: "Get back to your projects and make progress",
|
||||||
|
},
|
||||||
|
[EAuthSteps.PASSWORD]: {
|
||||||
|
header: "Sign in to Plane",
|
||||||
|
subHeader: "Get back to your projects and make progress",
|
||||||
|
},
|
||||||
|
[EAuthSteps.UNIQUE_CODE]: {
|
||||||
|
header: "Sign in to Plane",
|
||||||
|
subHeader: "Get back to your projects and make progress",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[EAuthModes.SIGN_UP]: {
|
||||||
|
[EAuthSteps.EMAIL]: {
|
||||||
|
header: "Create your account",
|
||||||
|
subHeader: "Start tracking your projects with Plane",
|
||||||
|
},
|
||||||
|
[EAuthSteps.PASSWORD]: {
|
||||||
|
header: "Create your account",
|
||||||
|
subHeader: "Progress, visualize, and measure work how it works best for you.",
|
||||||
|
},
|
||||||
|
[EAuthSteps.UNIQUE_CODE]: {
|
||||||
|
header: "Create your account",
|
||||||
|
subHeader: "Progress, visualize, and measure work how it works best for you.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const workSpaceService = new WorkspaceService();
|
||||||
|
|
||||||
|
export const AuthHeader: FC<TAuthHeader> = (props) => {
|
||||||
|
const { workspaceSlug, invitationId, invitationEmail, authMode, currentAuthStep, handleLoader } = props;
|
||||||
|
// state
|
||||||
|
const [invitation, setInvitation] = useState<IWorkspaceMemberInvitation | undefined>(undefined);
|
||||||
|
|
||||||
|
const getHeaderSubHeader = (
|
||||||
|
step: EAuthSteps,
|
||||||
|
mode: EAuthModes,
|
||||||
|
invitation: IWorkspaceMemberInvitation | undefined,
|
||||||
|
email: string | undefined
|
||||||
|
) => {
|
||||||
|
if (invitation && email && invitation.email === email && invitation.workspace) {
|
||||||
|
const workspace = invitation.workspace;
|
||||||
|
return {
|
||||||
|
header: (
|
||||||
|
<>
|
||||||
|
Join <WorkspaceLogo logo={workspace?.logo} name={workspace?.name} classNames="w-8 h-9" /> {workspace.name}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
subHeader: `${
|
||||||
|
mode == EAuthModes.SIGN_UP ? "Create an account" : "Sign in"
|
||||||
|
} to start managing work with your team.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return Titles[mode][step];
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (workspaceSlug && invitationId) {
|
||||||
|
handleLoader(true);
|
||||||
|
workSpaceService
|
||||||
|
.getWorkspaceInvitation(workspaceSlug, invitationId)
|
||||||
|
.then((res) => setInvitation(res))
|
||||||
|
.catch(() => setInvitation(undefined))
|
||||||
|
.finally(() => handleLoader(false));
|
||||||
|
} else setInvitation(undefined);
|
||||||
|
}, [workspaceSlug, invitationId, handleLoader]);
|
||||||
|
|
||||||
|
const { header, subHeader } = getHeaderSubHeader(currentAuthStep, authMode, invitation, invitationEmail);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-1 text-center">
|
||||||
|
<h3 className="text-3xl font-bold text-onboarding-text-100">{header}</h3>
|
||||||
|
<p className="font-medium text-onboarding-text-400">{subHeader}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,6 +1,5 @@
|
|||||||
import React from "react";
|
import { FC, FormEvent, useMemo, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
// icons
|
// icons
|
||||||
import { CircleAlert, XCircle } from "lucide-react";
|
import { CircleAlert, XCircle } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
@ -10,82 +9,72 @@ import { Button, Input } from "@plane/ui";
|
|||||||
// helpers
|
// helpers
|
||||||
import { checkEmailValidity } from "@/helpers/string.helper";
|
import { checkEmailValidity } from "@/helpers/string.helper";
|
||||||
|
|
||||||
type Props = {
|
type TAuthEmailForm = {
|
||||||
onSubmit: (data: IEmailCheckData) => Promise<void>;
|
|
||||||
defaultEmail: string;
|
defaultEmail: string;
|
||||||
|
onSubmit: (data: IEmailCheckData) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TEmailFormValues = {
|
export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
|
||||||
email: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AuthEmailForm: React.FC<Props> = observer((props) => {
|
|
||||||
const { onSubmit, defaultEmail } = props;
|
const { onSubmit, defaultEmail } = props;
|
||||||
// hooks
|
// states
|
||||||
const {
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
control,
|
const [email, setEmail] = useState(defaultEmail);
|
||||||
formState: { errors, isSubmitting, isValid },
|
|
||||||
handleSubmit,
|
|
||||||
} = useForm<TEmailFormValues>({
|
|
||||||
defaultValues: {
|
|
||||||
email: defaultEmail,
|
|
||||||
},
|
|
||||||
mode: "onChange",
|
|
||||||
reValidateMode: "onChange",
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleFormSubmit = async (data: TEmailFormValues) => {
|
const emailError = useMemo(
|
||||||
|
() => (email && !checkEmailValidity(email) ? { email: "Email is invalid" } : undefined),
|
||||||
|
[email]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFormSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsSubmitting(true);
|
||||||
const payload: IEmailCheckData = {
|
const payload: IEmailCheckData = {
|
||||||
email: data.email,
|
email: email,
|
||||||
};
|
};
|
||||||
onSubmit(payload);
|
await onSubmit(payload);
|
||||||
|
setIsSubmitting(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)} className="mx-auto mt-8 space-y-4 w-5/6 sm:w-96">
|
<form onSubmit={handleFormSubmit} className="mx-auto mt-8 space-y-4 w-5/6 sm:w-96">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
|
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
|
||||||
Email
|
Email
|
||||||
</label>
|
</label>
|
||||||
<Controller
|
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
||||||
control={control}
|
<Input
|
||||||
name="email"
|
id="email"
|
||||||
rules={{
|
name="email"
|
||||||
required: "Email is required",
|
type="email"
|
||||||
validate: (value) => checkEmailValidity(value) || "Email is invalid",
|
value={email}
|
||||||
}}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
render={({ field: { value, onChange } }) => (
|
hasError={Boolean(emailError?.email)}
|
||||||
<>
|
placeholder="name@company.com"
|
||||||
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400"
|
||||||
<Input
|
autoFocus
|
||||||
id="email"
|
/>
|
||||||
name="email"
|
{email.length > 0 && (
|
||||||
type="email"
|
<XCircle
|
||||||
value={value}
|
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||||
onChange={onChange}
|
onClick={() => setEmail("")}
|
||||||
hasError={Boolean(errors.email)}
|
/>
|
||||||
placeholder="name@company.com"
|
|
||||||
className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400"
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
{value.length > 0 && (
|
|
||||||
<XCircle
|
|
||||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
|
||||||
onClick={() => onChange("")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{errors.email && (
|
|
||||||
<p className="flex items-center gap-1 text-xs text-red-600 px-0.5">
|
|
||||||
<CircleAlert height={12} width={12} />
|
|
||||||
{errors.email.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
/>
|
</div>
|
||||||
|
{emailError?.email && (
|
||||||
|
<p className="flex items-center gap-1 text-xs text-red-600 px-0.5">
|
||||||
|
<CircleAlert height={12} width={12} />
|
||||||
|
{emailError.email}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={!isValid} loading={isSubmitting}>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
className="w-full"
|
||||||
|
size="lg"
|
||||||
|
disabled={email.length === 0 || Boolean(emailError?.email)}
|
||||||
|
loading={isSubmitting}
|
||||||
|
>
|
||||||
Continue
|
Continue
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
|
export * from "./sign-up-root";
|
||||||
|
export * from "./sign-in-root";
|
||||||
|
|
||||||
|
export * from "./auth-header";
|
||||||
|
export * from "./auth-banner";
|
||||||
|
|
||||||
export * from "./email";
|
export * from "./email";
|
||||||
export * from "./forgot-password-popover";
|
export * from "./forgot-password-popover";
|
||||||
export * from "./password";
|
export * from "./password";
|
||||||
export * from "./root";
|
|
||||||
export * from "./unique-code";
|
export * from "./unique-code";
|
||||||
|
@ -154,7 +154,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
{passwordSupport}
|
{passwordSupport}
|
||||||
</div>
|
</div>
|
||||||
{mode === EAuthModes.SIGN_UP && getPasswordStrength(passwordFormData.password) >= 3 && (
|
{mode === EAuthModes.SIGN_UP && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
|
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
|
||||||
Confirm password
|
Confirm password
|
||||||
|
@ -23,16 +23,15 @@ import { WorkspaceService } from "@/services/workspace.service";
|
|||||||
const authService = new AuthService();
|
const authService = new AuthService();
|
||||||
const workSpaceService = new WorkspaceService();
|
const workSpaceService = new WorkspaceService();
|
||||||
|
|
||||||
|
export enum EAuthModes {
|
||||||
|
SIGN_IN = "SIGN_IN",
|
||||||
|
SIGN_UP = "SIGN_UP",
|
||||||
|
}
|
||||||
|
|
||||||
export enum EAuthSteps {
|
export enum EAuthSteps {
|
||||||
EMAIL = "EMAIL",
|
EMAIL = "EMAIL",
|
||||||
PASSWORD = "PASSWORD",
|
PASSWORD = "PASSWORD",
|
||||||
UNIQUE_CODE = "UNIQUE_CODE",
|
UNIQUE_CODE = "UNIQUE_CODE",
|
||||||
OPTIONAL_SET_PASSWORD = "OPTIONAL_SET_PASSWORD",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum EAuthModes {
|
|
||||||
SIGN_IN = "SIGN_IN",
|
|
||||||
SIGN_UP = "SIGN_UP",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -53,10 +52,6 @@ const Titles = {
|
|||||||
header: "Sign in to Plane",
|
header: "Sign in to Plane",
|
||||||
subHeader: "Get back to your projects and make progress",
|
subHeader: "Get back to your projects and make progress",
|
||||||
},
|
},
|
||||||
[EAuthSteps.OPTIONAL_SET_PASSWORD]: {
|
|
||||||
header: "",
|
|
||||||
subHeader: "",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
[EAuthModes.SIGN_UP]: {
|
[EAuthModes.SIGN_UP]: {
|
||||||
[EAuthSteps.EMAIL]: {
|
[EAuthSteps.EMAIL]: {
|
||||||
@ -71,10 +66,6 @@ const Titles = {
|
|||||||
header: "Create your account",
|
header: "Create your account",
|
||||||
subHeader: "Progress, visualize, and measure work how it works best for you.",
|
subHeader: "Progress, visualize, and measure work how it works best for you.",
|
||||||
},
|
},
|
||||||
[EAuthSteps.OPTIONAL_SET_PASSWORD]: {
|
|
||||||
header: "",
|
|
||||||
subHeader: "",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -101,11 +92,11 @@ const getHeaderSubHeader = (
|
|||||||
return Titles[mode][step];
|
return Titles[mode][step];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AuthRoot = observer((props: Props) => {
|
export const SignInAuthRoot = observer((props: Props) => {
|
||||||
const { mode } = props;
|
const { mode } = props;
|
||||||
//router
|
//router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { email: emailParam, invitation_id, slug } = router.query;
|
const { email: emailParam, invitation_id, slug: workspaceSlug } = router.query;
|
||||||
// states
|
// states
|
||||||
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
|
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
|
||||||
const [email, setEmail] = useState(emailParam ? emailParam.toString() : "");
|
const [email, setEmail] = useState(emailParam ? emailParam.toString() : "");
|
||||||
@ -127,10 +118,10 @@ export const AuthRoot = observer((props: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (invitation_id && slug) {
|
if (invitation_id && workspaceSlug) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
workSpaceService
|
workSpaceService
|
||||||
.getWorkspaceInvitation(slug.toString(), invitation_id.toString())
|
.getWorkspaceInvitation(workspaceSlug.toString(), invitation_id.toString())
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setInvitation(res);
|
setInvitation(res);
|
||||||
})
|
})
|
||||||
@ -141,7 +132,7 @@ export const AuthRoot = observer((props: Props) => {
|
|||||||
} else {
|
} else {
|
||||||
setInvitation(undefined);
|
setInvitation(undefined);
|
||||||
}
|
}
|
||||||
}, [invitation_id, slug]);
|
}, [invitation_id, workspaceSlug]);
|
||||||
|
|
||||||
const { header, subHeader } = getHeaderSubHeader(authStep, mode, invitation, email);
|
const { header, subHeader } = getHeaderSubHeader(authStep, mode, invitation, email);
|
||||||
|
|
||||||
@ -205,6 +196,7 @@ export const AuthRoot = observer((props: Props) => {
|
|||||||
<p className="font-medium text-onboarding-text-400">{subHeader}</p>
|
<p className="font-medium text-onboarding-text-400">{subHeader}</p>
|
||||||
</div>
|
</div>
|
||||||
{authStep === EAuthSteps.EMAIL && <AuthEmailForm defaultEmail={email} onSubmit={handleEmailVerification} />}
|
{authStep === EAuthSteps.EMAIL && <AuthEmailForm defaultEmail={email} onSubmit={handleEmailVerification} />}
|
||||||
|
|
||||||
{authStep === EAuthSteps.UNIQUE_CODE && (
|
{authStep === EAuthSteps.UNIQUE_CODE && (
|
||||||
<UniqueCodeForm
|
<UniqueCodeForm
|
||||||
email={email}
|
email={email}
|
||||||
@ -216,6 +208,7 @@ export const AuthRoot = observer((props: Props) => {
|
|||||||
mode={mode}
|
mode={mode}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{authStep === EAuthSteps.PASSWORD && (
|
{authStep === EAuthSteps.PASSWORD && (
|
||||||
<AuthPasswordForm
|
<AuthPasswordForm
|
||||||
email={email}
|
email={email}
|
||||||
@ -228,8 +221,7 @@ export const AuthRoot = observer((props: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isOAuthEnabled && authStep !== EAuthSteps.OPTIONAL_SET_PASSWORD && <OAuthOptions />}
|
{isOAuthEnabled && <OAuthOptions />}
|
||||||
|
|
||||||
<TermsAndConditions isSignUp={mode === EAuthModes.SIGN_UP} />
|
<TermsAndConditions isSignUp={mode === EAuthModes.SIGN_UP} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
125
web/components/account/auth-forms/sign-up-root.tsx
Normal file
125
web/components/account/auth-forms/sign-up-root.tsx
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import { FC, useState } from "react";
|
||||||
|
import isEmpty from "lodash/isEmpty";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
// types
|
||||||
|
import { IEmailCheckData } from "@plane/types";
|
||||||
|
// ui
|
||||||
|
import { Spinner, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
|
// components
|
||||||
|
import {
|
||||||
|
AuthHeader,
|
||||||
|
AuthBanner,
|
||||||
|
AuthEmailForm,
|
||||||
|
AuthPasswordForm,
|
||||||
|
OAuthOptions,
|
||||||
|
TermsAndConditions,
|
||||||
|
UniqueCodeForm,
|
||||||
|
} from "@/components/account";
|
||||||
|
// helpers
|
||||||
|
import { EAuthModes, EAuthSteps, EErrorAlertType, TAuthErrorInfo } from "@/helpers/authentication.helper";
|
||||||
|
// hooks
|
||||||
|
import { useInstance } from "@/hooks/store";
|
||||||
|
// services
|
||||||
|
import { AuthService } from "@/services/auth.service";
|
||||||
|
|
||||||
|
// service initialization
|
||||||
|
const authService = new AuthService();
|
||||||
|
|
||||||
|
export const SignUpAuthRoot: FC = observer(() => {
|
||||||
|
//router
|
||||||
|
const router = useRouter();
|
||||||
|
const { email: emailParam, invitation_id, slug: workspaceSlug } = router.query;
|
||||||
|
// states
|
||||||
|
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
|
||||||
|
const [email, setEmail] = useState(emailParam ? emailParam.toString() : "");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
|
||||||
|
// hooks
|
||||||
|
const { instance } = useInstance();
|
||||||
|
// derived values
|
||||||
|
const authMode = EAuthModes.SIGN_UP;
|
||||||
|
const isSmtpConfigured = instance?.config?.is_smtp_configured;
|
||||||
|
|
||||||
|
// const redirectToSignUp = (email: string) => {
|
||||||
|
// if (isEmpty(email)) router.push({ pathname: "/", query: router.query });
|
||||||
|
// else router.push({ pathname: "/", query: { ...router.query, email: email } });
|
||||||
|
// };
|
||||||
|
|
||||||
|
// step 1 - email verification
|
||||||
|
const handleEmailVerification = async (data: IEmailCheckData) => {
|
||||||
|
setEmail(data.email);
|
||||||
|
await authService
|
||||||
|
.signUpEmailCheck(data)
|
||||||
|
.then(() => {
|
||||||
|
if (isSmtpConfigured) setAuthStep(EAuthSteps.UNIQUE_CODE);
|
||||||
|
else setAuthStep(EAuthSteps.PASSWORD);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log("error", err);
|
||||||
|
setToast({
|
||||||
|
type: TOAST_TYPE.ERROR,
|
||||||
|
title: "Error!",
|
||||||
|
message: err?.error_message ?? "Something went wrong. Please try again.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isOAuthEnabled =
|
||||||
|
instance?.config && (instance?.config?.is_google_enabled || instance?.config?.is_github_enabled);
|
||||||
|
|
||||||
|
if (isLoading)
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="relative max-w-lg mx-auto flex flex-col space-y-6">
|
||||||
|
<AuthHeader
|
||||||
|
workspaceSlug={workspaceSlug?.toString() || undefined}
|
||||||
|
invitationId={invitation_id?.toString() || undefined}
|
||||||
|
invitationEmail={emailParam?.toString() || undefined}
|
||||||
|
authMode={EAuthModes.SIGN_UP}
|
||||||
|
currentAuthStep={authStep}
|
||||||
|
handleLoader={setIsLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && (
|
||||||
|
<AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{authStep === EAuthSteps.EMAIL && <AuthEmailForm defaultEmail={email} onSubmit={handleEmailVerification} />}
|
||||||
|
|
||||||
|
{authStep === EAuthSteps.UNIQUE_CODE && (
|
||||||
|
<UniqueCodeForm
|
||||||
|
email={email}
|
||||||
|
handleEmailClear={() => {
|
||||||
|
setEmail("");
|
||||||
|
setAuthStep(EAuthSteps.EMAIL);
|
||||||
|
}}
|
||||||
|
submitButtonText="Continue"
|
||||||
|
mode={authMode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{authStep === EAuthSteps.PASSWORD && (
|
||||||
|
<AuthPasswordForm
|
||||||
|
email={email}
|
||||||
|
handleEmailClear={() => {
|
||||||
|
setEmail("");
|
||||||
|
setAuthStep(EAuthSteps.EMAIL);
|
||||||
|
}}
|
||||||
|
handleStepChange={(step) => setAuthStep(step)}
|
||||||
|
mode={authMode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isOAuthEnabled && <OAuthOptions />}
|
||||||
|
<TermsAndConditions isSignUp={authMode === EAuthModes.SIGN_UP} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -11,7 +11,7 @@ import { API_BASE_URL } from "@/helpers/common.helper";
|
|||||||
import useTimer from "@/hooks/use-timer";
|
import useTimer from "@/hooks/use-timer";
|
||||||
// services
|
// services
|
||||||
import { AuthService } from "@/services/auth.service";
|
import { AuthService } from "@/services/auth.service";
|
||||||
import { EAuthModes } from "./root";
|
import { EAuthModes } from "./sign-up-root";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
email: string;
|
email: string;
|
||||||
|
@ -6,7 +6,7 @@ import Link from "next/link";
|
|||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { Spinner } from "@plane/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { AuthRoot, EAuthModes } from "@/components/account";
|
import { SignUpAuthRoot } from "@/components/account";
|
||||||
import { PageHead } from "@/components/core";
|
import { PageHead } from "@/components/core";
|
||||||
// constants
|
// constants
|
||||||
import { NAVIGATE_TO_SIGNIN } from "@/constants/event-tracker";
|
import { NAVIGATE_TO_SIGNIN } from "@/constants/event-tracker";
|
||||||
@ -69,7 +69,7 @@ export const SignUpView = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mx-auto h-full">
|
<div className="mx-auto h-full">
|
||||||
<div className="h-full overflow-auto px-7 pb-56 pt-4 sm:px-0">
|
<div className="h-full overflow-auto px-7 pb-56 pt-4 sm:px-0">
|
||||||
<AuthRoot mode={EAuthModes.SIGN_UP} />
|
<SignUpAuthRoot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,53 +1,108 @@
|
|||||||
export enum ESignUpEMailCheck {
|
export enum EAuthModes {
|
||||||
INSTANCE_NOT_CONFIGURED = "INSTANCE_NOT_CONFIGURED",
|
SIGN_IN = "SIGN_IN",
|
||||||
USER_ALREADY_EXIST = "USER_ALREADY_EXIST",
|
SIGN_UP = "SIGN_UP",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ESignUp {
|
export enum EAuthSteps {
|
||||||
|
EMAIL = "EMAIL",
|
||||||
|
PASSWORD = "PASSWORD",
|
||||||
|
UNIQUE_CODE = "UNIQUE_CODE",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EAuthenticationErrorCodes {
|
||||||
|
// alert errors
|
||||||
INSTANCE_NOT_CONFIGURED = "INSTANCE_NOT_CONFIGURED",
|
INSTANCE_NOT_CONFIGURED = "INSTANCE_NOT_CONFIGURED",
|
||||||
REQUIRED_EMAIL_PASSWORD = "REQUIRED_EMAIL_PASSWORD",
|
SMTP_NOT_CONFIGURED = "SMTP_NOT_CONFIGURED",
|
||||||
|
AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED",
|
||||||
|
INVALID_TOKEN = "INVALID_TOKEN",
|
||||||
|
EXPIRED_TOKEN = "EXPIRED_TOKEN",
|
||||||
|
IMPROPERLY_CONFIGURED = "IMPROPERLY_CONFIGURED",
|
||||||
|
OAUTH_PROVIDER_ERROR = "OAUTH_PROVIDER_ERROR",
|
||||||
|
// banner errors
|
||||||
INVALID_EMAIL = "INVALID_EMAIL",
|
INVALID_EMAIL = "INVALID_EMAIL",
|
||||||
|
INVALID_PASSWORD = "INVALID_PASSWORD",
|
||||||
|
USER_DOES_NOT_EXIST = "USER_DOES_NOT_EXIST",
|
||||||
|
ADMIN_ALREADY_EXIST = "ADMIN_ALREADY_EXIST",
|
||||||
USER_ALREADY_EXIST = "USER_ALREADY_EXIST",
|
USER_ALREADY_EXIST = "USER_ALREADY_EXIST",
|
||||||
}
|
// inline errors from backend
|
||||||
|
REQUIRED_EMAIL_PASSWORD_FIRST_NAME = "REQUIRED_EMAIL_PASSWORD_FIRST_NAME",
|
||||||
export enum ESignInEMailCheck {
|
|
||||||
INSTANCE_NOT_CONFIGURED = "INSTANCE_NOT_CONFIGURED",
|
|
||||||
REQUIRED_EMAIL_PASSWORD = "REQUIRED_EMAIL_PASSWORD",
|
REQUIRED_EMAIL_PASSWORD = "REQUIRED_EMAIL_PASSWORD",
|
||||||
INVALID_EMAIL = "INVALID_EMAIL",
|
EMAIL_CODE_REQUIRED = "EMAIL_CODE_REQUIRED",
|
||||||
USER_ALREADY_EXIST = "USER_ALREADY_EXIST",
|
// inline local errors
|
||||||
|
INLINE_EMAIL = "INLINE_EMAIL",
|
||||||
|
INLINE_PASSWORD = "INLINE_PASSWORD",
|
||||||
|
INLINE_FIRST_NAME = "INLINE_FIRST_NAME",
|
||||||
|
INLINE_EMAIL_CODE = "INLINE_EMAIL_CODE",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ESignIn {
|
|
||||||
INSTANCE_NOT_CONFIGURED = "INSTANCE_NOT_CONFIGURED",
|
|
||||||
REQUIRED_EMAIL_PASSWORD = "REQUIRED_EMAIL_PASSWORD",
|
|
||||||
INVALID_EMAIL = "INVALID_EMAIL",
|
|
||||||
USER_ALREADY_EXIST = "USER_ALREADY_EXIST",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TErrorTypes = ESignUpEMailCheck | ESignUp | ESignInEMailCheck | ESignIn;
|
|
||||||
|
|
||||||
export enum EErrorAlertType {
|
export enum EErrorAlertType {
|
||||||
BANNER_ALERT = "BANNER_ALERT",
|
BANNER_ALERT = "BANNER_ALERT",
|
||||||
TOAST_ALERT = "TOAST_ALERT",
|
TOAST_ALERT = "TOAST_ALERT",
|
||||||
INLINE_FIRST_NAME = "INLINE_FIRST_NAME",
|
INLINE_FIRST_NAME = "INLINE_FIRST_NAME",
|
||||||
INLINE_EMAIL = "INLINE_EMAIL",
|
INLINE_EMAIL = "INLINE_EMAIL",
|
||||||
INLINE_PASSWORD = "INLINE_PASSWORD",
|
INLINE_PASSWORD = "INLINE_PASSWORD",
|
||||||
|
INLINE_EMAIL_CODE = "INLINE_EMAIL_CODE",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const errorHandler = (
|
export type TAuthErrorInfo = { type: EErrorAlertType; message: string };
|
||||||
errorType: TErrorTypes,
|
|
||||||
errorMessage: string | undefined
|
|
||||||
): { type: EErrorAlertType | undefined; message: string | undefined } => {
|
|
||||||
const errorPayload = {
|
|
||||||
type: undefined,
|
|
||||||
message: errorMessage || undefined,
|
|
||||||
};
|
|
||||||
const signUpErrorTypes = [""];
|
|
||||||
const signInErrorTypes = [""];
|
|
||||||
|
|
||||||
console.log("errorType", errorType);
|
export const errorHandler = (errorType: EAuthenticationErrorCodes, errorMessage: string | undefined) => {
|
||||||
console.log("signUpErrorTypes", signUpErrorTypes);
|
const toastAlertErrorCodes = [
|
||||||
console.log("signInErrorTypes", signInErrorTypes);
|
EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED,
|
||||||
|
EAuthenticationErrorCodes.SMTP_NOT_CONFIGURED,
|
||||||
|
EAuthenticationErrorCodes.AUTHENTICATION_FAILED,
|
||||||
|
EAuthenticationErrorCodes.INVALID_TOKEN,
|
||||||
|
EAuthenticationErrorCodes.EXPIRED_TOKEN,
|
||||||
|
EAuthenticationErrorCodes.IMPROPERLY_CONFIGURED,
|
||||||
|
EAuthenticationErrorCodes.OAUTH_PROVIDER_ERROR,
|
||||||
|
];
|
||||||
|
const bannerAlertErrorCodes = [
|
||||||
|
EAuthenticationErrorCodes.USER_DOES_NOT_EXIST,
|
||||||
|
EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST,
|
||||||
|
EAuthenticationErrorCodes.USER_ALREADY_EXIST,
|
||||||
|
];
|
||||||
|
const inlineFirstNameErrorCodes = [EAuthenticationErrorCodes.INLINE_FIRST_NAME];
|
||||||
|
const inlineEmailErrorCodes = [EAuthenticationErrorCodes.INLINE_EMAIL];
|
||||||
|
const inlineEmailCodeErrorCodes = [EAuthenticationErrorCodes.INLINE_EMAIL_CODE];
|
||||||
|
const inlinePasswordErrorCodes = [EAuthenticationErrorCodes.INLINE_PASSWORD];
|
||||||
|
|
||||||
|
let errorPayload: TAuthErrorInfo | undefined = undefined;
|
||||||
|
|
||||||
|
if (toastAlertErrorCodes.includes(errorType))
|
||||||
|
errorPayload = {
|
||||||
|
type: EErrorAlertType.TOAST_ALERT,
|
||||||
|
message: errorMessage || "Something went wrong",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (bannerAlertErrorCodes.includes(errorType))
|
||||||
|
errorPayload = {
|
||||||
|
type: EErrorAlertType.BANNER_ALERT,
|
||||||
|
message: errorMessage || "Something went wrong",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (inlineFirstNameErrorCodes.includes(errorType))
|
||||||
|
errorPayload = {
|
||||||
|
type: EErrorAlertType.INLINE_FIRST_NAME,
|
||||||
|
message: errorMessage || "Something went wrong",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (inlineEmailErrorCodes.includes(errorType))
|
||||||
|
errorPayload = {
|
||||||
|
type: EErrorAlertType.INLINE_EMAIL,
|
||||||
|
message: errorMessage || "Something went wrong",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (inlinePasswordErrorCodes.includes(errorType))
|
||||||
|
errorPayload = {
|
||||||
|
type: EErrorAlertType.INLINE_PASSWORD,
|
||||||
|
message: errorMessage || "Something went wrong",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (inlineEmailCodeErrorCodes.includes(errorType))
|
||||||
|
errorPayload = {
|
||||||
|
type: EErrorAlertType.INLINE_EMAIL_CODE,
|
||||||
|
message: errorMessage || "Something went wrong",
|
||||||
|
};
|
||||||
|
|
||||||
return errorPayload;
|
return errorPayload;
|
||||||
};
|
};
|
||||||
|
@ -14,7 +14,7 @@ import { resolveGeneralTheme } from "@/helpers/theme.helper";
|
|||||||
// hooks
|
// hooks
|
||||||
import { useInstance, useWorkspace, useUser } from "@/hooks/store";
|
import { useInstance, useWorkspace, useUser } from "@/hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
import InstanceLayout from "@/layouts/instance-layout";
|
import InstanceLayout from "@/lib/wrappers/instance-wrapper";
|
||||||
// dynamic imports
|
// dynamic imports
|
||||||
const StoreWrapper = dynamic(() => import("@/lib/wrappers/store-wrapper"), { ssr: false });
|
const StoreWrapper = dynamic(() => import("@/lib/wrappers/store-wrapper"), { ssr: false });
|
||||||
const PostHogProvider = dynamic(() => import("@/lib/posthog-provider"), { ssr: false });
|
const PostHogProvider = dynamic(() => import("@/lib/posthog-provider"), { ssr: false });
|
||||||
|
14
web/lib/wrappers/authentication-wrapper.tsx
Normal file
14
web/lib/wrappers/authentication-wrapper.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { FC, ReactNode } from "react";
|
||||||
|
|
||||||
|
type TPageType = "public" | "onboarding" | "private";
|
||||||
|
|
||||||
|
type TAuthenticationWrapper = {
|
||||||
|
children: ReactNode;
|
||||||
|
pageType: TPageType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AuthenticationWrapper: FC<TAuthenticationWrapper> = (props) => {
|
||||||
|
const { children, pageType } = props;
|
||||||
|
|
||||||
|
return <div key={pageType}>{children}</div>;
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import { FC, ReactNode, useState } from "react";
|
import { FC, ReactNode } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// ui
|
// ui
|
||||||
@ -8,17 +8,14 @@ import { InstanceNotReady } from "@/components/instance";
|
|||||||
// hooks
|
// hooks
|
||||||
import { useInstance } from "@/hooks/store";
|
import { useInstance } from "@/hooks/store";
|
||||||
|
|
||||||
type TInstanceLayout = {
|
type TInstanceWrapper = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const InstanceLayout: FC<TInstanceLayout> = observer((props) => {
|
const InstanceWrapper: FC<TInstanceWrapper> = observer((props) => {
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
// store
|
// store
|
||||||
const { isLoading, instance, error, fetchInstanceInfo } = useInstance();
|
const { isLoading, instance, error, fetchInstanceInfo } = useInstance();
|
||||||
// states
|
|
||||||
const [isGodModeEnabled, setIsGodModeEnabled] = useState(false);
|
|
||||||
const handleGodModeStateChange = (state: boolean) => setIsGodModeEnabled(state);
|
|
||||||
|
|
||||||
useSWR("INSTANCE_INFORMATION", () => fetchInstanceInfo(), {
|
useSWR("INSTANCE_INFORMATION", () => fetchInstanceInfo(), {
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
@ -44,11 +41,9 @@ const InstanceLayout: FC<TInstanceLayout> = observer((props) => {
|
|||||||
if (error && !error?.data?.is_activated) return <InstanceNotReady isGodModeEnabled={false} />;
|
if (error && !error?.data?.is_activated) return <InstanceNotReady isGodModeEnabled={false} />;
|
||||||
|
|
||||||
// instance is not ready and setup is not done
|
// instance is not ready and setup is not done
|
||||||
if (instance?.instance?.is_setup_done === false)
|
if (instance?.instance?.is_setup_done === false) return <InstanceNotReady isGodModeEnabled />;
|
||||||
// if (isGodModeEnabled) return <MiniGodModeForm />;
|
|
||||||
return <InstanceNotReady isGodModeEnabled handleGodModeStateChange={handleGodModeStateChange} />;
|
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
});
|
});
|
||||||
|
|
||||||
export default InstanceLayout;
|
export default InstanceWrapper;
|
@ -6,7 +6,7 @@ import Link from "next/link";
|
|||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { Spinner } from "@plane/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { AuthRoot, EAuthModes } from "@/components/account";
|
import { SignInAuthRoot } from "@/components/account";
|
||||||
import { PageHead } from "@/components/core";
|
import { PageHead } from "@/components/core";
|
||||||
// constants
|
// constants
|
||||||
import { NAVIGATE_TO_SIGNUP } from "@/constants/event-tracker";
|
import { NAVIGATE_TO_SIGNUP } from "@/constants/event-tracker";
|
||||||
@ -74,7 +74,7 @@ const SignInPage: NextPageWithLayout = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mx-auto h-full">
|
<div className="mx-auto h-full">
|
||||||
<div className="h-full overflow-auto px-7 pb-56 pt-4 sm:px-0">
|
<div className="h-full overflow-auto px-7 pb-56 pt-4 sm:px-0">
|
||||||
<AuthRoot mode={EAuthModes.SIGN_IN} />
|
<SignInAuthRoot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user