Meteorで非公開ツール用のログインフォームを作る
Meteorの名前をたまたま知って調べてみたら便利そうだなぁ、これで社内向けWebアプリとか作ると楽そうだなぁ、と思って連休中にちょっといじってみました。実際にいじってみると結構未知の部分が多くて、そんなに簡単ではなかったですが……。
- Meteorについては
体感!JavaScriptで超速アプリケーション開発 -Meteor完全解説 - ログイン周りはこちらも参考に
Meteor を使用したインスタント Web アプリケーション - 仕組みはこちらも参考になりました。
Meteorの仕組み(Live page update/Session) - 後は公式のリファレンス
とりあえず社内向けのツールを作ることを目的にしたので、ユーザー作成〜ログインフォームのところから手を付けることに。 仕様としては第三者がユーザーを作れないようにしたいので
- ユーザーがひとりも登録されていない状態でアクセスするとユーザー作成フォームが表示される
- ひとりめを作成した後はログインフォームが表示される
- ログインするとログアウトリンクや2人目のユーザー作成リンクが表示される
- ユーザーを作成できるのはスーパーユーザーのみ
という感じにしたい。
これを最初に作っておけば、色々なツールで使い回せて便利なはずです。
ログイン状態などを確認する
まず、最初のユーザーかどうかと、ログイン済みか否かをチェックする方法から調べます。公式リファレンスを見ると、Meteor.user()でログイン中ならユーザーオブジェクトが、未ログインならnullが返され、Meteor.users()でユーザー一覧のCollectionが返るとのこと。 もう1つMeteor.loggingIn()という紛らわしいのがあるのですが、これはログイン処理中にtrueを返すものでアニメーションを表示させるために使うモノだとか。
問題はクライアントもサーバーもJavaScriptなのでどっちに書くかなのですが、Meteor.user()はクライアント側に、Meteor.users()はサーバー側に書くようです。考えて見るとユーザー一覧をクライアントで取得できたら危ないので当然か。 どうもMeteorではサーバ側に書くのはDBにアクセスする範囲を決める程度で、大半の処理はクライアント側に書けば後はよろしくやってくれるようです。
というわけで、サーバー側のserver/bootstrap.jsにユーザー数が0ならtrueを返すisFirstUserという関数を作成。これはクライアント側から呼び出すものなので、Meteor.methods()を使って定義します。
Meteor.methods({ //最初のユーザーか確認する isFirstUser:function(){ if(Meteor.users.find().count() === 0) return true; else return false; }, });
続いてこの関数を呼び出す部分をclient/logging.jsに書く。Meteor.methods()で定義した関数はMeteor.call()で呼び出します。取得した結果はSession変数というものに保存しておきます。
/*ログインしていないときはログインフォームを表示*/ Session.set('firstuser', false); //最初のユーザーかどうかを確認する Meteor.call('isFirstUser', 1, function (error, result) { Session.set('firstuser', result); });
ログイン中かどうかを調べる部分はこんな感じ。テンプレート関数というものを介してHTMLに反映されるようにしておきます。
//ログイン中か確認する? Template.loginform.nowlogin = function(){ if(Meteor.user()==null) return false; else return true; };
HTMLを動的に書き換える
ログオン状態などを調べる方法がわかったので、続いてフォームの表示・非表示の書き換えをば。テンプレート中で「{{#if}}〜{{/if}}」で条件分岐できるのは知っていたのですが、isFirstUser関数は非同期に答えを返してくるので、そのタイミングで書き換えるにはどうしたらいいか……? テンプレートがセッション変数というものを参照するように設計しておけば紐付けされるので、後はセッション変数を上書きするだけでHTMLも書き換わるのだそうです。なかなか便利です。
crient/lwtaskman.htmlに、こんな感じでテンプレート関数で条件分岐するように書いておいて、
<template name="loginform"> {{#if needlogin}} <h2>ログインしてください(Please Login)</h2> <div> <input type="text" id="loginusername" placeholder="UserName"> <input type="password" id="loginuserpassword"> <button id="btn_login" class="btn btn-primary">Login</button> </div> {{/if}} {{#if firstuser}} <h2>最初のユーザーを作成してください(Please Create First User)</h2> {{> createuserform}} {{/if}} {{#if nowlogin}} <p>{{currentUser}}としてログイン中です。</p> {{> logincontrol}} {{/if}} {{loginerror}} </template>
crient/logging.jsにセッション変数を参照するテンプレート関数を定義しておく。
//ログインしていないときはログインフォームを表示する Template.loginform.needlogin = function(){ if(Meteor.user()==null && Session.get('firstuser') == false){ return true; } else return false; }; //最初のユーザー? Template.loginform.firstuser = function(){ return Session.get('firstuser'); };
これでHTMLが動的に書き換わります。テンプレートごとにコードが分かれるのでわかりやすい気がします。
ユーザー作成とログイン/ログアウト
さて、最後にユーザー作成やログイン/ログアウトの部分ですが、それぞれAccounts.createUser()、Meteor.loginWithPassword()、Meteor.logout()という関数が用意されているので、これらを適宜クライアントから呼び出せば後はよろしくやってくれます。暗号化もしてくれるそうなのでいたれり尽くせりです。 イベント処理の書き方も独特ですが、「'イベントタイプ CSSセレクタ'」と書けばいいそうなんで、慣れれば問題なし。 ユーザー作成部分は無駄に長いので、ログイン処理の部分だけ。
client/logging.js
//ログインボタンでログイン Template.loginform.events({ 'click #btn_login': function () { var user = document.querySelector('#loginusername').value; var pass = document.querySelector('#loginuserpassword').value; Meteor.loginWithPassword({username:user}, pass); } });
……といいつつ、ここでなかなかログインできず、長時間はまりました。色々試行錯誤した後で気が付いたのですが、どうもMeteorでフォームっぽいUIを作るときは、formタグで囲んではいけないらしいです。Meteorでは画面遷移しないのが基本なので、formタグを使うと画面遷移してしまい状態を維持できなくなる……? あまり自信がないのですが、とりあえずformタグをすべてdivタグに置き換えたら問題なく動くようになりました。
ログアウト部分はこんな感じです。 client/lwtaskman.html
<template name="logincontrol"> <ul> <li><a id="logout">ログアウト</a></li> <li><a id="newuser">新規ユーザー作成</a></li> <li><a id="usermanage">ユーザー管理</a></li> </ul> </template>
client/logging.js
//ログイン後のコントロール Template.logincontrol.events({ 'click #logout': function (event) { Meteor.logout(); } });
まぁとりあえずだいたい完成ということで、次はログイン中にユーザー作成できるようにしたいところです。コードがテンプレートと一体になっているので、ログイン後にフォームが表示されるようHTMLを変更すれば、最初のユーザー登録のコードを変更なしで使えるかもしれません。