fix: login process validation based on api config (#2361)

This commit is contained in:
sriram veeraghanta 2023-10-04 18:55:29 +05:30 committed by GitHub
parent 48c65c9c95
commit ea2c1e2d06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 283 additions and 175 deletions

View File

@ -1,12 +1,5 @@
import React, { useState } from "react"; import React from "react";
import { useRouter } from "next/router";
import Link from "next/link";
// react hook form
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
// components
import { EmailResetPasswordForm } from "components/account";
// ui // ui
import { Input, PrimaryButton } from "components/ui"; import { Input, PrimaryButton } from "components/ui";
// types // types
@ -18,14 +11,12 @@ type EmailPasswordFormValues = {
type Props = { type Props = {
onSubmit: (formData: EmailPasswordFormValues) => Promise<void>; onSubmit: (formData: EmailPasswordFormValues) => Promise<void>;
setIsResettingPassword: (value: boolean) => void;
}; };
export const EmailPasswordForm: React.FC<Props> = ({ onSubmit }) => { export const EmailPasswordForm: React.FC<Props> = (props) => {
const [isResettingPassword, setIsResettingPassword] = useState(false); const { onSubmit, setIsResettingPassword } = props;
// form info
const router = useRouter();
const isSignUpPage = router.pathname === "/sign-up";
const { const {
register, register,
handleSubmit, handleSubmit,
@ -42,16 +33,6 @@ export const EmailPasswordForm: React.FC<Props> = ({ onSubmit }) => {
return ( return (
<> <>
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100">
{isResettingPassword
? "Reset your password"
: isSignUpPage
? "Sign up on Plane"
: "Sign in to Plane"}
</h1>
{isResettingPassword ? (
<EmailResetPasswordForm setIsResettingPassword={setIsResettingPassword} />
) : (
<form <form
className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto" className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto"
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
@ -89,13 +70,6 @@ export const EmailPasswordForm: React.FC<Props> = ({ onSubmit }) => {
/> />
</div> </div>
<div className="text-right text-xs"> <div className="text-right text-xs">
{isSignUpPage ? (
<Link href="/">
<a className="text-custom-text-200 hover:text-custom-primary-100">
Already have an account? Sign in.
</a>
</Link>
) : (
<button <button
type="button" type="button"
onClick={() => setIsResettingPassword(true)} onClick={() => setIsResettingPassword(true)}
@ -103,7 +77,6 @@ export const EmailPasswordForm: React.FC<Props> = ({ onSubmit }) => {
> >
Forgot your password? Forgot your password?
</button> </button>
)}
</div> </div>
<div> <div>
<PrimaryButton <PrimaryButton
@ -112,24 +85,10 @@ export const EmailPasswordForm: React.FC<Props> = ({ onSubmit }) => {
disabled={!isValid && isDirty} disabled={!isValid && isDirty}
loading={isSubmitting} loading={isSubmitting}
> >
{isSignUpPage {isSubmitting ? "Signing in..." : "Sign in"}
? isSubmitting
? "Signing up..."
: "Sign up"
: isSubmitting
? "Signing in..."
: "Sign in"}
</PrimaryButton> </PrimaryButton>
{!isSignUpPage && (
<Link href="/sign-up">
<a className="block text-custom-text-200 hover:text-custom-primary-100 text-xs mt-4">
Don{"'"}t have an account? Sign up.
</a>
</Link>
)}
</div> </div>
</form> </form>
)}
</> </>
); );
}; };

View File

@ -0,0 +1,114 @@
import React from "react";
import Link from "next/link";
import { useForm } from "react-hook-form";
// ui
import { Input, PrimaryButton } from "components/ui";
// types
type EmailPasswordFormValues = {
email: string;
password?: string;
confirm_password: string;
medium?: string;
};
type Props = {
onSubmit: (formData: EmailPasswordFormValues) => Promise<void>;
};
export const EmailSignUpForm: React.FC<Props> = (props) => {
const { onSubmit } = props;
const {
register,
handleSubmit,
watch,
formState: { errors, isSubmitting, isValid, isDirty },
} = useForm<EmailPasswordFormValues>({
defaultValues: {
email: "",
password: "",
confirm_password: "",
medium: "email",
},
mode: "onChange",
reValidateMode: "onChange",
});
return (
<>
<form
className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto"
onSubmit={handleSubmit(onSubmit)}
>
<div className="space-y-1">
<Input
id="email"
type="email"
name="email"
register={register}
validations={{
required: "Email address is required",
validate: (value) =>
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
value
) || "Email address is not valid",
}}
error={errors.email}
placeholder="Enter your email address..."
className="border-custom-border-300 h-[46px]"
/>
</div>
<div className="space-y-1">
<Input
id="password"
type="password"
name="password"
register={register}
validations={{
required: "Password is required",
}}
error={errors.password}
placeholder="Enter your password..."
className="border-custom-border-300 h-[46px]"
/>
</div>
<div className="space-y-1">
<Input
id="confirm_password"
type="password"
name="confirm_password"
register={register}
validations={{
required: "Password is required",
validate: (val: string) => {
if (watch("password") != val) {
return "Your passwords do no match";
}
},
}}
error={errors.confirm_password}
placeholder="Confirm your password..."
className="border-custom-border-300 h-[46px]"
/>
</div>
<div className="text-right text-xs">
<Link href="/">
<a className="text-custom-text-200 hover:text-custom-primary-100">
Already have an account? Sign in.
</a>
</Link>
</div>
<div>
<PrimaryButton
type="submit"
className="w-full text-center h-[46px]"
disabled={!isValid && isDirty}
loading={isSubmitting}
>
{isSubmitting ? "Signing up..." : "Sign up"}
</PrimaryButton>
</div>
</form>
</>
);
};

View File

@ -1,29 +1,27 @@
import { useEffect, useState, FC } from "react"; import { useEffect, useState, FC } from "react";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// next-themes
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
// images // images
import githubBlackImage from "/public/logos/github-black.png"; import githubBlackImage from "/public/logos/github-black.png";
import githubWhiteImage from "/public/logos/github-white.png"; import githubWhiteImage from "/public/logos/github-white.png";
const { NEXT_PUBLIC_GITHUB_ID } = process.env;
export interface GithubLoginButtonProps { export interface GithubLoginButtonProps {
handleSignIn: React.Dispatch<string>; handleSignIn: React.Dispatch<string>;
clientId: string;
} }
export const GithubLoginButton: FC<GithubLoginButtonProps> = ({ handleSignIn }) => { export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
const { handleSignIn, clientId } = props;
// states
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined); const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
const [gitCode, setGitCode] = useState<null | string>(null); const [gitCode, setGitCode] = useState<null | string>(null);
// router
const { const {
query: { code }, query: { code },
} = useRouter(); } = useRouter();
// theme
const { theme } = useTheme(); const { theme } = useTheme();
useEffect(() => { useEffect(() => {
@ -42,7 +40,7 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = ({ handleSignIn })
return ( return (
<div className="w-full flex justify-center items-center"> <div className="w-full flex justify-center items-center">
<Link <Link
href={`https://github.com/login/oauth/authorize?client_id=${NEXT_PUBLIC_GITHUB_ID}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`} href={`https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
> >
<button className="flex w-full items-center justify-center gap-2 rounded border border-custom-border-300 p-2 text-sm font-medium text-custom-text-100 duration-300 hover:bg-custom-background-80 h-[46px]"> <button className="flex w-full items-center justify-center gap-2 rounded border border-custom-border-300 p-2 text-sm font-medium text-custom-text-100 duration-300 hover:bg-custom-background-80 h-[46px]">
<Image <Image

View File

@ -1,22 +1,23 @@
import { FC, CSSProperties, useEffect, useRef, useCallback, useState } from "react"; import { FC, useEffect, useRef, useCallback, useState } from "react";
import Script from "next/script"; import Script from "next/script";
export interface IGoogleLoginButton { export interface IGoogleLoginButton {
text?: string;
handleSignIn: React.Dispatch<any>; handleSignIn: React.Dispatch<any>;
styles?: CSSProperties; clientId: string;
} }
export const GoogleLoginButton: FC<IGoogleLoginButton> = ({ handleSignIn }) => { export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
const { handleSignIn, clientId } = props;
// refs
const googleSignInButton = useRef<HTMLDivElement>(null); const googleSignInButton = useRef<HTMLDivElement>(null);
// states
const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false); const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false);
const loadScript = useCallback(() => { const loadScript = useCallback(() => {
if (!googleSignInButton.current || gsiScriptLoaded) return; if (!googleSignInButton.current || gsiScriptLoaded) return;
window?.google?.accounts.id.initialize({ window?.google?.accounts.id.initialize({
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENTID || "", client_id: clientId,
callback: handleSignIn, callback: handleSignIn,
}); });
@ -39,7 +40,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = ({ handleSignIn }) => {
window?.google?.accounts.id.prompt(); // also display the One Tap dialog window?.google?.accounts.id.prompt(); // also display the One Tap dialog
setGsiScriptLoaded(true); setGsiScriptLoaded(true);
}, [handleSignIn, gsiScriptLoaded]); }, [handleSignIn, gsiScriptLoaded, clientId]);
useEffect(() => { useEffect(() => {
if (window?.google?.accounts?.id) { if (window?.google?.accounts?.id) {

View File

@ -3,3 +3,4 @@ export * from "./email-password-form";
export * from "./email-reset-password-form"; export * from "./email-reset-password-form";
export * from "./github-login-button"; export * from "./github-login-button";
export * from "./google-login"; export * from "./google-login";
export * from "./email-signup-form";

View File

@ -1,13 +1,14 @@
import React, { useEffect } from "react"; import React, { useEffect, useState } from "react";
import Image from "next/image"; import Image from "next/image";
import type { NextPage } from "next"; import type { NextPage } from "next";
import { useTheme } from "next-themes";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
// layouts // layouts
import DefaultLayout from "layouts/default-layout"; import DefaultLayout from "layouts/default-layout";
// services // services
import authenticationService from "services/authentication.service"; import authenticationService from "services/authentication.service";
import { AppConfigService } from "services/app-config.service";
// hooks // hooks
import useUserAuth from "hooks/use-user-auth"; import useUserAuth from "hooks/use-user-auth";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
@ -17,19 +18,19 @@ import {
GithubLoginButton, GithubLoginButton,
EmailCodeForm, EmailCodeForm,
EmailPasswordForm, EmailPasswordForm,
EmailResetPasswordForm,
} from "components/account"; } from "components/account";
// ui // ui
import { Spinner } from "components/ui"; import { Spinner } from "components/ui";
// images // images
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// next themes // types
import { useTheme } from "next-themes";
import { IUser } from "types"; import { IUser } from "types";
const appConfig = new AppConfigService();
// types // types
type EmailPasswordFormValues = { type EmailPasswordFormValues = {
email: string; email: string;
@ -39,11 +40,16 @@ type EmailPasswordFormValues = {
const HomePage: NextPage = observer(() => { const HomePage: NextPage = observer(() => {
const store: any = useMobxStore(); const store: any = useMobxStore();
// theme
const { setTheme } = useTheme(); const { setTheme } = useTheme();
// user
const { isLoading, mutateUser } = useUserAuth("sign-in"); const { isLoading, mutateUser } = useUserAuth("sign-in");
// states
const [isResettingPassword, setIsResettingPassword] = useState(false);
// toast
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// fetch app config
const { data } = useSWR("APP_CONFIG", () => appConfig.envConfig());
const handleTheme = (user: IUser) => { const handleTheme = (user: IUser) => {
const currentTheme = user.theme.theme ?? "system"; const currentTheme = user.theme.theme ?? "system";
@ -173,26 +179,43 @@ const HomePage: NextPage = observer(() => {
</> </>
<div className="grid place-items-center h-full overflow-y-auto py-5 px-7"> <div className="grid place-items-center h-full overflow-y-auto py-5 px-7">
<div> <div>
{parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0") ? (
<>
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100"> <h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100">
Sign in to Plane {isResettingPassword ? "Reset your password" : "Sign in to Plane"}
</h1> </h1>
{isResettingPassword ? (
<EmailResetPasswordForm setIsResettingPassword={setIsResettingPassword} />
) : (
<>
{data?.email_password_login && (
<EmailPasswordForm
onSubmit={handlePasswordSignIn}
setIsResettingPassword={setIsResettingPassword}
/>
)}
{data?.magic_login && (
<div className="flex flex-col divide-y divide-custom-border-200"> <div className="flex flex-col divide-y divide-custom-border-200">
<div className="pb-7"> <div className="pb-7">
<EmailCodeForm handleSignIn={handleEmailCodeSignIn} /> <EmailCodeForm handleSignIn={handleEmailCodeSignIn} />
</div> </div>
<div className="flex flex-col items-center justify-center gap-4 pt-7 sm:w-[360px] mx-auto overflow-hidden">
<GoogleLoginButton handleSignIn={handleGoogleSignIn} />
<GithubLoginButton handleSignIn={handleGitHubSignIn} />
</div> </div>
)}
<div className="flex flex-col items-center justify-center gap-4 pt-7 sm:w-[360px] mx-auto overflow-hidden">
{data?.google && (
<GoogleLoginButton
clientId={data?.google}
handleSignIn={handleGoogleSignIn}
/>
)}
{data?.github && (
<GithubLoginButton
clientId={data?.github}
handleSignIn={handleGitHubSignIn}
/>
)}
</div> </div>
</> </>
) : (
<EmailPasswordForm onSubmit={handlePasswordSignIn} />
)} )}
{parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0") ? (
<p className="pt-16 text-custom-text-200 text-sm text-center"> <p className="pt-16 text-custom-text-200 text-sm text-center">
By signing up, you agree to the{" "} By signing up, you agree to the{" "}
<a <a
@ -204,7 +227,6 @@ const HomePage: NextPage = observer(() => {
Terms & Conditions Terms & Conditions
</a> </a>
</p> </p>
) : null}
</div> </div>
</div> </div>
</> </>

View File

@ -1,8 +1,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect } from "react";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// next-themes // next-themes
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
// services // services
@ -13,9 +11,7 @@ import useToast from "hooks/use-toast";
// layouts // layouts
import DefaultLayout from "layouts/default-layout"; import DefaultLayout from "layouts/default-layout";
// components // components
import { EmailPasswordForm } from "components/account"; import { EmailPasswordForm, EmailSignUpForm } from "components/account";
// ui
import { Spinner } from "components/ui";
// images // images
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
// types // types
@ -27,8 +23,6 @@ type EmailPasswordFormValues = {
}; };
const SignUp: NextPage = () => { const SignUp: NextPage = () => {
const [isLoading, setIsLoading] = useState(true);
const router = useRouter(); const router = useRouter();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -70,18 +64,6 @@ const SignUp: NextPage = () => {
setTheme("system"); setTheme("system");
}, [setTheme]); }, [setTheme]);
useEffect(() => {
if (parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0")) router.push("/");
else setIsLoading(false);
}, [router]);
if (isLoading)
return (
<div className="grid place-items-center h-screen w-full">
<Spinner />
</div>
);
return ( return (
<DefaultLayout> <DefaultLayout>
<> <>
@ -96,7 +78,8 @@ const SignUp: NextPage = () => {
</> </>
<div className="grid place-items-center h-full w-full overflow-y-auto py-5 px-7"> <div className="grid place-items-center h-full w-full overflow-y-auto py-5 px-7">
<div> <div>
<EmailPasswordForm onSubmit={handleSignUp} /> <h1 className="text-2xl text-center font-">SignUp on Plane</h1>
<EmailSignUpForm onSubmit={handleSignUp} />
</div> </div>
</div> </div>
</DefaultLayout> </DefaultLayout>

View File

@ -0,0 +1,30 @@
// services
import APIService from "services/api.service";
// helper
import { API_BASE_URL } from "helpers/common.helper";
export interface IEnvConfig {
github: string;
google: string;
github_app_name: string | null;
email_password_login: boolean;
magic_login: boolean;
}
export class AppConfigService extends APIService {
constructor() {
super(API_BASE_URL);
}
async envConfig(): Promise<IEnvConfig> {
return this.get("/api/configs/", {
headers: {
"Content-Type": "application/json",
},
})
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
}