Help us understand the problem. What is going on with this article?

WEBでLINE風のチャットサイトを作る-その1

はじめに

Webで動作するLINE風のチャットインターフェースを作成します。クライアントは普通のブラウザを想定し、サーバー側はNode.jsとMongoDBを使います。
  
 機能としては基本となる書き込みと、それに対する簡単な自動応答を実装します。
 次回以降でログイン処理などを組み込み、その後はチャットボットや自然言語処理、画像認識やブロックチェーンなんかにも足を伸ばせればと思ってます。

環境構築

お手軽なクラウドサービスを使って環境構築を行います。

Paiza Cloud

paiza表紙.png
Paiza Cloudeにアクセスしてメールアドレスを登録すると、すぐに環境構築ができるようになります。

リンク先
https://paiza.cloud/ja/

サーバー作成

アカウントを作成したらサーバー作成ボタンを押しましょう
まだサーバーはありません.png
新規サーバー作成のポップアップで、Node.jsとMongoDBを選択してください。
サーバー設定.png
数秒間待っているとサーバー環境ができあがります。
ちなみに無料プランの場合は
* サーバーの最長利用時間は24時間
* サービスは外部へ公開されない
逆に言うと練習にはもってこいという事でしょうか。

アプリケーション構築

次に各種インストールを行い、アプリケーションの実行環境を構築します。
まずは画面からターミナルのアイコンをクリックしてください。

ターミナル.png

起動したターミナルに下記のコマンドを入れてgitからファイルを展開します。

git clone https://github.com/nstshirotays/chatapp-shot1.git

下記のようにディレクトリが作成されソースが展開されます。
gitclone.png

つぎにディレクトリを移動し、必要なパッケージを導入します。

cd chatapp-shot1
npm install

実行に必要なモジュールなどがpackage.jsonに従って自動的にインストールされます。これで環境が整いますので、あとはnodejsを起動してアプリを立ち上げます。

node server.js

install.png

エラーがでなければ、左側に緑色のブラウザアイコンが新しく点滅し始めます。
ブラウザ3000.png

このアイコンをクリックするとアプリが起動します。

実行画面.png

適当に入力すると、数秒後にEchoさんが応答を返してくれます。

コード解説

server.js

サーバー側のメイン処理となります。
 現状ではログイン処理がありませんので、友達を選択後という体で実装しています。
今回の友達はEchoさんです。Echoさんはこちらからのメッセージに「<相手のメッセージ>ですね」と回答するシンプルボットです。
 会話の内容はMongDBに格納しています。MongoDBはNonSQLでこのようなSQLサーバーを立てるほどではないが、色々なデータをストアしておきたい時にいいかなと思ってます。
最初にEchoさんからのご挨拶を登録したあとは、クライアントからのアクセス待ちとなります。
 Echoさんの会話は、MongoDBを一秒ごとにクエリして新しいメッセージが到着していれば応答する仕掛けとなっています。

server.js
var express  = require('express');
var app = express();
var mongoose = require('mongoose');
var bodyParser = require('body-parser');
var ejs = require("ejs");

app.engine('ejs',ejs.renderFile);
app.use(express.static(__dirname + '/public'));
app.use(bodyParser.json());

// (暫定)会話用のIDを設定
var myID = '01';
var f1ID = '02';

// (暫定)会話用のコレクション名を作成
var chatCollection = myID + f1ID;

// MongoDBに接続
mongoose.connect('mongodb://localhost/mydb');

// (暫定)会話格納用(スタンプや画像はまだ)
var Chats = mongoose.model(chatCollection, {
    fromAddress : String,
    toAddress : String,
    message : String,
    timeStamp :String
});

//  最初の挨拶を登録
Chats.create({
    fromAddress : f1ID,
    toAddress : myID,
    message : 'こんにちは',
    timeStamp : getDateTime()
    });

// 直近の会話を比較用に保存
var resentMsg = "---";
var query = { "fromAddress": "01" };
    Chats.find(query,{},{sort:{_id: -1},limit:1}, function(err, data){
        if(err){
            console.log(err);
        }
        if(!data){
            if (data[0].timeStamp !==''){
                resentMsg =data[0].timeStamp + data[0].message;
            }
        }
    });

// クライアントからgetされると会話全件をjsonで返す
app.get('/api/messages', (req, res) => {
    Chats.find()
            .then((messages) => {
            res.json(messages);
        })
        .catch((err) => {
            res.send(err);
        })
});

