Web3.0検証(17)-MeteorでTODO管理アプリの開発(ユーザーアカウント機能)

##### [前回] [Web3.0検証(16)-MeteorでTODO管理アプリの開発(タスク絞込み機能とUI改善)](https://qiita.com/mingchun_zhao/items/c857e5313b6eb547f440)

## はじめに

TODO管理アプリに、ユーザーアカウント機能を追加します。
ログイン画面やパスワード認証も実装します。

## 準備

- 端末を二つ用意します。
    - 端末1: インストール/起動/確認作業
    - 端末2: ソースコード改編

- 端末1から、Meteorアプリを実行
```
$ cd simple-todos-react
$ meteor run
```

- ブラウザでアプリを確認
    - `http://localhost:3000/`にアクセス
    - デベロッパーツールを開く(F12)
    - `デバイスのツールバーを切り替え`アイコンをクリック
    - デバイスを選択

## パスワード認証によるユーザーログイン

- 作業内容
    - Meteorには、基本認証およびアカウント管理システムが内蔵されている
    - `accounts-password`を追加するだけで、ユーザー名とパスワードによる認証を使用できる(すごい)
    - Facebook、Google、GitHubなどを使用したログイン認証も可能


- 端末1から、必要モジュールをインストール
```
$ meteor add accounts-password
$ meteor add accounts-base
$ meteor npm install --save bcrypt
```

- ※ `npm`でなく`meteor npm`を使用することで、常に同じnpmバージョンを担保
    - 異なるnpmバージョンを用いてインストールされた、モジュール不一致による予想外問題を回避

## ユーザーアカウントを作成

- 作業内容
    - アプリのデフォルトユーザーを作成
        - ユーザー名: `meteorite`
        - パスワード: `password`
    - データベースにユーザーが存在するかチェックし
        - 見つからなかった場合、新しいユーザーを作成

- 端末2から、コード修正
```
$ cd simple-todos-react
$ vi server/main.js
```
```server/main.js
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { TasksCollection } from '/imports/api/TasksCollection';

... ...

const SEED_USERNAME = 'meteorite';
const SEED_PASSWORD = 'password';

Meteor.startup(() => {
  if (!Accounts.findUserByUsername(SEED_USERNAME)) {
    Accounts.createUser({
      username: SEED_USERNAME,
      password: SEED_PASSWORD,
    });
  }
  ... ...
});
```

## ログインフォーム

- 作業内容
    - フォームを作成し、ユーザーが認証情報を入力可能に
    - `useState`フックを使用し実装
    - `LoginForm.jsx`という名前の新しいファイルを作成し、フォーム追加
    - `Meteor.loginWithPassword(username、password);`関数を使用し認証を行う

- コード
```imports/ui/LoginForm.jsx
import { Meteor } from 'meteor/meteor';
import React, { useState } from 'react';

export const LoginForm = () => {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const submit = e => {
    e.preventDefault();

    Meteor.loginWithPassword(username, password);
  };

  return (
    <form onSubmit={submit} className="login-form">
      <label htmlFor="username">Username</label>

      <input
        type="text"
        placeholder="Username"
        name="username"
        required
        onChange={e => setUsername(e.target.value)}
      />

      <label htmlFor="password">Password</label>

      <input
        type="password"
        placeholder="Password"
        name="password"
        required
        onChange={e => setPassword(e.target.value)}
      />

      <button type="submit">Log In</button>
    </form>
  );
};
```

## 認証処理

- 作業内容
    - 認証されたユーザーのみ、タスク管理機能にアクセスできるようにする
    - 認証ユーザーが存在しない場合
        - `LoginForm`コンポーネントを返す
    - 認証ユーザーが存在する場合
        - 3つのコンポーネント、`form`、`filter`、`list`を`<Fragment>`でラップしてから返す
        - Fragmentとは
            - Reactで複数コンポーネントをrender()関数で返す場合、１つのタグにまとめる必要あり
            - グループ化のため他の余分なタグを使用すると、不本意にUI(DOM)に影響する恐れあり
            - Fragmentタグを使用することで、複数コンポーネントをグループ化可能、かつUIに影響しない
    - 認証されたユーザーの取得
        - `Meteor.user()`を`useTracker`フックでラップし、リアクティブ化

- コード
```imports/ui/App.jsx
import { Meteor } from 'meteor/meteor';
import React, { useState, Fragment } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import { TasksCollection } from '/imports/api/TasksCollection';
import { Task } from './Task';
import { TaskForm } from './TaskForm';
import { LoginForm } from './LoginForm';

... ...
export const App = () => {
  const user = useTracker(() => Meteor.user());

  ... ...
  return (
      ... ...
      <div className="main">
        {user ? (
          <Fragment>
            <TaskForm />

            <div className="filter">
              <button onClick={() => setHideCompleted(!hideCompleted)}>
                {hideCompleted ? 'Show All' : 'Hide Completed'}
              </button>
            </div>

            <ul className="tasks">
              {tasks.map(task => (
                <Task
                  key={task._id}
                  task={task}
                  onCheckboxClick={toggleChecked}
                  onDeleteClick={deleteTask}
                />
              ))}
            </ul>
          </Fragment>
        ) : (
          <LoginForm />
        )}
      </div>
... ... 
```

## ログインフォームのスタイル

- 作業内容
    - ログインフォームのスタイルを設定
        - `label`、`input`、`button`などのタグを`div`でラップ
            - CSSで制御しやすくする

