Ajaxログイン認証 公開ソースコード

開発/実行環境

OS
Windows 7
サーバー言語
PHP
言語バージョン
ローカル 5.38
リモート 5.3.15
実行環境
Xampp 1.7.7
開発環境
Dreamweaver CS5.5
ローカルサーバー
Apache
RDBS
MySQL 5.5.16

ディレクトリ・ファイル構成

Ajaxログイン認証 デモページ(SSL通信)

  root(https://digick-wiz-code.ssl-lolipop.jp/)
   │
   │
   └─dev
       │
       ├─lib
       │   jquery
       │   bootstrap
       │   underscore
       │
       ├─js
       │   FSM-0.6.js
       │
       └─ajax-login
  
  ────────────────────────────────
  ajax-login
     │
     │  .htaccess
     │
     │  config.php
     │  functions.php
     │
     │  index.php
     │  login.php
     │  logout.php
     │  register.php
     │  
     ├─css
     │   ajax-login.css
     │  
     ├─js
     │   ajax-login.js
     │
     └─tpl
         admin-frag.inc.php
         ajax-login.inc.php
         auth-frag.inc.php
                

データベースの構成

データベース名
ajax_login
エンコーディング/照合順序
utf8_general_ci
カラム名 種別 ヌル(NULL) デフォルト値 その他
id varchar(255) いいえ なし 主キー
email varchar(255) はい NULL
password varchar(255) はい NULL
type varchar(255) はい NULL
create_time datetime はい NULL
update_time datetime はい NULL
last_pass_change_time datetime はい NULL

ステートマシン図

ステートマシン図

php.iniの設定(ローカル)

セッション関係

session.use_cookies = 1
session.use_only_cookies = 1
session.use_trans_sid = 0

session.gc_probability = 1
session.gc_divisor = 100
session.gc_maxlifetime = 3600

エンコーディング関係

mbstring.language = Japanese
mbstring.internal_encoding = UTF-8
mbstring.http_input = pass
mbstring.http_output = pass
mbstring.encoding_translation = Off
mbstring.detect_order = UTF-8,SJIS,EUC-JP,JIS,ASCII
mbstring.substitute_character = none
mbstring.func_overload = 0
mbstring.strict_detection = Off

-- AdminWeb php.iniファイルの確認と修正

ソースコード

config.php

  <?php
    
    $dbname = 'ajax_login';
    $host = '127.0.0.1';
    $dsn = "mysql:dbname={$dbname};host={$host};charset=utf8";
    $username = 'root';
    $password = 'wizard1977';
    $driver_options = array(
	    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
	    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
	    PDO::ATTR_EMULATE_PREPARES => false,
    );
    
  ?>
                

functions.php

  <?php
    /* FIXEDSALTに暗号論的擬似乱数生成器関数を使用 */
    //echo bin2hex(openssl_random_pseudo_bytes(20));
    
    define('FIXEDSALT', '5dd7599454e8068c0e740710fa50861711c65c94');
    define('STRETCH_COUNT', 1000);
    
    function get_salt($id) {
      return $id.pack('H*', FIXEDSALT);
    }
    
    function get_password_hash($id, $pwd) {
      $salt = get_salt($id);
      $hash = '';
      for ($i = 0; $i < STRETCH_COUNT; $i++) {
        $hash = hash('sha256', $hash.$pwd.$salt);
      }
      return $hash;
    }
    
    function json_safe_encode($val) {
      return json_encode($val, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
    }
    
    function h($str) {
      return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
    }	
  ?>
                

index.php

  <?php
  require_once './functions.php';
  
  /* クッキーに保存されるセッションIDの設定 */
  session_set_cookie_params(60 * 60, '/', '', TRUE, TRUE); //期間は60分。HttpOnlyかつSecure属性はTRUE
  
  session_start();
  
  if (!isset($_SESSION['user_id'])) {
    /* ユーザーのログイン/登録手続き用のCSRF対策トークン */
    $token = session_id();
    
    /* ログイン/登録画面のコンポーネントを先に変数に入れる */
    ob_start();
    include_once 'tpl/auth-frag.inc.php';
    $template_frag = ob_get_contents();
    ob_end_clean();
    
    /* 後からページのコンテナ部分を読み込む */
    include_once 'tpl/ajax-login.inc.php';
    
  } else {
    /* 認証済みの場合、セッションIDを振り直す */
    session_regenerate_id(TRUE);
    
    $user_id = $_SESSION['user_id'];
    $email = $_SESSION['email'];
    /* ユーザーの何らかの手続き用のCSRF対策トークン */
    $token = session_id();
    
    /* ユーザー管理画面のコンポーネントを変数に入れる */
    ob_start();
    include_once 'tpl/admin-frag.inc.php';
    $template_frag = ob_get_contents();
    ob_end_clean();
    
    /* 後からページのコンテナ部分を読み込む */
    include_once 'tpl/ajax-login.inc.php';
  }
  ?>
                

register.php

<?php
require_once './config.php';
require_once './functions.php';

/* クッキーに保存されるセッションIDの設定 */
session_set_cookie_params(60 * 60, '/', '', TRUE, TRUE);

header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');

$response = array();

session_start();

/* セッションがタイムアウトしていたら手続きを終了 */
if (!isset($_COOKIE[session_name()])) {
	$response['message'] = 'expired';
	echo json_safe_encode($response);
	exit;
}

/* Ajax以外のリクエストだったら強制終了 */
if (@$_SERVER['HTTP_X_REQUESTED_WITH'] !== 'XMLHttpRequest') {
	$response['message'] = 'not XHR';
	echo json_safe_encode($response);
	exit;
}

$user_id = $_POST['user-id'];
$email = $_POST['email'];
$pass = $_POST['password'];
$regi_token = $_POST['regi-token'];

/* トークンが異なっていたら強制終了 */
if (session_id() !== $regi_token) {
	$response['message'] = 'illicit access';
	echo json_safe_encode($response);
	exit;
}

/* サーバーサイドでも入力値の検証を行う */
if (preg_match('/\A[a-zA-Z0-9_]{4,20}\z/u', $user_id) !== 1) {
	$response['message'] = 'invalid userId';
	echo json_safe_encode($response);
	exit;
}

if (preg_match('/^(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))$/ui', $email) !== 1) {
	$response['message'] = 'invalid email';
	echo json_safe_encode($response);
	exit;
}

if (preg_match('/\A(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\d)[a-zA-Z\d]{8,16}+\z/u', $pass) !== 1) {
	$response['message'] = 'invalid password';
	echo json_safe_encode($response);
	exit;
}

try {

    /* データベース接続 */
    $pdo = new PDO($dsn, $username, $password, $driver_options);
    
    $sql = "SELECT * FROM account WHERE id=:id";
    $stmt = $pdo->prepare($sql);
    $stmt->bindParam(':id', $user_id, PDO::PARAM_STR);
    $stmt->execute();
    $user_id_duplicated = $stmt->fetch();
    
    $sql = "SELECT * FROM account WHERE email=:email";
    $stmt = $pdo->prepare($sql);
    $stmt->bindParam(':email', $email, PDO::PARAM_STR);
    $stmt->execute();
    $email_duplicated = $stmt->fetch();
    
    if (empty($user_id_duplicated) && empty($email_duplicated)) {
        /* 重複登録がないことを確認。登録処理へ */
        $time = date('Y-m-d H:i:s');
        $hash = get_password_hash($user_id, $pass);
        
        $sql = "INSERT INTO account (id, email, password, type, create_time, update_time, last_pass_change_time) VALUES(:id, :email, :pass, :type, :create_time, :update_time, :last_pass_change_time)";
        $stmt = $pdo->prepare($sql);
        $stmt->bindParam(':id', $user_id, PDO::PARAM_STR);
        $stmt->bindParam(':email', $email, PDO::PARAM_STR);
        $stmt->bindParam(':pass', $hash, PDO::PARAM_STR);
        $stmt->bindValue(':type', 'general', PDO::PARAM_STR);
        $stmt->bindParam(':create_time', $time, PDO::PARAM_STR);
        $stmt->bindParam(':update_time', $time, PDO::PARAM_STR);
        $stmt->bindParam(':last_pass_change_time', $time, PDO::PARAM_STR);
        $stmt->execute();
        
        /* 認証状態に移行 */
        session_regenerate_id(TRUE);
        $_SESSION['user_id'] = $user_id;
        $_SESSION['email'] = $email;
        
        /* ユーザーが何らかの手続きを行うときのCSRF対策トークン */
        $token = session_id();
        
        /* レスポンスをJSON形式で作成 */
        $response['message'] = 'success';
        $response['id'] = h($user_id);
        
        /* 認証後のページのHTMLデータを変数に入れる */
        ob_start();
        include_once 'tpl/admin-frag.inc.php';
        $template_frag = ob_get_contents();
        ob_end_clean();
        
        $response['html'] = $template_frag;
    } else {
        if (!empty($user_id_duplicated)) {
            $response['message'] = 'username duplicated';
        } else {
            $response['message'] = 'email duplicated';
        }
    }
    
    echo json_safe_encode($response);

} catch (PDOException $e) {
	$response['message'] = $e->getMessage();
	
	echo json_safe_encode($response);
	exit;
}
  ?>
                

login.php

<?php
require_once './config.php';
require_once './functions.php';

/* クッキーに保存されるセッションIDの設定 */
session_set_cookie_params(60 * 60, '/', '', TRUE, TRUE);

header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');

$response = array();

session_start();

/* セッションがタイムアウトしていたら手続きを終了 */
if (!isset($_COOKIE[session_name()])) {
	$response['message'] = 'expired';
	echo json_safe_encode($response);
	exit;
}

/* Ajax以外のリクエストだったら強制終了 */
if (@$_SERVER['HTTP_X_REQUESTED_WITH'] !== 'XMLHttpRequest') {
	$response['message'] = 'not XHR';
	echo json_safe_encode($response);
	exit;
}

$email = $_POST['email'];
$pass = $_POST['password'];
$login_token = $_POST['login-token'];

/* トークンが異なっていたら強制終了 */
if (session_id() !== $login_token) {
	$response['message'] = 'illicit access';
	echo json_safe_encode($response);
	exit;
}

/* サーバーサイドでも入力値の検証を行う */
if (preg_match('/^(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))$/ui', $email) !== 1) {
	$response['message'] = 'invalid email';
	echo json_safe_encode($response);
	exit;
}

if (preg_match('/\A(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\d)[a-zA-Z\d]{8,16}+\z/u', $pass) !== 1) {
	$response['message'] = 'invalid password';
	echo json_safe_encode($response);
	exit;
}

try {
	$pdo = new PDO($dsn, $username, $password, $driver_options);
	
	$sql = "SELECT * FROM account WHERE email=:email";
	$stmt = $pdo->prepare($sql);
	$stmt->bindParam(':email', $email, PDO::PARAM_STR);
	$stmt->execute();
	
	$account = $stmt->fetch();
	if (!empty($account)) {
		$hash = get_password_hash($account['id'], $pass);
		if ($account['password'] === $hash) {
			$time = date('Y-m-d H:i:s');
			$user_id = $account['id'];
			
			$sql = "UPDATE account SET update_time=:update_time WHERE id=:id";
			$stmt = $pdo->prepare($sql);
			$stmt->bindParam(':update_time', $time, PDO::PARAM_STR);
			$stmt->bindParam(':id', $user_id, PDO::PARAM_STR);
			$stmt->execute();
			
			/* 認証状態に移行 */
			session_regenerate_id(TRUE);
			$_SESSION['user_id'] = $user_id;
			$_SESSION['email'] = $email;
			
			/* ユーザーが何らかの手続きを行うときのCSRF対策トークン */
			$token = session_id();
			
			/* レスポンスをJSON形式で作成 */
			$response['message'] = 'success';
			$response['id'] = h($user_id);
			
			/* 認証後のページのHTMLデータを変数に入れる */
			ob_start();
			include_once 'tpl/admin-frag.inc.php';
			$template_frag = ob_get_contents();
			ob_end_clean();
			
			$response['html'] = $template_frag;
		} else {
			$response['message'] = 'failure';
		}
	} else {
		$response['message'] = 'failure';
	}
	
	echo json_safe_encode($response);
	
} catch (PDOException $e) {
	$response['message'] = $e->getMessage();
	
	echo json_safe_encode($response);
	exit;
}
  ?>
                

logout.php

  <?php
  require_once './functions.php';
  
  header('Content-Type: application/json; charset=utf-8');
  header('X-Content-Type-Options: nosniff');
  
  $response = array();
  
  if (@$_SERVER['HTTP_X_REQUESTED_WITH'] !== 'XMLHttpRequest') {
    $response['message'] = 'not XHR';
    echo json_safe_encode($response);
    die();
  }
  
  $logout_token = $_POST['logout-token'];
  
  session_start();
  
  /* トークンが異なっていたら強制終了 */
  if (session_id() !== $logout_token) {
    $response['message'] = 'illicit access';
    echo json_safe_encode($response);
    die();
  }
  
  $response['message'] = 'success';
  
  /* 念のためセッションIDを格納しているCookieを削除 */
  setcookie(session_name(), '', time() - 3600, '/');
  
  /* セッションデータを削除し、セッションを終了する */
  $_SESSION = array();
  session_destroy();
  
  echo json_safe_encode($response);
  ?>
                

ajax-login.inc.php

  <!DOCTYPE html>
  <html lang="ja">
    <head>
      <meta charset="utf-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="Description" content="JavaScriptやAjaxライブラリを利用したゲームなどを公開しています。">
      <meta name="Keywords" content="Ajax,JavaScript,HTML5,jQuery,Bootstrap,ゲーム">
      <meta name="author" content="masa">
      <meta name="copyright" content="Copyright &copy; 2009 WIZARD-CODE">
      <title>Ajaxログイン認証 - ウィザード・コード | WIZARD-CODE</title>
      <link rel="shortcut icon" href="//wiz-code.digick.jp/image/icon/favicon.ico">
      <link rel="copyright" href="//wiz-code.digick.jp/">
      <link href="/lib/bootstrap/3.3.1/css/bootstrap.css" rel="stylesheet">
      <link href="/test/ajax-login/css/ajax-login.css" rel="stylesheet">
    </head>
    <body>
      <div class="container-fluid">
        <div id="ajax-login" class="panel panel-default"><?=$template_frag?></div>
      </div>
      <script src="/lib/jquery/jquery-1.10.1.js"></script>
      <script src="/lib/bootstrap/3.3.1/js/bootstrap.js"></script>
      <script src="/lib/underscore/1.7.0/underscore.js"></script>
      <script src="/js/FSM-0.6.js"></script>
      <script src="/test/ajax-login/js/ajax-login.js"></script>
    </body>
  </html>
                

auth-frag.inc.php

  <div id="content" class="panel-body" data-authentication-status="unauthenticated">
    <h1 id="title" class="text-center">Ajaxログイン認証</h1>
    <div id="auth" class="text-center hidden">
      <button id="register-button" type="button" class="btn btn-primary">登録する</button>
      <button id="login-button" type="button" class="btn btn-default">ログインする</button>
    </div>
    <div id="return-home-button" class="hidden"><button type="button" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-home"></span></button></div>
    <div id="validation" class="text-danger bg-danger hidden"></div>
    <div id="register" class="hidden">
      <form class="form-horizontal">
        <fieldset>
          <div class="form-group form-group-xs">
            <label for="user-id" class="col-xs-offset-1 col-xs-4 control-label">登録ユーザー名</label>
            <div class="col-xs-4">
              <input type="text" class="form-control user-id" name="user-id" placeholder="登録ユーザー名">
            </div>
          </div>
          <div class="form-group form-group-xs">
            <label for="email" class="col-xs-offset-1 col-xs-4 control-label">登録Eメール</label>
            <div class="col-xs-4">
              <input type="email" class="form-control email" name="email" placeholder="登録Eメール">
            </div>
          </div>
          <div class="form-group form-group-xs">
            <label for="password" class="col-xs-offset-1 col-xs-4 control-label">登録パスワード</label>
            <div class="col-xs-4">
              <input type="password" class="form-control password" name="password" placeholder="登録パスワード">
            </div>
          </div>
          <input id="regi-token" name="regi-token" value="<?=h($token)?>" type="hidden">
          <div class="form-group form-group-xs">
            <div class="col-xs-offset-5 col-xs-4">
              <button type="submit" class="btn btn-primary btn-sm">登録する</button>
            </div>
          </div>
        </fieldset>
      </form>
    </div>
    <div id="login" class="hidden">
      <div class="row">
        <div class="col-xs-offset-4 col-xs-4">
          <form>
            <fieldset>
              <div class="form-group form-group-xs">
                <label for="email">Eメール</label>
                <input type="email" class="form-control email" name="email" placeholder="Eメール">
              </div>
              <div class="form-group form-group-xs">
                <label for="password">パスワード</label>
                <input type="password" class="form-control password" name="password" placeholder="パスワード">
              </div>
              <input id="login-token" name="login-token" value="<?=h($token)?>" type="hidden">
              <button type="submit" class="btn btn-primary btn-sm">サインイン</button>
            </fieldset>
          </form>
        </div>
      </div>
    </div>
    <div id="credit" class="text-center">
      <address>Copyright &copy; WIZARD-CODE 2015</address>
    </div>
  </div>
                

admin-frag.inc.php

  <div id="content" class="panel-body" data-authentication-status="authenticated">
    <h1 id="title" class="text-center">こんにちは! <?=h($user_id)?>さん!</h1>
    <div id="validation" class="text-danger bg-danger hidden"></div>
    <div id="return-home-button" class="hidden"><button type="button" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-home"></span></button></div>
    <div id="menu" class="hidden">
      <button id="chat" type="button" class="btn btn-default btn-block disabled">チャットする</button>
      <button id="find-friends" type="button" class="btn btn-default btn-block disabled">フレンドを見つける</button>
      <button id="setting-button" type="button" class="btn btn-default btn-block">設定</button>
      <button id="logout-button" type="button" class="btn btn-default btn-block">ログアウトする</button>
    </div>
    <div id="logout" class="hidden">
      <div class="alert alert-warning text-center" role="alert">
        <h4 class="text-center">ログアウトの確認</h4>
        <p>「ログアウトする」をクリックするとログアウト処理が完了します。</p>
        <form>
          <input id="logout-token" name="logout-token" value="<?=h($token)?>" type="hidden">
          <button id="confirm-logout-button" type="button" class="btn btn-warning">ログアウトする</button>
          <button id="return-menu-from-logout" type="button" class="btn btn-default">戻る</button>
        </form>
      </div>
    </div>
    <div id="setting" class="hidden">
      <form>
        <fieldset>
        <input id="setting-token" name="setting-token" value="<?=h($token)?>" type="hidden">
          <table class="table table-condensed">
            <tbody>
              <tr>
                <th class="col-md-4">ユーザー名</th>
                <td id="user-name" colspan="2" class="col-md-8"><?=h($user_id)?></td>
              </tr>
              <tr>
                <th class="col-md-4">Eメールアドレス</th>
                <td id="email" class="col-md-6"><?=h($email)?></td>
                <td class="col-md-2"><button id="change-email" type="button" class="btn btn-success btn-sm">変更</button></td>
              </tr>
              <tr class="hidden">
                <th>Eメール変更</th>
                <td class="text-right">
                  <label class="sr-only" for="new-email">Eメール</label>
                  <input type="email" class="form-control input-sm" id="new-email" name="new-email" placeholder="Eメール">
                </td>
                <td>
                  <button type="submit" class="btn btn-primary btn-sm">送信</button>
                </td>
              </tr>
              <tr>
                <th colspan="2">パスワード変更</th>
                <td><button id="change-email" type="button" class="btn btn-success btn-sm">変更</button></td>
              </tr>
              <tr class="hidden">
                <th>パスワード変更</th>
                <td>
                  <label class="sr-only" for="new-password">新しいパスワード</label>
                  <input type="email" class="form-control input-sm" id="new-password" name="new-password" placeholder="新しいパスワード">
                </td>
                <td></td>
              </tr>
              <tr class="hidden">
                <th class="delete-border-top"></th>
                <td class="delete-border-top">
                  <label class="sr-only" for="confirm-password">パスワードの再入力</label>
                  <input type="email" class="form-control input-sm" id="confirm-password" placeholder="パスワードの再入力">
                </td>
                <td class="delete-border-top">
                  <button type="submit" class="btn btn-primary btn-sm">送信</button>
                </td>
              </tr>
              <tr>
                <th colspan="2" class="insert-border-bottom">アカウント管理</th>
                <td class="insert-border-bottom"><button id="delete-account" type="button" class="btn btn-warning btn-sm">削除</button></td>
              </tr>
              <tr class="danger hidden">
                <th>アカウントの削除</th>
                <td>アカウントを削除します。よろしいですか?</td>
                <td>
                    <button type="submit" class="btn btn-danger btn-sm">送信</button>
                </td>
              </tr>
            </tbody>
          </table>
        </fieldset>
      </form>
    </div>
    <div id="credit" class="text-center">
      <address>Copyright &copy; WIZARD-CODE 2015</address>
    </div>
  </div>
                

ajax-login.js

  (function () {
    'use strict';
    
    var fsm,
      state,
      transit,
      
      user,
      
      compo,
      button,
      message,
      
      FORM_FADE_SPEED;
    
    /* ユーザーデータを集約するオブジェクト */
    user = {};
    
    /* コンポーネント要素を集約するオブジェクト */
    compo = {};
    
    /* ボタン要素を集約するオブジェクト */
    button = {};
    
    /* メッセージ表示要素を集約するオブジェクト */
    message = {};
    
    /* 画面の表示アニメーション速度 */
    FORM_FADE_SPEED = 200;
      
    /* Ajaxログインシステム */
    fsm = {};
    state = {};
    transit = {};
    
    fsm.ajaxLogin = new FSM('ajax-login');
    
    state.branch = new State('branch');
    fsm.ajaxLogin.addStateAsChoicePseudo(state.branch, function () {
      /* アプリケーション画面のフレーム */
      compo.ajaxLogin = $('#ajax-login');
      user.authentication = $('#content').attr('data-authentication-status');
      
      if (user.authentication !== 'authenticated') {
        return 'auth-init';
      } else {
        return 'admin-init';
      }
    });
    transit.firstTransit = new Transition('first-transit', null, 'branch');
    fsm.ajaxLogin.addTransition(transit.firstTransit);
    
    /* ログイン/登録に関するステート群 */
    state.authInit = new State('auth-init', {
      autoTransition: true,
      entryAction: function () {
        /* コンポーネント要素 */
        compo.auth = $('#auth');
        compo.register = $('#register');
        compo.login = $('#login');
        
        /* バリデーションのテキスト表示用要素 */
        message.validation = $('#validation');
        
        /* 最初の画面に戻るためのボタン */
        button.returnHome = $('#return-home-button');
        
        /* いったんボタンやコンポーネントを非表示に */
        compo.auth.hide().removeClass('hidden');
        compo.register.hide().removeClass('hidden');
        compo.login.hide().removeClass('hidden');
        message.validation.hide().removeClass('hidden');
        button.returnHome.hide().removeClass('hidden');
      }
    });
    fsm.ajaxLogin.addState(state.authInit);
      
    /* 認証画面の設定 */
    state.auth = new State('auth', {
      entryAction: function () {
        /* 認証画面を表示 */
        compo.auth.show();
        
        /* ボタンごとに遷移先を指定する */
        compo.auth.find('button')
        .on('click', function (e) {
          var target;
          e.preventDefault();
          e.stopPropagation();
          $(this).blur();
          target = e.target;
          
          if (target.id === 'login-button') {
            transit.authToLogin.trigger();
          } else if (target.id === 'register-button') {
            transit.authToRegister.trigger();
          }
        });
      },
      exitAction: function () {
        /* 画面遷移の際にイベントリスナーを削除 */
        compo.auth.find('button').off('click').end().hide();
      }
    });
    fsm.ajaxLogin.addState(state.auth);
    transit.authInitToAuth = new Transition('auth-init-to-auth', 'auth-init', 'auth');
    fsm.ajaxLogin.addTransition(transit.authInitToAuth);
  
    /* 登録画面の設定 */
    state.register = new State('register', {
      entryAction: function () {
        /* 登録画面を表示 */
        compo.register.show();
        
        /* 最初の画面へ戻るボタンを表示 */
        button.returnHome.show()
        .on('click', function (e) {
          /* 登録画面から最初の画面へ戻る */
          transit.registerToAuth.trigger();
        });
        
        /* 「登録する」ボタンを押した際の処理 */
        compo.register.find('button').on('click', function (e) {
          var form, userId, email, password, query;
          e.preventDefault();
          e.stopPropagation();
          $(this).blur();
          
          form = compo.register.find('form');
          userId = form.find('.user-id');
          email = form.find('.email');
          password = form.find('.password');
          message.validation.text('').hide();
          
          /* 入力値をトリミングする */
          userId.val($.trim(userId.val()));
          email.val($.trim(email.val()));
          password.val($.trim(password.val()));
          
          /* ユーザー名のバリデーション */
          if (!/^[a-zA-Z0-9_]{4,20}$/.test(userId.val())) {
            userId.val('');
            message.validation.text('ユーザー名は半角英数字かアンダースコア(_)のみ使用できます。4~20文字の範囲で入力してください。').show(FORM_FADE_SPEED);
            return;
          }
          
          /* メールアドレスのバリデーション */
          if (!/^(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))$/i.test(email.val())) {
            email.val('');
            message.validation.text('メールアドレスが正しくありません。').show(FORM_FADE_SPEED);
            return;
          }
          
          /* パスワードのバリデーション */
          if (!/^(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\d)[a-zA-Z\d]{8,16}$/.test(password.val())) {
            password.val('');
            message.validation.text('パスワードは半角英数字(大文字/小文字/数字)をそれぞれ1文字以上使用し、8~16文字の範囲で入力してください。').show(FORM_FADE_SPEED);
            return;
          }
          
          /* フォームが空でないか */
          if (userId.val() === '' || email.val() === '' || password.val() === '') {
            message.validation.text('フォームが空です。').show(FORM_FADE_SPEED);
            return;
          }
          
          /* 入力内容をシリアライズ */
          query = form.serialize();
          /* パスワード入力文字をクリア */
          password.val('');
          
          /* 誤動作防止のため、送信ボタンを無効化 */
          form.find('fieldset').prop('disabled', true);
          
          /* 入力内容をAjax送信 */
          $.post(
            './register.php',
            query,
            function (json) {
              var html;
              if (json && json.message) {
                /* レスポンスを受け取ったら、フォームの無効化を解除 */
                form.find('fieldset').prop('disabled', false);
                switch (json.message) {
                  /* 登録処理が成功した時 */
                  case 'success':
                  user.id = json.id;
                  html = $.parseHTML(json.html, false);
                  
                  userId.val('');
                  email.val('');
                  
                  /* ユーザー画面に切り替え */
                  compo.ajaxLogin.empty().append(html);
                  
                  /* マイページへ遷移 */
                  transit.registerToAdminInit.trigger();
                  break;
                  
                  /* 不正なユーザー名 */
                  case 'invalid userId':
                  message.validation.text('ユーザー名は半角英数字かアンダースコア(_)のみ使用できます。4~20文字の範囲で入力してください。').show(FORM_FADE_SPEED);
                  break;
                  
                  /* 不正なメールアドレス */
                  case 'invalid email':
                  message.validation.text('メールアドレスが正しくありません。').show(FORM_FADE_SPEED);
                  break;
                  
                  /* 不正なパスワード */
                  case 'invalid password':
                  message.validation.text('パスワードは半角英数字(大文字/小文字/数字)をそれぞれ1文字以上使用し、8~16文字の範囲で入力してください。').show(FORM_FADE_SPEED);
                  break;
                  
                  /* 登録ユーザー名の重複 */
                  case 'userId duplicated':
                  message.validation.text('登録済みのユーザー名です。').show(FORM_FADE_SPEED);
                  break;
                  
                  /* 登録メールアドレスの重複 */
                  case 'email duplicated':
                  message.validation.text('登録済みのメールアドレスです。').show(FORM_FADE_SPEED);
                  break;
                  
                  /* 入力時間がタイムオーバー */
                  case 'expired':
                  message.validation.text('登録期限が過ぎています。リロードを行ってください。').show(FORM_FADE_SPEED);
                  break;
                  
                  /* 不正なアクセス、その他のエラー */
                  case 'illicit access':
                  case 'not XHR':
                  case 'db error':
                  message.validation.text('不明なエラーが発生しました。リロードを行ってください。').show(FORM_FADE_SPEED);
                  break;
                }
              } else {
                message.validation.text('ユーザー登録に失敗しました。リロードを行ってください。').show(FORM_FADE_SPEED);
              }
            },
            'json'
          );
        });
      },
      exitAction: function () {
        /* 「ホーム」ボタンと登録画面を隠す */
        button.returnHome.off('click').hide();
        compo.register.find('button').off('click').end().hide();
        /* バリデーション用テキストを空に */
        message.validation.text('').hide();
      }
    });
    fsm.ajaxLogin.addState(state.register);
    
    transit.authToRegister = new Transition('auth-to-register', 'auth', 'register');
    transit.registerToAuth = new Transition('register-to-auth', 'register', 'auth');
    transit.registerToAdminInit = new Transition('register-to-admin-init', 'register', 'admin-init');
    fsm.ajaxLogin.addTransition(transit.authToRegister);
    fsm.ajaxLogin.addTransition(transit.registerToAuth);
    fsm.ajaxLogin.addTransition(transit.registerToAdminInit);
    
    /* ログイン画面の設定 */
    state.login = new State('login', {
      entryAction: function () {			
        /* ログイン画面を表示 */
        compo.login.show();
        
        /* 最初の画面へ戻るボタンを表示 */
        button.returnHome.show()
        .on('click', function (e) {
          /* ログイン画面から最初の画面へ戻る */
          transit.loginToAuth.trigger();
        });
        
        /* 「サインイン」ボタンを押した際の処理 */
        compo.login.find('button').on('click', function (e) {
          var form, email, password, query, sha, hash;
          e.preventDefault();
          e.stopPropagation();
          $(this).blur();
          
          form = compo.login.find('form');
          email = form.find('.email');
          password = form.find('.password');
          message.validation.text('').hide();
          
          /* 入力値をトリミングする */
          email.val($.trim(email.val()));
          password.val($.trim(password.val()));
          
          /* メールアドレスのバリデーション */
          if (!/^(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))$/i.test($.trim(email.val()))) {
            email.val('');
            message.validation.text('メールアドレスが正しくありません。').show(FORM_FADE_SPEED);
            return;
          }
          
          /* パスワードのバリデーション */
          if (!/^(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\d)[a-zA-Z\d]{8,16}$/.test(password.val())) {
            password.val('');
            message.validation.text('パスワードは半角英数字(大文字/小文字/数字)をそれぞれ1文字以上使用し、8~16文字の範囲で入力してください。').show(FORM_FADE_SPEED);
            return;
          }
          
          /* フォームが空でないか */
          if (email.val() === '' || password.val() === '') {
            message.validation.text('フォームが空です。').show(FORM_FADE_SPEED);
            return;
          }
          
          query = form.serialize();
          password.val('');
          
          form.find('fieldset').prop('disabled', true);
          
          /* ログイン処理をするPHPファイルにXHRリクエスト */
          $.post(
            './login.php',
            query,
            function (json) {
              var html;
              if (json && json.message) {
                form.find('fieldset').prop('disabled', false);
                switch (json.message) {
                  /* 登録処理が成功した時 */
                  case 'success':
                  user.id = json.id;
                  html = $.parseHTML(json.html, false);
                  
                  email.val('');
                  
                  /* ユーザー画面に切り替え */
                  compo.ajaxLogin.empty().append(html);
                  
                  /* マイページへ遷移 */
                  transit.loginToAdminInit.trigger();
                  break;
                  
                  /* 不正なメールアドレス */
                  case 'invalid email':
                  message.validation.text('メールアドレスが正しくありません。').show(FORM_FADE_SPEED);
                  break;
                  
                  /* 不正なパスワード */
                  case 'invalid password':
                  message.validation.text('パスワードは半角英数字(大文字/小文字/数字)をそれぞれ1文字以上使用し、8~16文字の範囲で入力してください。').show(FORM_FADE_SPEED);
                  break;
                  
                  /* メールアドレスかパスワードが不正 */
                  case 'failure':
                  message.validation.text('メールアドレスまたはパスワードが正しくありません。').show(FORM_FADE_SPEED);
                  break;
                  
                  /* 入力時間がタイムオーバー */
                  case 'expired':
                  message.validation.text('ログイン期限が過ぎています。リロードを行ってください。').show(FORM_FADE_SPEED);
                  break;
                  
                  /* 不正なアクセスやその他のエラー */
                  case 'illicit access':
                  case 'not XHR':
                  case 'db error':
                  message.validation.text('不明なエラーが発生しました。リロードを行ってください。').show(FORM_FADE_SPEED);
                  break
                }
              } else {
                message.validation.text('ログイン認証に失敗しました。リロードを行ってください。').show(FORM_FADE_SPEED);
              }
            },
            'json'
          );
        });
      },
      exitAction: function () {
        /* 「ホーム」ボタンと登録画面を隠す */
        button.returnHome.off('click').hide();
        compo.login.find('button').off('click').end().hide();
        /* バリデーション用テキストを空に */
        message.validation.text('').hide();
      }
    });
    fsm.ajaxLogin.addState(state.login);
    
    transit.authToLogin = new Transition('auth-to-login', 'auth', 'login');
    transit.loginToAuth = new Transition('login-to-auth', 'login', 'auth');
    transit.loginToAdminInit = new Transition('login-to-admin-init', 'login', 'admin-init');
    fsm.ajaxLogin.addTransition(transit.authToLogin);
    fsm.ajaxLogin.addTransition(transit.loginToAuth);
    fsm.ajaxLogin.addTransition(transit.loginToAdminInit);
    
    /* 認証後の機能ステート群 */
    state.adminInit = new State('admin-init', {
      autoTransition: true,
      entryAction: function () {
        /* ボタンやコンポーネント要素 */
        compo.menu = $('#menu');
        compo.setting = $('#setting');
        compo.logout = $('#logout');
        
        /* バリデーションのテキスト表示用要素 */
        message.validation = $('#validation');
        
        /* 最初の画面に戻るためのボタン */
        button.returnHome = $('#return-home-button');
        
        button.setting = $('#setting-button');
        button.logout = $('#logout-button');
        
        /* いったんコンポーネントを非表示に */
        message.validation.hide().removeClass('hidden');
        button.returnHome.hide().removeClass('hidden');
        compo.menu.hide().removeClass('hidden');
        compo.setting.hide().removeClass('hidden');
        compo.logout.hide().removeClass('hidden');
      }
    });
    fsm.ajaxLogin.addState(state.adminInit);
    
    state.menu = new State('menu', {
      entryAction: function () {
        compo.menu.show();
        
        compo.menu.find('button')
        .on('click', function (e) {
          var target;
          e.preventDefault();
          e.stopPropagation();
          target = e.target;
          
          if (target.id === 'setting-button') {
            transit.menuToSetting.trigger();
          } else if (target.id === 'logout-button') {
            transit.menuToLogout.trigger();
          }
        });
      },
      exitAction: function () {
        /* 画面遷移の際にイベントリスナーを削除 */
        compo.menu.find('button').off('click').end().hide();
      }
    });
    fsm.ajaxLogin.addState(state.menu);
    transit.adminInitToMenu = new Transition('admin-init-to-menu', 'admin-init', 'menu');
    fsm.ajaxLogin.addTransition(transit.adminInitToMenu);
    
    state.logout = new State('logout', {
      entryAction: function () {	
        var form, buttons;		
        /* ログアウト画面を表示 */
        compo.logout.show();
        form = compo.logout.find('form');
        buttons = form.find('button');
        
        buttons.on('click', function (e) {
          var target, query;
          e.preventDefault();
          e.stopPropagation();
          buttons.blur();
          target = e.target;
          
          if (target.id === 'confirm-logout-button') {
            query = form.serialize();
            buttons.addClass('disabled');
            $.post(
              './logout.php',
              query,
              function (json) {
                if (json && json.message) {
                  switch (json.message) {
                    /* ログアウト処理が成功した時 */
                    case 'success':
                    window.location.href = './';
                    break;
                    
                    /* 処理が失敗した時 */
                    case 'illicit access':
                    case 'not XHR':
                    buttons.removeClass('disabled');
                    message.validation.text('不正なアクセスを検知しました。操作をやり直してください。').show(FORM_FADE_SPEED);
                    break;
                  }
                }
              },
              'json'
            );
          } else if (target.id === 'return-menu-from-logout') {
            transit.logoutToMenu.trigger();
          }
        });
      },
      exitAction: function () {
        compo.logout.find('button').off('click').end().hide();
        /* バリデーション用テキストを空に */
        message.validation.text('').hide();
      }
    });
    fsm.ajaxLogin.addState(state.logout);
    transit.menuToLogout = new Transition('menu-to-logout', 'menu', 'logout');
    transit.logoutToMenu = new Transition('logout-to-menu', 'logout', 'menu');
    fsm.ajaxLogin.addTransition(transit.menuToLogout);
    fsm.ajaxLogin.addTransition(transit.logoutToMenu);
    
    state.setting = new State('setting', {
      entryAction: function () {	
        /* 設定画面を表示 */
        compo.setting.show();
        button.returnHome.show();
        
        
        
        
        
      },
      exitAction: function () {
        
      }
    });
    fsm.ajaxLogin.addState(state.setting);
    transit.menuToSetting = new Transition('menu-to-setting', 'menu', 'setting');
    transit.settingToMenu = new Transition('setting-to-menu', 'setting', 'menu');
    fsm.ajaxLogin.addTransition(transit.menuToSetting);
    fsm.ajaxLogin.addTransition(transit.settingToMenu);
    
    /* ステートマシンを起動する */
    fsm.ajaxLogin.start();
  }());
                

