Railsでセルフパスワード変更ページを作ってみた

# はじめに

本記事は、社内の古くなっていたシステムをリニューアルした際の手順やノウハウをまとめたものになります。
また、当記事は以前の投稿記事「[Ruby on Rails6.0の環境構築から新規プロジェクト作成まで（メモ）](https://qiita.com/yuki68427913/items/a87cbe68abede73a78c5)」で作成した環境を前提に作成していますので、参考にされる方はご注意ください。


# 前提条件

* 開発環境はCloud9
* Linuxコマンドの使い方がわかる程度の力量
* Web開発は初学者クラスの力量（ruby on rails開発未経験／Progateのレッスンは修了済）
* HTML／CSSは難しい事はできないけど書いて読める程度の力量
* javascriptは難しい事はできないけど書いて読める程度の力量


# 参考記事

> [RubyのNet::SSHの使い方．リモートサーバー内でsuしたりrsyncするrubyスクリプトが作れるようになる【外部サイト】](https://takuya-1st.hatenablog.jp/entry/20110808/1312764813)
> [Rails6プロジェクトの各種初期設定【外部サイト】](https://www.petitmonte.com/ruby/rails6_project.html)
> [rails newするときによく使うオプションと、rails newした後によく行う設定](https://qiita.com/jun_jun_jun/items/dd260c43387a8e17803d)
> [bootsnapについて調べてみた](https://qiita.com/Daniel_Nakano/items/aadeaa7ae4e227b73878)
> [Rails5.1ではAsset Pipeline捨てたほうがいいらしいので捨ててみた](https://qiita.com/ry_2718/items/9b824a3f9ca750ce403e)
> [application.html.erbのレイアウトの使い方と使わない方法【外部サイト】](https://pg-happy.jp/rails-applicationo-layout.html)
> [yieldとcontent_forを使ってページ毎にタイトルを変更【外部サイト】](https://www.sejuku.net/blog/75657)
> [最新版で学ぶwebpack 4入門JavaScriptのモジュールバンドラ【外部サイト】](https://ics.media/entry/12140/)
> [Rails 6+Webpacker開発環境をJS強者ががっつりセットアップしてみた（翻訳）【外部サイト】](https://techracho.bpsinc.jp/hachi8833/2019_11_28/83678)


# アプリケーションの概要

今回作成したアプリケーションは、Linux系サーバーＯＳ上にあるユーザーのパスワードをSSH等でログインしなくても、Webページ上で変更できるにするためのものです。
「どんな時に使うの？」という話ですが、自社運用のメールサーバーをユーザー自身で定期的にパスワードを変更させたい時に使う事を想定しています。
仕様的には下記のような感じにしました。

## 【ざっくり仕様】

- 入力された４つのパラメーター（ユーザーＩＤ、現行パスワード、新パスワード、新パスワード再確認）を元に対象となるサーバーへＳＳＨ接続してパスワード変更処理
- 正常終了された場合は、注意事項などが記載されたページを表示
- 異常終了された場合（パスワード間違いなど）は、再度元のページを表示して状況に応じて入力時の値を再設定


# プロジェクト環境準備

今回はアプリケーション解説がメインのため、準備は簡単に流していきます。
まずは、プロジェクトに必要なディレクトリを作成しておきます。


```shell-session:ディレクトリ作成
$ mkdir -p PasswordChange/vendor/bundle/

$ tree PasswordChange

PasswordChange
└── vendor
    └── bundle

2 directories, 0 files
```

次にRailsのインストールからbundleインストールまで実施します。
※手順の詳細は前回の記事を参照してください。
[Ruby on Rails6.0の環境構築から新規プロジェクト作成まで（メモ）](https://qiita.com/yuki68427913/items/a87cbe68abede73a78c5)

尚、今回のアプリケーションでは各種Action（Mail関連、データベース関連等）を利用しないため、Offにしてプロジェクトを作成します。

```shell-session:プロジェクト作成
$ bundle exec rails new . --skip-action-mailer --skip-action-mailbox --skip-action-text --skip-active-record --skip-active-storage --skip-action-cable --skip-test --skip-bootsnap --skip-turbolinks --skip-sprockets --skip-coffee --skip-bundle
```

| 各オプション | 説明 |
|:--------:|:--------:|
| --skip-action-mailer | action mailer のセットアップをスキップ |
| --skip-action-mailbox | action mailbox のセットアップをスキップ |
| --skip-action-text | action text のセットアップをスキップ |
| --skip-active-record | action record のセットアップをスキップ |
| --skip-active-storage | action storage のセットアップをスキップ |
| --skip-action-cable | action cable のセットアップをスキップ |
| --skip-test | Minitest のセットアップをスキップ |
| --skip-bootsnap | bootsnap のセットアップをスキップ |
| --skip-turbolinks | turbolinks のセットアップをスキップ |
| --skip-sprockets | sprockets のセットアップをスキップ |
| --skip-coffee | coffee のセットアップをスキップ |
| --skip-bundle | bundle のセットアップをスキップ |

次に「webpacker」をインストールします
※オプションにそれらしいのがあったので最初はいけるかと思ったのですが、それだけでは不十分らしいので別途インストール

```shell-session:webpackerインストール
$ bundle exec rails webpacker:install
```

次に今回のアプリケーションで使用するgemを追加します。
今回追加するgemは「SSH接続用（net-ssh）」「通信確認用（net-ping）」「共通パラメーター設定用（settingslogic）」の３つになります。
※ちなみに、何故railsのインストール時に一緒に記述しないかというとGemfile上書き時に消えてしまったためです。

```shell-session:Gemfile追記とインストール
$ vim Genfile

【追記】
gem 'net-ping'
gem 'net-ssh'
gem 'settingslogic'

$ bundle install
```

次に今回作成予定の「controller」と「view」ファイルを作成します。

```shell-session:controllerとview作成
$ bundle exec rails g controller users_password top_form complete

      create  app/controllers/users_password_controller.rb
       route  get 'users_password/top_form'
get 'users_password/complete'
      invoke  erb
      create    app/views/users_password
      create    app/views/users_password/top_form.html.erb
      create    app/views/users_password/complete.html.erb
      invoke  helper
      create    app/helpers/users_password_helper.rb
      invoke  assets
      invoke    scss
      create      app/assets/stylesheets/users_password.scss
```

次に今回のアプリケーション用の「routes」設定します。

```shell-session:routes設定
$ vim app/confing/routes.rb

【設定変更】
  get  'complete' => 'users_password#complete'
  get  'top'      => 'users_password#top_form'
  post 'top'      => 'users_password#top'
  get  '/'        => 'users_password#top_form'
```

「webpacker」関連の初期フォルダを作成します。
※フォルダ名はお好みでどうぞ

```shell-session:フォルダ作成
$ mkdir -p app/javascript/src app/javascript/stylesheets
```

「jquery」を「webpacker」にインストールします。

```shell-session:jqueryインストール
$ yarn add jquery
```

```shell-session:environment.jsへ設定追記
$ vim config/webpack/environment.js

【設定変更前】
const { environment } = require('@rails/webpacker')
module.exports = environment

【設定変更後】
const { environment } = require('@rails/webpacker')

const webpack = require('webpack')
environment.plugins.prepend('Provide',
  new webpack.ProvidePlugin({
    $: 'jquery/src/jquery',
    jQuery: 'jquery/src/jquery'
  })
)

module.exports = environment
```

```shell-session:application.jsへ設定追記
$ vim app/javascript/packs/application.js

【追記】
require("jquery")
```

「webpacker」でデフォルトエントリーポイント以外で、各ビュー単位のエントリーポイントを利用できるように設定を一部修正します。
※この設定をしないとコンパイルはされますが、エントリーポイント（CSS）の呼び出し（stylesheet_pack_tag）が上手くいきませんでした。
　理由は正直よくわかっていません(;^ω^)。

```shell-session:webpacker.ymlの修正
$ vim config/webpacker.yml

【設定変更前】
extract_css: false

【設定変更後】
extract_css: true
```

# 機能を実装

## 独自クラスの実装

まず、railsと直接関係ない「SSH接続パスワード変更」「共通パラメーター読込」の実装する準備をします。

```shell-session:フォルダと空ファイル追加
$ mkdir -p app/lib
$ touch app/lib/settings.rb app/lib/ssh_interactive.rb
```

次に「SSH接続パスワード変更」の機能を追加します。

このクラスでは、デフォルト設定だと通常の「ＩＤ・パスワード」でSSH接続（※１）、オプションを付け足す事で「公開鍵認証」でSSH接続します。

パスワード変更部分は対話式で「passwd」コマンド実行となるため、環境によって出力メッセージが違う場合、変更（※２）が必要となります。

※１.実はcloud9環境で作成した関係で「公開鍵認証」はテストしましたが、「ＩＤ・パスワード」はテストできていなかったりします。

※２.「if data =~ /^\(current\) UNIX password/ then」等の部分が該当します。

```ruby:SSH接続機能追加(ssh_interactive.rb)
require 'bundler/setup'
require 'rubygems'
require 'net/ssh'
require 'net/ping'

class SshInteractive

  def initialize

    # SSH接続先アドレス
    @host = "127.0.0.1" 
    
    # SSH接続用オプション設定
    @option = { :port => 22 }
  end
  
  def set_host(host)
    @host = host
  end
  
  def set_port(port)
    @option[:port] = port
  end
  
  def set_publickey_auth(keyfile, passphrase)
    @option[:keys] = keyfile
    @option[:passphrase] = passphrase
  end

  def password_change(user_id, pass_old, pass_new, pass_verify)

    # パスワード追加
    @option[:password] = pass_old unless @option.has_key?(:keys) && @option.has_key?(:passphrase)

    # コマンド設定
    cmd = "passwd"

    # SSH接続
    begin
        return -10 unless port_scan?                          #通信接続エラー
        Net::SSH.start(@host, user_id, @option) do |ssh|
          channel = ssh.open_channel do |ch|
            channel.request_pty do |ch, success|              # ptyチェック
              return -11 unless success
            end
            channel.exec cmd do |ch, success|                 # コマンド送信と対話入力
              return -12 unless success
              ch.on_data do |c, data|                         # メッセージ取り出し
                if data =~ /^\(current\) UNIX password/ then
                  channel.send_data "#{pass_old}\n"           #パスワードを送信する
                elsif data =~ /^.+Authentication token manipulation error/ then
                  return -2                                   # パスワード間違い
                elsif data =~ /^BAD PASSWORD: The password fails the dictionary check/ then
                  return -3                                   # 辞書攻撃チェック
                elsif data =~ /^New password/ then
                  channel.send_data "#{pass_new}\n"           #パスワードを送信する
                elsif data =~ /^Retype new/ then
                  channel.send_data "#{pass_verify}\n"        #パスワードを送信する
                elsif data =~ /^.+updated successfully/ then
                  return 0
                end
              end
            end
          end
          ssh.loop                                            # SSHループ用
        end
        return -99                                            # 予想しない終了
    rescue
        return -1                                             # SSH接続エラー（ユーザーＩＤ間違い）
    end
  end
  
  private
  
  def port_scan?
    ping_tcp = Net::Ping::TCP.new(@host, @option[:port])
    return ping_tcp.ping?
  end
end
```

次に「共通パラメーター読込」の機能を追加します。

このクラスでは、設定値やエラーメッセージ等を一元管理するために作成しています。
設定値は「config/application.yml」に記載します。

```ruby:共通パラメーター読込機能追加(settings.rb)
class Settings < Settingslogic
  source "#{Rails.root}/config/application.yml"
  namespace Rails.env
end
```

```yml:共通パラメーター設定
default: &default
  company: ＸＸＸＸＸ
  system: ＸＸＸＸＸサーバー
  password:
    limit: １８０日
    alert: ３０日
  message:
    normal: パスワード変更が完了しました
    regular_access_err: パスワード変更処理を実施してください
    password_check_err: 古いパスワードと新しいパスワードが同じです
    password_verify_err: 新しいパスワードと新しいパスワード（確認）が一致しません
    password_terms_err: 複雑性を満たすパスワードになっていません
    password_lenght_err: パスワードの文字数が基準を満たしていません
    password_matchid_err: 新しいパスワードにユーザーIDと同じ文字列が含まれています
    authenticate_err: ユーザーID又はパスワードが間違っています
    password_nomatch_err: パスワードが間違っています
    dictionary_check_err: 辞書攻撃チェックに該当します
    connection_err: サーバーへの接続に失敗しています【管理者へ問い合わせてください】
    unknown_err: 予期しないエラーが発生しました【管理者へ問い合わせてください】

production:
  <<: *default
  ssh_params:
    host: 000.000.000.000
    port: 22
    keys:  /home/【ユーザー名】/.ssh/id_rsa
    passphrase: test

development:
  <<: *default
  ssh_params:
    host: 000.000.000.000
    port: 22
    keys:  /home/【ユーザー名】/.ssh/id_rsa
    passphrase: test

test:
  <<: *default
  ssh_params:
    host: 000.000.000.000
    port: 22
    keys:  /home/【ユーザー名】/.ssh/id_rsa
    passphrase: test
```

## Web関連の機能作成（Controller）

さて、ここから本題のセルフパスワード変更ページを作成していきます。

まずは、パスワード変更処理前に完了画面に行けないようにするための設定を追加します。

```ruby:before機能追加(application_controller.rb)
class ApplicationController < ActionController::Base

    def authenticate_user
        if session[:user_id] == nil
            flash[:notice] = Settings.message.regular_access_err
            redirect_to("/top")
        end
    end
end
```

次にパスワード変更処理の機能追加します。

パスワード変更フォームでは、最初にパスワード入力チェックを実施します。
その後、問題なければ先程作成した「SSH接続機能」を呼び出してパスワード変更処理を実施します。
特に問題なく変更できれば完了画面へ飛ばして終了です。

入力チェックエラーの場合は、フォーム画面に戻してエラーメッセージ「error_message」を表示します。
画面が切り替わったり、注視して欲しいメッセージを表示する場合は「flash」を使用しています。

```ruby:パスワード変更用(users_password_controller.rb)
require 'ssh_interactive'
require 'settings'

class UsersPasswordController < ApplicationController
  before_action :authenticate_user, { only: [:complete]}
  
  def complete
    session[:user_id] = nil
  end
  
  def top_form
  end

  def top
    
    # パラメーター格納
    data = {
      :user_id => params[:user_id],
      :pass_old => params[:password_old],
      :pass_new => params[:password_new],
      :pass_verify => params[:password_verify]
    }
    
    # 旧パスワードと新パスワード比較
    if data[:pass_old] == data[:pass_new]
      data[:msg] = Settings.message.password_check_err
      error_msg(data) and return
    end

    # 新パスワード比較
    if data[:pass_new] != data[:pass_verify]
      data[:msg] = Settings.message.password_verify_err
      error_msg(data) and return
    end
    
    # 複雑性を満たすパスワード確認
    # 半角英小文１文字以上、半角大文字１文字以上、半角数字１文字以上、半角記号１文字以上 ! # $ % $ * + - / = @ ?
    if data[:pass_new] !~ /(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\d)(?=.*?[!#$%&*+\-_\/=@\?])/
      data[:msg] = Settings.message.password_terms_err
      error_msg(data) and return
    end
    
    # 文字数制限確認（8桁以上20桁以下）
    if data[:pass_new].length < 8 || data[:pass_new].length > 20
      data[:msg] = Settings.message.password_lenght_err
      error_msg(data) and return
    end

    # パスワード内にユーザーIDが含まれているか
    if data[:pass_new] =~ /#{data[:user_id]}/
      data[:msg] = Settings.message.password_matchid_err
      error_msg(data) and return
    end
    

    # SSHパスワード変更処理
    ssh = SshInteractive.new
    ssh.set_host(Settings.ssh_params.host)
    ssh.set_port(Settings.ssh_params.port)
    ssh.set_publickey_auth(Settings.ssh_params.keys, Settings.ssh_params.passphrase)
    result = ssh.password_change(data[:user_id],data[:pass_old],data[:pass_new],data[:pass_verify])
    
    # 結果判定
    case result
    when 0 then
      # パスワード変更確認
      flash[:notice] = Settings.message.normal
      session[:user_id] = data[:user_id]
      redirect_to("/complete")
    when -1 then
      data[:msg] = Settings.message.authenticate_err
      error_msg(data)
    when -2 then
      data[:msg] = Settings.message.password_nomatch_err
      error_msg(data)
    when -3 then
      data[:msg] = Settings.message.dictionary_check_err
      error_msg(data)
    when -10,-11,-12 then
      flash[:alert] = Settings.message.connection_err
      render("users_password/top_form")
    else
      flash[:alert] = Settings.message.unknown_err
      render("users_password/top_form")
    end
  end
  
  private
  
  def error_msg(**data)
    # 入力状態に戻してページ再表示
    @error_message = data[:msg]
    @user_id = data[:user_id]
    @password_old = data[:pass_old]
    @password_new = data[:pass_new]
    @password_verify = data[:pass_verify]
    render("users_password/top_form")
  end
end
```

## Web関連の見た目部分作成

次に各ビューファイルを設定します。

```html:共通画面（application.html.erb）
<!DOCTYPE html>
<html>
  <head>
    <title><%= content_for?(:html_title) ? yield(:html_title) : "パスワード変更システム" %></title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= javascript_pack_tag 'application' %>
    <%= stylesheet_pack_tag 'application' %>
    <%= yield(:html_head) %>
  </head>

  <body>
    <header>
       <div class="header-log">
         <h1><%= Settings.company %></h1>
       </div>
    </header>
    <% if flash[:notice] %>
      <div class="flash">
        <%= flash[:notice] %>
      </div>
    <% end %>
    <% if flash[:alert] %>
      <div class="flash">
        <%= flash[:alert] %>
      </div>
    <% end %>
    <%= yield %>
  </body>
</html>

```

```html:パスワード変更フォーム（top_form.html.erb）
<% content_for :html_head do %>
    <%= stylesheet_pack_tag 'top_form' %>
<% end %>

<div class="main">
  <div class="container">
    <div class="heading">パスワード変更システム</div>
    <div class="form">
      <div class="form-body">
        <% if @error_message %>
          <div class="form-error">
            <%= @error_message %>
          </div>
        <% end %>
        <%= form_tag("/top") do %>
          <p>ユーザーＩＤ</p>
          <input id="user_id" name="user_id" value="<%= @user_id %>" required max=10>
          <p>現行パスワード</p>
          <input type="password" id="password_old" name="password_old" value="<%= @password_old %>" required min=8 maxlength=20>
          <p>新しいパスワード</p>
          <input type="password" id="password_new" name="password_new" value="<%= @password_new %>" required min=8 maxlength=20>
          <p>新しいパスワード（確認）</p>
          <input type="password" id="password_verify" name="password_verify" value="<%= @password_verify %>" required min=8 maxlength=20>
          <input type="submit" value="変更">
        <% end %>
      </div>
    </div>
  </div>
  <div class="container">
    <div class="heading">システム説明</div>
    <div class="box">
      <div class="desc-body">
        <h2>概要</h2>
        <p>当システムでは、弊社で提供している<%= Settings.system %>のパスワード変更を実施できます。</p>
        <h2>パスワードポリシー</h2>
        <p>弊社提供の<%= Settings.system %>は下記ポリシーを適用しています。</p>
        <p>様々な脅威から情報資産を守るためにも、ポリシーを遵守いただくようお願い致します。</p>
        <ul>
          <li>
            有効期限<%= Settings.password.limit %>（期限切れ利用不可）
          </li>
          <li>
            パスワード文字数は８～２０文字以内
          </li>
          <li>
            パスワード複雑性（下記を含む文字列）
            <ul>
              <li>
                半角英小文字１文字以上（a-z）
              </li>
              <li>
                半角英大文字１文字以上（A-Z）
              </li>
              <li>
                半角数字１文字以上（0-9）
              </li>
              <li>
                半角記号１文字以上<br/>利用可　! # $ % & * + - _ / = @ ? 
              </li>
              <li>
                ユーザーIDと同じ文字列禁止<br/>ユーザーID：taro<br/>新しいパスワード：Ka@1<span class="font_red">taro</span>
              </li>
            </ul>
            <li>
              その他
              <ul>
                <li>
                  現行と類似したパスワードは使用しないでください。<br/>現行パスワード：Ka@isyai12<span class="font_red">3</span><br/>新しいパスワード：Ka@isya12<span class="font_red">4</span>
                </li>
              </ul>
            </li>
          </ul>
      </div>
    </div>
  </div>
</div>
```

```html:完了画面
<% content_for :html_head do %>
  <%= stylesheet_pack_tag 'complete' %>
<% end %>

<div class="main">
  <div class="container">
    <div class="heading">パスワード変更後の注意事項</div>
    <div class="box">
      <div class="body">
        <ul>
          <li>
            <p>お手数をおかけいたしますが、クライアント（パソコン等）側で設定されています<span class="font_red">パスワードの変更</span>をお願い致します。</p>
          </li>
          <li>
            <p>新しく設定したパスワードは忘れないように管理願います。</p>
            <p>万が一、パスワードがわからなくなってしまった場合は、申し訳ありませんが<span class="font_red">パスワードの再発行手続きのためにＸＸＸの申請</span>をお願い致します。</span></p>
          </li>
          <li>
            <p>パスワードの有効期限は<%= Settings.password.limit %>です。</p>
            <p>有効期限切れ後は、<span class="font_red">パスワード忘れと同じ扱い</span>となります。</p>
            <p>尚、有効期限<%= Settings.password.alert %>前より毎日警告メールが発信されます。</p>
          </li>
        </ul>
      </div>
    </div>
  </div>
</div>
```

次にCSS関連の設定を追加していきます。
まずは、cssファイルとエントリーポイント登録を実施します。

```shell-session:各種ファイルとエントリーポイント登録
$ touch app/javascript/stylesheets/style.scss app/javascript/stylesheets/top_form.scss app/javascript/stylesheets/complete.scss

$ touch app/javascript/packs/top_form.js app/javascript/packs/complete.js

$ vim app/javascript/packs/application.js

【設定追加】
import '../stylesheets/style.scss'

$ vim app/javascript/packs/top_form.js

【設定追加】
import '../stylesheets/top_form.scss'

$ vim app/javascript/packs/complete.js

【設定追加】
import '../stylesheets/complete.scss'
```

次にCSSファイルの設定を追加していきます。

```css:全ビュー共通設定（style.scss）
html {
  font: 100%/1.5 'Avenir Next', 'Hiragino Sans', sans-serif;
  line-height: 1.7;
  letter-spacing: 1px;
}

ul,li {
  list-style-type: none;
}


a {
  text-decoration: none;
  color: #2d3133;
  font-size: 14px;
}

h1, h2, h3, h4, h5, h6, p {
  margin: 0;
}

input {
  background-color: transparent;
  outline-width: 0;
}

form input[type="submit"] {
  border: none;
  cursor: pointer;
}

/* 共通レイアウト ================================ */
body {
  color: #2d3133;
  background-color: #3ecdc6;
  margin: 0;
  min-height: 1vh;
}

.main {
  position: absolute;
  top: 64px;
  width: 100%;
  height: auto;
  min-height: 100%;
  background-color: #f5f8fa;
}

.container {
  max-width: 600px;
  margin: 60px auto;
  padding-left: 15px;
  padding-right: 15px;
  clear: both;
}

/* ヘッダー ================================ */
header {
  height: 64px;
  position: absolute;
  z-index: 1;
  width: 100%;
}

.header-logo {
  float: left;
  padding-left: 20px;
  color: white;
  font-size: 22px;
  line-height: 64px;
}

/* フラッシュ ================================ */
.flash {
  padding: 10px 0;
  color: white;
  background: rgb(251, 170, 88);
  text-align: center;
  position: absolute;
  top: 64px;
  z-index: 10;
  width: 100%;
  border-radius: 0 0 2px 2px;
  font-size: 14px;
}

.font_red {
  color: red;
}
```

```css:パスワード変更フォーム用（top_form.scss）
.heading {
  font-weight: 300;
  margin: 60px 0 20px;
  font-size: 48px;
  color: #bcc8d4;
}

.form {
  max-width: 600px;
  margin: 0 auto;
  background-color: white;
  box-shadow: 0 2px 6px #c1ced7;
}

.form-body {
  padding: 30px;
}

.form-error {
  color: #ff4d75;
}

.form input {
  width: 100%;
  border: 1px solid #d8dadf;
  padding: 10px;
  color: #57575f;
  font-size: 16px;
  letter-spacing: 2px;
  border-radius: 2px;
  box-sizing: border-box;
}

.form textarea {
  width: 100%;
  min-height: 110px;
  font-size: 16px;
  letter-spacing: 2px;
}

.form input[type="submit"] {
  background-color: #3ecdc6;
  color: white;
  cursor: pointer;
  font-weight: 300;
  width: 120px;
  border-radius: 2px;
  margin-top: 8px;
  margin-bottom: 0;
  float: right;
}

.form-body:after {
  content: '';
  display: table;
  clear: both;
}


.box {
  max-width: 600px;
  margin: 0 auto;
  background-color: white;
  box-shadow: 0 2px 6px #c1ced7;
}

.desc-body {
  padding: 30px;
}

.box li {
  list-style-type: square;
  display: list-item;
}

h2 {
  position: relative;
  margin: 1.5em 0em;
  padding: 0.5em;
  background: #a6d3c8;
  color: white;
}

h2:before {
  position: absolute;
  margin-bottom: 1.0em;
  content: '';
  top: 100%;
  left: 0;
  border: none;
  border-bottom: solid 15px transparent;
  border-right: solid 20px rgb(149, 158, 155);
}
```
```css:完了画面用（complete.scss）
.heading {
  font-weight: 300;
  margin: 60px 0 20px;
  font-size: 36px;
  color: #bcc8d4;
}

.box {
  max-width: 600px;
  margin: 0 auto;
  background-color: white;
  box-shadow: 0 2px 6px #c1ced7;
}

.body {
  padding: 30px;
}

.box li {
  list-style-type: square;
  display: list-item;
}
```

後はどうでもいいおしゃれポイントとして「flash」が表示された後、
５秒後にフェードアウトするjsを追加します。

```shell-session:javascriptファイル追加
$ touch app/javascript/src/flash_message.js

$ vim app/javascript/packs/application.js

【設定追加】
import '../src/flash_message.js'
```

```javascript:メッセージフェードアウト（flash_message.js）
(function() {
    setTimeout("$('.flash').fadeOut('slow')", 5000)
})
```

これで一応作成完了です。
テスト機能とか本番環境用の設定とか色々ありますが、
それはまた別記事で紹介したいなと考えています。

# アプリケーション作成後の所感

題材選びに、失敗した失敗した失敗した失敗した失敗した
失敗した失敗した失敗した失敗した失敗した
失敗した失敗した失敗した失敗した失敗した
失敗した失敗した失敗した失敗した失敗した

なんでオーソドックスなMVCモデルのアプリではなく、
ＤＢを必要としない対話アプリを最初の題材に選んだのかというのが正直な感想。
※業務に関連する直近の課題をチョイスした結果なんですけどね。

おまけに、無駄なこだわりを色々入れてみた結果、
工数１日で作成完了していたアプリケーションが工数１０日（ほぼ調査）まで膨れ上がってしまった。

これを見た初学者の皆様は題材選びと仕様設定には気を付けてください。

## （おまけ）無駄にこだわって調べた点

* js、cssの管理をWebpackerで統一
* SSH接続部分は別クラスで実装したい
* 共通パラメーターの外部ファイル化
* css等のファイルをビュー単位で分割（applicationに書くのが嫌だった）
* 各ビューからhead内にCSSファイルを入れる
* プロジェクトで無駄に生成されるファイルを極力減らす
* 大してテストケースもないのに態々System Specを実装（別記事で紹介予定）