Chapter 06

Auth:ログイン処理

masalib
masalib
2020.12.21に更新

SignUpができたのでログイン処理を作ります。SignUpをベースに作るので
そこまで難しくないです

事前作業

ドキュメントルートに(/)アクセスした時、SignUp処理が動いています。
そちらを適当なHomeページを表示したいと思います。そこから
/signup
にアクセスされた時にSignupの画面を表示させます。

$ npm install react-router-dom

react-router-domはルーティングだったりリンクをはったり、リダレクトしたりします。

振り分け処理は簡単です。

/ → Home.js
/signup → Signup.js
/login → Login.js
/dashboard → Dashboard

/src/component/App.js
import Signup from "./Signup"
+ import Home from "./Home"
+ import Dashboard  from "./Dashboard"
+ import Login  from "./Login"

import { AuthProvider } from "../contexts/AuthContext"
+ import { BrowserRouter as Router, Switch, Route } from "react-router-dom"
function App() {
  return (
    <>
+     <Router>
       <AuthProvider>
+       <Switch>
+         <Route path="/signup" component={Signup} />
+         <Route exact path="/" component={Home} />
+         <Route path="/login" component={Login} />
+         <Route path="/dashboard" component={Dashboard} />
+       </Switch>
-       <Signup />
      </AuthProvider>
+    </Router>
    </>
  );
}
export default App;

homeのページは最終的にLPページです。ログインしていない人でもアクセスできるページです。ログインした場合はdashboardのページを表示させます。いずれはダッシュボードのページにログインチェックします。
homeは今のところはsingupとログインのリンクが貼っているという状態です。

/src/components/Home.js
import React from 'react'
import { Link } from "react-router-dom"
const Home = () => {
    return (
        <div>
            Home:
            <h2>
                <Link to="/login">Login</Link>
            </h2>
            <h2>
                <Link to="/signup">signup</Link>
            </h2>
        </div>
    )
}
export default Home

ダッシュボードのページも同じように作ります

/src/components/Dashboard.js
import React from 'react'
import { Link } from "react-router-dom"
const Dashboard = () => {
    return (
        <div>
            Dashboard:
            <h2>
                <Link to="/login">Login</Link>
            </h2>
            <h2>
                <Link to="/signup">signup</Link>
            </h2>
        </div>
    )
}
export default Dashboard

ログインページの処理

signupの処理ができたなら、比較的簡単です。signupと同様にcontextに処理をつくってその関数を共有させるだけです。

/src/contexts/AuthContext.js
import React, { useContext, useState, useEffect } from "react"
import { auth } from "../firebase"

const AuthContext = React.createContext()

export function useAuth() {
    return useContext(AuthContext)
}

export function AuthProvider({ children }) {
    const [currentUser, setCurrentUser] = useState()
    const [loading, setLoading] = useState(true)

    function signup(email, password) {
        return auth.createUserWithEmailAndPassword(email, password)
    }

+   function login(email, password) {
+     return auth.signInWithEmailAndPassword(email, password)
+   }

    const value = {
        currentUser,
        signup,
+       login
    }

    useEffect(() => {
        // Firebase Authのメソッド。ログイン状態が変化すると呼び出される
        auth.onAuthStateChanged(user => {
          setCurrentUser(user);
          setLoading(false)
        });
      }, []);

    return (
        <AuthContext.Provider value={value}>
          {!loading && children}
        </AuthContext.Provider>
    )
}    

ログインページの作成

ログインページもSignupのページとほぼ変わらないです。

  1. Signup.jsをコピーしてLogin.jsを作る(コピー新規)
  2. 「Signup」という文字列を「Login」に変える(大文字小文字の区別あり)
  3. 日本語表記のアカウント作成などをログインに修正する
  4. ログイン成功時にダッシュボードにリダレクト
  5. 通信エラーのハンドリング内容を修正する
エラーコード 概要
auth/invalid-email メールアドレスの形式が正しくない
auth/user-disabled ユーザーが無効化されている
auth/user-not-found ユーザーが見つからない
auth/wrong-password パスワードが間違っている
auth/network-request-failed 通信エラーまたはタイムアウト

ただアカウントの作成と違いエラーメッセージはゆるく表示しています。これはアタックされた時に推測されないようにするためです