// 会話内容がポストされれば、それを登録する
app.post('/api/messages', (req, res) => {
    var postData = req.body;

    Chats.create({
            fromAddress : myID,
            toAddress : f1ID,
            message : postData.mess,
            timeStamp : getDateTime()
        })
        .then((postData) => {
            res.json(postData);
        })
        .catch((err) => {
            res.send(err);
        });
});

// (暫定)1秒ごとに新しいメッセージを検索する
const timer = setInterval(function(){
    var query = { };
    Chats.find(query,{},{sort:{_id: -1},limit:1}, function(err, data){
        if(err){
            console.log(err);
        }
        if ( data[0].fromAddress == '01') {
            if ( resentMsg != data[0].timeStamp + data[0].message) {
                resentMsg = data[0].timeStamp + data[0].message;
                msgFooking(data[0].message);
            }
        }
    });
},1000);

// (暫定)ECHOさんの処理
function msgFooking(msg){
    Chats.create({
        fromAddress : f1ID,
        toAddress : myID,
        message : msg + "ですね",
        timeStamp : getDateTime()
    });
}


// ルートアクセス時にベースの画面を返す
// 友達の名前とそれぞれのIDをEJSでHTMLに埋め込む
app.get('/', (req, res) => {
    res.render('chatapp.ejs',
        {frendName: 'Echo' ,
         myidf: myID ,
         fiidf: f1ID });
});

// 日時の整形処理
function getDateTime(){
    var date = new Date();

    var year_str = date.getFullYear();
    var month_str = date.getMonth();
    var day_str = date.getDate();
    var hour_str = date.getHours();
    var minute_str = date.getMinutes();
    var second_str = date.getSeconds();

    month_str = ('0' + month_str).slice(-2);
    day_str = ('0' + day_str).slice(-2);
    hour_str = ('0' + hour_str).slice(-2);
    minute_str = ('0' + minute_str).slice(-2);
    second_str = ('0' + second_str).slice(-2);

    format_str = 'YYYY/MM/DD hh:mm:ss';
    format_str = format_str.replace(/YYYY/g, year_str);
    format_str = format_str.replace(/MM/g, month_str);
    format_str = format_str.replace(/DD/g, day_str);
    format_str = format_str.replace(/hh/g, hour_str);
    format_str = format_str.replace(/mm/g, minute_str);
    format_str = format_str.replace(/ss/g, second_str);

    return format_str;

}

// ポート3000で待ち受け
app.listen(3000, () => {
    console.log("起動しました ポート3000");
});

HTML

画面ファイルです。server.jsからのパラメーター受け渡しをするためにejsを利用しています。なので名称が.ejsとなっていますが、基本的にはHTMLファイルです。
ちなみにejsでのパラメータを埋め込んでいるのは3箇所で、それぞれ(自分のID,相手のID,相手の名前)が置換されて埋め込まれます。
このあたりの組み方とCSSについては試行錯誤の連続ですが必要最低限のカタチにはなったかなと思っています。

/public/views/chatapp.ejs
<!doctype html>
<html>
<head>
    <title>Chat App</title>
    <link rel="stylesheet" href="//cdn.rawgit.com/milligram/milligram/master/dist/milligram.min.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script>var myid = <%=myidf %>;</script>
    <script>var f1id = <%=fiidf %>;</script>
    <script src="/javascripts/client.js"></script>
    <link rel="stylesheet" type="text/css" href="/stylesheets/style.css">
</head>
<body>
    <div class="base_view ">
        <div class="title">
            <%=frendName %>さん
        </div>
        <div id="area" class="contents scroll">
        </div>
    </div>
        <div class="form-group" >
           <table>
                <tr>
                    <td>
                        <input id="inp" type="text" name="name" class="newMessage form-control" autocomplete="nope" >
                    </td>
                    <td width=80px>
                        <button type="submit" class="btn" onclick="pushMessage()" >送信</button>
                    </td>
                </tr>
            </table>
        </div>
</body>
</html>
/public/stylesheets/style.css
/* ベース部分 */
.base_view {
  padding:0;
  background: #729dda;
  overflow: hidden;
  margin: 20px auto;
  font-size: 80%;
}

/* タイトル部分 */
.base_view .title {
  background: #1b313f;
  padding: 10px;
  text-align: center;
  font-size: 150%;
  color: white;
}

/* 会話部分 */
.base_view .contents{
  padding: 10px;
  overflow: hidden;
  line-height: 135%;
}

.base_view .scroll {
  height: 400px;
  overflow-y: scroll;
}

