masalibの日記

システム開発、運用と猫の写真ブログです

React Firebase入門 ログイン処理

SignUpができたのでログイン処理を作りたいと思います。

React Firebase入門シリーズ
1-1・ React Firebase入門 初期設定とsignup - masalibの日記
1-2・ 「react-hook-form」を入れてみた - masalibの日記
1-3・ React Firebase入門 signupの通信エラー対応 - masalibの日記
2・React Firebase入門 ログイン処理 今ここ

事前作業

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

Reactの標準のライブラリーに入っていないのが不思議で仕方がない

npm install react-router-dom

をインストールします

react-router-domはルーティングだったりリンクをはったり、リダレクトしたりします。angularからreactに来た人間からするとなぜこの処理が標準じゃないのか未だに理解できないです。

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

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

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>
      </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

結果

f:id:masalib:20201125023014j:plain

デザインもくそもありません

ログインページの処理

signupの処理ができたなら、このページは比較的簡単です。signupと同様にcontextに処理をつくってその関数を共有させるだけです。全文はたぶん見なくても大丈夫かと思う。

クリックすると展開されます(長文なので注意)

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>
    )
}    

追加したのはloginという関数

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

共通するためのvalueの値を修正

    const value = {
        currentUser,
        signup,
+        login
    }

ログインページの作成

ログインページも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 通信エラーまたはタイムアウト

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

switch (e.code) {
    case "auth/network-request-failed":
        setError("通信がエラーになったのか、またはタイムアウトになりました。通信環境がいい所で再度やり直してください。");
        break;
    case "auth/weak-password": 
        setError("メールアドレスまたはパスワードが間違えています。");
        break;
    case "auth/invalid-email": 
        setError("メールアドレスまたはパスワードが間違えています。");
        break;
    case "auth/user-not-found":    
        setError("メールアドレスまたはパスワードが間違えています。");
        break;
    case "auth/user-disabled": 
        setError("入力されたメールアドレスは無効(BAN)になっています。");
        break;        
    default:  //想定外
        setError("ログインに失敗しました。通信環境がいい所で再度やり直してください。");
}

全文はたぶん見なくても大丈夫かと思うけど、みたい人はどうぞ

クリックすると展開されます(長文なので注意)

import React, { useState,useReducer, useEffect } 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"
import { Link , useHistory} from "react-router-dom"


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 = {
  username: string,
  password:  string,
  isButtonDisabled: boolean,
  helperText: string,
  isError: boolean
};


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

type Action =
  | { type: "setUsername", payload: string }
  | { type: "setPassword", payload: string }
  | { type: "setIsButtonDisabled", payload: boolean }
  | { type: "loginSuccess", payload: string }
  | { type: "loginFailed", payload: string }
  | { type: "setIsError", payload: boolean };

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case "setUsername":
      return {
        ...state,
        username: action.payload
      };
    case "setPassword":
      return {
        ...state,
        password: action.payload
      };
    case "setIsButtonDisabled":
      return {
        ...state,
        isButtonDisabled: action.payload
      };
    case "loginSuccess":
      return {
        ...state,
        helperText: action.payload,
        isError: false
      };
    case "loginFailed":
      return {
        ...state,
        helperText: action.payload,
        isError: true
      };
    case "setIsError":
      return {
        ...state,
        isError: action.payload
      };
    default:
       return state;
  }
};

const Login = () => {
    const classes = useStyles();
    const [state, dispatch] = useReducer(reducer, initialState);
    const [error, setError] = useState("")
    const { login } = useAuth()
    const { register, handleSubmit, errors, trigger  } = useForm();
    const history = useHistory()

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

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

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

            await login(state.username, state.password)
            history.push("/dashboard")

        } catch (e){
            //エラーのメッセージの表示
            switch (e.code) {
                case "auth/network-request-failed":
                    setError("通信がエラーになったのか、またはタイムアウトになりました。通信環境がいい所で再度やり直してください。");
                    break;
                case "auth/weak-password": 
                    setError("メールアドレスまたはパスワードが間違えています。");
                    break;
                case "auth/invalid-email": 
                    setError("メールアドレスまたはパスワードが間違えています。");
                    break;
                case "auth/user-not-found":    
                    setError("メールアドレスまたはパスワードが間違えています。");
                    break;
                case "auth/user-disabled": 
                    setError("入力されたメールアドレスは無効(BAN)になっています。");
                    break;                    
                default:  //想定外
                    setError("ログインに失敗しました。通信環境がいい所で再度やり直してください。");
            }
            //dispatch({
            //    type: "loginFailed",
            //    payload: "Incorrect username or password"
            //});
            //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()  
            }
          }
        }
    };

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

    }

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

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

  return (
    <form className={classes.container} noValidate autoComplete="off">
      <Card className={classes.card}>
        <CardHeader className={classes.header} title="Login" />
        <CardContent>
        <div>
            {error && <div style={{ color: "red" }}>{error}</div>}
            <TextField
                error={state.isError}
                fullWidth
                id="username"
                name="username"
                type="email"
                label="Username"
                placeholder="Username"
                margin="normal"
                onChange={handleUsernameChange}
                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.username?.type === "pattern" &&
            <div style={{ color: "red" }}>メールアドレスの形式で入力されていません</div>}

            <TextField
                error={state.isError}
                fullWidth
                id="password"
                name="password"
                type="password"
                label="Password"
                placeholder="Password"
                margin="normal"
                onChange={handlePasswordChange}
                onKeyPress={handleKeyPress}
                inputRef={register({ required: true, minLength: 6 })}
            />
            {errors.password?.type === "minLength" &&
            <div style={{ color: "red" }}>パスワードは6文字以上で入力してください</div>}
          </div>
          もしアカウントがないなら<Link to="/signup">こちら</Link>からアカウントを作成してください
        </CardContent>
        <CardActions>
          <Button
            variant="contained"
            size="large"
            color="secondary"
            className={classes.loginBtn}
            onClick={handleSubmit(handleLogin)}
            disabled={state.isButtonDisabled}
          >
            Login
          </Button>
        </CardActions>
      </Card>
    </form>
  );
};

export default Login;

結果

f:id:masalib:20201125030630j:plain

ログインが成功するとダッシュボードにリダレクトされます

f:id:masalib:20201125030752j:plain

感想

SignUPを作った時に比べる簡単にできた。でもSignUpとLoginってほぼ同じような画面なのに2つファイルで管理している・・・作りとしてはあまりよくないのかも・・・

このあとはダッシュボードにログアウトと現在のユーザーの情報を出したいです。