Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cloudbuild.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ steps:
- set
- image
- deployment/groomeong-backend
- backend-sha256-1=asia.gcr.io/project-groomeong/backend:1.6
- backend-sha256-1=asia.gcr.io/project-groomeong/backend:1.7
env:
- CLOUDSDK_COMPUTE_ZONE=asia-northeast3
- CLOUDSDK_CONTAINER_CLUSTER=autopilot-cluster-5
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: '3.7'

services:
my-backend:
image: asia.gcr.io/project-groomeong/backend:1.6
image: asia.gcr.io/project-groomeong/backend:1.7
platform: linux/x86_64
build:
context: .
Expand Down
6 changes: 3 additions & 3 deletions src/apis/auth/__test__/auth.resolver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ const EXAMPLE_USER: User = {
password: 'exampleUserPassword',
phone: 'exampleUserPhone',
image: 'exampleUserImage',
createAt: new Date(),
deleteAt: new Date(),
updateAt: new Date(),
createdAt: new Date(),
deletedAt: new Date(),
updatedAt: new Date(),
dogs: [null],
reservation: [null],
};
Expand Down
140 changes: 93 additions & 47 deletions src/apis/auth/__test__/auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { CACHE_MANAGER, UnprocessableEntityException } from '@nestjs/common';
import {
CACHE_MANAGER,
UnauthorizedException,
UnprocessableEntityException,
} from '@nestjs/common';
import { Cache } from 'cache-manager';
import { UsersService } from 'src/apis/users/user.service';
import { User } from 'src/apis/users/entities/user.entity';
import { AuthService } from '../auth.service';
import { IContext } from 'src/commons/interface/context';
import { JwtService } from '@nestjs/jwt';
import * as httpMocks from 'node-mocks-http';
import { MockUsersRepository } from './auth.mocking.dummy';
import { MailerModule } from '@nestjs-modules/mailer';

jest.mock('../auth.service');

Expand All @@ -20,45 +21,51 @@ const EXAMPLE_USER: User = {
password: 'exampleUserPassword',
phone: 'exampleUserPhone',
image: 'exampleUserImage',
createAt: new Date(),
deleteAt: new Date(),
updateAt: new Date(),
createdAt: new Date(),
deletedAt: new Date(),
updatedAt: new Date(),
dogs: [null],
reservation: [null],
};