/* 自分の会話 */
.base_view .myText {
  position: relative;
  display: block;
  margin: 5px 0;
  max-width: 75%;
  float: right;
  margin-right: 15px;
  clear: both;
}

/* 会話文 自分用*/
.base_view .myText .text {
  padding: 10px;
  border-radius: 20px;
  background-color: #81da45;
  margin: 0;
  margin-left: 80px;
}

/* 吹き出し 自分用*/
.base_view .myText .text::after {
  content: '';
  position: absolute;
  display: block;
  width: 0;
  height: 0;
  right: -10px;
  top: 10px;
  border-left: 20px solid #81da45;
  border-top: 10px solid transparent;
  border-bottom: 10px solid transparent;
}

/* 時刻 自分用 */
.base_view .myText .date {
  content: '';
  position: absolute;
  display: block;
  width: 100px;
  text-align: right;
  left: -30px;
  bottom: 0px;
  font-size: 80%;
  color: white;
}



 /* 相手の会話 */
.base_view .flText {
  width: 100%;
  position: relative;
  display: block;
  margin-bottom: 5px;
  max-width: calc(100% - 120px);;
  clear: both;
}

/* アイコン */
.base_view .flText figure {
  width: 50px;
  position: absolute;
  top: 0;
  left: 0;
  padding: 0;
  margin: 0;
}

/* 丸く切り抜く */
.base_view .flText figure img{
  border-radius: 50%;
  width: 50px;
  height: 50px;
}

/* テキスト 相手用 */
.base_view .flText .flText-text {
  float: left;
  margin-left: 70px;
}

.base_view .flText .flText-text .name {
  font-size: 80%;
  color: white;
}

/* 会話文 相手用 */
.base_view .flText .text {
  margin: 0;
  position: relative;
  padding: 10px;
  border-radius: 20px;
  background-color: white;
}


/* 吹き出し 相手用 */
.base_view .flText .text::after {
  content: '';
  position: absolute;
  display: block;
  width: 20;
  height: 0;
  left: -10px;
  top: 10px;
  border-right: 20px solid white;
  border-top: 10px solid transparent;
  border-bottom: 10px solid transparent;
}

/* 時刻 相手用 */
.base_view .flText .date {
  content: '';
  position: relative;
  display: block;
  width: 100px;
  text-align: right;
  top: -20px;
  left: 100%;
  bottom: 0px;
  font-size: 80%;
  color: white;
}

 .form-group {
  background-color: white;
 }

クライアントJavascript

クライアント側のJavascriptです。行っていることは

  1. GETしたサーバーからのJsonファイルを画面に表示する
  2. サーバーに会話データをPostする
  3. 3秒毎に再クエリを行う

このあたりはちょっと力技になっていて、画面は差分更新ではなく完全に全消去->全描画となっています。
また、友達側の画像ファイルは名前IDを元に取得しています。

/public/javascript/client.js
function render(getJsonData){
    // 一旦全部の会話を消去してから再描画する(暫定:力技)
    document.getElementById('area').innerHTML = '';
    // 受信したjsonデータを自分と相手に分けて描画する
    for(var i in getJsonData){
        if(getJsonData[i].fromAddress==myid){
            var cts ="";

            cts =  "<div class='myText'>";
            cts += "  <div class='text'>"+ getJsonData[i].message + "</div>";
            cts += "  <div class='date'>"+ getJsonData[i].timeStamp + "</div>";
            cts += "</div>";
            $('.contents').append(cts);
        } else {
            var cts ="";

            cts =  "<div class='flText'>"; 
            // 友達IDからアイコン画像ファイル名を生成している
            cts += "  <figure><img src='images/" + f1id + ".jpg'/></figure>";
            cts += "  <div class='flText-text'>";
            cts += "    <div class='text'>"+ getJsonData[i].message + "</div>";
            cts += "    <div class='date'>"+ getJsonData[i].timeStamp + "</div>";
            cts += "  </div>";
            cts += "</div>";
            $('.contents').append(cts);
        }
    }
    // 描画が終わったら画面下までスクロールさせる
    var obj = document.getElementById('area');
    obj.scrollTop = document.getElementById('area').scrollHeight;
}

// getを行いjsonデータを受領して描画関数に渡す
function getMessages(){
    fetch('/api/messages')
        .then((data) => data.json())
        .then((json) => {
            var getJsonData = json;
            render(getJsonData);
        });
}

// 入力データをサーバーにポストする
function pushMessage(){
    var text = $(".newMessage").val();
    if (text !=''){
        fetch('/api/messages', {
            method:"POST",
            headers: {'Content-Type': 'application/json',},
            body: JSON.stringify({mess: text}),
        })
        .then(() => {
            getMessages();
            // 入力領域をクリアする
            document.getElementById('inp').value="";
        });

    }
}