プログラム全文(長文なので注意)
/src/components/Login.js
import React, { useReducer, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import CardActions from "@material-ui/core/CardActions";
import CardHeader from "@material-ui/core/CardHeader";
import Button from "@material-ui/core/Button";
import { useAuth } from "../contexts/AuthContext";

const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    container: {
      display: "flex",
      flexWrap: "wrap",
      width: 400,
      margin: `${theme.spacing(0)} auto`
    },
    loginBtn: {

      marginTop: theme.spacing(2),
      flexGrow: 1
    },
    header: {
      textAlign: "center",
      background: "#212121",
      color: "#fff"
    },
    card: {
      marginTop: theme.spacing(10)
    }
  })
);

//state type
type State = {
  email: string,
  password: string,
  passwordconfirm: string,
  isButtonDisabled: boolean,
  helperText: string,
  isError: boolean
};

const initialState: State = {
  email: "",
  password: "",
  passwordconfirm: "",
  isButtonDisabled: true,
  helperText: "",
  isError: false
};

type Action =
  | { type: "setEmail", payload: string }
  | { type: "setPassword", payload: string }
  | { type: "setPasswordConfirm", payload: string }
  | { type: "setIsButtonDisabled", payload: boolean }
+   | { type: "loginSuccess", payload: string }
+   | { type: "loginFailed", payload: string }
-   | { type: "signupSuccess", payload: string }
-   | { type: "signupFailed", payload: string }
  | { type: "setIsError", payload: boolean };

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case "setEmail":
      return {
        ...state,
        email: action.payload
      };
    case "setPassword":
      return {
        ...state,
        password: action.payload
      };
    case "setPasswordConfirm":
      return {
        ...state,
        passwordconfirm: action.payload
      };
    case "setIsButtonDisabled":
      return {
        ...state,
        isButtonDisabled: action.payload
      };
+     case "loginSuccess":
-     case "signupSuccess":
      return {
        ...state,
        helperText: action.payload,
        isError: false
      };
+     case "loginFailed":
-     case "signupFailed":
      return {
        ...state,
        helperText: action.payload,
        isError: true
      };
    case "setIsError":
      return {
        ...state,
        isError: action.payload
      };
    default:
      return state;
  }
};

