Node.js(Express)×PostgressSQLを使ったチャットアプリ 3

## ユーザー機能の実装

### postgressにユーザー情報を保存するテーブルを作成

```sql
CREATE TABLE users (
 user_id SERIAL NOT NULL,
 user_name VARCHAR(255) NOT NULL,
 user_email VARCHAR(255) NOT NULL,
 user_password VARCHAR(255) NOT NULL,
 created_at TIMESTAMP NOT NULL,
 PRIMARY KEY (user_id)
);
```
誰がchannelを投稿したのか見られるように、``channel``と``message``のテーブルに``user_id``のカラムを追加します。

```sql
/*channelテーブル*/
ALTER TABLE channel ADD COLUMN user_id INTEGER;

/*messagesテーブル*/
ALTER TABLE messages ADD COLUMN user_id INTEGER;
```

## ユーザー登録機能

``app.js``にコードを追加します。

```js
// 中略
const message = require('./routes/message');
const signup = require('./routes/signup'); // 追加

const app = express();

// 中略
app.use('/message', message); 
app.use('/signup', signup); //追加

```

``routes/signup.js``を作成します。

```js
const express = require('express');
const router = express.Router();
const dayjs = require('dayjs');
const pool = require('../dbConnection');

router.get('/', (req, res, next) => {
  res.render('signup', {
    title: '新規会員登録'
  });
});

router.post('/', async (req, res, next) => {
  try {
    const userName = req.body.user_name;
    const email = req.body.email;
    const password = req.body.password;
    const createdAt = dayjs().format('YYYY-MM-DD HH:mm');

    const signupQuery = 
      'INSERT INTO users (user_name, user_email, user_password, created_at) VALUES ($1, $2, $3, $4)';

    // 既に登録されているメールアドレスかどうかを確認
    const emailExistsQuery = 'SELECT * FROM users WHERE user_email = $1 LIMIT 1';
    const emailExists = await pool.query(emailExistsQuery, [email]);

    if (emailExists.rows.length) {
      res.render('signup', {
        title: '新規会員登録',
        emailExists: '既に登録されているメールアドレスです'
      });
    } else {
      // ユーザーの新規登録
      await pool.query(signupQuery, [userName, email, password, createdAt]);
      res.redirect('/signup');
    }

  } catch (error) {
    console.error(error);
    res.status(500).send('内部サーバーエラー');
  }
});

module.exports = router;
```

``views/signup.ejs``を作成します。

```html
<!DOCTYPE html>
<html lang="ja">

  <head>
    <title><%= title %></title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
  </head>

<body>
  <div class="top_link">
    <a href="/" class="btn">← トップへもどる</a>
  </div>

  <p class="main-title"><%= title %></p>

  <% if (typeof emailExists !== 'undefined') { %>
    <p class="error"><%= emailExists %></p>
  <% } %>

  <form action="/signup" method="post" class="form">
    <div class="userName">
      <span class="label">userName</span>
      <input type="text" name="user_name" class="input" required>
    </div>

    <div class="email">
      <span class="label">Email</span>
      <input type="email" name="email" class="input" required>
    </div>

    <div class="password">
      <span class="label">password</span>
      <input type="password" name="password" class="input" required>
    </div>

    <div class="button-area">
      <button type="submit" class="submit">新規会員登録</button>
    </div>
  </form>
</body>
</html>
```

``/signup``にアクセスしてユーザー登録をしてみてください

http://localhost:3000/signup

![スクリーンショット 2023-12-21 2.06.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262417/32b90786-f957-da64-d9db-0dc6c89ba4c6.png)

![スクリーンショット 2023-12-21 2.12.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262417/b5c6cda8-1b55-be37-a22d-ad0bebce24e8.png)

すでに登録しているメールアドレスを登録してみてください
![スクリーンショット 2023-12-21 2.14.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262417/250468d2-132d-3f82-819e-ad99951eabcb.png)

エラーが表示されていればOKです

## passwordをhush値にしてデータベースに保存する

データベースに入力したままのpasswordを保存するのは良く無いのでモジュールを使ってpasswordをhush値にしてデータベースに登録します

```console
npm install --save bcrypt
```
``routes/signup.js``にコード追加

```js
const express = require('express');
const router = express.Router();
const dayjs = require('dayjs');
const bcrypt = require('bcrypt'); //追加
const pool = require('../dbConnection');

router.get('/', (req, res, next) => {
  res.render('signup', {
    title: '新規会員登録'
  });
});

router.post('/', async (req, res, next) => {
  try {
    const userName = req.body.user_name;
    const email = req.body.email;
    const password = req.body.password;
    const createdAt = dayjs().format('YYYY-MM-DD HH:mm:ss');

    const signupQuery = 'INSERT INTO users (user_name, user_email, user_password, created_at) VALUES ($1, $2, $3, $4)';

    // 既に登録されているメールアドレスかどうかを確認
    const emailExistsQuery = 'SELECT * FROM users WHERE user_email = $1 LIMIT 1';
    const emailExists = await pool.query(emailExistsQuery, [email]);

    if (emailExists.rows.length) {
      res.render('signup', {
        title: '新規会員登録',
        emailExists: '既に登録されているメールアドレスです'
      });
    } else {
      // パスワードのハッシュ化
      const hashedPassword = await bcrypt.hash(password, 10);

      // ユーザーの新規登録
      //password → hashedPasswordに変更
      await pool.query(signupQuery, [userName, email, hashedPassword, createdAt]);

      res.redirect('/signup');
    }

  } catch (error) {
    console.error(error);
    res.status(500).send('内部サーバーエラー');
  }
});

module.exports = router;
```
![スクリーンショット 2023-12-21 2.23.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262417/fe3725e1-befc-4e73-35f8-b390966fc5ca.png)

