hyper-tuner/hyper-tuner-cloud

View on GitHub
src/pages/auth/Login.tsx

Summary

Maintainability
D
2 days
Test Coverage
import {
  FacebookOutlined,
  GithubOutlined,
  GoogleOutlined,
  LockOutlined,
  MailOutlined,
  UnlockOutlined,
  UserAddOutlined,
  UserOutlined,
} from '@ant-design/icons';
import { Button, Divider, Form, Input, Space } from 'antd';
import { ReactNode, useCallback, useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { OAuthProviders, useAuth } from '../../contexts/AuthContext';
import { Routes } from '../../routes';
import { emailRules, requiredRules, usernameRules } from '../../utils/form';
import { buildRedirectUrl } from '../../utils/url';
import {
  emailNotVerified,
  logInFailed,
  logInSuccessful,
  signUpFailed,
  signUpSuccessful,
} from './notifications';
import validateMessages from './validateMessages';

const { Item } = Form;

export enum FormRoles {
  LOGIN = 'Login',
  SING_UP = 'Sign Up',
}

const Login = ({ formRole }: { formRole: FormRoles }) => {
  const [formEmail] = Form.useForm();
  const isLogin = formRole === FormRoles.LOGIN;
  const [isEmailLoading, setIsEmailLoading] = useState(false);
  const [isOAuthLoading, setIsOAuthLoading] = useState(false);
  const [providersReady, setProvidersReady] = useState(false);
  const [googleUrl, setGoogleUrl] = useState<string | null>(null);
  const [githubUrl, setGithubUrl] = useState<string | null>(null);
  const [facebookUrl, setFacebookUrl] = useState<string | null>(null);
  const [providersStatuses, setProvidersStatuses] = useState<Record<string, boolean>>({
    google: false,
    github: false,
    facebook: false,
  });

  const { login, signUp, listAuthMethods } = useAuth();
  const navigate = useNavigate();
  const isAnythingLoading = isEmailLoading || isOAuthLoading;
  const redirectAfterLogin = useCallback(() => {
    navigate(Routes.HUB);
  }, [navigate]);
  const isOauthEnabled = Object.values(providersStatuses).includes(true);

  const emailLogin = async ({ email, password }: { email: string; password: string }) => {
    setIsEmailLoading(true);
    try {
      const user = await login(email, password);
      logInSuccessful();

      if (!user.verified) {
        emailNotVerified();
      }

      redirectAfterLogin();
    } catch (error) {
      logInFailed(error as Error);
      formEmail.resetFields();
      setIsEmailLoading(false);
    }
  };

  const emailSignUp = async ({
    email,
    password,
    username,
  }: {
    email: string;
    password: string;
    username: string;
  }) => {
    setIsEmailLoading(true);
    try {
      const user = await signUp(email, password, username);
      signUpSuccessful();

      if (!user.verified) {
        emailNotVerified();
      }

      navigate(Routes.HUB);
    } catch (error) {
      signUpFailed(error as Error);
      formEmail.resetFields();
      setIsEmailLoading(false);
    }
  };

  const oauthMethods: Record<
    string,
    {
      label: string;
      icon: ReactNode;
      onClick: () => void;
    }
  > = {
    google: {
      label: 'Google',
      icon: <GoogleOutlined />,
      onClick: () => {
        setIsOAuthLoading(true);
        window.location.href = googleUrl!;
      },
    },
    github: {
      label: 'GitHub',
      icon: <GithubOutlined />,
      onClick: () => {
        setIsOAuthLoading(true);
        window.location.href = githubUrl!;
      },
    },
    facebook: {
      label: 'Facebook',
      icon: <FacebookOutlined />,
      onClick: () => {
        setIsOAuthLoading(true);
        window.location.href = facebookUrl!;
      },
    },
  };

  useEffect(() => {
    listAuthMethods().then((methods) => {
      const { authProviders } = methods;
      window.localStorage.setItem('authProviders', JSON.stringify(authProviders));

      authProviders.forEach((provider) => {
        if (provider.name) {
          setProvidersReady(true);
        }

        switch (provider.name) {
          case OAuthProviders.GOOGLE.toString(): {
            setProvidersStatuses((prevState) => ({
              ...prevState,
              [provider.name]: true,
            }));
            setGoogleUrl(
              `${provider.authUrl}${encodeURIComponent(
                buildRedirectUrl(Routes.REDIRECT_PAGE_OAUTH_CALLBACK, {
                  provider: provider.name,
                }),
              )}`,
            );
            break;
          }
          case OAuthProviders.GITHUB.toString(): {
            setProvidersStatuses((prevState) => ({
              ...prevState,
              [provider.name]: true,
            }));
            setGithubUrl(
              `${provider.authUrl}${encodeURIComponent(
                buildRedirectUrl(Routes.REDIRECT_PAGE_OAUTH_CALLBACK, {
                  provider: provider.name,
                }),
              )}`,
            );
            break;
          }
          case OAuthProviders.FACEBOOK.toString(): {
            setProvidersStatuses((prevState) => ({
              ...prevState,
              [provider.name]: true,
            }));
            setFacebookUrl(
              `${provider.authUrl}${encodeURIComponent(
                buildRedirectUrl(Routes.REDIRECT_PAGE_OAUTH_CALLBACK, {
                  provider: provider.name,
                }),
              )}`,
            );
            break;
          }
          default:
            throw new Error(`Unsupported provider: ${provider.name}`);
        }
      });
    });
  }, [listAuthMethods]);

  const OauthSection = () => {
    return isOauthEnabled ? (
      <>
        <Space direction="horizontal" style={{ width: '100%', justifyContent: 'center' }}>
          {providersReady &&
            Object.keys(oauthMethods).map(
              (provider) =>
                providersStatuses[provider] && (
                  <Button
                    key={provider}
                    icon={oauthMethods[provider].icon}
                    onClick={oauthMethods[provider].onClick}
                    loading={isOAuthLoading}
                  >
                    {oauthMethods[provider].label}
                  </Button>
                ),
            )}
        </Space>
        <Divider />
      </>
    ) : null;
  };

  const bottomLinksLogin = (
    <>
      <Link to={Routes.SIGN_UP}>Sign Up</Link>
      <Link to={Routes.RESET_PASSWORD} style={{ float: 'right' }}>
        Forgot password?
      </Link>
    </>
  );

  const bottomLinksSignUp = (
    <Link to={Routes.LOGIN} style={{ float: 'right' }}>
      Log In
    </Link>
  );

  const submitLogin = (
    <Button
      type="primary"
      htmlType="submit"
      style={{ width: '100%' }}
      loading={isEmailLoading}
      disabled={isAnythingLoading}
      icon={<UnlockOutlined />}
    >
      Login using password
    </Button>
  );

  const submitSignUp = (
    <Button
      type="primary"
      htmlType="submit"
      style={{ width: '100%' }}
      loading={isEmailLoading}
      disabled={isAnythingLoading}
      icon={<UserAddOutlined />}
    >
      Sign Up using password
    </Button>
  );

  return (
    <div className="auth-container">
      <Divider>{formRole}</Divider>
      {providersReady && <OauthSection />}
      <Form
        onFinish={isLogin ? emailLogin : emailSignUp}
        validateMessages={validateMessages}
        form={formEmail}
      >
        <Item name="email" rules={emailRules} hasFeedback>
          <Input
            prefix={<MailOutlined />}
            placeholder="Email"
            autoComplete="email"
            disabled={isAnythingLoading}
          />
        </Item>
        {!isLogin && (
          <Item name="username" rules={usernameRules} hasFeedback>
            <Input prefix={<UserOutlined />} placeholder="Username" autoComplete="name" />
          </Item>
        )}
        <Item name="password" rules={requiredRules} hasFeedback>
          <Input.Password
            placeholder="Password"
            autoComplete="current-password"
            prefix={<LockOutlined />}
            disabled={isAnythingLoading}
          />
        </Item>
        <Item>{isLogin ? submitLogin : submitSignUp}</Item>
        {isLogin ? bottomLinksLogin : bottomLinksSignUp}
      </Form>
    </div>
  );
};

export default Login;