+ const Login = () => {
- const Signup = () => {
  const classes = useStyles();
  const [state, dispatch] = useReducer(reducer, initialState);
+   const { login } = useAuth();
-   const { signup } = useAuth();
  const [error, setError] = useState("");
  const [successMessage, setSuccessMessage] = useState("");
  const { register, handleSubmit, errors, trigger } = useForm();

  useEffect(() => {
-     if (state.password.trim() !== state.passwordconfirm.trim()) {
-       //clearErrors()
-       dispatch({
-         type: "setIsButtonDisabled",
-         payload: true
-       });
-     } else if (state.email.trim() && state.password.trim()) {
    if (state.email.trim() && state.password.trim()) {
      //trigger();
      dispatch({
        type: "setIsButtonDisabled",
        payload: false
      });
    } else {
      //clearErrors()
      dispatch({
        type: "setIsButtonDisabled",
        payload: true
      });
    }
+   }, [state.email, state.password]);
-   }, [state.email, state.password, state.passwordconfirm]);

+   async function handleLogin(data) {
-   async function handleSignup(data) {
    //react-hook-formを導入したためevent -> dataに変更
    //event.preventDefault();   //react-hook-formを導入したため削除

    try {
      setError("");
      setSuccessMessage("");
      //sing up ボタンの無効化
      dispatch({
        type: "setIsButtonDisabled",
        payload: true
      });

-       await signup(state.email, state.passwordconfirm);
-       dispatch({
-         type: "signupSuccess",
-         payload: "Signup Successfully"
-       });
-       //sing up ボタンの有効化
-       dispatch({
-         type: "setIsButtonDisabled",
-         payload: false
-       });
-       setSuccessMessage("アカウントの作成に成功しました");

+       await login(state.email, state.password);
+       dispatch({
+         type: "loginSuccess",
+         payload: "Login Successfully"
+       });
+       //Loginボタンの有効化
+       dispatch({
+         type: "setIsButtonDisabled",
+         payload: false
+       });
+       setSuccessMessage("ログインに成功しました");

    } catch (e) {
      console.log(e);
      //エラーのメッセージの表示
      switch (e.code) {
        case "auth/network-request-failed":
          setError(
            "通信がエラーになったのか、またはタイムアウトになりました。通信環境がいい所で再度やり直してください。"
          );
          break;
        case "auth/weak-password": //バリデーションでいかないようにするので、基本的にはこのコードはこない
          setError("パスワードが短すぎます。6文字以上を入力してください。");
          break;
        case "auth/invalid-email": 
+           setError("メールアドレスまたはパスワードが正しくありません");
-           setError("メールアドレスが正しくありません");
          break;
+         case "auth/wrong-password": 
+           setError("メールアドレスまたはパスワードが正しくありません");
+           break;
-         case "auth/email-already-in-use":
-           setError(
-             "メールアドレスがすでに使用されています。ログインするか別のメールアドレスで作成してください"
-           );

        case "auth/user-disabled":
          setError("入力されたメールアドレスは無効(BAN)になっています。");
          break;
        default:
          //想定外
          setError(
            "アカウントの作成に失敗しました。通信環境がいい所で再度やり直してください。"
          );
      }
      //sing up ボタンの有効化
      dispatch({
        type: "setIsButtonDisabled",
        payload: false
      });
    }
  }

  const handleKeyPress = (event: React.KeyboardEvent) => {
    if (event.keyCode === 13 || event.which === 13) {
      if (!state.isButtonDisabled) {
        handleKeyPresstrigger();
        if (errors) {
          //errorメッセージを表示する
        } else {
+           handleLogin();
-           handleSignup();
        }
      }
    }
  };

  async function handleKeyPresstrigger() {
    const result = await trigger();
    return result;
  }

  const handleEmailChange: React.ChangeEventHandler<HTMLInputElement> = (
    event
  ) => {
    dispatch({
      type: "setEmail",
      payload: event.target.value
    });
  };

  const handlePasswordChange: React.ChangeEventHandler<HTMLInputElement> = (
    event
  ) => {
    dispatch({
      type: "setPassword",
      payload: event.target.value
    });
  };

-   const handlePasswordConfirmChange: React.ChangeEventHandler<HTMLInputElement> = (
-     event
-   ) => {
-     dispatch({
-       type: "setPasswordConfirm",
-       payload: event.target.value
-     });
-   };


  return (
    <form className={classes.container} noValidate autoComplete="off">
      <Card className={classes.card}>
+         <CardHeader className={classes.header} title="Login" />
-         <CardHeader className={classes.header} title="Sign UP " />
        <CardContent>
          <div>
            {error && <div variant="danger">{error}</div>}
            {successMessage && <div variant="danger">{successMessage}</div>}
            <TextField
              error={state.isError}
              fullWidth
              id="email"
              name="email"
              type="email"
              label="Email"
              placeholder="Email"
              margin="normal"
              onChange={handleEmailChange}
              onKeyPress={handleKeyPress}
              inputRef={register({
                pattern: /^[A-Za-z0-9]{1}[A-Za-z0-9_.-]*@{1}[A-Za-z0-9_.-]{1,}\.[A-Za-z0-9]{1,}$/
              })}
            />
            {errors.email?.type === "pattern" && (
              <div style={{ color: "red" }}>
                メールアドレスの形式で入力されていません
              </div>
            )}

            <TextField
              error={state.isError}
              fullWidth
              id="password"
              name="password"
              type="password"
              label="Password"
              placeholder="Password"
              margin="normal"
              helperText={state.helperText}
              onChange={handlePasswordChange}
              onKeyPress={handleKeyPress}
              inputRef={register({ required: true, minLength: 6 })}
            />
            {errors.password?.type === "minLength" && (
              <div style={{ color: "red" }}>
                パスワードは6文字以上で入力してください
              </div>
            )}
          </div>

-             <TextField
-               error={state.isError}
-               fullWidth
-               id="password-confirm"
-               name="password-confirm"
-               type="password"
-               label="Password-confirm"
-               placeholder="Password-confirm"
-               margin="normal"
-               helperText={state.helperText}
-               onChange={handlePasswordConfirmChange}
-               onKeyPress={handleKeyPress}
-             />
-         もしアカウントがあるなら Log In
+           アカウントがない場合はこちらから作成する

        </CardContent>
        <CardActions>
          <Button
            variant="contained"
            size="large"
            color="secondary"
+             className={classes.loginBtn}
+             onClick={handleSubmit(handleLogin)}
+             disabled={state.isButtonDisabled}
+           >
+             Login
-             className={classes.signupBtn}
-             onClick={handleSubmit(handleSignup)}
-             disabled={state.isButtonDisabled}
-           >
-             Signup


          </Button>
        </CardActions>
      </Card>
    </form>
  );
};

+ export default Login;
- export default Signup;

終了時点のソース

この画面でもユーザーを作成とログインができます。
React Developer Toolsならユーザーが作成された事が確認できます

この時点のソースです