arinoth's memo

arinothのメモ

Meteorで非公開ツール用のログインフォームを作る(その4)

さて、(たぶん)最後のステップとして、ユーザーの削除や情報変更を行うユーザー管理画面を作成してみます。

f:id:arinoth:20140119171014p:plain

これまでとだいぶ見た目が変わっていますが、見よう見まねでBootstrapなるものを入れ、CSSを少しいじって、ログインコントロール部分が固定ヘッダになるよう調整してみました。 また、テンプレートを利用してアプリケーション名を手軽に変更できるようにもしてみています。

ユーザー一覧へのアクセスを許可する

全ユーザーの情報はデフォルトでは非公開になっているので、サーバ側でパブリッシュ設定を行います(正確にはautopublishパッケージをリムーブした状態のデフォルト)。

参考:体感!JavaScriptで超速アプリケーション開発 -Meteor完全解説 第11回 データ同期とリアクティブ・プログラミング

だだ漏れにならないよう一応アクセスするユーザーがスーパーユーザーであることを確認してから 返すようにしましょうか。 publish内ではthisのuserIdに「データを求めてきたユーザーの_id」が入っているので、 それを元にそのユーザーのprofile.superuserがtrueかどうかを確認します。

server/bootstrap.js

//ユーザー管理のためにpublish
Meteor.publish("users", function(){
    //スーパーユーザー以外が尋ねてきたときは離脱
    var curuser = Meteor.users.findOne({_id: this.userId});
    if(curuser==null || curuser.profile.superuser == false) return null;
    return Meteor.users.find();
});

クライアント側では対になるサブスクライブ(購読)という処理をどこかで行う必要があります。 ログインが成功したタイミングあたりが妥当でしょうか?

//ログインボタンでログイン
Template.loginform.events({
  'click #btn_login': function () {
    var user = document.querySelector('#loginusername').value;
    var pass = document.querySelector('#loginuserpassword').value;
    Meteor.loginWithPassword({username:user}, pass, function(err){
        if(err){
            showLoginFormStatus('ログインできません。ユーザー名とパスワードを確認してください');
        } else {
            //スーパーユーザーならサブスクライブ
            if(Meteor.user().profile.superuser){
                g_usershandle = Meteor.subscribe('allusers');
            }
        }
    });
  },
  'click #loginname': function (event) {
    $(event.currentTarget).next().toggle('fast');
  }
});
var g_usershandle = null;

Meteor.subscribeはサブスクライブハンドルというものを返すので、それをグローバル変数g_usershandleに代入しておきます。 このサブスクライブハンドルは、購読を停止するstop()と購読可能な状態になっているかを確認するready()というメソッドを持っています。

ログオフ時に購読を停止する処理も書いておきます。

Template.logincontrol.events({
    'click #logout': function (event) {
        if(g_usershandle){
            g_usershandle.stop();
            g_usershandle = null;
        }
        Meteor.logout();
    },
……後略……

ユーザー情報の一覧を表示する

ユーザーの一覧を表として表示するので、{{#each 配列を返すテンプレート関数名}}を使ったテンプレートを書きます。 #eachの間では、配列の各要素のプロパティに{{プロパティ名}}でアクセスできます。

<template name="manageuserform">
    <div class="form-horizontal" role="form" id="dialog_manageuser">
        <table class="table table-bordered">
        <tr><th>username</th><th>fullname</th><th>supermode</th></tr>
        {{#each users}}
            <tr>
                <td>{{username}}</td>
                <td>{{profile.name}}</td>
                <td>{{profile.superuser}}</td>
            </tr>
        {{/each}}
        </table>
    </div>
</template>

そしてダイアログを表示するためのコード(前回参照)と、ユーザー一覧を返すテンプレート関数usersを定義します。

/**
ユーザー管理テンプレート
**/
//レンダリング時
Template.manageuserform.rendered = function () {
    $('#dialog_manageuser').dialog({
        title: 'Manage User',
        autoOpen: true,
        modal: true,
        width: '80%',
        close: function( event, ui ) {Session.set('usermanage', false);}
    });
};
//ユーザー一覧
Template.manageuserform.users = function(){
    return Meteor.users.find();
}

一覧いただきました。

f:id:arinoth:20140119171014p:plain

publishとsubscribeを使ってアクセス許可したら、後はサーバサイドと同じようにDBアクセスするだけでいいみたいです。 ずいぶんシンプルですね。 どうもこのへん、わかったようなわからないような……。

リロードすると購読範囲が変わってしまう

ここで問題発生。リロードしたら現在ログインしているユーザーの分しか取得できなくなってしまいました。

f:id:arinoth:20140119171333p:plain

ログイン時にサブスクライブしているので、リロードで自動ログインした場合は維持されないということのようです。

なので、サブスクライブ処理を書く場所を、ログイン時からユーザー一覧のテンプレート関数実行時に移動してみました。

//ユーザー一覧
Template.manageuserform.users = function(){
    //スーパーユーザーならサブスクライブ
    if(Meteor.user().profile.superuser){
        g_usershandle = Meteor.subscribe('allusers');
    }
    return Meteor.users.find();
}

うまく行ったかと思いきや、なんだか妙なことになってしまう。

f:id:arinoth:20140119172248p:plain

スーパーユーザーのみにユーザー管理ボタンを表示させるために定義しておいたテンプレート関数があったので、 そちらに移動してみます。

Template.logincontrol.superuser = function(){
    //スーパーユーザーならサブスクライブ
    if(Meteor.user().profile.superuser){
        g_usershandle = Meteor.subscribe('allusers');
    }
    return Meteor.user().profile.superuser;
};

今度は大丈夫みたいです。よくわからんのですが、表示のタイミングでサブスクライブしてはいかんようです。 (その後もユーザー管理ダイアログを出した状態でソースコードをいじるとダイアログが増えることがあったので、そもそも前回の記事に書いたレンダリング時にjQuery UIのdialogを呼び出すというやり方があまりよくないのかも)

(追記:調べたところdialogによって作られた要素には「aria-describedby="ダイアログ化した要素のID"」が付くので、それをjQueryで調べてdialogの複数作成を防ぐ処理を追加しました。あまり奇麗ではないけど)

リアクティブなんとかがすごい

ここでウィンドウを2つ開いてリアクティブなんちゃらのテストをしてみます。

一方で新規ユーザーを追加すると……

f:id:arinoth:20140119173326p:plain

f:id:arinoth:20140119173614p:plain

ひゅー、これはすごい。

次はユーザー管理画面でフルネームの変更やsupermodeのオン/オフ、ユーザーの削除などができるようにしてみます。 Excelみたいにセルを直接編集できるようにしてみたいところですが……。

その後デプロイを試してから(Appfogというところにデプロイできるらしい)、ようやく本番のアプリに手を付けてみようかな……的な。

ここまでの成果(ZIP)