こうなってればOK

## ログイン機能の実装
ログイン情報を保持する為に必要なモジュールをインストールします

```console
npm install --save express-session
```

``app.js``を書き換えます

```js
// 中略
const logger = require('morgan');
const session = require('express-session'); // 追加

const indexRouter = require('./routes/index');
const message = require('./routes/message');
const signup = require('./routes/signup');
const login = require('./routes/login'); // 追加

// 中略
app.use(express.static(path.join(__dirname, 'public')));

// 追加　session
app.use(session({
  secret: 'keyboard cat',
  resave: false,
  saveUninitialized: true
}));

app.use('/', indexRouter);
app.use('/message', message);
app.use('/signup', signup);
app.use('/login', login); // 追加
// 中略
```

``routes/login.js``を作成します。

```js
const express = require('express');
const router = express.Router();
const bcrypt = require('bcrypt');
const pool = require('../dbConnection');

router.get('/', function (req, res, next) {
  if (req.session.user_id) {
    res.redirect('/');
  } else {
    res.render('login', {
      title: 'ログイン'
    });
  }
});

router.post('/', async function (req, res, next) {
  try {
    const email = req.body.email;
    const password = req.body.password;

    const query = 'SELECT user_id, user_password FROM users WHERE user_email = $1 LIMIT 1';

    const result = await pool.query(query, [email]);

    if (result.rows.length > 0) {
      const user = result.rows[0];
      const userId = user.user_id;

      // パスワードの比較
      const isPasswordValid = await bcrypt.compare(password, user.user_password);

      if (isPasswordValid) {
        req.session.user_id = userId;
        res.redirect('/');
        return; // ログイン成功時に関数を終了
      }
    }

    // 行が見つからないか、パスワードが無効な場合のエラーメッセージ
    res.render('login', {
      title: 'ログイン',
      noUser: 'メールアドレスとパスワードが一致するユーザーはいません'
    });

  } catch (error) {
    console.error(error);
    res.status(500).send('内部サーバーエラー');
  }
});

module.exports = router;
```

``views/login.ejs``を作成します。

```html
<!DOCTYPE html>
<html lang="ja">

  <head>
    <title><%= title %></title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
  </head>
  
  <body>

    <div class="top_link">
      <a href="/" class="btn">← トップへもどる</a>
    </div>

    <p class="main-title"><%= title %></p>

    <% if (typeof noUser !== 'undefined') { %>
      <p class="error"><%= noUser %></p>
    <% } %>

    <form action="/login" method="post" class="form">

      <div>
        <span class="label">Eメール</span>
        <input type="email" name="email" class="input" required>
      </div>

      <div>
        <span class="label">パスワード</span>
        <input type="password" name="password" class="input" required>
      <div>

      <div class="button-area">
        <button type="submit" class="submit">ログイン</button>
      </div>

    </form>
  </body>
</html>
```
http://localhost:3000/login

ユーザー登録画面で登録したユーザーでログインしてみてください。
エラーが出ずにログインできたらOKです

## ログインしているユーザーを表示させる

``app.js``を書き換える

```js
// 中略
const login = require('./routes/login');
const setUser = require('./routes/setUser'); // 追加

const app = express();
// 中略

app.use('/', setUser, indexRouter); // 変更
app.use('/users', usersRouter);
app.use('/message', setUser, message); // 変更
app.use('/signup', signup);
app.use('/login', login);
// 中略
```

``routes/setUser.js``を作成します

```js
const pool = require('../dbConnection');

module.exports = async (req, res, next) => {
  const userId = req.session.user_id;

  if (userId) {
    try {
      const query = 'SELECT user_id, user_name FROM users WHERE user_id = $1';
      const result = await pool.query(query, [userId]);

      if (result.rows.length) {
        res.locals.user = result.rows[0];
      }
    } catch (error) {
      console.error(error);
    }
  }
  next();
};
```
``views/index.ejs``を書き換えます

```html
<% if (typeof user !=='undefined' ) { %>
  <span class="login-user">
    <%= user.user_name %>さんとしてログインしています
  </span>
<% } %>
```

``views/message.ejs``を書き換えます

```html
<% if (typeof user !=='undefined' ) { %>
  <span class="login-user">
    <%= user.user_name %>さんとしてログインしています
  </span>
<% } %>
```

## ログアウト機能を実装

app.jsを変更します

```js
// 中略
var login = require('./routes/login');
var logout = require('./routes/logout'); // 追加
var setUser = require('./routes/setUser');

const app = express();
// 中略

app.use('/login', login);
app.use('/logout', logout); // 追加
```

``routes/logout.js``を作成します

```js
const express = require('express');
const router = express.Router();

router.get('/', (req, res, next) => {
  req.session.destroy();
  res.redirect('/login');
});

module.exports = router;
```

http://localhost:3000/logout

ログアウトされていればOKです

## ユーザー登録ボタン、ログインボタン、ログアウトボタンを作成する

自走課題です、難しければDiscordで質問してください。

条件
・　ログインしていない状態の時は、``ユーザー登録ボタン``と``ログインボタン``の表示
・　ログインしている状態の時は、``ユーザー登録ボタン``と``ログアウトボタン``の表示