Twitterクローンを作ります #6 ログイン画面

今回はログイン画面を作成していきます。

# ログイン画面の作成 (見た目)

まずは見た目を作って行きましょう。下記のファイルを作成します。

```vue:client/src/views/LoginView.vue
<template>
    <div class="uk-container">
        <h1>Toitta Login</h1>

        <form class="uk-form-horizontal uk-margin-large" @submit.prevent>
            <div class="uk-margin">
                <label class="uk-form-label" for="login__email">メールアドレス</label>
                <div class="uk-form-controls">
                    <input
                        class="uk-input"
                        id="login__email"
                        type="text"
                        v-model="email"
                        required>
                </div>
            </div>
            <div class="uk-margin">
                <label class="uk-form-label" for="login__password">パスワード</label>
                <div class="uk-form-controls">
                    <input 
                        class="uk-input"
                        id="login__password"
                        type="password"
                        v-model="password"
                        required>
                </div>
            </div> 
            <div class="uk-margin">
                <input class="uk-button uk-button-primary" type="submit"  value="ログイン" @click="onClick">
            </div>      
        </form>
    </div>
</template>

<script>
import { ref } from 'vue';

export default {
    setup() {
        const email = ref("");
        const password = ref("");
        const onClick = () => {
            const credential = {
                email: email.value,
                password: password.value
            };

            console.log(credential);
        };
        return {
            email,
            password,
            onClick
        };
    }
}
</script>
```

http://localhost:8080/login で確認できるようにrouter/index.jsも修正します。

```javascript:client/src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'

const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/about',
    name: 'about',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
  },
  {
    path: '/register',
    name: 'register',
    component: () => import('../views/RegisterView.vue')
  },
  {
    path: '/login',
    name: 'login',
    component: () => import('../views/LoginView.vue')
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router
```

この状態で、http://localhost:8080/login にアクセスすると下記のような画面が表示されると思います。

![スクリーンショット 2022-03-12 21.47.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2536525/eecfd249-26f2-cd2e-1284-2374a6bc2ec1.png)

(相変わらず余計なメニューが表示されてしまっていますね。。。 次回修正しましょう)

# ログインAPIの実装

ひとまず見た目はできたのでAPI側を実装していきましょう。下記ファイルを追加します。

```python:server/apis/login.py
from flask import jsonify, request, session
from server.models.user import User
from server.main import app


@app.route('/api/login', methods=['POST'])
def login():
    body = request.json
    user = User._collection.find_one({'email': body['email']})

    if not user:
        return jsonify({'error': { 'message': 'ユーザーが存在しません。' }}), 400
    
    hashed_password = User.encrypt(body['password'])
    if user['hashed_password'] != hashed_password:
        return jsonify({'error': { 'message': 'パスワードが一致しません。' }}), 400
    
    
    del user['hashed_password']
    
    user['_id'] = str(user['_id'])
    session['user'] = user
    return jsonify(user)
```

登録処理とかなり似ていますね。

追加したファイルが読み込まれるように、server/__init__.pyも修正しておきます。
(もっとうまいやり方があれば教えていただけると嬉しいです)

```py:server/__init__.py
from server.apis import login, users
```

# ログイン画面の作成 (動き)

それでは画面側の動きを実装していきましょう。
storeにログイン処理を追加していきます。

```javascript:client/src/store/index.js
...

  actions: {
    registerUser ({ commit }, user) {
      axios.post('/api/users', user).then(resp => {
        commit('setUserLoggedIn', resp.data);
        router.push('/');
      })
      .catch(error => {
        UIkit.notification(error.response.data.error.message, {status: 'danger'});
      });
    },
    login ({ commit }, credential) {
      axios.post('/api/login', credential).then(resp => {
        commit('setUserLoggedIn', resp.data);
        router.push('/');
      })
      .catch(error => {
        UIkit.notification(error.response.data.error.message, {status: 'danger'});
      });
    }
  },

...
```

ログインAPIとユーザー登録APIのレスポンスは同一にしてあって、ログインユーザーの情報が返却されるのでミューテーション等は修正不要です。

最後にこのアクションを呼び出すようにしましょう。

```vue:client/src/views/LoginView.vue
...
<script>
import { ref } from 'vue';
import { useStore } from 'vuex';

export default {
    setup() {
        const store = useStore();

        const email = ref("");
        const password = ref("");
        const onClick = () => {
            const credential = {
                email: email.value,
                password: password.value
            };

            store.dispatch('login', credential);
        };
        return {
            email,
            password,
            onClick
        };
    }
}
</script>
```

問題なく動作しているようです。

TODOややっておきたいことが増えてきたので少し整理しましょう。

- ユーザー名のバリデーション(文字数, 利用可能文字)
- ユーザー名 or メールアドレスでのログイン
- 画面のレイアウト (変なメニューが表示されている)
- apiを追加する度に__init__.pyに追記が必要になっているのをどうにかしたい
- DB接続情報の管理 (SecretManagerを利用したい)
- 実行環境をクラウドに移す
- CI/CD
- 単体テスト

次回は上記からいくつか対応しておこうと思います。