// 3秒ごとに再描画のgetを行う
setInterval("getMessages()",3000);

$(getMessages);

次回は

 次回はログイン周りを作りつつ、少しだけセキュリティ機能も入れていければと思っています。上手く行ったら画像のアップロード機能も組み込みたいところです。
 

今回参考にさせていただいたサイト

  1. サイト 初心者のための Node.jsプログラミング入門
  2. サイト MongDB公式
  3. サイト Paiza Cloud 開発日誌 Node.js入門
  4. サイト HTMLとCSSでLINE風チャット画面(会話方式)を記事に表示する方法

0

@nstshirotays
2019年11月03日に投稿
1 views
編集する

WEBでLINE風のチャットサイトを作る-その2
Node.js
MongoDB
Express.js
HTML,CSS
はじめに
 前回作成した基本的なインターフェースを拡充してチャットアプリとしての体裁を整えたいと思います。
 今回追加する機能は、ログイン機能とユーザー登録機能、簡易的なセキュリティ対策、スタンプ機能、画像アップロード機能です。
  

環境構築
前回に引き続きお手軽なクラウドサービスを使って環境構築を行います。

Paiza Cloud
paiza表紙.png
Paiza Cloudeにアクセスしてメールアドレスを登録すると、すぐに環境構築ができるようになります。

リンク先
https://paiza.cloud/ja/

サーバー作成
アカウントを作成したらサーバー作成ボタンを押しましょう
まだサーバーはありません.png
新規サーバー作成のポップアップで、Node.jsとMongoDBを選択してください。
サーバー設定.png
数秒間待っているとサーバー環境ができあがります。
ちなみに無料プランの場合は
* サーバーの最長利用時間は24時間
* サービスは外部へ公開されない
逆に言うと練習にはもってこいという事でしょうか。

アプリケーション構築
次に各種インストールを行い、アプリケーションの実行環境を構築します。
まずは画面からターミナルのアイコンをクリックしてください。

s_ターミナル.png

起動したターミナルに下記のコマンドを入れてgitからファイルを展開します。

git clone https://github.com/nstshirotays/chatapp-shot2.git
下記のようにディレクトリが作成されソースが展開されます。
git2.png

つぎにディレクトリを移動し、必要なパッケージを導入します。

cd chatapp-shot2
npm install
実行に必要なモジュールなどがpackage.jsonに従って自動的にインストールされます。

以上で必要な準備が整いましたので、あとはnodejsを起動してアプリを立ち上げます。

npm start
git3.png

エラーがでなければ、左側に緑色のブラウザアイコンが新しく点滅し始めます。
s_ブラウザ3000.png

このアイコンをクリックするとアプリが起動します。

アプリ実行
ログイン画面
まずはログイン画面です。
login.png
初回は誰も登録されていないので、Create an Account を押してユーザー登録画面に移ります。

ユーザー登録画面
NickNameとPassCodeを入れてユーザーを登録しましょう。
NickNameは英文字で4から12文字。PassCodeは数字で6から12文字です。
regisit.png

お好みでFaceIconを変更(png 32kbまで)できます。

ユーザーを登録したら実際にログインしてみましょう。

友達選択画面
list.png
登録されている自分以外の友達が一覧で表示されます。今回もデフォルトでEchoさんが登録されています。友達を選択すると会話画面に遷移します。

会話画面
ベースとなる会話画面です。前回からスタンプと画像アップロード機能が追加されています。
chatapp.png
Echoさんはこちらの会話に相槌を打ってくれるチャットボットです。

スタンプ画面
スタンプボタンを押すと一覧が表示されます。
stamp.png
今回はクリスマススタンプを入れてみました。
お好きなpngを public/files/stampsに入れてください。

アイコンネタ元 speckyboy.com

(ちなみにEchoさんはスタンプをもらうとそのスタンプ名を言ってくれる仕様にしました)

stam2p.png

画像アップロード
今回は画像アップロード機能を加えています。画像はjpegのみで、サイズは1000×1000以下です。
image1.png

残念ですが現時点ではEchoさんは画像の内容を認識できません。次回あたりにチャレンジしたいと思います。

アプリケーション解説
 画面も増えましたので前回のソースをnode.jsのアプリケーションフレームワークであるExpressで再構成しました。
 このためディレクトリ構造は下記のようになっています。