describe('AuthResolver', () => {
let authService: AuthService;
let usersService: UsersService;
let jwtService: JwtService;
let cache: Cache;
let context: IContext;
let jwt: JwtService;
let cacheManager: Cache;

const context: IContext = {
req: httpMocks.createRequest(),
res: httpMocks.createResponse(),
};
context.req.user = new User();
context.req.user.id = EXAMPLE_USER.id;
const email = EXAMPLE_USER.email;
const password = EXAMPLE_USER.password;
const process = {
env: {
JWT_ACCESS_KEY: 'exampleAccess',
JWT_REFRESH_KEY: 'exampleRefresh',
},
};

beforeEach(async () => {
jest.clearAllMocks();
const usersModule: TestingModule = await Test.createTestingModule({
imports: [
MailerModule.forRootAsync({
useFactory: () => ({
transport: {
service: 'Gmail',
host: process.env.EMAIL_HOST,
port: Number(process.env.DATABASE_PORT),
secure: false,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
},
}),
}),
],
const modules: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
UsersService,
{
provide: getRepositoryToken(User),
useClass: MockUsersRepository,
provide: UsersService,
useValue: {
findOneByEmail: jest.fn(() => EXAMPLE_USER),
create: jest.fn(() => EXAMPLE_USER),
},
},
{
provide: JwtService,
useValue: {
verify: jest.fn(() => 'EXAMPLE_TOKEN'),
},
},
{
provide: CACHE_MANAGER,
Expand All @@ -69,22 +76,13 @@ describe('AuthResolver', () => {
},
],
}).compile();
const authModule: TestingModule = await Test.createTestingModule({
providers: [AuthService],
}).compile();

usersService = usersModule.get<UsersService>(UsersService);
authService = authModule.get<AuthService>(AuthService);
cache = usersModule.get(CACHE_MANAGER);
context = {
req: httpMocks.createRequest(),
res: httpMocks.createResponse(),
};
authService = modules.get<AuthService>(AuthService);
usersService = modules.get<UsersService>(UsersService);
jwt = modules.get<JwtService>(JwtService);
cacheManager = modules.get(CACHE_MANAGER);
});

const email = EXAMPLE_USER.email;
const password = EXAMPLE_USER.password;

describe('login', () => {
it('의존성주입한 usersService 에서 email로 찾아오기', async () => {
jest
Expand Down Expand Up @@ -129,10 +127,58 @@ describe('AuthResolver', () => {
});

describe('logout', () => {
//
});
const req = context.req;
it('토큰 증명해서 로그아웃 인가', async () => {
const req = {
headers: {
authorization: 'Bearer ACCESS_TOKEN',
cookie: 'refreshToken=REFRESH_TOKEN',
},
};

describe('restoreAccessToken', () => {
//
try {
const accessToken = await req.headers['authorization'].replace(
'Bearer ',
'',
);
const refreshToken = await req.headers['cookie'].split(
'refreshToken=',
)[1];

// accessToken 토큰
const jwtAccessKey = jwt.verify(accessToken);

// refresh 토큰
const jwtRefreshKey = jwt.verify(refreshToken);

await cacheManager.set(`accessToken:${accessToken}`, 'accessToken', {
ttl: jwtAccessKey['exp'] - jwtAccessKey['iat'],
});

await cacheManager.set(`refreshToken:${refreshToken}`, 'refreshToken', {
ttl: jwtRefreshKey['exp'] - jwtRefreshKey['iat'],
});

expect(jwt.verify).toBeCalled();

return '로그아웃에 성공했습니다.';
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect(error.message).toBe('로그아웃을 실패했습니다.');
throw new UnauthorizedException('로그아웃을 실패했습니다.');
}
});

describe('restoreAccessToken', () => {
const user = EXAMPLE_USER;

beforeEach(async () => {
await authService.getAccessToken({ user });
});

it('getAccessToken should be called', () => {
expect(authService.getAccessToken).toBeCalled();
});
});
});
});
53 changes: 50 additions & 3 deletions src/apis/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,48 @@ export class AuthService {
},
);

// 개발 환경
// res.setHeader('Authorization', ''); // Authorization 헤더 값을 빈 문자열로 설정합니다.
// res.clearCookie('refreshToken'); // refreshToken 쿠키를 삭제합니다.

// 배포 환경
const originList = [
'http://localhost:3000',
'http://127.0.0.1:3000',
'http://34.64.53.80:3000',
'https://groomeong.shop',
'https://groomeong.store',
];
const origin = req.headers.origin;
if (originList.includes(origin)) {
// 리소스에 엑세스하기 위해 코드 요청을 허용하도록 브라우저에 알리는 응답
res.setHeader('Access-Control-Allow-Origin', origin);
}

// 프런트엔드 js 코드에 대한 응답을 노출할지 여부를 브라우저에 알려준다.
res.setHeader('Access-Control-Allow-Credentials', 'true');
// 리소스에 엑세스할 때 허용되는 하나 이상의 메서드를 지정해준다.
res.setHeader(
'Access-Control-Allow-Methods', //
'GET, HEAD, OPTIONS, POST, PUT',
);
// 실제 요청 중에 사용할 수 있는 HTTP 헤더를 나타내는 실행 전 요청에 대한 응답.
// X-Custom-Header => 서버에 대한 cors 요청에 의해 지원
// Upgrade-Insecure-Requests => 여러 헤더에 대한 지원을 지정
res.setHeader(
'Access-Control-Allow-Headers',
'Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers',
);

res.clearCookie('refreshToken', {
path: '/',
domain: '.groomeong.shop',
secure: true,
httpOnly: true,
sameSite: 'none',
maxAge: 0,
});

return '로그아웃 성공';
} catch (err) {
throw new UnauthorizedException('로그아웃 실패');
Expand All @@ -92,7 +134,7 @@ export class AuthService {
getAccessToken({ user }: IAuthServiceGetAccessToken): string {
return this.jwtService.sign(
{ sub: user.id, email: user.email }, //ƒ
{ secret: process.env.JWT_ACCESS_KEY, expiresIn: '2w' },
{ secret: process.env.JWT_ACCESS_KEY, expiresIn: '10h' },
);
}

Expand All @@ -101,7 +143,12 @@ export class AuthService {
{ sub: user.id, email: user.email }, //
{ secret: process.env.JWT_REFRESH_KEY, expiresIn: '2w' },
);
console.log('🐳🐳🐳🐳🐳', refreshToken);

// 로컬(개발환경)
// res.setHeader('set-Cookie', `refreshToken=${refreshToken}; path=/;`);

// 배포 환경
const originList = [
'http://localhost:3000',
'http://127.0.0.1:3000',
Expand All @@ -126,8 +173,8 @@ export class AuthService {
// X-Custom-Header => 서버에 대한 cors 요청에 의해 지원
// Upgrade-Insecure-Requests => 여러 헤더에 대한 지원을 지정
res.setHeader(
'Access-Control-Allow-Headers', //
'Access-Control-Allow-Headers, Origin, Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers',
'Access-Control-Allow-Headers',
'Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers',
);

res.setHeader(
Expand Down
3 changes: 1 addition & 2 deletions src/apis/auth/strategies/jwt-refresh.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'refresh') {
super({
jwtFromRequest: (req) => {
const cookie = req.headers.cookie;
const refreshToken = cookie.replace('refreshToken=', '');
return refreshToken;
if (cookie) return cookie.replace('refreshToken=', '');
},
secretOrKey: process.env.JWT_REFRESH_KEY,
passReqToCallback: true,
Expand Down
Loading