ajax-login.css

  @charset "UTF-8";
  
  .container-fluid {
    padding: 15px;
    max-width: 768px;
    max-height: 576px;
    height: 100vh;
    min-width: 480px;
    min-height: 360px;
  }
  
  .container-fluid > #ajax-login {
    margin-bottom: 0;
    height: 100%;
  }
  
  #content {
    height: 100%;
    position: relative;
  }
  #title {
    margin: auto;
    color: #333px;
    font-size: 60px;
    position: absolute;
    top: 25%;
    left: 0;
    right: 0;
    text-shadow: 4px 2px 2px #05F;
  }
  
  #return-home-button {
    position: absolute;
    right: 10px;
    top: 10px;
  }
  #return-home-button > button {
    padding-top: 4px;
  }
  #return-home-button span {
    font-size: 18px;
    color: #777;
  }
  
  #auth {
    margin: auto;
    position: absolute;
    top: 50%;
    left: 0;
    right: 0;
  }
  #auth > button {
    margin-right: 10px;
  }
  
  #register {
    margin: auto;
    position: absolute;
    top: 45%;
    left: 0;
    right: 0;
  }
  #login {
    margin: auto;
    position: absolute;
    top: 45%;
    left: 0;
    right: 0;
  }
  #validation {
    margin: auto;
    padding: 0 10px;
    width: 360px;
    position: absolute;
    top: 15%;
    left: 0;
    right: 0;
  }
  #validation p {
    margin-top: 2px;
    padding: 2px;
  }
  
  #menu {
    margin: auto;
    width: 300px;
    position: absolute;
    top: 50%;
    left: 0;
    right: 0;
  }
  #menu > button {
    width: 100%;
    white-space: normal;
  }
  
  #setting {
    margin: auto;
    width: 65%;
    position: absolute;
    top: 50%;
    left: 0;
    right: 0;
  }
  .delete-border-top {
    border-top: none !important;
  }
  .insert-border-bottom {
    border-bottom: 1px solid #ddd !important;
  }
  
  #logout {
    margin: auto;
    width: 70%;
    position: absolute;
    top: 55%;
    left: 0;
    right: 0;
  }
  #logout form {
    margin-top: 10px;
  }
  
  #credit {
    margin: auto;
    font-size: 12px;
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
  }
  #credit address {
    margin-bottom: 4px;
  }
  
  @media screen and (max-width: 768px) {
    #title {
      font-size: 50px;
    }
  }
  @media screen and (max-height: 576px) {
    #title {
      top: 15%;
    }
    #register {
      top: 35%;
    }
    #login {
      top: 35%;
    }
    #menu {
      top: 40%;
    }
    #setting {
      top: 35%;
    }
  }