chore: authentication workflow changes

This commit is contained in:
gurusainath 2024-04-30 17:04:49 +05:30
parent c480ea34de
commit 49fe79367a
18 changed files with 443 additions and 141 deletions

View File

@ -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)

View File

@ -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

View File

@ -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):

View File

@ -26,6 +26,6 @@ export interface IPasswordSignInData {
password: string; password: string;
} }
export interface ICsrfTokenData { export interface ICsrfTokenData {
csrf_token: string; csrf_token: string;
}; }

View 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>
);
};

View 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>
);
};

View File

@ -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>

View File

@ -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";

View File

@ -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

View File

@ -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} />
</> </>
); );

View 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>
</>
);
});

View File

@ -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;

View File

@ -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>

View File

@ -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;
}; };

View File

@ -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 });

View 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>;
};

View File

@ -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;

View File

@ -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>