1
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 3 years have passed since last update.

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

Last updated at Posted at 2020-11-13

はじめに

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

今回の対象

動画の下記内容までです。
1:09:23 Register Resolver
1:23:27 Login Resolver

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

#始める前に

ブランチをコピー

動画の内容ごとに細かくブランチを切ってくれています。ありがたや。
まずはブランチをローカルにコピーして、そのブランチに移ります。

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

#Userテーブルを作る

Userスキーマを作成

動画のこの部分です
まずはMikroORMでTypeScriptによってUserテーブルを定義します。
スクリーンショット 2020-11-11 18.18.54.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を実行

動画のこの部分です
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のコマンドはこちらにあります。

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コンパイル時に使用していない変数をエラーとして扱う

動画のこの部分です

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

tsconfig.json
{
  "compilerOptions": {
    "noUnusedLocals": true,
    "noUnusedParameters": true,
  }
}

#ユーザー登録機能を実装する
ユーザー登録とpasswordの暗号化を行います。

入力のためのGraphQLの型を定義して、user登録機能を実装する

動画のこの部分です
前回まで、GraphQLにリクエストするための入力値は@Arg()デコレータで定義していましたが、入力値が多くある場合は@InputType()デコレータで入力値の型を定義する方が分かりやすくなります。(公式ドキュメント参照)

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を暗号化する

動画のこの部分です
node-argon2を使ってpasswordを暗号化します。
ハッシュ関数を用いた暗号化には、こちらの記事が分かりやすかったです。

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,
    };
  }
}

エラーオブジェクトの型を定義する

動画のこの部分です
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を認証する

動画のこの部分です
データベースに保存されている、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

1
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
1
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?