지난 시간에 이어 이번에는 User 테이블을 생성하고 User를 검증하는 로직을 작성해서 validate
메소드에 추가할 것이다.
우선 User Entity를 작성한다.
import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm';
import { UserRole } from '../const/role.enum';
import { AuthProvider } from '../../auth/const/provider.enum';
@Entity()
@Unique(['email', 'provider'])
export class User {
@PrimaryGeneratedColumn({
type: 'bigint',
unsigned: true,
primaryKeyConstraintName: 'PK_user_id',
comment: '유저 아이디 PK',
})
id: number;
@Column({
comment: 'Oauth 플랫폼 유저 아이디',
})
externalId: string;
// @Column({
// comment: '사용자 닉네임',
// })
// nickname: string;
@Column({
nullable: false,
comment: '사용자 이메일',
})
email: string;
@Column({
type: 'enum',
enum: AuthProvider,
nullable: false,
comment: 'Oauth 제공자',
})
provider: string;
@Column({
type: 'enum',
enum: UserRole,
default: UserRole.USER,
comment: '사용자 role',
})
role: UserRole;
}
위와 같이 User Entity를 작성해주고 TypeORM으로 DB를 연결한다.
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
import { ConfigModule } from '@nestjs/config';
import * as process from 'node:process';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
ENV_DB_DATABASE_KEY,
ENV_DB_HOST_KEY,
ENV_DB_PASSWORD_KEY,
ENV_DB_PORT_KEY,
ENV_DB_USERNAME_KEY,
} from './common/const/env-keys.const';
import { UsersModule } from './users/users.module';
import { User } from './users/entities/user.entity';
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: `.env.${process.env.NODE_ENV}`,
isGlobal: true,
}),
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env[ENV_DB_HOST_KEY],
port: Number.parseInt(process.env[ENV_DB_PORT_KEY]),
username: process.env[ENV_DB_USERNAME_KEY],
password: process.env[ENV_DB_PASSWORD_KEY],
database: process.env[ENV_DB_DATABASE_KEY],
entities: [User],
synchronize: true,
logging: true,
}),
AuthModule,
UsersModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
이번에는 UserService를 작성한다.
UserService에 회원을 검증하는 로직을 담당하는 verifyUser
메소드를 작성할 것이다.
사용자를 찾지 못하면 새로운 user를 생성하여 DB에 저장한다.
import { Injectable, NotFoundException } from '@nestjs/common';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { AuthProvider } from '../auth/const/provider.enum';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
async verifyUser(id: number): Promise<User>;
async verifyUser(
email: string,
provider: keyof typeof AuthProvider,
): Promise<User>;
/**
* 사용자가 존재하는지 검증
* */
async verifyUser(
idOrEmail: number | string,
provider?: keyof typeof AuthProvider,
): Promise<User> {
// idOrEmail이 string인데 provider에 값이 없으면 에러 발생
if (typeof idOrEmail === 'string' && !provider) {
throw new Error('email로 회원 검증 시 OAuth 제공자가 반드시 필요합니다.');
}
// idOrEmail 매개변수
// number 타입인 경우 id로 회원 조회
// string 타입인 경우 email, provider로 회원 조회
return this.findUser(
typeof idOrEmail === 'number'
? { id: idOrEmail }
: { email: idOrEmail, provider },
);
}
/**
* DB에서 사용자 찾기
* */
async findUser(
criteria: Pick<User, 'id'> | Pick<User, 'email' | 'provider'>,
) {
const user = await this.userRepository.findOne({ where: criteria });
if (!user) {
throw new NotFoundException('사용자를 찾을 수 없습니다.');
}
return user;
}
/**
* user 생성 후 DB 저장
* */
async createUser(createUserDto: CreateUserDto) {
const user = this.userRepository.create({
...createUserDto,
});
return this.userRepository.save(user);
}
}
이제 GoogleStrategy
클래스의 validate
메소드 안에 작성한 verifyUser
를 포함시킨다.
import { PassportStrategy } from '@nestjs/passport';
import { Profile, Strategy } from 'passport-google-oauth20';
import { Injectable, NotFoundException } from '@nestjs/common';
import * as process from 'node:process';
import {
ENV_GOOGLE_CALLBACK_URL_KEY,
ENV_GOOGLE_CLIENT_ID_KEY,
ENV_GOOGLE_CLIENT_SECRET_KEY,
} from '../../common/const/env-keys.const';
import { UsersService } from '../../users/users.service';
import { UserRole } from '../../users/const/role.enum';
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor(private readonly userService: UsersService) {
super({
clientID: process.env[ENV_GOOGLE_CLIENT_ID_KEY],
clientSecret: process.env[ENV_GOOGLE_CLIENT_SECRET_KEY],
callbackURL: process.env[ENV_GOOGLE_CALLBACK_URL_KEY],
scope: ['email', 'profile'],
});
}
async validate(accessToken: string, refreshToken: string, profile: Profile) {
const { emails } = profile;
const email = emails[0].value;
let user;
try {
user = await this.userService.verifyUser(email, 'GOOGLE');
} catch (err) {
if (err instanceof NotFoundException) {
user = await this.userService.createUser({
email,
provider: 'GOOGLE',
externalId: profile.id,
role: UserRole.USER,
});
} else {
throw err;
}
}
/* const user = {
email: emails[0].value,
firstName: name.givenName,
lastName: name.familyName,
externalId: profile.id,
accessToken,
refreshToken,
};*/
return user;
}
}
이제 구글 로그인 api를 요청을 하게 되면
import { Controller, Get, Req, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthGuard } from '@nestjs/passport';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Get('google')
@UseGuards(AuthGuard('google'))
async googleAuthenticate() {}
@Get('google/callback')
@UseGuards(AuthGuard('google'))
async googleRedirect(@Req() req) {
console.log(req.user);
}
}
구글 로그인 요청을 하면 구글 로그인 페이지로 redirect 되고, 구글 로그인이 완료되면, GCP 프로젝트에서 등록했던 callback url로 구글 서버에서 redirect 시킨다.
요청 결과로 다음과 같이 user 가 잘 생성되었다.
google oauth 로그인을 passport를 이용해서 구현하는 것까지 마무리 되었다.
user가 없다면 user를 생성하여 저장하고 기존 user가 있다면 user를 반환하게 된다.
다음에는 jwt 토큰을 자체적으로 발행하여 사용자 인가 처리 작업을 진행할 것이다.