はじめに
こちらの記事でFirebaseのGoogle認証はできたのですが、アホすぎてパスワード認証をするのにハマったので、備忘録として残します。
間違ったこと書いてたら罵詈雑言でも構いませんので教えてください。
やりたいこと
フォームに任意のemailとpasswordを入力して、Firebase認証し、認証結果を表示します。
開発環境
Mac
create-elm-app使用
コード全体
こちら参照ください
index.jsには各自のFirebaseのapiKey等を記載してください。
大まかな流れ
- Firebase Authにユーザーを登録する
- Elm側でemail、パスワード入力フォームを作る
- フォームに入力されたemail、パスワード情報をJavaScriptに送る
- JavaScriptでemail、パスワード情報を受け取り、認証結果を送信する
- 認証結果をElmで受け取る
1. Firebase Authにユーザーを登録する
まず以下を参考にして、新規のFirebaseプロジェクトを作成してください。
分からなければ、ググれば他の方が詳細を書いてくださっているのでここでは省略します。
もしくはコメントしていただければお教えします(わかる範囲で
Firebaseプロジェクトを作成したらプロジェクトのコンソール画面を開き、左上のAutenticationをクリック。
次に、”Sign-in method”タブから、”メール/パスワード”のステータスを"有効"にします。
多分Google認証もすると思うのでGoogleも"有効"にして良いと思います。
次に、"Users"タブから"ユーザーを追加"をクリックして、ユーザー名とパスワードを設定してください。
※自分はこれをやり忘れて存在していないユーザーと認証しようとしていました。アホすぎて自分でも引いてます。
これで事前準備はOKだと思います。
抜けてたらすみません。
2. Elm側でemail、パスワード入力フォームを作る
まずはこんな感じの入力フォームを作ります。
入力フォームの作り方をざっと解説します。
「入力フォームくらい秒で作れるわ!」という方は3まで読み飛ばしてください。
MODEL
まず、Emailとパスワードの型エイリアスを定義します。ここは特に説明不要ですかね。
type alias EmailAndPassword =
{ email : String
, password : String
}
type alias Model =
{ ...
, emailAndPassword : EmailAndPassword
}
UPDATE
Updateにて、フォーム内で文字が入力されるたびに、emailとpasswordを更新していきます。
カスタム型を使用しているので少々ややこしいかもしれません。
ですがカスタム型は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は、最初は以下のような感じで更新しようとしていました。
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)は更新できないのだと分かりました。
そこで、こちらの記事や、こちらの記事を参考にさせていただき、以下のように記載することで解決できました(もっと良い書き方がある?)
let
emailAndPassword =
model.emailAndPassword
in
case msg of
EmailChanged email ->
( { model | emailAndPassword = { emailAndPassword | email = email } }
, Cmd.none
)
VIEW
email, passwordフォームを表示します。
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を使用する必要があることくらいですかね。
Html.map ChangedEmailAndPasswordMsg (viewEmail model)
以上で入力フォームを作成できました。
3. フォームに入力されたemail、パスワード情報をJavaScriptに送る
次に、フォームに入力したemailとpasswordを、portを使ってJavaScriptに送り、JavaScriptでfirebase認証をします。
portに関しての詳しい解説は公式ドキュメントや他の方の記事を読むと良いと思います。
PORT
port signingInWithEmailAndPassword : Encode.Value -> Cmd msg
UPDATE
そして、ここで忘れてはいけないのが、emailとpasswordという文字列をJavascriptに送るためには、"エンコード"をしないといけないということです。
エンコードに関しての詳しい解説は公式ドキュメントや他の方の記事を読むと良いと思います。
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ボタンも作っておきます。
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関係のコードは基本的に同じになると思います。
...
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, パスワード情報を受け取り、認証結果を送信
こちらの公式ドキュメントを参考に、email, パスワード情報を受け取って、認証結果を送信します。
portを使用してJavaScriptとデータのやり取りをしたい場合、以下のように、app.ports.(Elm側で記載したポート名).subscribe...というように記載します。
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"に紐づいています。
-- 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)にしています。
port validateAuthState : (String -> msg) -> Sub msg
MODEL
認証の状態を表すカスタム型を定義します。
type AuthState
= SignedOut --サインアウト
| SignedIn --サインイン
| SignedInWithError --認証失敗
type alias Model =
{ authState : AuthState
...
}
SUBSCRIPTIONS
portで受け取ったデータをElmのUPDATEで使用するために、SUBSCRIPTIONに以下のように記載します。
カスタム型を使用しているので、Sub.mapを使用しています。
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ validateAuthState ValidateAuthState
|> Sub.map ValidateAuthStateMsg
]
UPDATE
JavaScriptから受け取った文字列に応じて、authStateを更新します。
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に応じて表示する文字列を変更しています。
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公式ドキュメント
型エイリアス
カスタム型
ポート
Json.Encode
・Firebase公式ドキュメント
JavaScript でパスワード ベースのアカウントを使用して Firebase 認証を行う
・Qiitaに投稿してくださった記事(参考になります本当にありがとうございます!!)
Elm portsでFirebase Firestoreを触ろう!
(ネストした)recordの更新でエラーが出る
ElmでネストしたRecordを更新する
・関連する自分の記事
【Elm】Firebase Firestoreにデータを追加する