|-helper       共通機能系
|-models       データモデル系
|-public
|---files
|-----stamps     スタンプの場所
|---images
|---javascripts
|---stylesheets
|-routes       get,postで呼ばれるjavascript
|-views        html
参考にした記事
Express + Node.jsで基本を理解した次の一歩 - ディレクトリ構成をルーティング・ミドルウェアを理解して考えてみる

コード解説
それでは今回加えた主なソースを解説していきます。チャット画面については前回とほぼ同様ですので割愛します。

app.js
メインのプログラムです。前回はserver.jsとして実装しました。今回はexpressのアプリケーション自動生成(generator)機能を使ったのでこの生成されたapp.jsに各画面の呼び出しを加えています。

参考サイト 初心者のための Node.jsプログラミング入門

app.js
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var mongoose = require('mongoose');

// ルーティング処理の呼び出し先を追加
var loginRouter = require('./routes/login');
var registerRouter = require('./routes/register');
var listRouter = require('./routes/list');
var chatRouter = require('./routes/chatapp');
var errRouter = require('./routes/errorpage');
var apiRouter = require('./routes/api');
var logoutRouter = require('./routes/logout');

var app = express();

// 変数宣言
var MyID = "";
var MyName ="";
var FrID = "";
var FrName ="";
var botTimer;

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// ファイルアップロードに関する拡張
app.use(logger('dev'));
//app.use(express.json());
//app.use(express.urlencoded({ extended: false }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use(express.json({ extended: true, limit: '10mb' }));

app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

// ルーティング処理の登録
app.use('/', loginRouter);
app.use('/auth', loginRouter);
app.use('/register', registerRouter);
app.use('/home', listRouter);
app.use('/chat', chatRouter);
app.use('/errorpage', errRouter);
app.use('/api/messages', apiRouter);
app.use('/logout', logoutRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};

// render the error page
res.status(err.status || 500);
res.render('error');
});

module.exports = app;
基本的にはクライアントからのGETやPOSTに対して応答する処理(ルーティング)を登録しています。requireでルーティング処理が書かれたJavascriptを変数に登録し、それをapp.useでクライアントからのURLに紐づけています。

処理全体で利用する変数として自分と相手のID,Nameを変数として宣言しています。また、チャットボットの本体となるタイマー起動処理用の変数もここで宣言しています。本来であれば、別ファイルにしてexportsでオブジェクト風に見せるのがお作法かとも思うのですが、シンプルに直接宣言したほうがわかりやすいかと思ってこっちにしました。

今回はファイルのアップロードがあるためpostデータが大きくなり、そのままでは413エラー(request entity too large)となってしまいます。このための設定としてオプション値を設定しています。

参考にした記事
Express4でエラー「request entity too large」が発生する

login.js ログイン処理
login.js
var express = require('express');
var router = express.Router();
var { check, validationResult } = require('express-validator');
var sanitize = require('mongo-sanitize');
var db = require('../helper/db');
var User = db.User;
var bcrypt = require('bcryptjs');

var jsonwebtoken = require('jsonwebtoken');
const config = require('../config');

//--------------------------------------------------------
// ログイン画面の表示
//--------------------------------------------------------
/* GET home page. */
router.get('/', function(req, res, next) {
res.clearCookie("auth");
res.render('login', {error: false, errors:false});
});

//--------------------------------------------------------
// ログイン処理
//--------------------------------------------------------
//ユーザ認証
router.post('/',[check('nickName', 'ニックネームを入力して下さい').not().isEmpty().trim().escape().customSanitizer(value => {
// MongoDB Operator Injectionを防ぐために、ユーザー提供のデータをサニタイズします
value = value.replace(/[$.]/g, "");
return value;
}),
check('passCode', 'パスコードを入力して下さい').not().isEmpty().trim().escape(),
], (req, res) => {

    var errors = validationResult(req);
    //検証エラー
    if (!errors.isEmpty()) {
        res.render('register.ejs',{data: req.body, errors: errors.mapped() });
    }

    const  nickName  =  sanitize(req.body.nickName);
    const  passcode  =  sanitize(req.body.passCode);

    User.findOne({nickName}, (err, user)=>{
    if (err) return  res.status(500).send(err);

    if (!user) return  res.render('login.ejs',  {error: 'ユーザーが見つかりません', errors:false});

    //パスコードチェック
    bcrypt.compare(passcode, user.passCode, function(err, result) {

    if(!result) return  res.render('login.ejs',  {error: 'パスコードが違います', errors:false});
    //JSON Webトークンを生成する,トークンの有効期限を15分に設定
    const  expiresIn  =  900;
    const  accessToken  =  jsonwebtoken.sign({id : user._id, name :  user.nickName }, config.secret, {
        expiresIn:  expiresIn
    });
    //トークンをクッキーに保存する
    res.cookie('auth',accessToken , { maxAge: 900000, httpOnly: true });
    MyID = user._id;
    MyName = user.nickName;

    res.redirect('/home');
});

});
});

