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:
|
||||
# Redirection params
|
||||
params = {
|
||||
"error_code": "REQUIRED_EMAIL_PASSWORD",
|
||||
"error_message": "Both email and password are required",
|
||||
"error_code": "INSTANCE_NOT_CONFIGURED",
|
||||
"error_message": "Instance is not configured",
|
||||
}
|
||||
if 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.license.models import Instance
|
||||
from plane.authentication.utils.host import base_host
|
||||
from plane.db.models import User
|
||||
from plane.db.models import User, Profile
|
||||
|
||||
|
||||
class MagicGenerateEndpoint(APIView):
|
||||
@ -123,11 +123,12 @@ class MagicSignInEndpoint(View):
|
||||
request=request, key=f"magic_{email}", code=code
|
||||
)
|
||||
user = provider.authenticate()
|
||||
profile = Profile.objects.get(user=user)
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user)
|
||||
# Process workspace and project invitations
|
||||
process_workspace_project_invitations(user=user)
|
||||
if user.is_password_autoset:
|
||||
if user.is_password_autoset and profile.is_onboarded:
|
||||
path = "accounts/set-password"
|
||||
else:
|
||||
# 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.license.models import Instance
|
||||
from plane.authentication.utils.host import base_host
|
||||
from plane.db.models import User
|
||||
from plane.db.models import User, Profile
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export interface ICsrfTokenData {
|
||||
export interface ICsrfTokenData {
|
||||
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 { Controller, useForm } from "react-hook-form";
|
||||
// icons
|
||||
import { CircleAlert, XCircle } from "lucide-react";
|
||||
// types
|
||||
@ -10,82 +9,72 @@ import { Button, Input } from "@plane/ui";
|
||||
// helpers
|
||||
import { checkEmailValidity } from "@/helpers/string.helper";
|
||||
|
||||
type Props = {
|
||||
onSubmit: (data: IEmailCheckData) => Promise<void>;
|
||||
type TAuthEmailForm = {
|
||||
defaultEmail: string;
|
||||
onSubmit: (data: IEmailCheckData) => Promise<void>;
|
||||
};
|
||||
|
||||
type TEmailFormValues = {
|
||||
email: string;
|
||||
};
|
||||
|
||||
export const AuthEmailForm: React.FC<Props> = observer((props) => {
|
||||
export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
|
||||
const { onSubmit, defaultEmail } = props;
|
||||
// hooks
|
||||
const {
|
||||
control,
|
||||
formState: { errors, isSubmitting, isValid },
|
||||
handleSubmit,
|
||||
} = useForm<TEmailFormValues>({
|
||||
defaultValues: {
|
||||
email: defaultEmail,
|
||||
},
|
||||
mode: "onChange",
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
// states
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [email, setEmail] = useState(defaultEmail);
|
||||
|
||||
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 = {
|
||||
email: data.email,
|
||||
email: email,
|
||||
};
|
||||
onSubmit(payload);
|
||||
await onSubmit(payload);
|
||||
setIsSubmitting(false);
|
||||
};
|
||||
|
||||
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">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
|
||||
Email
|
||||
</label>
|
||||
<Controller
|
||||
control={control}
|
||||
name="email"
|
||||
rules={{
|
||||
required: "Email is required",
|
||||
validate: (value) => checkEmailValidity(value) || "Email is invalid",
|
||||
}}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
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 className="relative flex items-center rounded-md bg-onboarding-background-200">
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
hasError={Boolean(emailError?.email)}
|
||||
placeholder="name@company.com"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400"
|
||||
autoFocus
|
||||
/>
|
||||
{email.length > 0 && (
|
||||
<XCircle
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => setEmail("")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</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>
|
||||
<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
|
||||
</Button>
|
||||
</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 "./forgot-password-popover";
|
||||
export * from "./password";
|
||||
export * from "./root";
|
||||
export * from "./unique-code";
|
||||
|
@ -154,7 +154,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
</div>
|
||||
{passwordSupport}
|
||||
</div>
|
||||
{mode === EAuthModes.SIGN_UP && getPasswordStrength(passwordFormData.password) >= 3 && (
|
||||
{mode === EAuthModes.SIGN_UP && (
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
|
||||
Confirm password
|
||||
|
@ -23,16 +23,15 @@ import { WorkspaceService } from "@/services/workspace.service";
|
||||
const authService = new AuthService();
|
||||
const workSpaceService = new WorkspaceService();
|
||||
|
||||
export enum EAuthModes {
|
||||
SIGN_IN = "SIGN_IN",
|
||||
SIGN_UP = "SIGN_UP",
|
||||
}
|
||||
|
||||
export enum EAuthSteps {
|
||||
EMAIL = "EMAIL",
|
||||
PASSWORD = "PASSWORD",
|
||||
UNIQUE_CODE = "UNIQUE_CODE",
|
||||
OPTIONAL_SET_PASSWORD = "OPTIONAL_SET_PASSWORD",
|
||||
}
|
||||
|
||||
export enum EAuthModes {
|
||||
SIGN_IN = "SIGN_IN",
|
||||
SIGN_UP = "SIGN_UP",
|
||||
}
|
||||
|
||||
type Props = {
|
||||
@ -53,10 +52,6 @@ const Titles = {
|
||||
header: "Sign in to Plane",
|
||||
subHeader: "Get back to your projects and make progress",
|
||||
},
|
||||
[EAuthSteps.OPTIONAL_SET_PASSWORD]: {
|
||||
header: "",
|
||||
subHeader: "",
|
||||
},
|
||||
},
|
||||
[EAuthModes.SIGN_UP]: {
|
||||
[EAuthSteps.EMAIL]: {
|
||||
@ -71,10 +66,6 @@ const Titles = {
|
||||
header: "Create your account",
|
||||
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];
|
||||
};
|
||||
|
||||
export const AuthRoot = observer((props: Props) => {
|
||||
export const SignInAuthRoot = observer((props: Props) => {
|
||||
const { mode } = props;
|
||||
//router
|
||||
const router = useRouter();
|
||||
const { email: emailParam, invitation_id, slug } = router.query;
|
||||
const { email: emailParam, invitation_id, slug: workspaceSlug } = router.query;
|
||||
// states
|
||||
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
|
||||
const [email, setEmail] = useState(emailParam ? emailParam.toString() : "");
|
||||
@ -127,10 +118,10 @@ export const AuthRoot = observer((props: Props) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (invitation_id && slug) {
|
||||
if (invitation_id && workspaceSlug) {
|
||||
setIsLoading(true);
|
||||
workSpaceService
|
||||
.getWorkspaceInvitation(slug.toString(), invitation_id.toString())
|
||||
.getWorkspaceInvitation(workspaceSlug.toString(), invitation_id.toString())
|
||||
.then((res) => {
|
||||
setInvitation(res);
|
||||
})
|
||||
@ -141,7 +132,7 @@ export const AuthRoot = observer((props: Props) => {
|
||||
} else {
|
||||
setInvitation(undefined);
|
||||
}
|
||||
}, [invitation_id, slug]);
|
||||
}, [invitation_id, workspaceSlug]);
|
||||
|
||||
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>
|
||||
</div>
|
||||
{authStep === EAuthSteps.EMAIL && <AuthEmailForm defaultEmail={email} onSubmit={handleEmailVerification} />}
|
||||
|
||||
{authStep === EAuthSteps.UNIQUE_CODE && (
|
||||
<UniqueCodeForm
|
||||
email={email}
|
||||
@ -216,6 +208,7 @@ export const AuthRoot = observer((props: Props) => {
|
||||
mode={mode}
|
||||
/>
|
||||
)}
|
||||
|
||||
{authStep === EAuthSteps.PASSWORD && (
|
||||
<AuthPasswordForm
|
||||
email={email}
|
||||
@ -228,8 +221,7 @@ export const AuthRoot = observer((props: Props) => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isOAuthEnabled && authStep !== EAuthSteps.OPTIONAL_SET_PASSWORD && <OAuthOptions />}
|
||||
|
||||
{isOAuthEnabled && <OAuthOptions />}
|
||||
<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";
|
||||
// services
|
||||
import { AuthService } from "@/services/auth.service";
|
||||
import { EAuthModes } from "./root";
|
||||
import { EAuthModes } from "./sign-up-root";
|
||||
|
||||
type Props = {
|
||||
email: string;
|
||||
|
@ -6,7 +6,7 @@ import Link from "next/link";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Spinner } from "@plane/ui";
|
||||
// components
|
||||
import { AuthRoot, EAuthModes } from "@/components/account";
|
||||
import { SignUpAuthRoot } from "@/components/account";
|
||||
import { PageHead } from "@/components/core";
|
||||
// constants
|
||||
import { NAVIGATE_TO_SIGNIN } from "@/constants/event-tracker";
|
||||
@ -69,7 +69,7 @@ export const SignUpView = observer(() => {
|
||||
</div>
|
||||
<div className="mx-auto h-full">
|
||||
<div className="h-full overflow-auto px-7 pb-56 pt-4 sm:px-0">
|
||||
<AuthRoot mode={EAuthModes.SIGN_UP} />
|
||||
<SignUpAuthRoot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,53 +1,108 @@
|
||||
export enum ESignUpEMailCheck {
|
||||
INSTANCE_NOT_CONFIGURED = "INSTANCE_NOT_CONFIGURED",
|
||||
USER_ALREADY_EXIST = "USER_ALREADY_EXIST",
|
||||
export enum EAuthModes {
|
||||
SIGN_IN = "SIGN_IN",
|
||||
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",
|
||||
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_PASSWORD = "INVALID_PASSWORD",
|
||||
USER_DOES_NOT_EXIST = "USER_DOES_NOT_EXIST",
|
||||
ADMIN_ALREADY_EXIST = "ADMIN_ALREADY_EXIST",
|
||||
USER_ALREADY_EXIST = "USER_ALREADY_EXIST",
|
||||
}
|
||||
|
||||
export enum ESignInEMailCheck {
|
||||
INSTANCE_NOT_CONFIGURED = "INSTANCE_NOT_CONFIGURED",
|
||||
// inline errors from backend
|
||||
REQUIRED_EMAIL_PASSWORD_FIRST_NAME = "REQUIRED_EMAIL_PASSWORD_FIRST_NAME",
|
||||
REQUIRED_EMAIL_PASSWORD = "REQUIRED_EMAIL_PASSWORD",
|
||||
INVALID_EMAIL = "INVALID_EMAIL",
|
||||
USER_ALREADY_EXIST = "USER_ALREADY_EXIST",
|
||||
EMAIL_CODE_REQUIRED = "EMAIL_CODE_REQUIRED",
|
||||
// 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 {
|
||||
BANNER_ALERT = "BANNER_ALERT",
|
||||
TOAST_ALERT = "TOAST_ALERT",
|
||||
INLINE_FIRST_NAME = "INLINE_FIRST_NAME",
|
||||
INLINE_EMAIL = "INLINE_EMAIL",
|
||||
INLINE_PASSWORD = "INLINE_PASSWORD",
|
||||
INLINE_EMAIL_CODE = "INLINE_EMAIL_CODE",
|
||||
}
|
||||
|
||||
export const errorHandler = (
|
||||
errorType: TErrorTypes,
|
||||
errorMessage: string | undefined
|
||||
): { type: EErrorAlertType | undefined; message: string | undefined } => {
|
||||
const errorPayload = {
|
||||
type: undefined,
|
||||
message: errorMessage || undefined,
|
||||
};
|
||||
const signUpErrorTypes = [""];
|
||||
const signInErrorTypes = [""];
|
||||
export type TAuthErrorInfo = { type: EErrorAlertType; message: string };
|
||||
|
||||
console.log("errorType", errorType);
|
||||
console.log("signUpErrorTypes", signUpErrorTypes);
|
||||
console.log("signInErrorTypes", signInErrorTypes);
|
||||
export const errorHandler = (errorType: EAuthenticationErrorCodes, errorMessage: string | undefined) => {
|
||||
const toastAlertErrorCodes = [
|
||||
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;
|
||||
};
|
||||
|
@ -14,7 +14,7 @@ import { resolveGeneralTheme } from "@/helpers/theme.helper";
|
||||
// hooks
|
||||
import { useInstance, useWorkspace, useUser } from "@/hooks/store";
|
||||
// layouts
|
||||
import InstanceLayout from "@/layouts/instance-layout";
|
||||
import InstanceLayout from "@/lib/wrappers/instance-wrapper";
|
||||
// dynamic imports
|
||||
const StoreWrapper = dynamic(() => import("@/lib/wrappers/store-wrapper"), { 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 useSWR from "swr";
|
||||
// ui
|
||||
@ -8,17 +8,14 @@ import { InstanceNotReady } from "@/components/instance";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
type TInstanceLayout = {
|
||||
type TInstanceWrapper = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const InstanceLayout: FC<TInstanceLayout> = observer((props) => {
|
||||
const InstanceWrapper: FC<TInstanceWrapper> = observer((props) => {
|
||||
const { children } = props;
|
||||
// store
|
||||
const { isLoading, instance, error, fetchInstanceInfo } = useInstance();
|
||||
// states
|
||||
const [isGodModeEnabled, setIsGodModeEnabled] = useState(false);
|
||||
const handleGodModeStateChange = (state: boolean) => setIsGodModeEnabled(state);
|
||||
|
||||
useSWR("INSTANCE_INFORMATION", () => fetchInstanceInfo(), {
|
||||
revalidateOnFocus: false,
|
||||
@ -44,11 +41,9 @@ const InstanceLayout: FC<TInstanceLayout> = observer((props) => {
|
||||
if (error && !error?.data?.is_activated) return <InstanceNotReady isGodModeEnabled={false} />;
|
||||
|
||||
// instance is not ready and setup is not done
|
||||
if (instance?.instance?.is_setup_done === false)
|
||||
// if (isGodModeEnabled) return <MiniGodModeForm />;
|
||||
return <InstanceNotReady isGodModeEnabled handleGodModeStateChange={handleGodModeStateChange} />;
|
||||
if (instance?.instance?.is_setup_done === false) return <InstanceNotReady isGodModeEnabled />;
|
||||
|
||||
return <>{children}</>;
|
||||
});
|
||||
|
||||
export default InstanceLayout;
|
||||
export default InstanceWrapper;
|
@ -6,7 +6,7 @@ import Link from "next/link";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Spinner } from "@plane/ui";
|
||||
// components
|
||||
import { AuthRoot, EAuthModes } from "@/components/account";
|
||||
import { SignInAuthRoot } from "@/components/account";
|
||||
import { PageHead } from "@/components/core";
|
||||
// constants
|
||||
import { NAVIGATE_TO_SIGNUP } from "@/constants/event-tracker";
|
||||
@ -74,7 +74,7 @@ const SignInPage: NextPageWithLayout = observer(() => {
|
||||
</div>
|
||||
<div className="mx-auto h-full">
|
||||
<div className="h-full overflow-auto px-7 pb-56 pt-4 sm:px-0">
|
||||
<AuthRoot mode={EAuthModes.SIGN_IN} />
|
||||
<SignInAuthRoot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user