
user.entity.ts
registerEnumType을 이용해서 GraphQL에서 enum타입을 정의할 수 있습니다.
@Column({ type: 'enum', enum: UserRole })을 이용해서 Database에 enum타입을 정의할 수 있습니다.
import {
Field,
InputType,
ObjectType,
registerEnumType,
} from '@nestjs/graphql';
import { CoreEntity } from 'src/common/entities/core.entity';
import { Column, Entity } from 'typeorm';
enum UserRole {
Client,
Owner,
Delivery,
}
registerEnumType(UserRole, { name: 'UserRole' });
@InputType({ isAbstract: true })
@ObjectType()
@Entity()
export class User extends CoreEntity {
@Column()
@Field((type) => String)
email: string;
@Column()
@Field((type) => String)
password: string;
@Column({ type: 'enum', enum: UserRole })
@Field((type) => UserRole)
role: UserRole;
}
core.entity.ts
import { Field } from '@nestjs/graphql';
import {
CreateDateColumn,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
export class CoreEntity {
@PrimaryGeneratedColumn()
@Field((type) => Number)
id: number;
@CreateDateColumn() // entity를 만들었을때 자동으로 설정해 주는 special column
@Field((type) => Date)
createdAt: Date;
@UpdateDateColumn()
@Field((type) => Date)
updatedAt: Date;
}
users/dtos/create-account.dto.ts 파일 만들기
PickType을 통해 상속해주는 User의 타입에서 원하는 타입만 가져올 수 있습니다.
import { Field, InputType, ObjectType, PickType } from '@nestjs/graphql';
import { User } from '../entities/user.entity';
@InputType()
export class CreateAccountInput extends PickType(User, [
'email',
'password',
'role',
]) {}
@ObjectType()
export class CreateAccountOutput {
@Field((type) => String, { nullable: true })
error?: string;
@Field((type) => Boolean)
ok: boolean;
}
users.resolver.ts
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import {
CreateAccountInput,
CreateAccountOutput,
} from './dtos/create-account.dto';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';
@Resolver((of) => User)
export class UsesrResolver {
constructor(private readonly usesrService: UsersService) {}
@Query((returns) => Boolean)
hi(): boolean {
return true;
}
@Mutation((returns) => CreateAccountOutput)
createAccount(@Args('input') createAccountInput: CreateAccountInput) {}
}
users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateAccountInput } from './dtos/create-account.dto';
import { User } from './entities/user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly users: Repository<User>,
) {}
async createAccount({
email,
password,
role,
}: CreateAccountInput): Promise<{ ok: boolean; error?: string }> {
try {
// check new user
const exists = await this.users.findOne({ email });
if (exists) {
//return error Message
return { ok: false, error: 'There is a user with that email already' };
}
// Create user and Save
await this.users.save(this.users.create({ email, password, role }));
return { ok: true };
} catch (error) {
//return error Message
return { ok: false, error: "couldn't create account" };
}
}
}
User Repository의 findOne메서드는 argument에 적어준 값이 있는지 체크후 있다면 해당 User Repository를 반환하고 없다면 undefinded를 반환합니다.
User Repository의 findOne을 통해 user가 처음 가입하는 것인지 체크하고 만약 존재하는 유저라면 error Message가 담긴 객체를 리턴합니다.
존재하지 않는 user라면 create하고 save해줍니다. 또한 ok:true인 객체를 리턴합니다.
users.resolver.ts
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import {
CreateAccountInput,
CreateAccountOutput,
} from './dtos/create-account.dto';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';
@Resolver((of) => User)
export class UsesrResolver {
constructor(private readonly usesrService: UsersService) {}
@Query((returns) => Boolean)
...
}
@Mutation((returns) => CreateAccountOutput)
async createAccount(
@Args('input') createAccountInput: CreateAccountInput,
): Promise<CreateAccountOutput> {
try {
const { ok, error } = await this.usesrService.createAccount(
createAccountInput,
);
return {
ok,
error,
};
} catch (error) {
return {
ok: false,
error,
};
}
}
}
usesrService의 createAccount메서드를 실행했을때 return값에 error가있다면 error Message가 담긴 객체를 리턴해주고 return값이 없다면 (error가 없는경우 ) ok가 true인 객체를 리턴합니다.
오류를 throw하는 대신 오류를 리턴하는 방식으로 처리시 이점
프론트엔드에서 더 예측 가능한 방식으로 오류를 얻을 수 있고 알림도 표시할 수 있습니다.
password를 hash하기위해 typeorm의 Listeners을 사용할 겁니다.
Entity Listeners란 ?
entity에는 특정 entity이벤트에 수신하는 custom logic메서드를 가질수 있는데 이러한 메서드들을 데코레이터로 표시할 수 있습니다.
@BeforeInsert
entity에 repository메서드인 save를 사용해 db에 저장하기전에 custom logic메서드를 실행시켜 줍니다.
hash를위해 bcrypt설치
npm i bcrypt
npm i @types/bcrypt
user.entity.ts
import {
Field,
InputType,
ObjectType,
registerEnumType,
} from '@nestjs/graphql';
import { CoreEntity } from 'src/common/entities/core.entity';
import * as bcrypt from 'bcrypt';
import { BeforeInsert, Column, Entity } from 'typeorm';
import { InternalServerErrorException } from '@nestjs/common';
enum UserRole {
Client,
Owner,
Delivery,
}
registerEnumType(UserRole, { name: 'UserRole' });
@InputType({ isAbstract: true })
@ObjectType()
@Entity()
export class User extends CoreEntity {
@Column()
@Field((type) => String)
email: string;
@Column()
@Field((type) => String)
password: string;
@Column({ type: 'enum', enum: UserRole })
@Field((type) => UserRole)
role: UserRole;
@BeforeInsert()
async hashpassword(): Promise<void> {
try {
this.password = await bcrypt.hash(this.password, 10);
} catch (error) {
console.log(error);
throw new InternalServerErrorException();
}
}
}
위와 같이 코드를 작성해주면 user가 Insert되기전 hashpassword메서드를 호출합니다. hashpassword메서드에 있는 bcrypt함수를 통해 비밀번호를 해쉬할 수 있습니다.
InternalServerErrorException
Nest는 자체적으로 예외 레이어를 두고 있습니다. 애플리케이션을 통틀어 제대로 처리하지 못한 예외를 처리하는 역할을 합니다. 아무런 작업을 하지 않아도 기본 예외 처리기가 예외를 잡아서 유저가 이해하기 쉬운 형태로 변환하여 전송합니다.
InternalServerError는 "요청을 처리하는 과정에서 서버가 예상하지 못한 상황에 놓였다는 것을 나타낸다"고 되어 있습니다.
users/login.dto.ts 파일 만들기
login할때는 email,password만 필요합니다.
output dto는 많이 쓰일 것 같아 common/dto/output.dto.ts에 작성했습니다.
import { Field, InputType, ObjectType, PickType } from '@nestjs/graphql';
import { MutationOutput } from 'src/common/dtos/output.dto';
import { User } from '../entities/user.entity';
@InputType()
export class LoginInput extends PickType(User, ['email', 'password']) {}
@ObjectType()
export class LoginOutput extends MutationOutput {
@Field((type) => String, { nullable: true })
token?: string;
}
common/dto/output.dto.ts
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class MutationOutput {
@Field((type) => String, { nullable: true })
error?: string;
@Field((type) => Boolean)
ok: boolean;
}
users.resolver.ts
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import {
CreateAccountInput,
CreateAccountOutput,
} from './dtos/create-account.dto';
import { LoginInput, LoginOutput } from './dtos/login.dto';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';
@Resolver((of) => User)
export class UsesrResolver {
constructor(private readonly usesrService: UsersService) {}
...Query
...Mutation
@Mutation((returns) => LoginOutput)
async login(@Args('input') loginInput: LoginInput) {}
}
user가 로그인시 먼저 해당 이메일을 가진 user가 있는지 확인후 없으면 에러메세지를 보여줍니다.
해당 user가 있으면 user entity에 있는 checkpassword 메서드를 이용해 비밀번호를 해쉬후 비교하고
해당 user의 비밀번호와 일치하지 않는다면 에러메세지를 보여줍니다.
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateAccountInput } from './dtos/create-account.dto';
import { LoginInput } from './dtos/login.dto';
import { User } from './entities/user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly users: Repository<User>,
) {}
//create User
...
//Login User
async login({
email,
password,
}: LoginInput): Promise<{ ok: boolean; error?: string; token?: string }> {
// make a JMT and give it to the user
try {
// find ths user with the email
const user = await this.users.findOne({ email });
if (!user) {
return { ok: false, error: 'User not found' };
}
// check if the passowrd is correct
const passwordCorrect = await user.checkPassword(password);
if (!passwordCorrect) {
return { ok: false, error: 'Wrong password' };
}
return { ok: true, token: 'goododo' };
} catch (error) {
return { ok: false, error };
}
}
}
user.entity.ts
import {
Field,
InputType,
ObjectType,
registerEnumType,
} from '@nestjs/graphql';
import { CoreEntity } from 'src/common/entities/core.entity';
import * as bcrypt from 'bcrypt';
import { BeforeInsert, Column, Entity } from 'typeorm';
import { InternalServerErrorException } from '@nestjs/common';
import { IsEmail, IsEnum } from 'class-validator';
enum UserRole {
Client,
Owner,
Delivery,
}
registerEnumType(UserRole, { name: 'UserRole' });
@InputType({ isAbstract: true })
@ObjectType()
@Entity()
export class User extends CoreEntity {
@Column()
@Field((type) => String)
@IsEmail()
email: string;
@Column()
@Field((type) => String)
password: string;
@Column({ type: 'enum', enum: UserRole })
@Field((type) => UserRole)
@IsEnum(UserRole)
role: UserRole;
@BeforeInsert()
async hashpassword(): Promise<void> {
...
}
async checkPassword(aPassword: string): Promise<boolean> {
try {
const ok = await bcrypt.compare(aPassword, this.password);
return ok;
} catch (error) {
console.log(error);
throw new InternalServerErrorException();
}
}
}
bcrypt의 compare메서드를 통해 user가 입력한 비밀번호를 해쉬한 뒤 해당 user의 해쉬된 비밀번호와 비교하여 일치하면 true 일치하지 않으면 false를 리턴합니다.
users.resolver.ts
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import {
CreateAccountInput,
CreateAccountOutput,
} from './dtos/create-account.dto';
import { LoginInput, LoginOutput } from './dtos/login.dto';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';
@Resolver((of) => User)
export class UsesrResolver {
constructor(private readonly usesrService: UsersService) {}
@Query((returns) => Boolean)
hi(): boolean {
return true;
}
@Mutation((returns) => CreateAccountOutput)
async createAccount(
@Args('input') createAccountInput: CreateAccountInput,
): Promise<CreateAccountOutput> {
try {
return await this.usesrService.createAccount(createAccountInput);
} catch (error) {
return {
ok: false,
error,
};
}
}
@Mutation((returns) => LoginOutput)
async login(@Args('input') loginInput: LoginInput) {
try {
return await this.usesrService.login(loginInput);
} catch (error) {
return { ok: false, error };
}
}
}