3. Register-Login Resolver
2020-09-25
Parameter: 매개변수, 함수와 메서드 입력 변수(Variable) 명
Argument: 전달인자, 함수와 메서드의 입력 값(Value)
Context: 모든 resolver 함수에 전달되며, 현재 로그인한 사용자, 데이터베이스 액세스와 같은 중요한 문맥 정보를 보유하는 값
1. register
패스워드를 해싱하기 위해 argon2를 설치한다.
entitie/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" })
password!: string;
}
@Field() 데코레이터가 없으므로
graphql 쿼리로 password를 검색할 수 없다
여기에 이미지1
등록과 로그인의 기능을 가진 resolver를 만들 것이다.
먼저 Context에 response, request를 추가해준다.
types.ts
export type MyContext = {
em: EntityManager<any> & EntityManager<IDatabaseDriver<Connection>>;
req: Request & { session: Express.Session };
res: Response;
};
resolvers/user.ts
Argument의 타입으로 UsernamePasswordInput을 정의해준다.
@InputType()
class UsernamePasswordInput {
@Field()
username: string;
@Field()
password: string;
}
return값으로 User와 error 둘 다 받기 위한 타입을 정의한다.
@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(User)
export class UserResolver {
@Mutation(() => UserResponse)
async register(
@Arg("options") options: UsernamePasswordInput,
@Ctx() { em, req }: MyContext
): Promise<UserResponse> {
//ID와 패스워드의 조건(<=2)을 만족하지 못하면 error을 반환한다
if (options.username.length <= 2) {
return {
errors: [
{
field: "username",
message: "length must be greater than 2",
},
],
};
}
if (options.password.length <= 2) {
return {
errors: [
{
field: "password",
message: "length must be greater than 2",
},
],
};
}
const hashedPassword = await argon2.hash(options.password);
const user = em.create(User, {
username: options.username,
password: hashedPassword,
});
try {
await em.persistAndFlush(user);
} catch (err) {
if (err.code === "23505") {
// duplicate username error
return {
errors: [
{
field: "username",
message: "username already taken",
},
],
};
}
}
return { user };
}
}
index.ts
import { UserResolver } from "./resolvers/user";
const main = async () => {
const apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [PostResolver, UserResolver],
//UserResolver를 추가
}),
});
};
mikro-orm.config.ts
export default {
entities: [Post, User],
} as Parameters<typeof MikroORM.init>[0];
2. login
@Resolver(User)
export class UserResolver {
@Mutation(() => UserResponse)
async login(
@Arg("options") options: UsernamePasswordInput,
@Ctx() { em, req }: MyContext
): Promise<UserResponse> {
const user = await em.findOne(User, { username: options.username });
if (!user) {
return {
errors: [
{
field: "username",
message: "that username dosen't exist",
},
],
};
}
const valid = await argon2.verify(user.password, options.password);
if (!valid) {
return {
errors: [
{
field: "password",
message: "incorrect password",
},
],
};
}
/*
store user id session
this will set a cookie on the user
keep them logged in
*/
req.session.userId = user.id;
return {
user,
};
}
}
로그인 상태를 유지하기 위해 쿠키를 설정해주자.
링크 express-session connect-redis
index.ts
import redis from "redis";
import session from "express-session";
import connectRedis from "connect-redis";
const main = async () => {
const RedisStore = connectRedis(session);
const redisClient = redis.createClient();
app.use(
session({
name: "qid",
store: new RedisStore({
client: redisClient,
disableTouch: true,
}),
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 365 * 10,
// 10년
httpOnly: true,
sameSite: "lax", //csrf
secure: __prod__, // only https
},
saveUninitialized: false,
secret: "qawsedqawsed",
//used to sign the session ID cookie
resave: false,
})
);
apolloServer.applyMiddleware({ app });
};
req.session.userId = user.id;
graphql playground 설정을 수정해야지 제대로 동작한다.
"request.credentials": "include",
- disableTouch
- Disables re-saving and resetting the TTL when using touch (default: false). The express-session package uses touch to signal to the store that the user has interacted with the session but hasn’t changed anything in its data.
- TTL
- Time to live. If the session cookie has a expires date, connect-redis will use it as the TTL.
- cookie.sameSite
- Specifies the boolean or string to be the value for the SameSite Set-Cookie attribute. ‘lax’ will set the SameSite attribute to Lax for lax same site enforcement.
- cookie.secure
- Specifies the boolean value for the Secure Set-Cookie attribute. When truthy, the Secure attribute is set, otherwise it is not. By default, the Secure attribute is not set.
- saveUninitialized
- Forces a session that is “uninitialized” to be saved to the store. A session is uninitialized when it is new but not modified. Choosing false is useful for implementing login sessions, reducing server storage usage, or complying with laws that require permission before setting a cookie. Choosing false will also help with race conditions where a client makes multiple parallel requests without a session. The default value is true, but using the default has been deprecated, as the default will change in the future. Please research into this setting and choose what is appropriate to your use-case.
- resave
- Forces the session to be saved back to the session store, even if the session was never modified during the request. Depending on your store this may be necessary, but it can also create race conditions where a client makes two parallel requests to your server and changes made to the session in one request may get overwritten when the other request ends, even if it made no changes (this behavior also depends on what store you’re using). The default value is true, but using the default has been deprecated, as the default will change in the future. Please research into this setting and choose what is appropriate to your use-case. Typically, you’ll want false.