【Elm】Firebase Authenticationでパスワード認証をする

## はじめに

[こちら](https://qiita.com/ababup1192/items/f27f9af282d9fa642eb5)の記事でFirebaseのGoogle認証はできたのですが、アホすぎてパスワード認証をするのにハマったので、備忘録として残します。
間違ったこと書いてたら罵詈雑言でも構いませんので教えてください。

## やりたいこと
フォームに任意のemailとpasswordを入力して、Firebase認証し、認証結果を表示します。
![test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/516892/584af623-1b55-2e1e-ae44-e03d278d2f0d.gif)

## 開発環境
Mac
[create-elm-app](https://github.com/halfzebra/create-elm-app)使用

## コード全体
[こちら](https://github.com/syobott/elm_firebase_auth/tree/main/src)参照ください
index.jsには各自のFirebaseのapiKey等を記載してください。

## 大まかな流れ

1. Firebase Authにユーザーを登録する
2. Elm側でemail、パスワード入力フォームを作る
3. フォームに入力されたemail、パスワード情報をJavaScriptに送る
4. JavaScriptでemail、パスワード情報を受け取り、認証結果を送信する
5. 認証結果をElmで受け取る

## 1. Firebase Authにユーザーを登録する
まず以下を参考にして、新規のFirebaseプロジェクトを作成してください。
分からなければ、ググれば他の方が詳細を書いてくださっているのでここでは省略します。
もしくはコメントしていただければお教えします（わかる範囲で

https://firebase.google.com/docs/auth/web/password-auth

Firebaseプロジェクトを作成したらプロジェクトのコンソール画面を開き、左上のAutenticationをクリック。
![スクリーンショット 2021-07-10 15.11.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/516892/a76a6d6d-7c57-73b9-7ec4-565ac2b57beb.png)

次に、”Sign-in method”タブから、”メール/パスワード”のステータスを"有効"にします。
多分Google認証もすると思うのでGoogleも"有効"にして良いと思います。

![スクリーンショット 2021-07-10 15.15.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/516892/13a70f33-b2f0-a986-01d2-73e005f5a4fe.png)

次に、"Users"タブから"ユーザーを追加"をクリックして、ユーザー名とパスワードを設定してください。
※自分はこれをやり忘れて存在していないユーザーと認証しようとしていました。アホすぎて自分でも引いてます。

![スクリーンショット 2021-07-10 15.11.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/516892/a81f1cc7-9493-c474-935f-9fca268c8208.png)

これで事前準備はOKだと思います。
抜けてたらすみません。


## 2. Elm側でemail、パスワード入力フォームを作る

まずはこんな感じの入力フォームを作ります。

![スクリーンショット 2021-07-11 8.35.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/516892/8ba7cc84-9707-6561-80f4-2fd8b42e66fa.png)

入力フォームの作り方をざっと解説します。
「入力フォームくらい秒で作れるわ！」という方は3まで読み飛ばしてください。

### MODEL
まず、Emailとパスワードの[型エイリアス](https://guide.elm-lang.jp/types/type_aliases.html)を定義します。ここは特に説明不要ですかね。

```Elm

type alias EmailAndPassword =
    { email : String
    , password : String
    }

type alias Model =
    { ...
    , emailAndPassword : EmailAndPassword
    }

```

### UPDATE

Updateにて、フォーム内で文字が入力されるたびに、emailとpasswordを更新していきます。
[カスタム型](https://guide.elm-lang.jp/types/custom_types.html)を使用しているので少々ややこしいかもしれません。
ですがカスタム型はElmのコードを分かりやすくするために必須なので使用するようにしています。

```Elm

type ChangedEmailAndPasswordMsg
    = EmailChanged String
    | PasswordChanged String

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ...
        ChangedEmailAndPasswordMsg changedEmailAndPasswordMsg ->
            updateEmailAndPassword changedEmailAndPasswordMsg model
        ...

updateEmailAndPassword : ChangedEmailAndPasswordMsg -> Model -> ( Model, Cmd Msg )
updateEmailAndPassword msg model =
    let
        emailAndPassword =
            model.emailAndPassword
    in
    case msg of
        EmailChanged email ->
            ( { model | emailAndPassword = { emailAndPassword | email = email } }
            , Cmd.none
            )

        PasswordChanged password ->
            ( { model | emailAndPassword = { emailAndPassword | password = password } }
            , Cmd.none
            )

```

ここで自分がハマったのは、"updateEmailAndPassword"内の、emailやpasswordの更新についてです。
例えばemailは、最初は以下のような感じで更新しようとしていました。

```Elm

EmailChanged email ->
            ( { model | emailAndPassword.email = email }
            , Cmd.none
            )

```

しかし以下のエラーが出ました。

```
Type mismatch error.
Expected: `EmailAndPassword`
Found: `String`
```

説明しづらいのですが、modelが更新できるのはあくまでmodelの中のemailAndPassword(model.emailAndPassword)だけであって、emailAndPasswordの中のemail(model.emailAndPassword.email)は更新できないのだと分かりました。

そこで、[こちら](https://qiita.com/miyamo_madoka/items/f225803d275019cc34c1)の記事や、[こちら](https://qiita.com/ababup1192/items/34e926ffc934336242ca)の記事を参考にさせていただき、以下のように記載することで解決できました（もっと良い書き方がある？）

```Elm

    let
        emailAndPassword =
            model.emailAndPassword
    in
    case msg of
        EmailChanged email ->
            ( { model | emailAndPassword = { emailAndPassword | email = email } }
            , Cmd.none
            )

```

### VIEW
email, passwordフォームを表示します。

```Elm

view : Model -> Html Msg
view model =
    div []
        [ h1 [] [ text "Firebase Authentication" ]
        , Html.map ChangedEmailAndPasswordMsg (viewEmail model)
        , Html.map ChangedEmailAndPasswordMsg (viewPassword model)
        , ...
        ]

viewEmail : Model -> Html ChangedEmailAndPasswordMsg
viewEmail model =
    div []
        [ input
            [ onInput EmailChanged
            , value model.emailAndPassword.email
            , placeholder "User or Email"
            ]
            []
        ]


viewPassword : Model -> Html ChangedEmailAndPasswordMsg
viewPassword model =
    div []
        [ input
            [ type_ "password"
            , onInput PasswordChanged
            , value model.emailAndPassword.password
            , placeholder "Password"
            ]
            []
        ]

```

ここでのポイントは、ChangedEmailAndPasswordMsgというカスタム型をMsgに変換するために、以下のように[Html.map](https://package.elm-lang.org/packages/elm/html/latest/Html#map)を使用する必要があることくらいですかね。

```Elm

Html.map ChangedEmailAndPasswordMsg (viewEmail model)

```

以上で入力フォームを作成できました。

## 3. フォームに入力されたemail、パスワード情報をJavaScriptに送る
次に、フォームに入力したemailとpasswordを、portを使ってJavaScriptに送り、JavaScriptでfirebase認証をします。
portに関しての詳しい解説は[公式ドキュメント](https://guide.elm-lang.jp/interop/ports.html)や他の方の記事を読むと良いと思います。

### PORT

```Elm

port signingInWithEmailAndPassword : Encode.Value -> Cmd msg

```

### UPDATE
そして、ここで忘れてはいけないのが、emailとpasswordという文字列をJavascriptに送るためには、"エンコード"をしないといけないということです。
エンコードに関しての詳しい解説は[公式ドキュメント](https://package.elm-lang.org/packages/elm/json/latest/Json.Encode)や他の方の記事を読むと良いと思います。

```Elm

import Json.Encode as Encode

type AuthMsg
    = SignIn

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        AuthMsg tryToAuthMsg ->
            updateAuth tryToAuthMsg model

        ...

-- エンコードした文字列をportに引数として渡す
updateAuth : AuthMsg -> Model -> ( Model, Cmd Msg )
updateAuth msg model =
    case msg of
        SignIn ->
            ( model, signingInWithEmailAndPassword <| loginInfoEncoder model )

-- emailとpasswordのエンコード
loginInfoEncoder : Model -> Encode.Value
loginInfoEncoder model =
    Encode.object
        [ ( "email", Encode.string model.emailAndPassword.email )
        , ( "password", Encode.string model.emailAndPassword.password )
        ]

```

ここでは、loginInfoEncorderで、Encode.objectを使ってJSONオブジェクトを作成しています。
例えば、email=hoge@hoge.com, password=hogehogeだとすると、以下のようなJSONオブジェクトが出来上がります。

```
{ 
  email : hoge@hoge.com
  passowrd : hogehoge
}
```

これをportに渡すことで、portを通してJavaScriptにこのJSONオブジェクトが渡されるわけです。

### VIEW
SignInボタンも作っておきます。

```Elm
view : Model -> Html Msg
view model =
    div []
        [ ...
        , Html.map AuthMsg viewLoginButton
        , ...
        ]

viewLoginButton : Html AuthMsg
viewLoginButton =
    div []
        [ button [ onClick SignIn ] [ text "SignIn" ] ]
```


## 4. JavaScriptでemail、パスワード情報を受け取り、認証結果を送信する

### 初期設定
JavaScript側の初期設定です。
これは　1. Firebase Authにユーザーを登録する　でできていますかね。
自分はcreat-elm-appを使っているので、appの定義とかは皆さんと異なるかもしれません。
とりあえずappの定義は忘れずにやってください。
firebase関係のコードは基本的に同じになると思います。

```JavaScript

...
import firebase from "firebase/app";
import "firebase/analytics";
import "firebase/auth";
import "firebase/firestore";

// ここが異なるかも
const app = Elm.Main.init({
  node: document.getElementById("root"),
});

const firebaseConfig = {
  // 各自のapi key等を記載してください
};

// firebaseの初期化
firebase.initializeApp(firebaseConfig);
const Googleprovider = new firebase.auth.GoogleAuthProvider();
const DB = firebase.firestore();

```

### Email, パスワード情報を受け取り、認証結果を送信
[こちら](https://firebase.google.com/docs/auth/web/password-auth?hl=ja)の公式ドキュメントを参考に、email, パスワード情報を受け取って、認証結果を送信します。
portを使用してJavaScriptとデータのやり取りをしたい場合、以下のように、app.ports.(Elm側で記載したポート名).subscribe...というように記載します。


```JavaScript

app.ports.signingInWithEmailAndPassword.subscribe((input) => {
  firebase
    .auth()
    //emailとpasswordを受け取る
    .signInWithEmailAndPassword(input.email, input.password)
    .then((_) => {
      //認証が成功すれば、"SignedIn"をElmに送信
      app.ports.validateAuthState.send("SignedIn");
    })
    .catch((error) => {
      //認証が失敗すれば、"SignedInWithError"をElmに送信
      app.ports.validateAuthState.send("SignedInWithError");
    });
});

```

上記で記載している　input.email, input.password　は、Elmでエンコードした以下のコードの"email"と"password"に紐づいています。

```Elm

-- emailとpasswordのエンコード
loginInfoEncoder : Model -> Encode.Value
loginInfoEncoder model =
    Encode.object
        [ ( "email", Encode.string model.emailAndPassword.email )
        , ( "password", Encode.string model.emailAndPassword.password )
        ]

```

なので、例えば、 input."E"mail, input."P"assword のように記載してしまい、Elm側とJavascript側の文字列が異なってしまうと認証に失敗します。


## 5. 認証結果をElmで受け取る

### PORT
JavaScriptから認証状態の文字列を受け取る用のportsを用意します。
文字列を受け取るので(String -> msg)にしています。

```Elm
port validateAuthState : (String -> msg) -> Sub msg
```

### MODEL
認証の状態を表すカスタム型を定義します。

```Elm
type AuthState
    = SignedOut --サインアウト
    | SignedIn --サインイン
    | SignedInWithError --認証失敗

type alias Model =
    { authState : AuthState
    ...
    }
```

### SUBSCRIPTIONS
portで受け取ったデータをElmのUPDATEで使用するために、SUBSCRIPTIONに以下のように記載します。
カスタム型を使用しているので、Sub.mapを使用しています。

```Elm
subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch
        [ validateAuthState ValidateAuthState
            |> Sub.map ValidateAuthStateMsg
        ]
```

### UPDATE
JavaScriptから受け取った文字列に応じて、authStateを更新します。

```Elm
type ValidateAuthStateMsg
    = ValidateAuthState String

type Msg
    = ...
    | ValidateAuthStateMsg ValidateAuthStateMsg

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ...

        ValidateAuthStateMsg validateAuthMsg ->
            updateValidateAuth validateAuthMsg model

--受け取った文字列に応じてauthStateを更新する
updateValidateAuth : ValidateAuthStateMsg -> Model -> ( Model, Cmd Msg )
updateValidateAuth msg model =
    case msg of
        ValidateAuthState authState ->
            case authState of
                "SignedOut" ->
                    ( { model | authState = SignedOut }
                    , Cmd.none
                    )

                "SignedIn" ->
                    ( { model | authState = SignedIn }
                    , Cmd.none
                    )

                "SignedInWithError" ->
                    ( { model | authState = SignedInWithError }
                    , Cmd.none
                    )

                _ ->
                    ( model
                    , Cmd.none
                    )
```

### VIEW
認証の結果を表示します。
authStateに応じて表示する文字列を変更しています。

```Elm
view : Model -> Html Msg
view model =
    div []
        [ ...
        , viewValidateSignIn model
        ]

viewValidateSignIn : Model -> Html Msg
viewValidateSignIn model =
    case model.authState of
        SignedOut ->
            div []
                [ div [] [ text "Status: SignOut" ]
                ]

        SignedIn ->
            div []
                [ div [] [ text "Status: SiginIn" ] ]

        SignedInWithError ->
            div []
                [ div [] [ text "Status: ERROR" ] ]
```

以上でパスワード認証ができました！

## まとめ
　・Firebase認証をするためには、portを使ってElmとJavaScriptを相互に結びつける必要がある
　・パスワード認証をするためには、emailやpasswordといったユーザー情報をエンコードしてJavaScriptに送る必要がある

私もportの仕組みやエンコードの理解には苦しみましたが、慣れればなんとなく分かるようになります。
少しずつ理解していきましょう！

## 参考文献
・Elm公式ドキュメント
　[型エイリアス](https://guide.elm-lang.jp/types/type_aliases.html)
　[カスタム型](https://guide.elm-lang.jp/types/custom_types.html)
　[ポート](https://guide.elm-lang.jp/interop/ports.html)
　[Json.Encode](https://package.elm-lang.org/packages/elm/json/latest/Json.Encode)

・Firebase公式ドキュメント
　[JavaScript でパスワード ベースのアカウントを使用して Firebase 認証を行う](https://firebase.google.com/docs/auth/web/password-auth)


・Qiitaに投稿してくださった記事（参考になります本当にありがとうございます！！）
　[Elm portsでFirebase Firestoreを触ろう！](https://qiita.com/ababup1192/items/f27f9af282d9fa642eb5)
　[（ネストした）recordの更新でエラーが出る](https://qiita.com/miyamo_madoka/items/f225803d275019cc34c1)
　[ElmでネストしたRecordを更新する](https://qiita.com/ababup1192/items/34e926ffc934336242ca)

・関連する自分の記事
　[【Elm】Firebase Firestoreにデータを追加する](https://qiita.com/syobot/items/15ae3b9011889bfaa2f4)