この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
以前のブログにて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