[Ruby on Rails]AdminLTEのログインテンプレートを使用してみる
はじめに
以前のブログにてAdminLTEについて紹介しました。今回はアプリケーションを作成する上で実装することが多いと思われるログイン・ログアウト周りの機能を、AdminLTEのテンプレートを用いて実装してみました。この実装のクライアント側について、主に書いていきたいと思います。
サーバについては、以下の記事で作成したsorceryを使用した認証APIと同じソースとなるので、必要な場合はそちらを参考にしてください。
[Ruby on Rails]sorceryによる認証 – (5)APIでの認証 #1 実装の概要
[Ruby on Rails]sorceryによる認証 – (5)APIでの認証 #2 コントローラ
[Ruby on Rails]sorceryによる認証 – (5)APIでの認証 #3 モデル
[Ruby on Rails]sorceryによる認証 – (7)APIでのパスワードリセット
アプリの概要
以前の記事と同様、サーバとクライアントを明確に分離させ
- サーバ ・・・ RailsによるAPIサーバで、認証の結果をHTTPステータスで返却する
- クライアント ・・・ AdminLTEのライブラリとHTMLのみで、サーバより結果を取得してビューを作成する
という構成にしました。クライアントについては、今回はオーソドックスなjQueryによる実装としてあります。クライアントの構成は[Ruby on Rails]AdminLTEをテンプレートとして使用する(1) – アプリの概要を踏襲しているので、未読の場合は一読をお勧めします。
フォルダ構成
以下のようなフォルダ構成としました。
. ├── client ・・・クライアント │ ├── assets │ │ └── javascript │ │ └── config.js ・・・共通の定義、メソッドを記述 │ ├── lib │ │ ├── adminlte ・・・AdminLTEのライブラリをそのまま格納 │ │ ├── bootstrap │ │ ├── dist │ │ ├── plugins │ ├── pages │ │ └── examples │ │ ├── 500.html ・・・エラーページ │ │ ├── blank.html ・・・ログインした場合のみ表示する画面 │ │ ├── login.html ・・・ログイン画面 │ │ ├── register.html ・・・ユーザ登録画面 │ │ └── reset_password.html ・・・パスワードリセット画面 │ └── starter.html └── server ・・・サーバ(Rails) ├── app │ ├── controllers │ │ ├── api │ │ │ └── v1 │ │ │ ├── application_base_controller.rb │ │ │ ├── auth_check_controller.rb │ │ │ ├── password_resets_controller.rb │ │ │ ├── user_sessions_controller.rb │ │ │ └── users_controller.rb │ │ ├── application_controller.rb │ │ └── concerns │ ├── mailers │ │ ├── application_mailer.rb │ │ └── user_mailer.rb │ ├── models │ │ ├── api_key.rb │ │ ├── concerns │ │ └── user.rb │ └── views │ └── user_mailer │ └── reset_password_email.text.erb ├── config (以降略)
クライアントについては/pages/examplesフォルダ内に、今回必要なhtmlを配置しました。サーバについては上記にリンクした記事を参考にしてください。
画面フロー
今回作成する機能のイメージを掴みやすくするため、ログイン〜ログアウト等の画面を処理順に並べておきます。
1.ユーザの作成
2.ログイン
3.ログイン後の画面遷移
4.ログアウト
上記のblank.htmlの右上より「Sign Out」ボタンを表示し、押下することでログアウトします。
5.パスワードリセット
「I forgot my password」リンクを押下すると、トークン付きのパスワードリセット画面のURLをメールで送信します。
メールに記述されたURLにアクセスすると、パスワードリセット画面を表示します。
パスワードリセットが完了すると、ログイン画面に遷移してリセットした旨を画面に表示します。
クライアントの実装
ではクライアントの実装についてです。AdminLTEのページよりテンプレートをダウンロードします。テンプレート内の「pages/examples」フォルダ内より、先のフォルダ構成図に載っているhtmlをコピーします。
1.ユーザの作成
「register.html」になります。AdminLTEのオリジナルのhtmlから変更した箇所は以下の通りです。
register.html
<!DOCTYPE html> <html> <head> (中略) </head> <body class="hold-transition register-page"> <div class="register-box"> <div class="register-logo"> <a href="../../lib/adminlte/index2.html"><b>Admin</b>LTE</a> </div> <div id="warning" class="callout callout-danger"> <h4>Warning!</h4> <div id="message"> This is a message. This is a message. This is a message. </div> </div> (中略) <form> (中略) <div class="row"> <div class="col-xs-8"> </div><!-- /.col --> <div class="col-xs-4"> <button id="register" type="button" class="btn btn-primary btn-block btn-flat">Register</button> </div><!-- /.col --> </div> </form> (中略) <!-- common --> <script src="../../assets/javascript/config.js"></script> <script> $(function () { $('input').iCheck({ checkboxClass: 'icheckbox_square-blue', radioClass: 'iradio_square-blue', increaseArea: '20%' // optional }); $("#warning").css("display", "none"); $("#register").click(function(){ var input = { user: { email: $("#email").val(), name: $("#name").val(), password: $("#password").val(), password_confirmation: $("#password_confirmation").val() } }; $.ajax({ url: requestUrl('api/v1/users.json'), type: 'POST', data: input, dataType: 'json', complete: function(XMLHttpRequest, textStatus, errorThrown) { switch (XMLHttpRequest.status){ case 201: sessionStorage.info = 'Login user is registered. Please login.'; document.location.href = "login.html"; break; case 400: var messages = []; messages = XMLHttpRequest.responseJSON; var message = ""; for(i = 0; i < messages.length; i++){ message += messages[i] + "\n"; } $("#warning").toggle(); $("#message").text(message); break; default: console.log("error"); break; } } }); }); }); </script> </body> </html>
12〜19行目にメッセージを表示するためのdivタグを定義しています。また54行目以降で「Register」ボタンが押下された場合にサーバ側のAPIを呼び出し、ユーザを登録しています。登録後は「login.html」に遷移しています(63行目)。
呼び出しているAPIのURL、レスポンスの例は以下の通りです。
$ curl -i -X POST http://localhost:3000/api/v1/users.json -d 'user[email]=sample@ggg.com' -d 'user[name]=sample' -d 'user[password]=password' -d 'user[password_confirmation]=password' HTTP/1.1 201 Created
2.ログイン
「login.html」になります。AdminLTEのオリジナルのhtmlから変更した箇所は以下の通りです。
login.html
<!DOCTYPE html> <html> (中略) <body class="hold-transition login-page"> <div class="login-box"> <div class="login-logo"> <a href="../../lib/adminlte/index2.html"><b>Admin</b>LTE</a> </div><!-- /.login-logo --> <div id="warning" class="callout callout-danger"> <h4>Warning!</h4> <div id="warning_message"> This is a message. </div> </div> <div id="info" class="callout callout-info"> <h4>Info</h4> <div id="info_message"> This is a message. </div> </div> (中略) <form> <div class="row"> <div class="col-xs-8"> <div class="checkbox icheck"> <label> <input id="remember_me" type="checkbox"> Remember Me </label> </div> </div><!-- /.col --> <div class="col-xs-4"> <button id="login" type="button" class="btn btn-primary btn-block btn-flat">Sign In</button> </div><!-- /.col --> </div> </form> <a id="forgot_password" href="#">I forgot my password</a><br> <a href="register.html" class="text-center">Register a new membership</a> </div><!-- /.login-box-body --> </div><!-- /.login-box --> <!-- jQuery 2.1.4 --> <script src="../../lib/adminlte/plugins/jQuery/jQuery-2.1.4.min.js"></script> <!-- Bootstrap 3.3.5 --> <script src="../../lib/adminlte/bootstrap/js/bootstrap.min.js"></script> <!-- iCheck --> <script src="../../lib/adminlte/plugins/iCheck/icheck.min.js"></script> <!-- cookie --> <script src="../../lib/jquery-cookie/jquery.cookie.js"></script> <!-- common --> <script src="../../assets/javascript/config.js"></script> <script> function clearDisplays(){ $("#warning").css("display", "none"); $("#info").css("display", "none"); } function displayWarning(message){ clearDisplays(); $("#warning").toggle(); $("#warning_message").text(message); } function displayInfo(message){ clearDisplays(); $("#info").toggle(); $("#info_message").text(message); } $(function () { $('input').iCheck({ checkboxClass: 'icheckbox_square-blue', radioClass: 'iradio_square-blue', increaseArea: '20%' // optional }); // set warning message. if(sessionStorage.warning === undefined){ clearDisplays(); }else{ displayWarning(sessionStorage.warning); delete sessionStorage.warning; } // set info message. if(sessionStorage.info === undefined){ clearDisplays(); }else{ displayInfo(sessionStorage.info); delete sessionStorage.info; } // login. $("#login").click(function(){ var input = { user: { email: $("#email").val(), password: $("#password").val() } }; $.ajax({ url: requestUrl('api/v1/user_sessions.json'), type: 'POST', data: input, dataType: 'json', complete: function(XMLHttpRequest, textStatus, errorThrown) { switch (XMLHttpRequest.status){ case 200: if($("#remember_me").prop('checked')){ $.cookie('adminlte_rails_sample_access_token', XMLHttpRequest.responseJSON.access_token, { expires: 7, path: '/' }); }else{ $.cookie('adminlte_rails_sample_access_token', XMLHttpRequest.responseJSON.access_token, { path: '/' }); } sessionStorage.adminlte_rails_sample_user_name = XMLHttpRequest.responseJSON.user.name; document.location.href = "blank.html"; break; case 404: displayWarning('Login failed. Check your email or password.'); break; default: console.log("error"); break; } } }); }); // login. }); </script> </body> </html>
96〜131行目で「Sign in」ボタンを押下したときの動作を記述しています。ログイン用のAPIを呼び出し(106行目)、成功したら次のページに遷移しています(119行目)。また「Remember Me」にチェックを入れた場合、一定期間トークンをブラウザに保持することで、ログイン画面を経由することなくログイン状態としています(113〜117行目)。
「Sign in」で呼び出すAPIのURL、レスポンスの例は以下の通りです。
$ curl -i -X POST http://localhost:3000/api/v1/user_sessions.json -d 'user[email]=sample@ggg.com' -d 'user[password]=password' HTTP/1.1 200 OK (中略) {"user":{"id":51,"email":"sample@ggg.com","name":"sample"},"access_token":"8835b89b52db281a9988847a0cda2e58"}
3.ログイン後の画面遷移、4.ログアウト
「blank.htm」の表示時に、ログイン済みのユーザかをAPIを呼び出すことで確認しています。またログアウトも今回は「blank.htm」の中に記述しました。画面共通のヘッダー部となるため、共通のhtmlの中に本来は記述するべきかもしれません。
blank.html
<!DOCTYPE html> <html> (中略) <body class="hold-transition skin-blue sidebar-mini"> <!-- Site wrapper --> <div class="wrapper"> <header class="main-header"> (中略) <!-- Menu Footer--> <li class="user-footer"> <div class="pull-left"> <a href="#" class="btn btn-default btn-flat">Profile</a> </div> <div class="pull-right"> <a id="logout" class="btn btn-default btn-flat">Sign out</a> </div> </li> (中略) </header> (中略) <!-- cookie --> <script src="../../lib/jquery-cookie/jquery.cookie.js"></script> <!-- common --> <script src="../../assets/javascript/config.js"></script> <script> $(function () { // access token check. var access_token = ''; if($.cookie('adminlte_rails_sample_access_token') !== undefined){ access_token = $.cookie('adminlte_rails_sample_access_token'); } $.ajax({ url: requestUrl('api/v1/auth_check/restrict.json'), type: 'GET', headers: { 'ACCESS_TOKEN': access_token }, dataType: 'json', complete: function(XMLHttpRequest, textStatus, errorThrown) { switch (XMLHttpRequest.status){ case 200: break; case 401: sessionStorage.warning = "You have to login."; document.location.href = "login.html"; break; default: console.log("error"); document.location.href = "500.html"; break; } } }); // set user name. var user_name = sessionStorage.adminlte_rails_sample_user_name; $("#user_name_left").text(user_name); $("#user_name_top").text(user_name); // logout $("#logout").click(function(){ $.ajax({ url: requestUrl('api/v1/user_sessions.json'), type: 'DELETE', headers: { 'ACCESS_TOKEN': access_token }, dataType: 'json', complete: function(XMLHttpRequest, textStatus, errorThrown) { switch (XMLHttpRequest.status){ case 200: $.removeCookie('adminlte_rails_sample_access_token'); sessionStorage.info = "Logout success."; document.location.href = "login.html"; break; case 401: sessionStorage.warning = "You have to login."; document.location.href = "login.html"; break; default: console.log("error"); document.location.href = "500.html"; break; } } }); }); }); </script> (中略)
28〜56行目でAPIにトークンを渡し、ログイン済みかをチェックしています。64〜89行目ではログアウト用のAPIを呼び出し、ログアウト処理をしています。それぞれで呼び出すAPIのURL、レスポンスの例は以下の通りです。
$ curl -i -X GET -H 'ACCESS_TOKEN: 8835b89b52db281a9988847a0cda2e58' http://localhost:3000/api/v1/sample/restrict.json HTTP/1.1 200 OK (中略) {"message":"authorized"}
$ curl -i -X DELETE -H 'ACCESS_TOKEN: 8835b89b52db281a9988847a0cda2e58' http://localhost:3000/api/v1/user_sessions.json HTTP/1.1 200 OK
5.パスワードリセット
「login.html」の「Forgot password」リンクを押下すると、パスワードリセット用のAPIを呼び出します。
login.html
<script> // forgot_password. $("#forgot_password").click(function(){ $.ajax({ url: requestUrl('api/v1/password_resets.json'), type: 'POST', data: 'email=' + $("#email").val(), dataType: 'json', complete: function(XMLHttpRequest, textStatus, errorThrown) { switch (XMLHttpRequest.status){ case 201: displayInfo('Email is send. Please Check your Email.'); break; case 404: displayWarning('Email is not found.'); break; default: console.log("error"); break; } } }); }); // forgot_password }); </script>
呼び出すAPIのURL、レスポンスの例は以下の通りです。
$ curl -i -X POST http://localhost:3000/api/v1/password_resets.json -d 'email=xxxx@xxxx.co.jp' HTTP/1.1 201 Created
次にパスワードを再設定するための画面です。「reset_password.html」ですが、これはAdminLTEの「login.html」を流用して作成しました。主なソースは以下のようになります。
reset_password.html
<!DOCTYPE html> <html> (中略) <body class="hold-transition register-page"> <div class="register-box"> <div class="register-logo"> <a href="../../lib/adminlte/index2.html"><b>Admin</b>LTE</a> </div> <div id="warning" class="callout callout-danger"> <h4>Warning!</h4> <div id="warning_message"> This is a message. This is a message. This is a message. </div> </div> <div class="register-box-body"> <p class="login-box-msg">Reset Password</p> <form> <div class="form-group has-feedback"> <input id="password" type="password" class="form-control" placeholder="Password"> <span class="glyphicon glyphicon-lock form-control-feedback"></span> </div> <div class="form-group has-feedback"> <input id="password_confirmation" type="password" class="form-control" placeholder="Retype password"> <span class="glyphicon glyphicon-log-in form-control-feedback"></span> </div> <div class="row"> <div class="col-xs-8"> </div><!-- /.col --> <div class="col-xs-4"> <button id="register" type="button" class="btn btn-primary btn-block btn-flat">Register</button> </div><!-- /.col --> </div> </form> <a href="login.html" class="text-center">I already have a membership</a> </div><!-- /.form-box --> </div><!-- /.register-box --> <!-- jQuery 2.1.4 --> <script src="../../lib/adminlte/plugins/jQuery/jQuery-2.1.4.min.js"></script> <!-- Bootstrap 3.3.5 --> <script src="../../lib/adminlte/bootstrap/js/bootstrap.min.js"></script> <!-- iCheck --> <script src="../../lib/adminlte/plugins/iCheck/icheck.min.js"></script> <!-- common --> <script src="../../assets/javascript/config.js"></script> <script> // reset passoword function. function reset_password(token){ var input = { user: { password: $("#password").val(), password_confirmation: $("#password_confirmation").val() } }; $.ajax({ url: requestUrl('api/v1/password_resets/' + token + '.json'), type: 'PUT', data: input, dataType: 'json', complete: function(XMLHttpRequest, textStatus, errorThrown) { switch (XMLHttpRequest.status){ case 200: sessionStorage.info = 'Reset password success. Please login.'; document.location.href = "login.html"; break; case 404: $("#warning").toggle(); $("#warning_message").text('Request is not found.'); break; case 406: $("#warning").toggle(); $("#warning_message").text('Reset password failed. Please check your passwords.'); default: console.log("error"); break; } } }); } $(function () { $('input').iCheck({ checkboxClass: 'icheckbox_square-blue', radioClass: 'iradio_square-blue', increaseArea: '20%' // optional }); $("#warning").css("display", "none"); // register. $("#register").click(function(){ var parameters = location.href.split("?"); var token = parameters[1].split("=")[1]; // token check, reset password. $.ajax({ url: requestUrl('api/v1/password_resets/' + token + '/edit.json'), type: 'GET', dataType: 'json', complete: function(XMLHttpRequest, textStatus, errorThrown) { switch (XMLHttpRequest.status){ case 200: reset_password(token); break; case 404: $("#warning").toggle(); $("#warning_message").text('Request is not found.'); break; default: console.log("error"); break; } } }); }); }); </script> </body> </html>
97行目以降で「Register」ボタンが押下された時のパスワードリセットの動作を記述していますが、2回APIを呼び出しています。まずは103行目でパスワードリセット用のトークンが正しいかのチェックを行い、正しい場合は108行目で「reset_password」メソッドを呼び出します。53行目から始まるreset_password」メソッドで、パスワードリセット用のAPIを呼び出しています。それぞれで呼び出すAPIのURL、レスポンスの例は以下の通りです。
$ curl -i -X GET http://0.0.0.0:3000/api/v1/password_resets/CpYDpyuHKrLVUJFjUzAE/edit.json HTTP/1.1 200 OK
$ curl -i -X PUT http://0.0.0.0:3000/api/v1/password_resets/CpYDpyuHKrLVUJFjUzAE.json -d 'user[password]=password' -d 'user[password_confirmation]=password' HTTP/1.1 200 OK
まとめ
以前にも書きましたが、サーバとクライアントを完全に分離することで、今回はクライアントの開発に専念することができました(サーバは以前に作成した処理を流量したため)。クライアントの開発に専念できる=AdminLTEなどのテンプレートを崩さずに使うことができることが、この構成のメリットだと思います。
今回作成したソースコードは以下のGithubに置いてあります。全ソースを見たい方は参考にしてください。
adminlte_rails_sample