- コード
```imports/ui/LoginForm.jsx
<form onSubmit={submit} className="login-form">
  <div>
    <label htmlFor="username">Username</label>

    <input
      type="text"
      placeholder="Username"
      name="username"
      required
      onChange={(e) => setUsername(e.target.value)}
    />
  </div>

  <div>
    <label htmlFor="password">Password</label>

    <input
      type="password"
      placeholder="Password"
      name="password"
      required
      onChange={(e) => setPassword(e.target.value)}
    />
  </div>

  <div>
    <button type="submit">Log In</button>
  </div>
</form>
```

- CSSを更新
```client/main.css
.login-form {
  display: flex;
  flex-direction: column;
  height: 100%;

  justify-content: center;
  align-items: center;
}

.login-form > div {
  margin: 8px;
}

.login-form > div > label {
  font-weight: bold;
}

.login-form > div  > input {
  flex-grow: 1;
  box-sizing: border-box;
  padding: 10px 6px;
  background: transparent;
  border: 1px solid #aaa;
  width: 100%;
  font-size: 1em;
  margin-right: 16px;
  margin-top: 4px;
}

.login-form > div > input:focus {
  outline: 0;
}

.login-form > div > button {
  background-color: #62807e;
}
```

これで、ログインフォームが一元化され、かっこよく見えます。

## サーバーの起動

- 作業内容
    - すべてのタスクに所有者を付与
    - `db.tasks.remove({});`を実行し、データベースのすべてのタスクを削除
    - `server/main.js`を変更し、`meteorite`ユーザーを所有者とするシードタスクを追加
        - insertされるレコードのフィールド
            - `text`: タスク内容
            - `userId`: userの`_id`値
            - `createdAt`: 日付
    - 変更後、meteorを再起動
        - `Meteor.startup`ブロックを再実行するため

- コード
```server/main.js
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { TasksCollection } from '/imports/api/TasksCollection';

const insertTask = (taskText, user) =>
  TasksCollection.insert({
    text: taskText,
    userId: user._id,
    createdAt: new Date(),
  });

const SEED_USERNAME = 'meteorite';
const SEED_PASSWORD = 'password';

Meteor.startup(() => {
  if (!Accounts.findUserByUsername(SEED_USERNAME)) {
    Accounts.createUser({
      username: SEED_USERNAME,
      password: SEED_PASSWORD,
    });
  }

  const user = Accounts.findUserByUsername(SEED_USERNAME);

  if (TasksCollection.find().count() === 0) {
    [
      'First Task',
      'Second Task',
      'Third Task',
      'Fourth Task',
      'Fifth Task',
      'Sixth Task',
      'Seventh Task',
    ].forEach(taskText => insertTask(taskText, user));
  }
});
```

## 所有者によるタスクの絞込み

- 作業内容
    - ログイン済みユーザーが所有するタスクのみ絞り込む
        - Mini Mongoからタスク取得時、userの`_id`値を持つ`userId`フィールドをMongoセレクターに追加

- コード
```imports/ui/App.jsx
... ...
    const hideCompletedFilter = { isChecked: { $ne: true } };

    const userFilter = user ? { userId: user._id } : {};

    const pendingOnlyFilter = { ...hideCompletedFilter, ...userFilter };

    const tasks = useTracker(() => {
      if (!user) {
        return [];
      }

      return TasksCollection.find(
        hideCompleted ? pendingOnlyFilter : userFilter,
        {
          sort: { createdAt: -1 },
        }
      ).fetch();
    });

    const pendingTasksCount = useTracker(() => {
      if (!user) {
        return 0;
      }

      return TasksCollection.find(pendingOnlyFilter).count();
    });
... ...

    <TaskForm user={user} />
... ...
```

- insert処理を更新し、Taskフォームに`userId`フィールドを含める
    - Appコンポーネントから`TaskForm`にユーザーを渡す必要あり
- コード
```imports/ui/TaskForm.jsx
... ...
export const TaskForm = ({ user }) => {
  const [text, setText] = useState('');

  const handleSubmit = e => {
    e.preventDefault();

    if (!text) return;

    TasksCollection.insert({
      text: text.trim(),
      createdAt: new Date(),
      userId: user._id
    });

    setText('');
  };
... ...
```

## ログアウト

- 作業内容
    - アプリバーの下に所有者のユーザー名を表示
    - タスクをまとめて表示するため、`Fragment`開始タグの直後に新しいdivを含める
    - `onClick`ハンドラーを追加し、Meteor.logout()を呼び出すことでログアウト

- コード
```imports/ui/App.jsx
... ...
  const logout = () => Meteor.logout();

  return (
... ...
    <Fragment>
      <div className="user" onClick={logout}>
        {user.username} 🚪
      </div>
... ...
```

- ユーザー名のスタイル設定

```client/main.css
.user {
  display: flex;

  align-self: flex-end;

  margin: 8px 16px 0;
  font-weight: bold;
}
```


## ブラウザでアプリを確認

ユーザー名とパスワードを入力し、ログインします。
- ユーザー名: `meteorite`
- パスワード: `password`

|ログイン前|ログイン後|
|:--:|:--:|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/eb3b6271-5f0a-a394-e8d6-bb4f0c91d9c2.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/008ed36f-b349-9e07-753e-f0d4435c9f41.png)|

認証成功し、タスク一覧が表示されました。やったー。

## おわりに

今回は、ユーザーアカウントと認証機能を追加しました。
ユーザーとタスクの関連付けも行いました。
次回も続きます。お楽しみに。

##### [次回] [Web3.0検証(18)-MeteorでTODO管理アプリの開発(Githubアカウントでログイン)](https://qiita.com/mingchun_zhao/items/8bcfdcd53ed45c9d00b3)