fix: authentication views updated with new workflow (#4547)

* dev: update email check endpoint

* fix: auth magic login check

* chore: updated the error code handler and handled authentication workflow

* dev: add magic link login

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: guru_sainath <gurusainath007@gmail.com>
This commit is contained in:
sriram veeraghanta 2024-05-22 14:49:06 +05:30 committed by GitHub
parent 509d5fe554
commit 9013497a5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 275 additions and 235 deletions

View File

@ -2,13 +2,12 @@ from django.urls import path
from .views import ( from .views import (
CSRFTokenEndpoint, CSRFTokenEndpoint,
EmailCheckSignInEndpoint,
EmailCheckSignUpEndpoint,
ForgotPasswordEndpoint, ForgotPasswordEndpoint,
SetUserPasswordEndpoint, SetUserPasswordEndpoint,
ResetPasswordEndpoint, ResetPasswordEndpoint,
ChangePasswordEndpoint, ChangePasswordEndpoint,
# App # App
EmailCheckEndpoint,
GitHubCallbackEndpoint, GitHubCallbackEndpoint,
GitHubOauthInitiateEndpoint, GitHubOauthInitiateEndpoint,
GoogleCallbackEndpoint, GoogleCallbackEndpoint,
@ -22,7 +21,7 @@ from .views import (
ForgotPasswordSpaceEndpoint, ForgotPasswordSpaceEndpoint,
ResetPasswordSpaceEndpoint, ResetPasswordSpaceEndpoint,
# Space # Space
EmailCheckEndpoint, EmailCheckSpaceEndpoint,
GitHubCallbackSpaceEndpoint, GitHubCallbackSpaceEndpoint,
GitHubOauthInitiateSpaceEndpoint, GitHubOauthInitiateSpaceEndpoint,
GoogleCallbackSpaceEndpoint, GoogleCallbackSpaceEndpoint,
@ -154,18 +153,13 @@ urlpatterns = [
), ),
# Email Check # Email Check
path( path(
"sign-up/email-check/", "email-check/",
EmailCheckSignUpEndpoint.as_view(), EmailCheckEndpoint.as_view(),
name="email-check-sign-up", name="email-check",
),
path(
"sign-in/email-check/",
EmailCheckSignInEndpoint.as_view(),
name="email-check-sign-in",
), ),
path( path(
"spaces/email-check/", "spaces/email-check/",
EmailCheckEndpoint.as_view(), EmailCheckSpaceEndpoint.as_view(),
name="email-check", name="email-check",
), ),
# Password # Password

View File

@ -4,7 +4,7 @@ from .common import (
SetUserPasswordEndpoint, SetUserPasswordEndpoint,
) )
from .app.check import EmailCheckSignInEndpoint, EmailCheckSignUpEndpoint from .app.check import EmailCheckEndpoint
from .app.email import ( from .app.email import (
SignInAuthEndpoint, SignInAuthEndpoint,
@ -47,7 +47,7 @@ from .space.magic import (
from .space.signout import SignOutAuthSpaceEndpoint from .space.signout import SignOutAuthSpaceEndpoint
from .space.check import EmailCheckEndpoint from .space.check import EmailCheckSpaceEndpoint
from .space.password_management import ( from .space.password_management import (
ForgotPasswordSpaceEndpoint, ForgotPasswordSpaceEndpoint,

View File

@ -1,3 +1,6 @@
# Python imports
import os
# Django imports # Django imports
from django.core.validators import validate_email from django.core.validators import validate_email
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -16,8 +19,12 @@ from plane.authentication.adapter.error import (
AUTHENTICATION_ERROR_CODES, AUTHENTICATION_ERROR_CODES,
) )
from plane.authentication.rate_limit import AuthenticationThrottle from plane.authentication.rate_limit import AuthenticationThrottle
from plane.license.utils.instance_value import (
get_configuration_value,
)
class EmailCheckSignUpEndpoint(APIView):
class EmailCheckEndpoint(APIView):
permission_classes = [ permission_classes = [
AllowAny, AllowAny,
@ -28,128 +35,99 @@ class EmailCheckSignUpEndpoint(APIView):
] ]
def post(self, request): def post(self, request):
try:
# Check instance configuration # Check instance configuration
instance = Instance.objects.first() instance = Instance.objects.first()
if instance is None or not instance.is_setup_done: if instance is None or not instance.is_setup_done:
raise AuthenticationException( exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[ error_code=AUTHENTICATION_ERROR_CODES[
"INSTANCE_NOT_CONFIGURED" "INSTANCE_NOT_CONFIGURED"
], ],
error_message="INSTANCE_NOT_CONFIGURED", error_message="INSTANCE_NOT_CONFIGURED",
) )
return Response(
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
(EMAIL_HOST, ENABLE_MAGIC_LINK_LOGIN) = get_configuration_value(
[
{
"key": "EMAIL_HOST",
"default": os.environ.get("EMAIL_HOST", ""),
},
{
"key": "ENABLE_MAGIC_LINK_LOGIN",
"default": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "1"),
},
]
)
smtp_configured = bool(EMAIL_HOST)
is_magic_login_enabled = ENABLE_MAGIC_LINK_LOGIN == "1"
email = request.data.get("email", False) email = request.data.get("email", False)
# Return error if email is not present # Return error if email is not present
if not email: if not email:
raise AuthenticationException( exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["EMAIL_REQUIRED"], error_code=AUTHENTICATION_ERROR_CODES["EMAIL_REQUIRED"],
error_message="EMAIL_REQUIRED", error_message="EMAIL_REQUIRED",
) )
return Response(
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
# Validate email # Validate email
try:
validate_email(email) validate_email(email)
existing_user = User.objects.filter(email=email).first()
if existing_user:
# check if the account is the deactivated
if not existing_user.is_active:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"USER_ACCOUNT_DEACTIVATED"
],
error_message="USER_ACCOUNT_DEACTIVATED",
)
# Raise user already exist
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"USER_ALREADY_EXIST"
],
error_message="USER_ALREADY_EXIST",
)
return Response(
{"status": True},
status=status.HTTP_200_OK,
)
except ValidationError: except ValidationError:
raise AuthenticationException( exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"], error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
error_message="INVALID_EMAIL", error_message="INVALID_EMAIL",
) )
except AuthenticationException as e:
return Response( return Response(
e.get_error_dict(), status=status.HTTP_400_BAD_REQUEST exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
) )
# Check if a user already exists with the given email
class EmailCheckSignInEndpoint(APIView):
permission_classes = [
AllowAny,
]
throttle_classes = [
AuthenticationThrottle,
]
def post(self, request):
try:
# Check instance configuration
instance = Instance.objects.first()
if instance is None or not instance.is_setup_done:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"INSTANCE_NOT_CONFIGURED"
],
error_message="INSTANCE_NOT_CONFIGURED",
)
email = request.data.get("email", False)
# Return error if email is not present
if not email:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["EMAIL_REQUIRED"],
error_message="EMAIL_REQUIRED",
)
# Validate email
validate_email(email)
existing_user = User.objects.filter(email=email).first() existing_user = User.objects.filter(email=email).first()
# If existing user # If existing user
if existing_user: if existing_user:
# Raise different exception when user is not active
if not existing_user.is_active: if not existing_user.is_active:
raise AuthenticationException( exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[ error_code=AUTHENTICATION_ERROR_CODES[
"USER_ACCOUNT_DEACTIVATED" "USER_ACCOUNT_DEACTIVATED"
], ],
error_message="USER_ACCOUNT_DEACTIVATED", error_message="USER_ACCOUNT_DEACTIVATED",
) )
# Return true return Response(
exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST
)
return Response( return Response(
{ {
"status": True, "existing": True,
"is_password_autoset": existing_user.is_password_autoset, "status": (
"MAGIC_CODE"
if existing_user.is_password_autoset
and smtp_configured
and is_magic_login_enabled
else "CREDENTIAL"
),
}, },
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
# Else return response
# Raise error
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
error_message="USER_DOES_NOT_EXIST",
)
except ValidationError:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
error_message="INVALID_EMAIL",
)
except AuthenticationException as e:
return Response( return Response(
e.get_error_dict(), status=status.HTTP_400_BAD_REQUEST {
"existing": False,
"status": (
"MAGIC_CODE"
if smtp_configured and is_magic_login_enabled
else "CREDENTIAL"
),
},
status=status.HTTP_200_OK,
) )

