GraphQL + Node.js -ログイン機能実装編-
ここでは前回から作っているTodoアプリにログイン機能を他していきます。(前回スキップした方はここから)
まずprisma/schema.prismaにUserモデルを足しましょう。
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Todo {
id Int @default(autoincrement()) @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
content String
user User? @relation(fields: [userId], references: [id])
userId Int?
isDone Boolean @default(false)
}
model User {
id Int @id @default(autoincrement())
username String
email String @unique
password String
todos Todo[]
}
前回と同じように足しているのでなんとなく理解できると思いますが、問題はTodoモデルに追加されているリレーションフィールドのuserとUserモデルのTodoを複数持つtodosフィールドです。簡単にまとめるとUserとTodoモデルは1対多の関係でTodoモデルからも誰がtodoを追加したかがわかります。
@relationsのfieldsは自身のTodoモデルのuserIdの指し、referencesに示されているidはUserモデルのidを指しています。TodoモデルのuserIdは外部キーと呼ばれ、Userモデルのidを参照しています。くわしい説明はPrismaの公式ページ出されているので興味ある方は是非。
ちなみにTodoモデルのuserとuserIdについている?はnullableという意味で先ほど作ったtodoのデータがこれらのフィールドを持っていないためにnullable型にしています。
ここまでできれば先ほどと同じように下記のコマンドでマイグレーションします。
npx prisma migrate dev --name "add-user-model" --preview-feature
npx prisma generate
続いてsrc/index.jsのtypeDefsのスキーマに変更を加えていきます。またsrc/index.jsのコード量が多くなるので好みで新しくファイルを作るなどしてください。
まずMutationから
type Mutation {
addTodo(content: String!): Todo!
register(username: String!, email: String!, password: String!): User
login(email: String!, password: String!): User
}
もともとあるaddTodoに加えてUserをかえすregisterとloginを追加します。共にエラーが発生するとnullを消すようにするので!は省いています。
続いてQueryとTodo、Userスキーマも。
type Query {
me: User
}
type Todo {
id: ID!
content: String!
createdAt: String!
updatedAt: String
isDone: Boolean!
user: User
userId: Int!
}
type User {
id: ID!
username: String!,
email: String!
password: String!
todos: [Todo!]!
}
meはログインしているユーザーをかえすQueryです。
次はユーザー認証のresolversを追加していきます。その前にパスワードの暗号化とセッションを利用するため以下のパッケージの追加を
npm i bcryptjs express-session
さらにsrc/index.jsを少し変更します。
// src/index.js
const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient()
const express = require('express');
const { ApolloServer } = require('apollo-server-express')
const session = require('express-session')
const { typeDefs } = require('./typeDefs')
const { resolvers } = require('./resolvers')
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
return {
...req,
prisma,
}
}
})
const app = express();
app.use(
session({
name: "todo",
secret: "todo",
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: false,
maxAge: 1000 * 60 * 60 * 24 * 7 * 365,
}
})
)
server.applyMiddleware({ app });
app.listen({ port: 4000 }, () =>
console.log('🚀 Server ready at http://localhost:4000/graphql')
)
いろいろ追加しているので上から順に説明していきます。ちなみにtypeDefsとresolversはそれぞれ他のファイルsrc/typeDefs.jsとsrc/resolvers.jsに移動させてあります。少しわかりずらければページの最後にGithubのリンクを載せているのでよかったら参考に。
まずcontextの部分は前回まではprismaのみをわたしていましたが、これではcontextを呼び出すときにprismaしかアクセスできません。今回、上記のようにcontextをcontextをかえす関数にすることで、GraphQLのqueryやmutationを扱うHTTPリクエストを contextに取り付けることができます。そして、ここではsessionを扱うときに役立ちます。
次はexpress-sessionでインポートしているsessionに関するコードです。sessionとcookieに関してはここでは詳しく解説はしませんが(exporess-sessionに関するQiitaの記事)今回は上記のように設定しました。name,secret,cookieのmaxAgeなどは適当で大丈夫です。
resolverについてはまず、Queryのmeという関数より先にMutationの関数、registerとloginについて説明していきます。
//src/index.js
register: async(_, { username, email, password }, context ) => {
const hashedPassword = await bcrypt.hash(password, 10)
const newUser = await context.prisma.user.create({
data: {
username,
email,
password: hashedPassword
}
})
return newUser
},
login: async(_, { email, password }, context ) => {
const user = await context.prisma.user.findUnique({ where: { email }})
if(!user){
return null
}
const valid = await bcrypt.compare(password, user.password)
if(!valid){
return null
}
context.session.userId = user.id;
return user
}
registerに関しては先ほど作ったaddTodoと同じようにcontext.prisma.user.createで新規ユーザーを登録しています。また登録する前にbcryptjsをインポートし、パスワードをハッシュ化しています。本格的なユーザー認証を実装しているわけではないのでそこの部分は書く必要ないかもしれませんが、 書く人は必ずawaitで非同期処理にしてください。
loginはおそらくこれまで実装してきた方法とほとんど同じだと思います。context.prisma.user.findUniqueでemailを使ってユーザーを探して、パスワードを一致を確かめるという処理です。最後にcontext.session.userId=user.idでセッションデータを格納しています。
ここまで実装しましたらさっそくGraphQLのPlaygroundで試してみましょう。
mutation {
register(username: "testuser",email: "test123@test.com", password: "test123"){
id
username
}
}
以下のようになりました、既にひとりユーザーを作成してしまっているのでidが2担っていますがおそらく初めて登録する人は1になっているはずです。
問題なくユーザーがかえってきたら次はログインを確かめましょう。
先ほど作成したユーザーがかえってくれば成功です。nullがかえってきた場合はエラーが発生しています。また、cookieが登録されているか確認するために右クリックの検証か右上のその他ツールからディベロッパーツールからApplicationを開いてCookiesのhttp://localhost:4000を見てみてください。
src/index.jsで登録したcookie情報が反映されていればOKです。
続いてはログインしているユーザを確認する処理、meを実装しましょう。まずsrc/resolver.jsにログインユーザーの情報をかえすme関数を
me: (_,__, context ) => {
if(!context.session.userId){
return null;
}
return context.prisma.user.findUnique({ where: { id: context.session.userId }})
}
sessionに格納されているuserIdと一致するユーザーをかえす処理です。
先ほどログインしたユーザーがかえってくるはずです。以上が簡単のGraphQLを使ったユーザー認証の実装でした。
最後に前回実装したaddTodo関数にuser情報を付け足すコードを書きましょう。
addTodo: async(_, { content }, context ) => {
if(!context.session.userId){
return null;
}
const newTodo = await context.prisma.todo.create({
data: {
content,
user: { connect: { id: context.session.userId } },
}
})
return newTodo
}
まず最初にユーザーがログインしているかを確認するためにsessionをみています。ここではエラー処理は全てnullでかえしています。そしてnewTodoの作成の時のわたすデータにuser: { connect: { id: context.session.userId } }を加えてます。 これによって新しいtodoのユーザー情報をTodoスキーマとつなげられます。くわしいnested writeについてはこちらから。
しかしながら上のコードだけではどこからユーザー情報を取得のかをGraphQLサーバーは推測することはできません。なので上に加えてresolversのTodoに新しく下記の関数userを加えます。
Todo: {
user: (parent, _, context) => {
return context.prisma.todo.findUnique({ where: { id: parent.id } }).user()
}
}
ここで新しく引数parentについてはQuery編で少しふれましたが、親resolverの結果を含むオブジェクトでありparent.idでデータベースからprismaを使いTodoを取得したときに上のuserを呼び出す。また先ほどsrc/typeDefs.jsでuserフィールドを定義してるためuserは呼ばれる必要があります。
ここまで実装したら、サーバーをはしらせてaddTodoを試してみましょう。
mutation {
addTodo(content: "Learing Typescript"){
id
content
userId
user {
username
}
}
}
上のようにuserまでかえってきたら成功です。nullがかえってきた場合、sessionが切れているかもしれないので一度ログインしなおしてもう一度試してみてください。それでもうまくいかない方はGithubのページを参照してみてください。
Githubページはこちらから
少し長かったですがこれでユーザー認証を含むtodoアプリのAPIの完成です。ここまで進められた方はもうこれで基本的なアプリケーションのバックエンドをGraphQLを使って実装できると思います。しかしまだSubscriptionを含む応用やTypescriptやTypeORMというまた別のORMを使った セクションもあるので挑戦してみてください、