0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-05-26
[前回] Web3.0検証(16)-MeteorでTODO管理アプリの開発(タスク絞込み機能とUI改善)

はじめに

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つのコンポーネント、formfilterlist<Fragment>でラップしてから返す
      • Fragmentとは
        • Reactで複数コンポーネントをrender()関数で返す場合、1つのタグにまとめる必要あり
        • グループ化のため他の余分なタグを使用すると、不本意に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>
... ... 

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

  • 作業内容

    • ログインフォームのスタイルを設定
      • labelinputbuttonなどのタグを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 image.png

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

おわりに

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

[次回] Web3.0検証(18)-MeteorでTODO管理アプリの開発(Githubアカウントでログイン)
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?