module.exports = router;

login.jsは冒頭の宣言部分と、クライアントからのGET処理とPOST処理の3パートで構成されています。

GET処理ではクッキーをクリアして、res.renderでログインフォームをレンダリングしています。それだけです。

POSTの方は実際のログイン処理を実施しています。
router.post( URL、処理1、処理2、処理3・・・)
という感じでポスト後の処理を書いています。
まずはニックネームとパスワードの未入力をチェックしたあと、実際のDBへ接続してユーザーの有無を問い合わせています。DBへの接続についてはいわゆるSQLインジェクションという攻撃への備えが必要です。mongodbはSQLデーターベースではありませんが、やはり検索文字列に特殊なコードを入れると悪意のあるコードが実行されてしまいます。このためニックネームとパスワードについてはサニタイズ処理をしています。これはmongo-sanitizeというパッケージを利用しています。

参考サイト: HACKING NODEJS AND MONGODB

ニックネームとパスワードが一致すると、ユーザーIDとNameをセットしたクッキーが発行されます。これ以降はこのクッキーが認証済みの証となります。
ということで、クッキーが改ざんされても判別できるようにここではJson Web Token という仕様を使ってクッキーを署名付きにします。

参考記事: NodeJS + MongoDB - Simple API for Authentication, Registration and User Management
参考記事: JSON Web Token の効用

login.ejs ログイン画面
login.ejs
<!doctype html>




Login








Chat App




<% if (error) { %>
<%= error %>

<% } %>
<% if (errors.nickName) { %>
<%= errors.nickName.msg %>

<% } %>
<% if (errors.passCode) { %>
<%= errors.passCode.msg %>

<% } %>


Log in
Create an account







 今回は画面をレスポンシブにするためにbootstrapのグリッドシステムを利用しています。
 このシステムは全体を12の列に分け、画面の解像度に応じて利用する列数を変更することで、一定の見栄えを維持するものです。
 LINE風ということで、スマホの縦長画面をイメージしたいので、PC画面やタブレット画面では左右にマージンを置きたいと考えました。本来であればoffset指定でできるはずですが、上手くいかなかったので、空のカラムdivを挟んであります。
 また、スマホなどの高解像度換算表示をさせないために、メタタグとしてwidth=device-widthを指定しています。

参考サイト: Bootstrap3の使い方

register.js ユーザー登録処理
ログイン画面から「Create an account」を選択すると表示されます。

register.js
var express = require('express');
var router = express.Router();
var { check, validationResult } = require('express-validator');
var db = require('../helper/db');
var User = db.User;
const userService = require('../models/user.service');

//--------------------------------------------------------
// ユーザー登録画面の表示
//--------------------------------------------------------
/* GET home page. */
router.get('/', function(req, res, next) {
res.clearCookie("auth");
res.render('register', {data: req.body, error: false, errors:false});
});

//--------------------------------------------------------
// ユーザー登録処理
//--------------------------------------------------------
router.post('/', [
check('nickName', 'Nick name は英文字のみです').not().isEmpty().isAlpha().trim().escape(),
check('passCode', 'Pass code は数字のみです').not().isEmpty().isAlphanumeric().trim().escape(),
check('nickName', 'Nick name は4文字以上12文字までです.').not().isEmpty().isLength({min: 4, max: 12}).trim().escape(),
check('passCode', 'Pass code は6文字以上12文字までです.').not().isEmpty().isLength({min: 6, max: 12}).trim().escape(),
check('nickName').custom(value => {
// MongoDB Operator Injectionを防ぐために、ユーザー提供のデータをサニタイズします
value = value.replace(/[$.]/g, "");

//ニックネームを検証する
return User.findOne({'nickName': value}).then(user => {
  if (user) {
    return Promise.reject(user['nickName'] + 'さんは登録済みです.');
  }
});

})
], (req, res) => {

  var errors = validationResult(req);
//検証エラー

if (!errors.isEmpty()) {
res.render('register.ejs',{data: req.body, errors: errors.mapped() });
}else{
//エラーなし, データベースにユーザー情報を保存する

userService.create(req.body);

    res.redirect('/');

}

});

