Monaca + NCMBでTwitter風アプリを作る（その2：認証の実装）

NCMBでは公式SDKとしてSwift/Objective-C/Kotlin/Java/Unity/JavaScript SDKを用意しています。また、それ以外にもコミュニティSDKとして、非公式ながらFlutter/React Native/Google Apps Script/C#/Ruby/Python/PHPなど幅広い言語向けにSDKが開発されています。

今回は公式SDKの一つ、JavaScript SDKとFramework7を使ってTwitter風のアプリを作ってみます。[前回は画面の仕様とSDKの初期化について解説しました](https://qiita.com/goofmint/items/28912497257bd45881ff)ので、今回はログイン周りの処理を解説します。

## **途中のコード**

まだ完成にはほど遠いですが、プロフィールとフォロー/フォロワーの仕組みまで実装した分を [NCMBMania/monaca-social-app](https://github.com/NCMBMania/monaca-social-app) にアップロードしてあります。

## **利用技術について**

今回は次のような組み合わせになっています。

- Monaca
- Framework7
- NCMB

## 認証を実装する箇所

前回の記事で `/js/routes.js` にて `beforeEnter` という関数を定義していました。この関数はルーティングでその画面に移動する前に実行されます。そして、何らかの条件（今回は認証状態）によって別な画面に飛ばしたり、あらかじめデータを準備したりできます。

`beforeEnter` は以下のように変数を受け取ります。

```jsx
const beforeEnter = async ({resolve, reject, router}) => {
};
```

そのまま画面遷移して良い場合は `resolve()` を呼び、リダイレクトなど別な画面への移動を行う場合には `reject()` を実行します。実際の画面遷移は `router` で指定します。

## beforeEnterでの処理

beforeEnterではNCMBの認証状態をチェックします。もしログイン前だったり、ログイン情報はあるもののセッションの有効期限が切れている場合には認証処理を行います。

`openLoginScreen` がユーザー登録&ログイン処理を行うのですが、ログイン画面が消えるまでをawaitで待っています。こうすることで、リダイレクトのような処理もなく、本来表示したかった画面を表示するだけになります。

```jsx
// 認証が必要な画面にアクセスした時の処理
const beforeEnter = async ({resolve, reject, router}) => {
  // 現在ログインしているユーザー情報を取得
	const currentUser = ncmb.User.getCurrentUser();
	// 認証していない場合はnullになる
	if (!currentUser) {
    // 認証画面を出して、処理が完了するのを待つ
    await openLoginScreen();
    // resolveを実行（元々指定されている画面を表示）
    resolve();
	}
	try {
		// データストアにダミーアクセス
		await ncmb.DataStore('Hello').fetch();
		// アクセスできればresolveを実行（元々指定されている画面を表示）
		const userProfile = await setUserProfile();
		app.store.dispatch('setProfile', userProfile);
		resolve();
	} catch (e) {
		// 駄目だった場合は認証情報を削除
		localStorage.removeItem("NCMB/"+ncmb.apikey+"/currentUser");
		// 認証画面を出して、処理が完了するのを待つ
		await openLoginScreen();
		// resolveを実行（元々指定されている画面を表示）
		resolve();
	}
};
```

![0934 localhost - 0318220034.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/53ad3a64-a916-4a8a-ac32-6be556bd6696.jpeg)


## openLoginScreenの処理

`openLoginScreen` ではログイン画面の表示と、ログイン画面のイベント処理を行っています。アプリが立ち上がった直後は `app.loginScreen` がないので、それができるまで処理を待ちます。

ログインボタンを押したタイミングで `loginOrSignup` を実行し、その結果がうまくいったらログイン画面を閉じて処理を完了します。

```jsx
// ログイン画面を表示する処理
const openLoginScreen = () => {
  return new Promise((res, rej) => {
    const id = setInterval(() => {
      // ログイン画面の準備ができるのを待つ
      if (!app.loginScreen) {
        return;
      }
      // 準備できていたらインターバル処理を止める
      clearInterval(id);
      // ログイン画面を表示
      app.loginScreen.open('#login-screen');
      // ログイン画面が開いた時の処理
      $('.login-screen').on('loginscreen:opened', (_) => {
        $('.login-screen .login-button').on('click', async (e) => {
          e.preventDefault();
          try {
            // ログイン&新規登録処理
            await loginOrSignup();
            // ログイン画面を閉じる
            app.loginScreen.close();
            // 自分のプロフィールを取得する
            const userProfile = await setUserProfile();
            // ストアに保存
            app.store.dispatch('setProfile', userProfile);
            res();
          } catch (_) {
            // ログインに失敗した場合はエラーメッセージを表示
            app.toast.create({
              text: 'ログインに失敗しました',
              position: 'center',
              closeTimeout: 2000,
            }).open();
          }
        });
      });
    }, 100);
  });
};
```

## loginOrSignupの処理

`loginOrSignup`はNCMBの認証処理になります。最初にユーザー登録処理を行い、その結果いかんに関わらずログイン処理を行います。もしすでにユーザーがいる場合にはユーザー登録処理はエラーになりますが、そのままログインを行うことでユーザー登録とログイン処理を一つの処理でできます。

今回はユーザーIDとパスワードを認証に利用しています。

```jsx
// ログイン&新規登録処理
const loginOrSignup = async () => {
  // フォームからユーザー名とパスワードを取得
  const params = app.form.convertToData($('.login-screen #login-form'));
  try {
    // ユーザー登録
    const user = new ncmb.User;
    await user
      .set("userName", params.username)
      .set("password", params.password)
      .signUpByAccount();
  } catch (_) {
    // 失敗した場合はすでにユーザーが存在する場合
  }
  // ログイン処理
  await ncmb.User.login(params.username, params.password);
  return true;
};
```

## プロフィールの作成 or 取得

認証で利用するユーザークラス（Userクラス）は、基本的に自分しか閲覧や更新ができない方が理想です。他のユーザーが見る情報は別途UserProfileクラスにまとめておきます。このための関数 `setUserProfile` は最終的にユーザープロフィールのインスタンスを返却します。

```jsx
// 自分のプロフィールを取得、なければ作成する
const setUserProfile = async () => {
  // 自分のプロフィールを取得
  const user = ncmb.User.getCurrentUser();
  const UserProfile = ncmb.DataStore('UserProfile');
  let userProfile = await UserProfile
    .equalTo('user', {
      __type: 'Pointer',
      className: 'user',
      objectId: user.objectId,
    })
    .fetch();
  // プロフィールがあれば終了
  if (userProfile.objectId) {
    return userProfile;
  }
  // なければデフォルトのプロフィールを作成
  userProfile = new UserProfile();
  await userProfile
    .set('user', user)
    .set('userName', user.userName)
    .set('image', '/assets/images/default.png')
    .set('profile', '')
    .set('posts', 0)
    .set('followers_count', 0)
    .set('followings_count', 0)
    .set('acl', getDefaultAcl())
    .save();
  // フォロー情報を作成
  setFollow(userProfile);
  return userProfile;
};
```

`getDefaultAcl` はこのアプリ全体のデフォルトのACL（アクセス権限）を返す関数です。誰でも閲覧可能、ユーザー自身と管理者ロール（admin）にしか編集できないデータとして定義してあります。これはスクリプトなどからデータ更新を行う際を考慮しています。

```javascript
// デフォルトのACLを作成
const getDefaultAcl = () => {
  const user = ncmb.User.getCurrentUser();
  const acl = new ncmb.Acl;
  acl
    .setPublicReadAccess(true)
    .setUserWriteAccess(user, true)
    .setRoleWriteAccess('admin', true);
  return acl;
};
```

### フォロー情報の作成

誰かをフォローした際の情報はデータストアのFollowクラスに保存します。このためのデフォルトデータを `setFollow` 関数にて作成します。最後にストアに保存します（ストアについては下記参照）。

```javascript
// フォロー情報を作成
const setFollow = async (userProfile) => {
  const Follow = ncmb.DataStore('Follow');
  const follow = new Follow();
  await follow
    .set('profile', userProfile)
    .set('follows', [])
    .set('acl', getDefaultAcl())
    .save();
  app.store.dispatch('setFollow', follow);
};

```

## プロフィールの保存

プロフィールはFramework7のストアに保存します。ストアはアプリ全体で利用する変数として考えれば良いでしょう。ストアの情報を変更すると、利用している画面にも反映されます。

ストアは `js/store.js` にて定義します。

```jsx
const createStore = Framework7.createStore;
const store = createStore({
  state: {
    // 自分のプロフィールオブジェクト
    profile: null,
    // フォロー情報のオブジェクト
    follow: null,
  },
  getters: {
    profile({ state }) {
      return state.profile;
    },
    follow({ state }) {
      return state.follow;
    },
  },
  actions: {
    setProfile({ state }, profile) {
      state.profile = profile;
    },
    setFollow({ state }, follow) {
      state.follow = follow;
    },
  },
})
```

このように定義しておくと、以下のようにしてデータを変更できます。

```jsx
// ストアに保存
app.store.dispatch('setProfile', userProfile);
```

## ログアウト

認証した後はホーム画面 `pages/home.html` を表示しますが、メニューではログインやプロフィール画面へのリンクを表示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/114cb3d8-ac32-1bfe-26f9-94be5cafd565.png)

このURLは動的に作成しています。

```javascript
const user = ncmb.User.getCurrentUser();
$('.profile').attr('href', `/users/${user.userName}/`);
```

ログアウトは　`js/app.js` にてイベントを処理しています。ログアウト処理した後は、画面全体をリロードしています。

```javascript
  $('.logout').on('click', async (e) => {
    e.preventDefault();
    try {
      await ncmb.User.logout();
    } catch (e) {
      // 駄目だった場合は認証情報を削除
      localStorage.removeItem("NCMB/"+ncmb.apikey+"/currentUser");
    }
    location.reload();
  });
```

## まとめ

今回はTwitter風アプリの認証機能を実装しました。次はプロフィール画面とフォロー機能について解説します。