본격적인 인스타그램 클론코딩 시작이다. 시작에 앞서 전에 만들었던 prisma폴더와 movies폴더와 db를 삭제해준다.
//schema.prisma
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 //prisma에서 지원한다.
}
//users.mutations.js
import bycrypt from "bcrypt";
import client from "../client";
export default {
Mutation: {
createAccount: async (_, {
firstName,
lastName,
username,
email,
password,
}) => {//prisma 는 promise를 리턴 하므로 async, await 사용해야한다.
try {
const existingUser = await client.user.findFirst({
where: {
OR: [//fillter는 대문자를 사용햐여한다.
{ username },
{ email },
]
}
});
if (existingUser) {// 이미 있는 user이면 error
throw new Error("This username/email is already taken.");
}
const uglyPassword = await bycrypt.hash(password, 10);
return client.user.create({
data: {
username,
firstName,
lastName,
email,
password: uglyPassword,
}
});
} catch (err) {
return err;
}
}
}
}
DB에서 User를 Read한다.
여기서 findUnique는 unique한 필드에만 적용할 수 있다.
//users.queries.js
import client from "../client";
export default {
Query: {
seeProfile: (_, { username }) => client.user.findUnique({
where: {
username,
}
})
}
};
token은 서버가 프론트엔드에 연결되어 있지 않을 때나 다른 장소에 있을 때 사용한다.
여기서는 JWT를 이용한다.
누구나 token안을 볼 수 있다. 따라서 token에는 비밀정보를 담으면 안된다.
token의 목적은 토큰안에 정보를 넣고, 그 토큰이 우리가 싸인했던 토큰인지 확인하는 것이다.
sign을 할 때 private_key와 함께 싸인을 하게되는데 이 key는 절대 노출되면 안된다. 노출되면 그 key 주인의 토큰을 함부로 만들 수 있다.
//users.mutations.js
login: async (_, { username, password }) => {
const user = await client.user.findFirst({ where: { username } });
if (!user) {
return {
ok: false,
error: "User not found"
}
}
const passwordOk = await bcrypt.compare(password, user.password);
if (!passwordOk) {
return {
ok: false,
error: "Incorrect password"
}
}
const token = jwt.sign({ id: user.id }, process.env.SECRET_KEY);
return {
ok: true,
token,
};
}
update될 때 값을 안바꾸는 undefined된 값은 DB에 반영되지 않는다.
비밀번호는 hash를 다시 해줘야 한다.
context는 모든 resolver에서 접근 가능한 정보를 넣을 수 있는 object이다.
resolver함수를 protectedResolver함수로 감싸서 다른 js파일에서 매번 login한 user인지 확인해주지 않고 함수로 만든다. login한 user이면 해당 resolver를 실행한다.
graphql-resolver가 받는 인자는 root, args, context, info 이다.
//user.utils.js
import jwt from "jsonwebtoken";
import client from "../client";
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;
}
};
export const protectedResolver = (ourResolver) => (root, args, context, info) => {
if (!context.loggedInUser) {
return {
ok: false,
error: "Please log in to perform this action"
};
}
return ourResolver(root, args, context, info);
};
모든 resolver에서 context로 접근 가능하게끔 apollo server에서 context 수정이 가능하다.
//server.js
const server = new ApolloServer({
schema,
cnotext: async ({ req }) => {
return {
loggedInUser: await getUser(req.headers.token),
};
},
});
//updateProfile.resolvers.js
import bcrypt from "bcrypt";
import client from "../../client";
import { protectedResolver } from "../users.utils";
export default {
Mutation: {
updateProfile: protectedResolver(
async (_,
{ firstName, lastName, username, email, password: newPassword },
{ loggedInUser }
) => {
let uglyPassword = null;
if (newPassword) {
uglyPassword = await bcrypt.hash(newPassword, 10);
}
const updatedUser = await client.user.update({
where: {
id: loggedInUser.id,
},
data: {
firstName,
lastName,
username,
email,
...(uglyPassword && { password: uglyPassword }),
},
});
if (updatedUser.id) {
return {
ok: true,
};
} else {
return {
ok: false,
error: "Could not update profile.",
};
}
}
)
}
}
Velog 잘 보고 있습니다.
혹시 create Account에서
const existingUser = await client.user.findFirst({...})을 호출하면 아래와 같이 에러가 발생합니다.
find, create 다 안됩니다.
혹시 어떤 문제인지 알 수있을까요?
meta: {
query_validation_error: 'Field does not exist on enclosing type.',
query_position: 'Query.findFirstUser'
}