module.exports = router;

 ここもGET処理は単にユーザー登録画面をレンダリングするだけです。POST処理でユーザー登録をしています。
 この際にexpress-validatorを使って文字種別や文字長の検査をしています。そしてDBを確認して既登録がなければ登録を行います。
 実際の登録はuserService = require('../models/user.service');で指定されたソースで行っています。

共通関数:ユーザー登録処理(user.service.js)
/models/user.service.js
// ユーザー登録操作
var bcrypt = require('bcryptjs');

const db = require('../helper/db');
const User = db.User;
const saltRounds = 10;

module.exports = {
create
};

// ユーザーモデルを作成し、データベースに保存する
function create(userParam) {

const user = new User();
user.nickName = userParam.nickName;
// 画像はオプションです。デフォルト画像を使用して選択されていません
if(userParam.ufile){
    user.userImage=userParam.ufile;

}
if (userParam.passCode) {
    // ハッシュパスコードを保存する
    user.passCode = bcrypt.hashSync(userParam.passCode, bcrypt.genSaltSync(saltRounds));
}
// save user
user.save();

}
パスワードはbcryptを使って10多重でハッシュ化しています。

参考記事: BCryptのすすめ
参考サイト: 本当は怖いパスワードの話 (1/4)

register.ejs ユーザー登録画面
register.ejs
<!DOCTYPE html>




Register










Chat App



                <div class="row">
                    <div class="                  col-md-2 col-lg-3"></div>
                    <div class="col-xs-8 col-sm-8 col-md-5 col-lg-4">
                        <label for="nickName" class="col-form-label">NickName</label>
                        <input type="text" class="form-control" id="nickName" name ="nickName" placeholder="Nickname"  maxlength="12" required>
                        <% if (errors.nickName) { %>
                        <div class="text-danger"><%= errors.nickName.msg %></div>
                        <% } %>
                        <label for="passCode" class="col-form-label">PassCode</label>
                        <input type="password" class="form-control" id="passCode" name="passCode" placeholder="Passcode" maxlength="12" required>
                        <% if (errors.passCode) { %>
                        <div class="text-danger"><%= errors.passCode.msg %></div>
                        <% } %>
                    </div>

                    <div class="col-xs-4 col-sm-4 col-md-3 col-lg-2">
                        <label for="FaceIcon" class="col-form-label">FaceIcon</label>
                        <img src="/images/defaultFace.png" class="image" id="image-frame" height="50pv" width="50pv"/>
                        <input id="imageFile" type="file" style="visibility:hidden" name="imageFile"/>
                        <input type="button" style="width: 100%;" value="Change" onclick="$('#imageFile').click();" class="btn" name="imagePath"/>
                        <input id="b64" name="ufile" type="hidden" value=""/>
                        <div class="text-danger" id="error"></div>
                    </div>
                </div>

                <div class="row">
                    <div class="                    col-md-2 col-lg-3"></div>
                    <div class="col-xs-12 col-sm-12 col-md-8 col-lg-6">
                        </br>
                        <button type="submit" style="width: 100%;" class="btn rgst">Create</button>
                    </div>
                </div>
            </div>
        </div>
    </form>
</body>
<script>

/*$("#imageFile").change(function(){
readURL(this);
});
function readURL(input) {
if (input.files && input.files[0]) {
var reader = new FileReader();

        reader.onload = function (e) {
        console.log(e.target.result);
            $('#imgsrc').attr('src', e.target.result);
        }

        reader.readAsDataURL(input.files[0]);
    }
}*/
showImage(true);
var targetfile = null;
$("#imageFile").onchange = function(evt){
$("#error").innerHTML = '';
    showImage(true);
    var files = evt.target.files;
    if(files.length == 0) return;
    targetFile = files[0];
    console.log(targetFile);
    if(!targetFile.type.match(/image/)) {
    $("#error").innerHTML ='Select Image File';
        return;
    }

    if(targetFile.size > 35000){
    $("#error").innerHTML ='Image file size should be less than 35KB';
        return;
    }
    var breader = new FileReader();
    breader.onload = readPNGFile;
    breader.readAsBinaryString(targetFile);
}