View File

@ -1,3 +1,6 @@
# Python imports
import os
# Django imports # Django imports
from django.core.validators import validate_email from django.core.validators import validate_email
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -16,8 +19,10 @@ from plane.authentication.adapter.error import (
AuthenticationException, AuthenticationException,
) )
from plane.authentication.rate_limit import AuthenticationThrottle from plane.authentication.rate_limit import AuthenticationThrottle
from plane.license.utils.instance_value import get_configuration_value
class EmailCheckEndpoint(APIView):
class EmailCheckSpaceEndpoint(APIView):
permission_classes = [ permission_classes = [
AllowAny, AllowAny,
@ -42,6 +47,22 @@ class EmailCheckEndpoint(APIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
(EMAIL_HOST, ENABLE_MAGIC_LINK_LOGIN) = get_configuration_value(
[
{
"key": "EMAIL_HOST",
"default": os.environ.get("EMAIL_HOST", ""),
},
{
"key": "ENABLE_MAGIC_LINK_LOGIN",
"default": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "1"),
},
]
)
smtp_configured = bool(EMAIL_HOST)
is_magic_login_enabled = ENABLE_MAGIC_LINK_LOGIN == "1"
email = request.data.get("email", False) email = request.data.get("email", False)
# Return error if email is not present # Return error if email is not present
@ -86,12 +107,25 @@ class EmailCheckEndpoint(APIView):
return Response( return Response(
{ {
"existing": True, "existing": True,
"is_password_autoset": existing_user.is_password_autoset, "status": (
"MAGIC_CODE"
if existing_user.is_password_autoset
and smtp_configured
and is_magic_login_enabled
else "CREDENTIAL"
),
}, },
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
# Else return response # Else return response
return Response( return Response(
{"existing": False, "is_password_autoset": False}, {
"existing": False,
"status": (
"MAGIC_CODE"
if smtp_configured and is_magic_login_enabled
else "CREDENTIAL"
),
},
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )

View File

@ -5,8 +5,7 @@ export interface IEmailCheckData {
} }
export interface IEmailCheckResponse { export interface IEmailCheckResponse {
is_password_autoset: boolean; status: "MAGIC_CODE" | "CREDENTIAL";
status: boolean;
existing: boolean; existing: boolean;
} }

View File

@ -21,30 +21,30 @@ type TAuthHeader = {
const Titles = { const Titles = {
[EAuthModes.SIGN_IN]: { [EAuthModes.SIGN_IN]: {
[EAuthSteps.EMAIL]: { [EAuthSteps.EMAIL]: {
header: "Sign in to Plane", header: "Log in or sign up",
subHeader: "Get back to your projects and make progress", subHeader: "",
}, },
[EAuthSteps.PASSWORD]: { [EAuthSteps.PASSWORD]: {
header: "Sign in to Plane", header: "Log in or sign up",
subHeader: "Get back to your projects and make progress", subHeader: "Log in using your password.",
}, },
[EAuthSteps.UNIQUE_CODE]: { [EAuthSteps.UNIQUE_CODE]: {
header: "Sign in to Plane", header: "Log in or sign up",
subHeader: "Get back to your projects and make progress", subHeader: "Log in using your unique code.",
}, },
}, },
[EAuthModes.SIGN_UP]: { [EAuthModes.SIGN_UP]: {
[EAuthSteps.EMAIL]: { [EAuthSteps.EMAIL]: {
header: "Create your account", header: "Sign up or log in",
subHeader: "Start tracking your projects with Plane", subHeader: "",
}, },
[EAuthSteps.PASSWORD]: { [EAuthSteps.PASSWORD]: {
header: "Create your account", header: "Sign up or log in",
subHeader: "Progress, visualize, and measure work how it works best for you.", subHeader: "Sign up using your password",
}, },
[EAuthSteps.UNIQUE_CODE]: { [EAuthSteps.UNIQUE_CODE]: {
header: "Create your account", header: "Sign up or log in",
subHeader: "Progress, visualize, and measure work how it works best for you.", subHeader: "Sign up using your unique code",
}, },
}, },
}; };

View File

@ -37,67 +37,83 @@ export const AuthRoot: FC<TAuthRoot> = observer((props) => {
const router = useRouter(); const router = useRouter();
const { email: emailParam, invitation_id, slug: workspaceSlug, error_code } = router.query; const { email: emailParam, invitation_id, slug: workspaceSlug, error_code } = router.query;
// props // props
const { authMode } = props; const { authMode: currentAuthMode } = props;
// states // states
const [authMode, setAuthMode] = useState<EAuthModes | undefined>(undefined);
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() : "");
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined); const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
const [isPasswordAutoset, setIsPasswordAutoset] = useState(true);
// hooks // hooks
const { config } = useInstance(); const { config } = useInstance();
useEffect(() => { useEffect(() => {
if (error_code) { if (!authMode && currentAuthMode) setAuthMode(currentAuthMode);
}, [currentAuthMode, authMode]);
useEffect(() => {
if (error_code && authMode) {
const errorhandler = authErrorHandler(error_code?.toString() as EAuthenticationErrorCodes); const errorhandler = authErrorHandler(error_code?.toString() as EAuthenticationErrorCodes);
if (errorhandler) { if (errorhandler) {
if ( // password error handler
[ if ([EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP].includes(errorhandler.code)) {
EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN, setAuthMode(EAuthModes.SIGN_UP);
EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP,
].includes(errorhandler.code)
)
setAuthStep(EAuthSteps.PASSWORD); setAuthStep(EAuthSteps.PASSWORD);
}
if ([EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN].includes(errorhandler.code)) {
setAuthMode(EAuthModes.SIGN_IN);
setAuthStep(EAuthSteps.PASSWORD);
}
// magic_code error handler
if ( if (
[ [
EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN, EAuthenticationErrorCodes.INVALID_MAGIC_CODE_SIGN_UP,
EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP, EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP,
EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_IN,
EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_UP, EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_UP,
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN,
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP, EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP,
].includes(errorhandler.code) ].includes(errorhandler.code)
) ) {
setAuthMode(EAuthModes.SIGN_UP);
setAuthStep(EAuthSteps.UNIQUE_CODE); setAuthStep(EAuthSteps.UNIQUE_CODE);
}
if (
[
EAuthenticationErrorCodes.INVALID_MAGIC_CODE_SIGN_IN,
EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN,
EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_IN,
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN,
].includes(errorhandler.code)
) {
setAuthMode(EAuthModes.SIGN_IN);
setAuthStep(EAuthSteps.UNIQUE_CODE);
}
setErrorInfo(errorhandler); setErrorInfo(errorhandler);
} }
} }
}, [error_code, authMode]); }, [error_code, authMode]);
const isSMTPConfigured = config?.is_smtp_configured || false; const isSMTPConfigured = config?.is_smtp_configured || false;
const isMagicLoginEnabled = config?.is_magic_login_enabled || false;
const isEmailPasswordEnabled = config?.is_email_password_enabled || false;
// submit handler- email verification // submit handler- email verification
const handleEmailVerification = async (data: IEmailCheckData) => { const handleEmailVerification = async (data: IEmailCheckData) => {
setEmail(data.email); setEmail(data.email);
const emailCheckRequest = await authService
authMode === EAuthModes.SIGN_IN ? authService.signInEmailCheck(data) : authService.signUpEmailCheck(data); .emailCheck(data)
await emailCheckRequest
.then(async (response) => { .then(async (response) => {
if (authMode === EAuthModes.SIGN_IN) { if (response.existing) {
if (response.is_password_autoset) { if (currentAuthMode === EAuthModes.SIGN_UP) setAuthMode(EAuthModes.SIGN_IN);
if (response.status === "MAGIC_CODE") {
setAuthStep(EAuthSteps.UNIQUE_CODE); setAuthStep(EAuthSteps.UNIQUE_CODE);
generateEmailUniqueCode(data.email); generateEmailUniqueCode(data.email);
} else if (isEmailPasswordEnabled) { } else if (response.status === "CREDENTIAL") {
setIsPasswordAutoset(false);
setAuthStep(EAuthSteps.PASSWORD); setAuthStep(EAuthSteps.PASSWORD);
} }
} else { } else {
if (isSMTPConfigured && isMagicLoginEnabled) { if (currentAuthMode === EAuthModes.SIGN_IN) setAuthMode(EAuthModes.SIGN_UP);
if (response.status === "MAGIC_CODE") {
setAuthStep(EAuthSteps.UNIQUE_CODE); setAuthStep(EAuthSteps.UNIQUE_CODE);
generateEmailUniqueCode(data.email); generateEmailUniqueCode(data.email);
} else if (isEmailPasswordEnabled) { } else if (response.status === "CREDENTIAL") {
setAuthStep(EAuthSteps.PASSWORD); setAuthStep(EAuthSteps.PASSWORD);
} }
} }
@ -108,8 +124,17 @@ export const AuthRoot: FC<TAuthRoot> = observer((props) => {
}); });
}; };
const handleEmailClear = () => {
setAuthMode(currentAuthMode);
setErrorInfo(undefined);
setEmail("");
setAuthStep(EAuthSteps.EMAIL);
router.push(currentAuthMode === EAuthModes.SIGN_IN ? `/` : "/sign-up", undefined, { shallow: true });
};
// generating the unique code // generating the unique code
const generateEmailUniqueCode = async (email: string): Promise<{ code: string } | undefined> => { const generateEmailUniqueCode = async (email: string): Promise<{ code: string } | undefined> => {
if (!isSMTPConfigured) return;
const payload = { email: email }; const payload = { email: email };
return await authService return await authService
.generateUniqueCode(payload) .generateUniqueCode(payload)
@ -121,6 +146,7 @@ export const AuthRoot: FC<TAuthRoot> = observer((props) => {
}); });
}; };
if (!authMode) return <></>;
return ( return (
<div className="relative flex flex-col space-y-6"> <div className="relative flex flex-col space-y-6">
<AuthHeader <AuthHeader
@ -138,23 +164,16 @@ export const AuthRoot: FC<TAuthRoot> = observer((props) => {
<AuthUniqueCodeForm <AuthUniqueCodeForm
mode={authMode} mode={authMode}
email={email} email={email}
handleEmailClear={() => { handleEmailClear={handleEmailClear}
setEmail("");
setAuthStep(EAuthSteps.EMAIL);
}}
generateEmailUniqueCode={generateEmailUniqueCode} generateEmailUniqueCode={generateEmailUniqueCode}
/> />
)} )}
{authStep === EAuthSteps.PASSWORD && ( {authStep === EAuthSteps.PASSWORD && (
<AuthPasswordForm <AuthPasswordForm
mode={authMode} mode={authMode}
isPasswordAutoset={isPasswordAutoset}
isSMTPConfigured={isSMTPConfigured} isSMTPConfigured={isSMTPConfigured}
email={email} email={email}
handleEmailClear={() => { handleEmailClear={handleEmailClear}
setEmail("");
setAuthStep(EAuthSteps.EMAIL);
}}
handleAuthStep={(step: EAuthSteps) => { handleAuthStep={(step: EAuthSteps) => {
if (step === EAuthSteps.UNIQUE_CODE) generateEmailUniqueCode(email); if (step === EAuthSteps.UNIQUE_CODE) generateEmailUniqueCode(email);
setAuthStep(step); setAuthStep(step);

View File

@ -57,7 +57,7 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
type="email" type="email"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
placeholder="name@company.com" placeholder="name@example.com"
className={`disable-autofill-style h-[46px] w-full placeholder:text-onboarding-text-400 autofill:bg-red-500 border-0 focus:bg-none active:bg-transparent`} className={`disable-autofill-style h-[46px] w-full placeholder:text-onboarding-text-400 autofill:bg-red-500 border-0 focus:bg-none active:bg-transparent`}
onFocus={() => setFocused(true)} onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)} onBlur={() => setFocused(false)}

View File

@ -20,7 +20,6 @@ import { AuthService } from "@/services/auth.service";
type Props = { type Props = {
email: string; email: string;
isPasswordAutoset: boolean;
isSMTPConfigured: boolean; isSMTPConfigured: boolean;
mode: EAuthModes; mode: EAuthModes;
handleEmailClear: () => void; handleEmailClear: () => void;

View File

@ -8,8 +8,7 @@ type TOAuthOptionProps = {
isSignUp?: boolean; isSignUp?: boolean;
}; };
export const OAuthOptions: React.FC<TOAuthOptionProps> = observer((props) => { export const OAuthOptions: React.FC<TOAuthOptionProps> = observer(() => {
const { isSignUp = false } = props;
// hooks // hooks
const { config } = useInstance(); const { config } = useInstance();
@ -17,8 +16,6 @@ export const OAuthOptions: React.FC<TOAuthOptionProps> = observer((props) => {
if (!isOAuthEnabled) return null; if (!isOAuthEnabled) return null;
const oauthProviderButtonText = `Sign ${isSignUp ? "up" : "in"} with`;
return ( return (
<> <>
<div className="mt-4 flex items-center"> <div className="mt-4 flex items-center">
@ -29,10 +26,10 @@ export const OAuthOptions: React.FC<TOAuthOptionProps> = observer((props) => {
<div className={`mt-7 grid gap-4 overflow-hidden`}> <div className={`mt-7 grid gap-4 overflow-hidden`}>
{config?.is_google_enabled && ( {config?.is_google_enabled && (
<div className="flex h-[42px] items-center !overflow-hidden"> <div className="flex h-[42px] items-center !overflow-hidden">
<GoogleOAuthButton text={`${oauthProviderButtonText} Google`} /> <GoogleOAuthButton text="Continue with Google" />
</div> </div>
)} )}
{config?.is_github_enabled && <GithubOAuthButton text={`${oauthProviderButtonText} Github`} />} {config?.is_github_enabled && <GithubOAuthButton text="Continue with Github" />}
</div> </div>
</> </>
); );

View File

@ -34,6 +34,9 @@ export enum EAuthenticationErrorCodes {
INVALID_EMAIL = "5005", INVALID_EMAIL = "5005",
EMAIL_REQUIRED = "5010", EMAIL_REQUIRED = "5010",
SIGNUP_DISABLED = "5015", SIGNUP_DISABLED = "5015",
MAGIC_LINK_LOGIN_DISABLED = "5016",
PASSWORD_LOGIN_DISABLED = "5018",
USER_ACCOUNT_DEACTIVATED = "5019",
// Password strength // Password strength
INVALID_PASSWORD = "5020", INVALID_PASSWORD = "5020",
SMTP_NOT_CONFIGURED = "5025", SMTP_NOT_CONFIGURED = "5025",
@ -45,7 +48,6 @@ export enum EAuthenticationErrorCodes {
INVALID_EMAIL_MAGIC_SIGN_UP = "5050", INVALID_EMAIL_MAGIC_SIGN_UP = "5050",
MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED = "5055", MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED = "5055",
// Sign In // Sign In
USER_ACCOUNT_DEACTIVATED = "5019",
USER_DOES_NOT_EXIST = "5060", USER_DOES_NOT_EXIST = "5060",
AUTHENTICATION_FAILED_SIGN_IN = "5065", AUTHENTICATION_FAILED_SIGN_IN = "5065",
REQUIRED_EMAIL_PASSWORD_SIGN_IN = "5070", REQUIRED_EMAIL_PASSWORD_SIGN_IN = "5070",
@ -82,6 +84,9 @@ export enum EAuthenticationErrorCodes {
ADMIN_AUTHENTICATION_FAILED = "5175", ADMIN_AUTHENTICATION_FAILED = "5175",
ADMIN_USER_ALREADY_EXIST = "5180", ADMIN_USER_ALREADY_EXIST = "5180",
ADMIN_USER_DOES_NOT_EXIST = "5185", ADMIN_USER_DOES_NOT_EXIST = "5185",
ADMIN_USER_DEACTIVATED = "5190",
// Rate limit
RATE_LIMIT_EXCEEDED = "5900",
} }
export type TAuthErrorInfo = { export type TAuthErrorInfo = {
@ -99,10 +104,30 @@ const errorCodeMessages: {
title: `Instance not configured`, title: `Instance not configured`,
message: () => `Instance not configured. Please contact your administrator.`, message: () => `Instance not configured. Please contact your administrator.`,
}, },
[EAuthenticationErrorCodes.INVALID_EMAIL]: {
title: `Invalid email`,
message: () => `Invalid email. Please try again.`,
},
[EAuthenticationErrorCodes.EMAIL_REQUIRED]: {
title: `Email required`,
message: () => `Email required. Please try again.`,
},
[EAuthenticationErrorCodes.SIGNUP_DISABLED]: { [EAuthenticationErrorCodes.SIGNUP_DISABLED]: {
title: `Sign up disabled`, title: `Sign up disabled`,
message: () => `Sign up disabled. Please contact your administrator.`, message: () => `Sign up disabled. Please contact your administrator.`,
}, },
[EAuthenticationErrorCodes.MAGIC_LINK_LOGIN_DISABLED]: {
title: `Magic link login disabled`,
message: () => `Magic link login disabled. Please contact your administrator.`,
},
[EAuthenticationErrorCodes.PASSWORD_LOGIN_DISABLED]: {
title: `Password login disabled`,
message: () => `Password login disabled. Please contact your administrator.`,
},
[EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED]: {
title: `User account deactivated`,
message: () => `User account deactivated. Please contact your administrator.`,
},
[EAuthenticationErrorCodes.INVALID_PASSWORD]: { [EAuthenticationErrorCodes.INVALID_PASSWORD]: {
title: `Invalid password`, title: `Invalid password`,
message: () => `Invalid password. Please try again.`, message: () => `Invalid password. Please try again.`,
@ -112,16 +137,6 @@ const errorCodeMessages: {
message: () => `SMTP not configured. Please contact your administrator.`, message: () => `SMTP not configured. Please contact your administrator.`,
}, },
// email check in both sign up and sign in
[EAuthenticationErrorCodes.INVALID_EMAIL]: {
title: `Invalid email`,
message: () => `Invalid email. Please try again.`,
},
[EAuthenticationErrorCodes.EMAIL_REQUIRED]: {
title: `Email required`,
message: () => `Email required. Please try again.`,
},
// sign up // sign up
[EAuthenticationErrorCodes.USER_ALREADY_EXIST]: { [EAuthenticationErrorCodes.USER_ALREADY_EXIST]: {
title: `User already exists`, title: `User already exists`,
@ -159,12 +174,6 @@ const errorCodeMessages: {
message: () => `Invalid email. Please try again.`, message: () => `Invalid email. Please try again.`,
}, },
// sign in
[EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED]: {
title: `User account deactivated`,
message: () => <div>Your account is deactivated. Contact support@plane.so.</div>,
},
[EAuthenticationErrorCodes.USER_DOES_NOT_EXIST]: { [EAuthenticationErrorCodes.USER_DOES_NOT_EXIST]: {
title: `User does not exist`, title: `User does not exist`,
message: (email = undefined) => ( message: (email = undefined) => (
@ -324,6 +333,14 @@ const errorCodeMessages: {
</div> </div>
), ),
}, },
[EAuthenticationErrorCodes.ADMIN_USER_DEACTIVATED]: {
title: `Admin user deactivated`,
message: () => <div>Your account is deactivated</div>,
},
[EAuthenticationErrorCodes.RATE_LIMIT_EXCEEDED]: {
title: "",
message: () => `Rate limit exceeded. Please try again later.`,
},
}; };
export const authErrorHandler = ( export const authErrorHandler = (
@ -335,6 +352,9 @@ export const authErrorHandler = (
EAuthenticationErrorCodes.INVALID_EMAIL, EAuthenticationErrorCodes.INVALID_EMAIL,
EAuthenticationErrorCodes.EMAIL_REQUIRED, EAuthenticationErrorCodes.EMAIL_REQUIRED,
EAuthenticationErrorCodes.SIGNUP_DISABLED, EAuthenticationErrorCodes.SIGNUP_DISABLED,
EAuthenticationErrorCodes.MAGIC_LINK_LOGIN_DISABLED,
EAuthenticationErrorCodes.PASSWORD_LOGIN_DISABLED,
EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED,
EAuthenticationErrorCodes.INVALID_PASSWORD, EAuthenticationErrorCodes.INVALID_PASSWORD,
EAuthenticationErrorCodes.SMTP_NOT_CONFIGURED, EAuthenticationErrorCodes.SMTP_NOT_CONFIGURED,
EAuthenticationErrorCodes.USER_ALREADY_EXIST, EAuthenticationErrorCodes.USER_ALREADY_EXIST,
@ -362,6 +382,7 @@ export const authErrorHandler = (
EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN, EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN,
EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN, EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN,
EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD, EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD,
EAuthenticationErrorCodes.MISSING_PASSWORD,
EAuthenticationErrorCodes.INVALID_NEW_PASSWORD, EAuthenticationErrorCodes.INVALID_NEW_PASSWORD,
EAuthenticationErrorCodes.PASSWORD_ALREADY_SET, EAuthenticationErrorCodes.PASSWORD_ALREADY_SET,
EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST, EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST,
@ -372,7 +393,8 @@ export const authErrorHandler = (
EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED, EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED,
EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST, EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST,
EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST, EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST,
EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED, EAuthenticationErrorCodes.ADMIN_USER_DEACTIVATED,
EAuthenticationErrorCodes.RATE_LIMIT_EXCEEDED,
]; ];
if (bannerAlertErrorCodes.includes(errorCode)) if (bannerAlertErrorCodes.includes(errorCode))

View File

@ -34,6 +34,11 @@ const nextConfig = {
return [ return [
{ {
source: "/accounts/sign-up", source: "/accounts/sign-up",
destination: "/sign-up",
permanent: true
},
{
source: "/sign-in",
destination: "/", destination: "/",
permanent: true permanent: true
} }

View File

@ -8,7 +8,7 @@ import { useTheme } from "next-themes";
import { AuthRoot } from "@/components/account"; import { AuthRoot } 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_SIGNUP } from "@/constants/event-tracker";
// helpers // helpers
import { EAuthModes, EPageTypes } from "@/helpers/authentication.helper"; import { EAuthModes, EPageTypes } from "@/helpers/authentication.helper";
// hooks // hooks
@ -31,7 +31,7 @@ const HomePage: NextPageWithLayout = observer(() => {
return ( return (
<div className="relative w-screen h-screen overflow-hidden"> <div className="relative w-screen h-screen overflow-hidden">
<PageHead title="Sign Up" /> <PageHead title="Log in to continue" />
<div className="absolute inset-0 z-0"> <div className="absolute inset-0 z-0">
<Image <Image
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern} src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
@ -46,18 +46,18 @@ const HomePage: NextPageWithLayout = observer(() => {
<span className="text-2xl font-semibold sm:text-3xl">Plane</span> <span className="text-2xl font-semibold sm:text-3xl">Plane</span>
</div> </div>
<div className="flex flex-col items-end sm:items-center sm:gap-2 sm:flex-row text-center text-sm font-medium text-onboarding-text-300"> <div className="flex flex-col items-end sm:items-center sm:gap-2 sm:flex-row text-center text-sm font-medium text-onboarding-text-300">
Already have an account?{" "} New to Plane?{" "}
<Link <Link
href="/sign-in" href="/sign-up"
onClick={() => captureEvent(NAVIGATE_TO_SIGNIN, {})} onClick={() => captureEvent(NAVIGATE_TO_SIGNUP, {})}
className="font-semibold text-custom-primary-100 hover:underline" className="font-semibold text-custom-primary-100 hover:underline"
> >
Sign In Create an account
</Link> </Link>
</div> </div>
</div> </div>
<div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10 lg:pt-28 transition-all"> <div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10 lg:pt-28 transition-all">
<AuthRoot authMode={EAuthModes.SIGN_UP} /> <AuthRoot authMode={EAuthModes.SIGN_IN} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -7,7 +7,7 @@ import { useTheme } from "next-themes";
import { AuthRoot } from "@/components/account"; import { AuthRoot } 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_SIGNIN } from "@/constants/event-tracker";
// helpers // helpers
import { EAuthModes, EPageTypes } from "@/helpers/authentication.helper"; import { EAuthModes, EPageTypes } from "@/helpers/authentication.helper";
// hooks // hooks
@ -48,18 +48,18 @@ const SignInPage: NextPageWithLayout = observer(() => {
<span className="text-2xl font-semibold sm:text-3xl">Plane</span> <span className="text-2xl font-semibold sm:text-3xl">Plane</span>
</div> </div>
<div className="flex flex-col items-end sm:items-center sm:gap-2 sm:flex-row text-center text-sm font-medium text-onboarding-text-300"> <div className="flex flex-col items-end sm:items-center sm:gap-2 sm:flex-row text-center text-sm font-medium text-onboarding-text-300">
New to Plane?{" "} Already have an account?{" "}
<Link <Link
href="/" href="/"
onClick={() => captureEvent(NAVIGATE_TO_SIGNUP, {})} onClick={() => captureEvent(NAVIGATE_TO_SIGNIN, {})}
className="font-semibold text-custom-primary-100 hover:underline" className="font-semibold text-custom-primary-100 hover:underline"
> >
Create an account Log in
</Link> </Link>
</div> </div>
</div> </div>
<div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10 lg:pt-28 transition-all"> <div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10 lg:pt-28 transition-all">
<AuthRoot authMode={EAuthModes.SIGN_IN} /> <AuthRoot authMode={EAuthModes.SIGN_UP} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -18,15 +18,8 @@ export class AuthService extends APIService {
}); });
} }
signUpEmailCheck = async (data: IEmailCheckData): Promise<IEmailCheckResponse> => emailCheck = async (data: IEmailCheckData): Promise<IEmailCheckResponse> =>
this.post("/auth/sign-up/email-check/", data, { headers: {} }) this.post("/auth/email-check/", data, { headers: {} })
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
signInEmailCheck = async (data: IEmailCheckData): Promise<IEmailCheckResponse> =>
this.post("/auth/sign-in/email-check/", data, { headers: {} })
.then((response) => response?.data) .then((response) => response?.data)
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;