Fullstack React GraphQL TypeScript Tutorial をやってみた #3 ~ユーザー登録とログイン機能の実装まで~

# はじめに
30代未経験からエンジニアを目指して勉強中のYNと申します。
この記事は`Ben Awad`さんの[Fullstack React GraphQL TypeScript Tutorial](https://youtu.be/I6ypD7qv3Z8)を初学者が進めていく、という内容です。
`Ben`さんの動画は本当に質が高く、とても学びが多いのですが、自分のような初学者は躓きが多く、なかなか前に進まなかったので、振り返りのメモとして書きます。

# 今回の対象
動画の下記内容までです。
1:09:23 [Register Resolver](https://youtu.be/I6ypD7qv3Z8?t=4163)
1:23:27 [Login Resolver]()

前回 => https://qiita.com/theFirstPenguin/items/5ce4ca5b424b4fb4d9ef

#始める前に

### ブランチをコピー
動画の内容ごとに細かく[ブランチ](https://github.com/benawad/lireddit/tree/5_login-and-validation)を切ってくれています。ありがたや。
まずはブランチをローカルにコピーして、そのブランチに移ります。

```
git pull origin 5_login-and-validation:5_login-and-validation
git checkout 5_login-and-validation
```

#Userテーブルを作る
### Userスキーマを作成
[動画のこの部分です](https://youtu.be/I6ypD7qv3Z8?t=4163)
まずはMikroORMでTypeScriptによってUserテーブルを定義します。
![スクリーンショット 2020-11-11 18.18.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572098/eaeb526a-b116-7302-fc7c-0c954e42f028.png)

```src/entities/User.ts
import { Entity, PrimaryKey, Property } from "@mikro-orm/core";

@Entity()
export class User {
  @PrimaryKey()
  id!: number;

  @Property({ type: "date" })
  createdAt = new Date();

  @Property({ type: "date", onUpdate: () => new Date() })
  updatedAt = new Date();

  @Property({ type: "text", unique: true })
  username!: string;

  @Property({ type: "text" })
  password!: string;
}
```
さらに、`@ObjectType()`デコレータと`@Field()`デコレータを使って、GraphQLの型の定義を追記します。
ここで着目するのは、passwordにはGraphQLの型を付けない、ということです。このことによって、passwordがリクエストから読み込まれることは出来なくなります。

```src/entities/User.ts
import { Entity, PrimaryKey, Property } from "@mikro-orm/core";
import { ObjectType, Field } from "type-graphql";

@ObjectType()
@Entity()
export class User {
  @Field()
  @PrimaryKey()
  id!: number;

  @Field(() => String)
  @Property({ type: "date" })
  createdAt = new Date();

  @Field(() => String)
  @Property({ type: "date", onUpdate: () => new Date() })
  updatedAt = new Date();

  @Field()
  @Property({ type: "text", unique: true })
  username!: string;

  @Property({ type: "text" }) // @Field()デコレータは付けない
  password!: string;
}
```

### Userテーブルのmigrationを実行
[動画のこの部分です](https://youtu.be/I6ypD7qv3Z8?t=4302)
`src/entities/user.ts`にテーブルスキーマを記述したので、migrationを実行します。
まずは`mikro-orm.config.ts`にUserエンティティを追加します。

```mikro-orm.config.ts
...
export default {
  migrations: {
    path: path.join(__dirname, "./migrations"),
    pattern: /^[\w-]+\d+\.[tj]s$/,
  },
  entities: [Post, User], // ここにUserを追加する。
  dbName: "lireddit",
  type: "postgresql",
  debug: !__prod__,
} as Parameters<typeof MikroORM.init>[0];
```
そしてmigrationコマンドを実行します。

```
npx mikro-orm migration:create
```

ちなみに、Mikro-ORMのコマンドは[こちら](https://mikro-orm.io/docs/installation)にあります。

```
Usage: mikro-orm <command> [options]

Commands:
  mikro-orm cache:clear             Clear metadata cache
  mikro-orm cache:generate          Generate metadata cache for production
  mikro-orm generate-entities       Generate entities based on current database
                                    schema
  mikro-orm database:import <file>  Imports the SQL file to the database
  mikro-orm schema:create           Create database schema based on current
                                    metadata
  mikro-orm schema:drop             Drop database schema based on current
                                    metadata
  mikro-orm schema:update           Update database schema based on current
                                    metadata
  mikro-orm migration:create        Create new migration with current schema
                                    diff
  mikro-orm migration:up            Migrate up to the latest version
  mikro-orm migration:down          Migrate one step down
  mikro-orm migration:list          List all executed migrations
  mikro-orm migration:pending       List all pending migrations
  mikro-orm debug                   Debug CLI configuration

Options:
  -v, --version  Show version number                                   [boolean]
  -h, --help     Show help                                             [boolean]

Examples:
  mikro-orm schema:update --run  Runs schema synchronization
```

### TypeScriptコンパイル時に使用していない変数をエラーとして扱う
[動画のこの部分です](https://youtu.be/I6ypD7qv3Z8?t=4348)

`tsconfig.json`の設定で使用していない変数を許さない設定は下記の部分です。

```tsconfig.json
{
  "compilerOptions": {
    "noUnusedLocals": true,
    "noUnusedParameters": true,
  }
}
```
#ユーザー登録機能を実装する
ユーザー登録とpasswordの暗号化を行います。
### 入力のためのGraphQLの型を定義して、user登録機能を実装する
[動画のこの部分です](https://youtu.be/I6ypD7qv3Z8?t=4539)
[前回](https://qiita.com/theFirstPenguin/items/5ce4ca5b424b4fb4d9ef#mikroorm%E3%81%A8typegraphql%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6graphql%E3%81%AEmutation%E3%82%92%E5%AE%9F%E7%8F%BE%E3%81%99%E3%82%8B)まで、GraphQLにリクエストするための入力値は`@Arg()`デコレータで定義していましたが、入力値が多くある場合は`@InputType()`デコレータで入力値の型を定義する方が分かりやすくなります。([公式ドキュメント参照](https://typegraphql.com/docs/resolvers.html#input-types))


```src/resolvers/user.ts
import { Resolver, Mutation, Arg, InputType, Field, Ctx } from "type-graphql";
import { MyContext } from "../types";
import { User } from "../entities/User";

@InputType()
class UsernamePasswordInput {
  @Field()
  username: string;
  @Field()
  password: string;
}

@Resolver()
export class UserResolver {
  @Mutation(() => User)
  async register(
    @Arg("options") options: UsernamePasswordInput, // ここで@InputType()の型を指定する
    @Ctx() { em }: MyContext // index.tsのcontextプロパティからMikroORMオブジェクトを引用する
  ) {
  ...
  }
}
```

### passwordを暗号化する
[動画のこの部分です](https://youtu.be/I6ypD7qv3Z8?t=4738)
[node-argon2](https://github.com/ranisalt/node-argon2)を使ってpasswordを暗号化します。
ハッシュ関数を用いた暗号化には、[こちらの記事](https://recruit.cct-inc.co.jp/tecblog/security/hash-function/)が分かりやすかったです。


```src/resolvers/user.ts
import { Resolver, Mutation, Arg, InputType, Field, Ctx } from "type-graphql";
import { MyContext } from "../types";
import { User } from "../entities/User";
import argon2 from "argon2";

@InputType()
class UsernamePasswordInput {
  @Field()
  username: string;
  @Field()
  password: string;
}

@Resolver()
export class UserResolver {
  @Mutation(() => User)
  async register(
    @Arg("options") options: UsernamePasswordInput,
    @Ctx() { em }: MyContext
  ) {
    const hashedPassword = await argon2.hash(options.password);
    // リクエストから送られてきたpasswordをArgon2というハッシュ関数でハッシュ化する

    const user = em.create(User, {
      username: options.username,
      password: hashedPassword,
    });
    // https://mikro-orm.io/docs/entity-manager-api#createentityname-entitynamet-data-entitydatat-newt-p

    await em.persistAndFlush(user); // https://mikro-orm.io/docs/entity-manager/
    return user;
  }
}
```

# ログイン機能を実装する
resolverの書き方は基本的にuser登録の時と大体一緒ですが、エラーオブジェクトの定義とpasswordの認証が必要です。
最終的にログイン機能の部分のコードはこうなります。

```src/resolvers/user.ts
@ObjectType()
class FieldError {
  @Field()
  field: string;
  @Field()
  message: string;
}

@ObjectType()
class UserResponse {
  @Field(() => [FieldError], { nullable: true })
  errors?: FieldError[];

  @Field(() => User, { nullable: true })
  user?: User;
}

@Resolver()
export class UserResolver {

  @Mutation(() => UserResponse)
  async login(
    @Arg("options") options: UsernamePasswordInput,
    @Ctx() { em }: MyContext
  ): Promise<UserResponse> {
    const user = await em.findOne(User, { username: options.username });
    if (!user) {
      return {
        errors: [
          {
            field: "username",
            message: "that username doesn't exist",
          },
        ],
      };
    }
    const valid = await argon2.verify(user.password, options.password);
    if (!valid) {
      return {
        errors: [
          {
            field: "password",
            message: "incorrect password",
          },
        ],
      };
    }

    return {
      user,
    };
  }
}
```


### エラーオブジェクトの型を定義する
[動画のこの部分です](https://youtu.be/I6ypD7qv3Z8?t=5127)
type-graphqlのクラスデコレータ`@ObjectType()`とプロパティデコレータ`@Field()`を用いて、GraphQLの型をTypeScriptのクラスとして生成します。生成されたクラスは、GraphQLの型としてだけでなく、TypeScriptの型として関数の戻り値に設定できます。

```src/resolvers/user.ts
...
@ObjectType()
class FieldError {
  @Field()
  field: string; // エラーが起因するGraphQLのフィールド（つまりテーブルのカラム）を返す
  @Field()
  message: string; // エラーメッセージを返す
}

@ObjectType()
class UserResponse {
  @Field(() => [FieldError], { nullable: true }) // nullableオプションを明示する
  errors?: FieldError[]; // undefinedを許容する

  @Field(() => User, { nullable: true })  // nullableオプションを明示する
  user?: User; // undefinedを許容する

}
...
```

### passwordを認証する
[動画のこの部分です](https://youtu.be/I6ypD7qv3Z8?t=5397)
データベースに保存されている、passwordを暗号化したもの(`user.password`)と、リクエストで送られてきたpassword(`options.passwprd`)を比較して認証します。

```src/resolvers/user.ts
...
const valid = await argon2.verify(user.password, options.password);
...
```
# 最後に
今回は`Login Resolver`の設定まで書きました。
チュートリアル全14時間のうち、90分ぐらい進みました。
先は長い。。。

次回 => https://qiita.com/theFirstPenguin/items/177ca0d09c02b0a16c9e