function readPNGFile(evt) {
    var bin = evt.target.result;
    var sig = String.fromCharCode( 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a);
    var head = bin.substr(0, 8);
    if(sig != head) {
        $("#error").innerHTML ="Image file type should be PNG";
        return;
    }
    showImage(true);
    var width = getBinValue(bin, 8 + 0x08, 4);
    var height = getBinValue(bin, 8 + 0x0c, 4);
    var depth = bin.charCodeAt(8 + 0x10);

    /*$("#info").innerHTML =
        "width: " + width + "px<br>" +
        "height: " + height + "px<br>" +
        "depth: " + depth + "bit";*/
    var reader = new FileReader();
    reader.onload = function(e) {
    console.log(reader);
        $("#image-frame").src = reader.result;
        $("#b64").value=reader.result;
    }
    reader.readAsDataURL(targetFile)
}

function getBinValue(bin, i, size) {
    var v = 0;
    for(var j= 0; j < size; j++){
        var b = bin.charCodeAt(i + j);
        v = (v << 8) + b;
    }
    return v;
}

function showImage(b) {
    var val = b ? "block" : "none";
    //$("#upbtn").style.display = val;
    console.log("val",val);
    $("#image-frame").style.display = val;
    //$("#info").style.display = val;
}

function $(id) {
    return document.querySelector(id);
}



ここではpngファイルの選択と表示を行っています。

参考サイト: HTML5のFile APIでローカルファイル情報取得してやんよ!!!

list.js 友達リスト処理
list.js
var express = require('express');
var router = express.Router();
var db = require('../helper/db');
var User = db.User;
const verifyToken = require('../helper/VerifyToken');

//--------------------------------------------------------
// 友達リスト画面の出力
//--------------------------------------------------------
router.get('/',verifyToken, function(req, res, next) {
var users = [];
// ログインしたユーザーを除くすべての登録ユーザーをデータベースから取得し、ユーザー名とプロファイル画像のjsonオブジェクトを作成します
User.find({ nickName: {$ne: req.name}}).stream().on('data', function(doc) {
var base64Data;

  if(doc.userImage !== undefined){

    base64Data = doc.userImage.replace(/^data:image\/png;base64,/, "")

  }
    users.push({id : doc._id, nickName : doc.nickName, userImage: base64Data });

 }).on('error', function(err){
    res.send(err);
})
.on('end', function(){

res.render('list.ejs', {listUsers : users });

});

});

module.exports = router;
データベースから自分以外の友達を検索し、その一覧を引数としてリスト画面のレンダリング処理を呼び出しています。

ちなみに、冒頭のrouter.get('/',verifyToken, function(req, res, next) { に書かれている verifyTokenが前述のJson Web Tokenの検証処理です。

共通関数:JSON Web Tokenの処理(VerifyToken.js)
/helper/VerifyToken.js
// Json Web Tokenのチェック処理
var jwt = require('jsonwebtoken');
const config = require('../config');
var db = require('../helper/db');
var Waste = db.Waste;

function verifyToken(req, res, next) {
var token = req.cookies.auth;
if (!token) {
return res.status(403).render('errorpage.ejs', { error: '15分間無操作のためログアウトしました', errors: false });
}

// 破棄済みトークンを検索
Waste.findOne({'cookie': token}).then(data => {
    if (data) {
        return res.status(403).render('errorpage.ejs', { error: '不正なトークンです', errors: false });
    }
});

jwt.verify(token, config.secret, function (err, decoded) {
    if (err) {
        return res.status(500).render('errorpage.ejs', { error: 'Failed to authenticate token.', errors: false });
    } else {
        // すべてうまくいけば、他のルートで使用するために保存して次へ
        req.name = decoded.name;
        req.id = decoded.id;
        next();
    }
});

}

module.exports = verifyToken;
 JWTが期限切れ(15分)もしくは不正なトークンの場合はエラーを返しています。
 また、ここではログアウト時に登録された破棄済みのクッキーと照合することによりクッキーの使い回しを防御しています。

最後に
いかがだったでしょうか。ちょっと前回から間が空いてしまいましたが、一応チャットサイトとして使えるようになったかと思います。パスワード変更ができないとか、エラー処理が中途半端とか、ログ機能がないとか、色々と細かくは実装していませんが友人同士での遊び程度では利用できるかと思っています。

Special Thanks
今回のソースはRupaliさんの支援を受けています。ありがとうございます。

次回は...
 次回はこのチャットサイトからGoogle Dialogflowを呼び出して、本格的なチャットボットを実現してみたいと思います。
 最終的には自然言語処理や画像認識、ブロックチェーンなんかにも足を伸ばせればと思ってます。

記事一覧

WEBでLINE風のチャットサイトを作る-その1
WEBでLINE風のチャットサイトを作る-その2

nstshirotays
NTTデータシステム技術 デジタル技術推進室の城田です。よろしくお願いいたします。
http://www.nttdst.com/employee/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした