Node.js ( レッスンⅤ - Ⅵ )


サイト -Web開発コース(Node.js) | Progate リンク


環境構築

$ npm install express-session セッションを管理するための機能を提供
$ npm install bcrypt パスワードをハッシュ化する機能を提供

画面イメージ

1 トップ localhost:3000  top.ejs
2 一覧 localhost:3000/list  list.ejs
3 新規登録 localhost:3000/signup  signup.ejs
4 ログイン localhost:3000/login  login.ejs
5 記事 localhost:3000/article/1  article.ejs


ミドルウェア関数(バリデーション)

1 ミドルウェア関数とは Expressでは、このリクエストとレスポンスの間にサーバーが実行する関数のことをミドルウェア関数という
2 ミドルウェア関数の例 これまでルーティングに対応する処理を書いてきた関数が、ミドルウェア関数
3 ミドルウェア関数を追加 リクエストを受け取ってからレスポンスを返す間に、複数のミドルウェア関数をつくることができる
4 ミドルウェア関数の実行順 ミドルウェア関数は、上から順番に実行される
5 正常/異常の分岐 配列errorsの要素数で判定


補足

1 articlesテーブル
2 usersテーブル
3 ログイン機能の作成手順
4 セッション管理とは
5 ルーティングの仕組み
6 res.localsオブジェクトからEJSファイルへ値を渡す
7 ログイン状態と限定記事の 表示
8 ユーザーIDの取得
9 配列に追加(pushメソッド)
10 パスワードのハッシュ化には、bcrypt(ビークリプト)というパッケージを用いることに


app.js
  1 const express = require('express');
  2 const mysql = require('mysql');
  3 const session = require('express-session');
  4 const bcrypt = require('bcrypt');
  5 const app = express();
  6 
  7 app.use(express.static('public'));
  8 app.use(express.urlencoded({extended: false}));
  9 
 10 const connection = mysql.createConnection({
 11   host: 'localhost',
 12   user: 'progate',
 13   password: 'password',     
 14   database: 'blog'
 15 });
 16 
 17 app.use(
 18   session({
 19     secret: 'my_secret_key',
 20     resave: false,
 21     saveUninitialized: false,
 22   })
 23 );
 24 
 25 app.use((req, res, next) => {
 26   if (req.session.userId === undefined) {
 27     res.locals.username = 'ゲスト';
 28     res.locals.isLoggedIn = false;
 29   } else {
 30     res.locals.username = req.session.username;
 31     res.locals.isLoggedIn = true;
 32   }
 33   next();
 34 });
 35 
 36 app.get('/', (req, res) => {
 37   res.render('top.ejs');
 38 });
 39 
 40 app.get('/list', (req, res) => {
 41   connection.query(
 42     'SELECT * FROM articles',
 43     (error, results) => {
 44       res.render('list.ejs', { articles: results });
 45     }
 46   );
 47 });
 48 
 49 app.get('/article/:id', (req, res) => {
 50   const id = req.params.id;
 51   connection.query(
 52     'SELECT * FROM articles WHERE id = ?',
 53     [id],
 54     (error, results) => {
 55       res.render('article.ejs', { article: results[0] });
 56     }
 57   );
 58 });
 59 
 60 app.get('/signup', (req, res) => {
 61   res.render('signup.ejs', { errors: [] });
 62 });
 63 
 64 app.post('/signup', 
 65   (req, res, next) => {
 66     console.log('入力値の空チェック');
 67     const username = req.body.username;
 68     const email = req.body.email;
 69     const password = req.body.password;
 70     const errors = [];
 71 
 72     if (username === '') {
 73       errors.push('ユーザー名が空です');
 74     }
 75 
 76     if (email === '') {
 77       errors.push('メールアドレスが空です');
 78     }
 79 
 80     if (password === '') {
 81       errors.push('パスワードが空です');
 82     }
 83 
 84     if (errors.length > 0) {
 85       res.render('signup.ejs', { errors: errors });
 86     } else {
 87       next();
 88     }
 89   },
 90   (req, res, next) => {
 91     console.log('メールアドレスの重複チェック');
 92     const email = req.body.email;
 93     const errors = [];
 94     connection.query(
 95       'SELECT * FROM users WHERE email = ?',
 96       [email],
 97       (error, results) => {
 98         if (results.length > 0) {
 99           errors.push('ユーザー登録に失敗しました');
100           res.render('signup.ejs', { errors: errors });
101         } else {
102           next();
103         }
104       }
105     );
106   },
107   (req, res) => {
108     console.log('ユーザー登録');
109     const username = req.body.username;
110     const email = req.body.email;
111     const password = req.body.password;
112     bcrypt.hash(password, 10, (error, hash) => {
113       connection.query(
114         'INSERT INTO users (username, email, password) VALUES (?, ?, ?)',
115         [username, email, hash],
116         (error, results) => {
117           req.session.userId = results.insertId;
118           req.session.username = username;
119           res.redirect('/list');
120         }
121       );
122     });
123   }
124 );
125 
126 app.get('/login', (req, res) => {
127   res.render('login.ejs');
128 });
129 
130 app.post('/login', (req, res) => {
131   const email = req.body.email;
132   connection.query(
133     'SELECT * FROM users WHERE email = ?',
134     [email],
135     (error, results) => {
136       if (results.length > 0) {
137         // 定数plainを定義してください
138         const plain = req.body.password;
139         
140         // 定数hashを定義してください
141         const hash = results[0].password;
142         
143         // パスワードを比較するためのcompareメソッドの処理を追加してください
144         bcrypt.compare(plain, hash, (error, isEqual) => {
145           if (isEqual) {
146             req.session.userId = results[0].id;
147             req.session.username = results[0].username;
148             res.redirect('/list');
149           } else {
150             res.redirect('/login');
151           }
152         });
153       } else {
154         res.redirect('/login');
155       }
156     }
157   );
158 });
159 
160 app.get('/logout', (req, res) => {
161   req.session.destroy((error) => {
162     res.redirect('/list');
163   });
164 });
165 
166 app.listen(3000);

【get / post のルーティング】

  60 app.get('/signup' は新規登録画面への画面遷移時

  64 app.post('/signup' は新規登録画面からの「登録する」ボタン時

126 app.get('/login' はログイン画面への画面遷移時

130 app.post('/login' はログイン画面からの「ログイン」ボタン時

 


【フォームの値】

131 req.body.email:フォームの値「メールアドレス」

138 req.body.passwird:フォームの値「パスワード」

 


【EJSに値を渡す】 

  27 res.localsオブジェクトを使うと、res.renderがなくてもEJSファイル 

    に値を渡すことができる。アプリケーション全体で使うような値も、

    res.localsオブジェクトを使う

 


【ログイン】

133 フォーム値「メールアドレス」でユーザー情報を取得

136 要素数が0より大きい:ユーザー情報が見つかった⇒認証処理へ

154 要素数が0以下:ユーザー情報なし⇒ログイン画面にリダイレクト

 


【セッション管理】

  17 express-sessionを使う準備(補足4)

  25 ログイン状態 (セッション情報にデータが保存されているか) を確認

  app.use関数

  ・リクエストの度に毎回実行される

  ・ルーティング処理の一番最初に書く必要がある(補足5)

  ・受け取ることのできる引数はreq, res, nextの3つ 

  33 リクエストに一致する次の処理を実行する

117 新規登録時、セッション情報にユーザーIDを保存(補足8)

118 新規登録時、セッション情報にユーザー名を保存

146 ログイン時、セッション情報にユーザーIDを保存

147 ログイン時、セッション情報にユーザー名を保存

 


【ログアウト】 

161 保存したセッション情報を消す

 


【バリデーション】 

  60 app.getでの新規登録画面表示時は、ejsに空を渡す
  85 app.postでの新規登録画面再表示時は、ejsに配列errorsを渡す

 

  65 未入力チェック。エラーメッセージを配列errorsに追加

  84 配列errorsの要素数が

 ・0より大きい場合、ユーザー登録画面へリダイレクト

 ・空ではない場合、next関数を実行してユーザー登録処理へ進む

 

  90 重複チェック。エラーメッセージを配列errorsに追加

  


【パスワードのハッシュ化】 

    4 bcryptパッケージを読み込む

112 bcryptパッケージのhashメソッドでハッシュ化する
 ・
password:入力されたパスワード

 ・10:パスワードの強さ。値が大きいほど強い。処理速度は遅くなる

 ・hash:ハッシュ化されたパスワード

115 ハッシュ化されたパスワードをusersテーブルに追加

144 ユーザーの入力した平文のパスワードと、usersテーブルのハッシ

  ュ化されたパスワードをbcryptパッケージのcompareメソッドで

  比較する。パスワードが一致している場合、isEqualはtrueになる

 


top.ejs - トップ
  1 <!DOCTYPE html>
  2 <html>
  3   <head>
  4     <meta charset="utf-8">
  5     <title>BLOG</title>
  6     <link rel="stylesheet" href="/css/style.css">
  7     <script src="/send_url.js"></script>
  8   </head>
  9   <body>
 10     <div class="top">
 11       <div class="wrapper">
 12         <div class="content">
 13           <h2>わんこの学びブログ</h2>
 14           <h1><img src="/images/top-logo.svg" alt=""></h1>
 15           <p>プログラミングに関する雑学ブログ。<br>だれかに教えたくなる豆知識をお届けします。</p>
 16           <a class="btn" href="/list">読みはじめる</a>
 17         </div>
 18         <img class="image" src="/images/top.svg" alt="">
 19       </div>
 20     </div>
 21   </body>
 22 </html>

 

hoge

header.ejs - ヘッダー
  1 <header>
  2   <div class="header-nav">
  3     <a href="/">BLOG</a>
  4     <p>ようこそ、<%= locals.username %>さん</p>
  5     <ul>
  6       <li><a href="/list">記事一覧</a></li>
  7       <% if (locals.isLoggedIn) { %>
  8         <li><a href="/logout">ログアウト</a></li>
  9       <% } else { %>
 10         <li><a href="/signup">新規登録</a></li>
 11         <li><a href="/login">ログイン</a></li>
 12       <% } %>
 13     </ul>
 14   </div>
 15   <p>わんこの学びブログ</p>
 16 </header>

【EJSに値を渡す】 

    4 app.jsで代入されたres.localsオブジェクトからから値を取得(補足6)

    7 ログイン状態の時は「ログアウト」 と表示

  10 ログインしてない時は「新規登録 ログイン」と表示

 


list.ejs - 一覧
  1 <!DOCTYPE html>
  2 <html>
  3   <head>
  4     <meta charset="utf-8">
  5     <title>BLOG</title>
  6     <link rel="stylesheet" href="/css/style.css">
  7     <script src="/send_url.js"></script>
  8   </head>
  9   <body>
 10     <%- include('header'); %>
 11     <main>
 12       <ul class="list">
 13         <% articles.forEach((article) => { %>
 14         <li>
 15           <% if (article.category === 'limited') {%>
 16             <i>会員限定</i>
 17           <% } %>
 18           <h2><%= article.title %></h2>
 19           <p><%= article.summary %></p>
 20           <a href="/article/<%= article.id %>">続きを読む</a>
 21         </li>
 22         <% }) %>
 23       </ul>
 24     </main>
 25   </body>
 26 </html>

【共通ファイルのinclude】

  10 ヘッダーのコードだけを書いたEJSファイルを呼び出す

 


【閲覧の制御】

  15 articlesテーブルのカラム「category 」が"limited"の場合、

  「会員限定」と表示する


signup.ejs - 新規登録
  1 <!DOCTYPE html>
  2 <html>
  3   <head>
  4     <meta charset="utf-8">
  5     <title>BLOG</title>
  6     <link rel="stylesheet" href="/css/style.css">
  7     <script src="/send_url.js"></script>
  8   </head>
  9   <body>
 10     <div class="sign">
 11       <div class="container">
 12         <h1><a href="/list">BLOG</a></h1>
 13         <div class="panel">
 14           <h2>新規登録</h2>
 15           <% if (errors.length > 0) { %>
 16             <ul class="errors">
 17               <% errors.forEach(error => { %>
 18                 <li><%= error %></li>
 19               <% }); %>
 20             </ul>
 21           <% } %>
 22           <form action="/signup" method="post">
 23             <p>ユーザー名</p>
 24             <input type="text" name="username">
 25             <p>メールアドレス</p>
 26             <input type="text" name="email">
 27             <p>パスワード</p>
 28             <input type="password" name="password">
 29             <input type="submit" value="登録する">
 30             <a href="/list">一覧にもどる</a>
 31           </form>
 32         </div>
 33       </div>
 34     </div>
 35   </body>
 36 </html>

【バリデーション】 

  15 エラーメッセージの表示

 


【新規登録の入力項目】

  22 ユーザー名、メールアドレス、パスワード


login.ejs - ログイン
  1 <!DOCTYPE html>
  2 <html>
  3   <head>
  4     <meta charset="utf-8">
  5     <title>BLOG</title>
  6     <link rel="stylesheet" href="/css/style.css">
  7     <script src="/send_url.js"></script>
  8   </head>
  9   <body>
 10     <div class="sign">
 11       <div class="container">
 12         <h1><a href="/list">BLOG</a></h1>
 13         <div class="panel">
 14           <h2>ログイン</h2>
 15           <form action="/login" method="post">
 16             <p>メールアドレス</p>
 17             <input type="text" name="email">
 18             <p>パスワード</p>
 19             <input type="password" name="password">
 20             <input type="submit" value="ログイン">
 21             <a href="/list">一覧にもどる</a>
 22           </form>
 23         </div>
 24       </div>
 25     </div>
 26   </body>
 27 </html>

【パスワードの安全処理】

  19 type="password" 入力した値が伏字になる

 


article.ejs - 記事
  1 <!DOCTYPE html>
  2 <html>
  3   <head>
  4     <meta charset="utf-8">
  5     <title>BLOG</title>
  6     <link rel="stylesheet" href="/css/style.css">
  7     <script src="/send_url.js"></script>
  8   </head>
  9   <body>
 10     <%- include('header'); %>
 11     <main>
 12       <div class="article">
 13         <% if (article.category === 'all') { %>
 14           <h1><%= article.title %></h1>
 15           <p><%= article.content %></p>
 16         <% } %>
 17         <% if (article.category === 'limited') { %>
 18           <i>会員限定</i>
 19           <h1><%= article.title %></h1>
 20           <% if (locals.isLoggedIn) { %>
 21             <p><%= article.content %></p>
 22           <% } else { %>
 23             <div class="article-login">
 24               <p>今すぐログインしよう!</p>
 25               <p>記事の続きは<br>ログインすると読むことができます</p>
 26               <img src="/images/login.svg">
 27               <a class="btn" href="/login">ログイン</a>
 28             </div>
 29           <% } %>
 30         <% } %>
 31       </div>
 32     </main>
 33     <footer>
 34       <a class="btn sub" href="/list">一覧にもどる</a>
 35     </footer>
 36   </body>
 37 </html>

【共通ファイルのinclude】

  10 ヘッダーのコードだけを書いたEJSファイルを呼び出す

 


【閲覧の制御】

  13 articlesテーブルのカラム「category 」が"all"の場合、

  一般記事として表示する

  17 articlesテーブルのカラム「category 」が"limited"の場合、

  限定記事として「会員限定」を表示する

  20 locals.isLoggedInがtue(ログイン状態)のとき限定記事を表示(補足7)

  22 locals.isLoggedInがfalseのとき限定記事は表示せず、ログイン画面へ

  のリンクを表示(補足7)


style.css
  1 @charset "UTF-8";
  2 
  3 *,
  4 *::before,
  5 *::after {
  6   box-sizing: border-box;
  7 }
  8 
  9 ul,
 10 ol {
 11   padding: 0;
 12   list-style: none;
 13 }
 14 
 15 body,
 16 h1,
 17 h2,
 18 h3,
 19 p,
 20 ul {
 21   margin: 0;
 22 }
 23 
 24 body {
 25   min-height: 100vh;
 26   text-rendering: optimizeSpeed;
 27   color: #2B546A;
 28   line-height: 1.6;
 29   display: flex;
 30   flex-direction: column;
 31   font-family: Lato, "Hiragino Maru Gothic Pro", "Meiryo UI", Meiryo, "MS PGothic", sans-serif;
 32 }
 33 
 34 img {
 35   max-width: 100%;
 36   display: block;
 37 }
 38 
 39 input,
 40 button,
 41 textarea,
 42 select {
 43   font: inherit;
 44 }
 45 
 46 input:-webkit-autofill,
 47 input:-webkit-autofill:hover, 
 48 input:-webkit-autofill:focus, 
 49 input:-webkit-autofill:active  {
 50   box-shadow: 0 0 0 1000px #FCFCFD inset;
 51 }
 52 
 53 a {
 54   text-decoration: none;
 55 }
 56 
 57 .article-forms {
 58   position: relative;
 59   padding: 50px 0 0;
 60 }
 61 
 62 .article-forms > i {
 63   position: absolute;
 64   top: -45px;
 65   right: 0;
 66   font-style: normal;
 67   font-weight: bold;
 68   display: inline-flex;
 69   align-items: center;
 70   border-width: 2px;
 71   border-style: solid;
 72   color: #9eb0b9;
 73   border-color: #E1E9F1;
 74   height: 40px;
 75   padding: 0 20px;
 76   border-radius: 6px;
 77   letter-spacing: 0.05em;
 78 }
 79 
 80 .article-forms > i.limited {
 81   color: #FF708F;
 82   border-color: #FF708F;
 83 }
 84 
 85 .article {
 86   padding: 20px 0 0;
 87 }
 88 
 89 .article > h1 {
 90   font-size: 30px;
 91   word-wrap: break-word;
 92   white-space: pre-wrap;
 93   line-height: 1.5;
 94   margin-bottom: 60px;
 95 }
 96 
 97 .article > p {
 98   font-size: 16px;
 99   word-wrap: break-word;
100   white-space: pre-wrap;
101   line-height: 2.2;
102 }
103 
104 .article > i {
105   display: inline-flex;
106   align-items: center;
107   background-color: #FF708F;
108   color: #ffffff;
109   font-size: 14px;
110   font-weight: bold;
111   padding: 4px 20px;
112   font-style: normal;
113   margin-bottom: 24px;
114   margin-top: 0px;
115 }
116 
117 .article-login {
118   text-align: center;
119   background-color: #F6FAFF;
120   border-radius: 10px;
121   padding: 60px 40px 50px;
122 }
123 
124 .article-login p:nth-child(1) {
125   font-weight: bold;
126   font-size: 26px;
127 }
128 
129 .article-login p:nth-child(2) {
130   font-size: 18px;
131   margin-top: 30px;
132   line-height: 1.8;
133 }
134 
135 .article-login img {
136   margin: 30px auto 30px;
137 }
138 
139 .article-login a + p {
140   margin-top: 36px;
141   font-weight: bold;
142   font-size: 14px;
143 }
144 
145 .article-login a + p > a {
146   display: inline-block;
147   margin-left: 20px;
148   color: #4CCFC9;
149 }
150 
151 .article-login a + p > a:hover {
152   color: #2DC2BC;
153 }
154 
155 .btn {
156   display: inline-flex;
157   align-items: center;
158   font-size: 18px;
159   font-weight: bold;
160   color: #ffffff;
161   background-color: #4CCFC9;
162   height: 54px;
163   padding: 0 90px;
164   border-radius: 27px;
165 }
166 
167 .btn:hover {
168   background-color: #2DC2BC;
169 }
170 
171 .btn.sub {
172   background-color: #B7C7DF;
173 }
174 
175 .btn.sub:hover {
176   background-color: #A9B9D2;
177 }
178 
179 .btn-confirm {
180   width: auto;
181   display: inline-flex;
182   align-items: center;
183   font-size: 18px;
184   font-weight: bold;
185   color: #ffffff;
186   background-color: #E75252;
187   height: 44px;
188   padding: 0 65px;
189   border-radius: 22px;
190 }
191 
192 .btn-confirm:hover {
193   background-color: #D84444;
194 }
195 
196 .btn-confirm.sub {
197   background-color: #B7C7DF;
198 }
199 
200 .btn-confirm.sub:hover {
201   background-color: #A9B9D2;
202 }
203 
204 .confirm {
205   border: 2px solid #E29A9A;
206   border-radius: 0.4rem;
207   padding: 40px 40px 50px 40px;
208   margin-bottom: 90px;
209   text-align: center;
210 }
211 
212 .confirm > h1 {
213   font-size: 24px;
214   color: #E75252;
215 }
216 
217 .confirm > p {
218   padding: 20px 0 40px;
219 }
220 
221 .confirm > a {
222   margin: 0 5px;
223 }
224 
225 .confirm > form {
226   display: inline;
227 }
228 
229 .create-area {
230   z-index: 1;
231   position: fixed;
232   bottom: 0;
233   left: 0;
234   width: 100%;
235   background-color: #F6FAFF;
236   padding: 40px 0;
237 }
238 
239 .create-area .container {
240   display: flex;
241   align-items: center;
242   justify-content: space-around;
243   width: 700px;
244   margin: 0 auto;
245 }
246 
247 .create-area .container > p {
248   font-size: 18px;
249   font-weight: bold;
250   padding-right: 40px;
251 }
252 
253 .create-area .container > .btn {
254   padding: 0 60px;
255 }
256 
257 .errors {
258   font-size: 14px;
259   font-weight: bold;
260   color: #ED5974;
261   border: 1px solid #e29a9a;
262   padding: 12px 34px;
263   line-height: 1.6;
264   border-radius: 6px;
265   list-style: outside;
266   background-color: #fff;
267   margin-top: 30px;
268 }
269 
270 footer {
271   background-color: #F6FAFF;
272   text-align: center;
273   padding: 70px 0 100px;
274   flex: 1;
275 }
276 
277 form > p {
278   font-size: 16px;
279   font-weight: bold;
280   margin-bottom: 15px;
281 }
282 
283 form > a {
284   display: block;
285   width: 50%;
286   text-align: center;
287   margin: 10px auto 0;
288   color: #A4B5C8;
289   font-weight: bold;
290   padding: 15px 0;
291 }
292 
293 input,
294 textarea {
295   width: 100%;
296   padding: 16px 20px;
297   line-height: 1.8;
298   border: 1px solid #d5dee8;
299   background-color: #FCFCFD;
300   border-radius: 4px;
301   outline: none;
302   font-size: 16px;
303   color: #2B546A;
304 }
305 
306 input:hover,
307 textarea:hover {
308   border-color: #b6c4d2;
309   box-shadow: 0px 0px 12px 1px rgba(160, 195, 247, 0.2);
310 }
311 
312 input:focus,
313 textarea:focus {
314   border-color: #b6c4d2;
315 }
316 
317 textarea {
318   resize: vertical;
319 }
320 
321 input[name="title"] {
322   margin-bottom: 30px;
323 }
324 
325 textarea[name="summary"] {
326   margin-bottom: 30px;
327   height: calc( 2em * 3 +  1em);
328   line-height: 2;
329 }
330 
331 textarea[name="content"] {
332   padding: 24px 28px;
333   height: calc( 2em * 15 +  1em);
334   line-height: 2;
335 }
336 
337 input[type="submit"] {
338   font-size: 18px;
339   font-weight: bold;
340   color: #ffffff;
341   background-color: #4CCFC9;
342   height: 54px;
343   padding: 0 90px;
344   border-radius: 27px;
345   width: 60%;
346   display: block;
347   margin: 80px auto 0;
348 }
349 
350 input[type="submit"]:hover {
351   background-color: #2DC2BC;
352   cursor: pointer;
353 }
354 
355 header {
356   background-color: #88E7CE;
357 }
358 
359 header.admin {
360   background-color: #A0C3F7;
361 }
362 
363 header.admin > p:after {
364   background-image: url("/img/wanko_admin.svg");
365 }
366 
367 header > p {
368   position: relative;
369   max-width: 700px;
370   margin: 0 auto;
371   color: #ffffff;
372   font-size: 36px;
373   font-weight: bold;
374   padding: 40px 0 80px;
375 }
376 
377 header > p:after {
378   position: absolute;
379   content: "";
380   display: block;
381   width: 148px;
382   height: 136px;
383   bottom: -16px;
384   right: -10px;
385   background-size: 148px 136px;
386   background-repeat: no-repeat;
387   background-image: url('/images/wanko.svg');
388 }
389 
390 .header-nav {
391   display: flex;
392   flex-wrap: nowrap;
393   justify-content: space-between;
394   align-items: center;
395   min-width: 700px;
396   max-width: 1200px;
397   padding: 0 20px;
398   margin: 0 auto;
399   height: 80px;
400 }
401 
402 .header-nav > a {
403   color: #2B546A;
404   font-weight: bold;
405 }
406 
407 .header-nav p {
408   padding-left: 30px;
409   font-size: 15px;
410   color: #2B546A;
411   font-weight: bold;
412   word-wrap: break-word;
413   opacity: 0.7;
414   flex: 1;
415 }
416 
417 .header-nav ul {
418   text-align: right;
419   display: flex;
420 }
421 
422 .header-nav li > a {
423   white-space: nowrap;
424   display: inline-block;
425   font-size: 15px;
426   color: #2B546A;
427   font-weight: bold;
428   padding-left: 30px;
429 }
430 
431 .heading {
432   font-size: 30px;
433 }
434 
435 .list li, .list-admin li {
436   position: relative;
437   padding: 28px 40px 54px;
438   border: 1px solid #d5dee8;
439   border-radius: 6px;
440 }
441 
442 .list li:not(:first-child),
443 .list-admin li:not(:first-child) {
444   margin-top: 20px;
445 }
446 
447 .list h2,
448 .list-admin h2 {
449   font-size: 22px;
450 }
451 
452 .list p,
453 .list-admin p {
454   font-size: 16px;
455   color: #8491A5;
456 }
457 
458 .list i,
459 .list-admin i {
460   display: inline-flex;
461   align-items: center;
462   background-color: #FF708F;
463   color: #ffffff;
464   font-size: 14px;
465   font-weight: bold;
466   padding: 3px 16px;
467   font-style: normal;
468   margin-bottom: 12px;
469   margin-top: 5px;
470   letter-spacing: 0.05em;
471 }
472 
473 .list h2 {
474   margin-bottom: 12px;
475 }
476 
477 .list a {
478   position: absolute;
479   bottom: 18px;
480   right: 30px;
481   display: inline-block;
482   color: #30C8D6;
483 }
484 
485 .list a:hover {
486   color: #21BBC9;
487 }
488 
489 .list-admin {
490   margin-top: 30px;
491   padding-bottom: 80px;
492 }
493 
494 .list-admin li {
495   padding-bottom: 28px;
496   padding-right: 160px;
497   border-radius: 0;
498 }
499 
500 .list-admin li:not(:first-child) {
501   margin-top: -1px;
502 }
503 
504 .list-admin li:first-child {
505   border-top-left-radius: 6px;
506   border-top-right-radius: 6px;
507 }
508 
509 .list-admin li:last-child {
510   border-bottom-right-radius: 6px;
511   border-bottom-left-radius: 6px;
512 }
513 
514 .list-admin .control {
515   position: absolute;
516   display: flex;
517   align-items: center;
518   justify-content: center;
519   height: 100%;
520   top: 0;
521   right: 0;
522   width: 160px;
523 }
524 
525 .list-admin a {
526   display: inline-flex;
527   color: #8098a5;
528   font-size: 16px;
529   text-align: center;
530   flex-direction: column;
531   width: 60px;
532   align-items: center;
533   letter-spacing: 0.1em;
534 }
535 
536 .list-admin a img {
537   width: 32px;
538 }
539 
540 .list-admin a:hover {
541   opacity: 0.7;
542 }
543 
544 main {
545   max-width: 700px;
546   width: 100%;
547   margin: 0 auto;
548   padding: 80px 0 100px;
549 }
550 
551 .sign {
552   border-top: 20px solid #88E7CE;
553   display: flex;
554   align-items: center;
555   justify-content: center;
556   min-height: 100vh;
557 }
558 
559 .sign > .container {
560   width: 550px;
561   padding: 60px 0;
562 }
563 
564 .sign > .container > p {
565   margin-top: 36px;
566   font-weight: bold;
567   font-size: 14px;
568   text-align: center;
569 }
570 
571 .sign > .container > p > a {
572   display: inline-block;
573   margin-left: 20px;
574   color: #4CCFC9;
575 }
576 
577 .sign > .container > p > a:hover {
578   color: #2DC2BC;
579 }
580 
581 .sign > .container form {
582   padding-top: 40px;
583 }
584 
585 .sign .panel {
586   padding: 60px 80px 40px;
587   background-color: #F6FAFF;
588 }
589 
590 .sign h1 {
591   text-align: center;
592   font-size: 28px;
593   margin-bottom: 50px;
594 }
595 
596 .sign h1 > a {
597   color: #2B546A;
598 }
599 
600 .sign h1 > a:hover {
601   color: #2b6d90;
602 }
603 
604 .sign h2 {
605   text-align: center;
606   font-size: 24px;
607 }
608 
609 .sign form > p {
610   margin-bottom: 10px;
611 }
612 
613 .sign input {
614   padding: 10px 20px;
615 }
616 
617 .sign input:-webkit-autofill {
618   box-shadow: 0 0 0px 1000px #fff inset;
619 }
620 
621 .sign input[name="username"] {
622   margin-bottom: 30px;
623 }
624 
625 .sign input[name="email"] {
626   margin-bottom: 30px;
627 }
628 
629 .sign input[type="submit"] {
630   margin-top: 50px;
631   width: 90%;
632 }
633 
634 .top {
635   display: flex;
636   align-items: center;
637   justify-content: center;
638   min-height: 100vh;
639 }
640 
641 .top .wrapper {
642   position: relative;
643   width: 100%;
644   height: 700px;
645   max-height: 700px;
646   background-color: #fbfbfb;
647 }
648 
649 .top .wrapper:after {
650   display: block;
651   position: absolute;
652   content: "";
653   bottom: 0;
654   left: 0;
655   height: 24px;
656   background-color: #fff;
657   width: 100%;
658 }
659 
660 .top .content {
661   position: relative;
662   z-index: 2;
663   display: flex;
664   align-items: center;
665   flex-direction: column;
666   height: 100%;
667   text-align: center;
668   letter-spacing: 0.1em;
669 }
670 
671 .top h1 {
672   width: 350px;
673   margin-bottom: 30px;
674 }
675 
676 .top h2 {
677   text-align: center;
678   font-size: 18px;
679   margin: 152px 0 20px;
680 }
681 
682 .top a {
683   margin-top: 30px;
684 }
685 
686 .top .image {
687   z-index: 1;
688   position: absolute;
689   width: 1000px;
690   min-width: 1000px;
691   top: 0;
692   left: 50%;
693   transform: translateX(-50%);
694 }
695 
696 input[type="submit"].btn-confirm {
697   width: auto;
698   display: inline-flex;
699   align-items: center;
700   font-size: 18px;
701   font-weight: bold;
702   color: #ffffff;
703   background-color: #E75252;
704   height: 44px;
705   padding: 0 65px;
706   border-radius: 22px;
707   margin: 0 auto;
708 }
709 
710 input[type="submit"].btn-confirm:hover {
711   background-color: #D84444;
712 }
713 
714 input[type="submit"].btn-confirm.sub {
715   background-color: #B7C7DF;
716 }
717 
718 input[type="submit"].btn-confirm.sub:hover {
719   background-color: #A9B9D2;
720 }
hoge