노마드코더 인스타그램 클론코딩 바로가기 https://nomadcoders.co/instaclone
InstaClone을 위한 prisma를 다시 생성 npx prisma init
User 모델 생성
model User{ id Int @id @default(autoincrement()) firstName String lastName String? (선택사항) username String @unique email String @unique password String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }
User 관련 typeDefs와 resolvers 구현
// users.typeDefs.js export default gql` type CreateAccountResult{ ok:Boolean! error:String } type Mutation{ createAccount( firstName: String! lastName: String username: String! email: String! password: String! bio: String avatar: Upload ): CreateAccountResult! } ` // users.resolvers.js import client from "../client"; export default{ Mutation:{ // DB에 동일한 username과 email이 존재하는지 체크 createAccount:async(_,{firstName,lastName,username,email,password})=>{ const existingUser = await client.user.findFirst({ where:{ OR:[ { username, }, { email, }, ], }, }); if (existingUser) { throw new Error("This username/email is already taken."); } // password의 hash화 const uglyPassword = await bcrypt.hash(password, 10); await client.user.create({ data: { username, email, firstName, lastName, password: uglyPassword, }, }); return { ok: true, }; } catch (e) { return { ok: false, error: "Can't create account", }; } } } }
Prisma를 이용한 작업은 Promise를 리턴하기 때문에 코드에서 즉시 이뤄지지 않는 것을 뜻한다.
성공한다면 성공했을때의 data를 실패한다면 실패했을때의 data를 리턴
따라서 코드가 실행되면 정보를 확인하러 DB로 가고 다시 돌아와야 하기 때문에 기다려야 한다.
이를 위해 async await를 사용
단 return할때는 prisma가 끝날때까지 자동으로 기다려주기 때문에 await를 사용할 필요가 없다.
OR : 이는 조건들의 Array을 필요로한다.
위 코드에서는 OR에 username=username이란 조건과 email=email이란 조건이 존재하는 Array를 매칭해줬다.
따라서 Prisma는 username이 존재하거나 email이 존재 할때 그 user를 가져온다.
이를 이용해 Error작업을 해줬다.
Hash Password
npm i bcrypt
findFirst는 모든 필드를 이용해서 필터링 가능함
findUnique는 unique필드만 이용해서 필터링 가능하다. ex) id,@unique로 지정한 필드 ...
login 기능을 구현하기 위해서 우리는 JWT(JSONWebToken)을 사용
npm i jsonwebtoken
기본적으로 token은 server와 frontend가 서로 분리 되어져 있을때 사용한다.
프로젝트의 client가 browser,android,ios 등 여러가지가 될 수 있다.
따라서 server와 frontend가 분리된 형태로 프로젝트를 진행한다.
로그인 할 때 우리가 사인을 한 token이 발급된다.
이 token은 비밀정보를 담아 암호화한 것이 아니라 누구나 볼 수 있고 누구나 만들 수 있다.
그러면 이 token은 어떤 목적으로 사용하는 것일까 ?
단지 token에 정보를 담고 우리가 사인을 한 token이 맞는지 체크하고 다른 사람이 이를 변경하지 않았다는 것을 체크하는 것이 목적이다. 따라서 비밀정보를 담아서는 안된다.
우리가 발급했다는 것을 증명하기 위해 서버는 payload에 사인을하고 이를 토큰으로 변경해준다.
또한 누구나 볼 수 있기때문에 user의 id를 token에 담아서 암호화할 것이고 필요한 secret key도 넣어줘야한다.
당연히 secret key이기 때문에 .env에서 작성해줘 함 !!!
구현
1. 가져온 username과 일치하는 user를 DB에서 찾는다. (없다면 error를 리턴)
2. user가 존재한다면 password를 비교한다.await bcrypt.compare(password, user.password);
3. password까지 일치한다면 로그인이 성공할 경우 -> 토큰 발급await jwt.sign({ id: user.id }, process.env.SECRET_KEY);
Secret Key 발급 사이트 -> https://randomkeygen.com
발급된 토큰은 로그인이 필요한 Action에서 사용된다.
기존의 방법대로 모든 query와 mutation에 token을 다 적어주는 것은 매우 비효율적이다.
예) EditProfile이라는 Mutation을 구현한다고 할때 현재 로그인한 user의 정보가 필요하다.
그 user의 id를 이용해서 update해야 하기 때문에 따라서 이 경우에는 token을 이용해 user를 알 수 있다.
Token을 모든 Mutation에서 접근 가능하도록 해야하며 headers로 받은 token에 접근해야 한다.
따라서 HTTP Headers를 사용할 것인데 HTTP Headers는 request,response의 한 부분이고 어떠한 값이든 넣을 수 있다.
그래서 HTTP Headers에 token을 넣어줘 이를 해결해 줄 것이다.
const server = new ApolloServer({
schema,
context:({request})=>{
return{
token:request.headers.token,
}
}
})
각 resolver에서는 context를 통해서 이 token에 접근 가능한 것이기때문에 이 token을 해독하는 코드를 반복적으로 적어야한다.
해결 방안 !!!!!
context로 token을 넘겨주는 것이아니라 이 token을 해독해 얻은 user의 id를 이용해 찾은 User 자체를 전달 해준다.
// users.utils.js
export const getUser = async(token)=>{
try{
if(!token){
return null;
}
const {id} = await jwt.verify(token,process.env.SECRET_KEY);
const user = await client.user.findUnique({
where:{
id,
},
});
if(user){
return user;
}else{
return null;
}
}catch{
return null;
}
}
// js
const server = new ApolloServer({
schema,
context:async ({request})=>{
return{
loggedInUser:await getUser(request.headers.token)
}
}
})
loggedInUser가 null인 상태에서 mutation을 post 한다면 loggedInUser가 null인 시점에서 바로 코드가 멈추고 그 아래 코드는 실행되지 않는 것이 좋다.
현재는 client.user.update의 where까지 코드가 실행되고서야 error가 발생하는 것을 볼 수 있다.
이 조건에 해당되는 그 순간 바로 코드를 멈추고 다음과 같은 error Obj를 리턴해주고 싶다면 어떻게 해야할까 ?{ ok:"false", error:"로그인 해주세요.", }
// users.utils.js
export const protectedResolver = (ourResolver)=>(root,args,context,info)=>{
if(!context.loggedInUser){
//로그인한 user가 없다면
return{
ok:false,
error:"Please logIn to perform this action",
};
}
return ourResolver(root,args,context,info);
};
// 위 코드와 같은 기능이나 arrow function을 사용하지 않은 코드
export function protectedResolver(ourResolver){
return function (root,args,context,info){
if(!context.loggedInUser){
return {
ok:false,
error:"Please logIn to perform this action",
};
}
return ourResolver(root,args,context,info)
}
}
loggedInUser가 null이라면 protectedResolver의 조건에 걸려 error Object가 리턴되는 것이고
loggedInUser가 존재한다면 그냥 resolver가 진행되는 것을 볼 수 있다.
위의 protectedResolver이 필요한 Mutation인 editProfile을 구현해보자
// editProfile.resolvers.js
const resolverFn = async(_,{firstName,lastName,username,email,password:newPassword,bio},{loggedInUser})=>{
// 받아온 newPassword도 hash화 해줘야한다.
let HashPW = null;
if(newPassword){
HashPW = await bcrypt.hash(newPassword,10);
}
const updateUser = await client.user.update({
where:{
id:loggedInUser.id,
},
data:{
firstName,
lastName,
username,
email,
bio,
...(HashPW && {password:HashPW}),
}
})
}
export default{
Mutation:{
editProfile:protectedResolver(resolverFn),
}
}
update할때는 부분적 업데이트가 가능하도록 구현해야 하기 때문에 typeDefs에서 !(required)를 해주지 않았다.
여기서 username만 값을 받아서 업데이트를 한다고 했을때 console.log로 다른 값들을 찍어보면 undefined가 찍히는 것을 볼 수 있었다.
그렇다면 위 코드에서 firstName,lastName,email,bio ... 에는 undefined가 전달되는 것인데 Prisma Stuido에서 확인해보면 정상적으로 username만 변경된 것을 볼 수 있다.
이것은 Prisma Client가 매우 똑똑해서 undefined를 받으면 그 값은 DB로 전달하지 않기 때문이다.
코드 분석
...(HashPW && {password:HashPW}
- HashPW가 존재한다면 =>
...{password:HashPW}
- ...는 Object를 unpack =>
password:HashPW