Skip to content

Commit

Permalink
Feat: 쿠키 설정 추가
Browse files Browse the repository at this point in the history
- 리프레시 토큰을 쿠키에 저장하도록 설정
- 쿠키 설정을 위한 모듈 설치
- 쿠키 설정을 위한 모듈 설정
- 쿠키 설정을 위한 모듈을 main.ts에 적용
- 리프레시 토큰을 가지고 액세스 토큰을 재발급하는 로직 추가

Related to #21
  • Loading branch information
Zamoca42 committed Jan 27, 2024
1 parent 94e03d7 commit bbe3404
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 24 deletions.
2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@supabase/supabase-js": "^2.39.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
"cross-env": "^7.0.3",
"nest-winston": "^1.9.4",
"passport": "^0.7.0",
Expand All @@ -41,6 +42,7 @@
"devDependencies": {
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/cookie-parser": "^1.4.6",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
Expand Down
51 changes: 38 additions & 13 deletions backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
import { Controller, Post, Body, Get, HttpCode } from '@nestjs/common';
import { Controller, Post, Body, Get, HttpCode, Res } from '@nestjs/common';
import { AuthService } from './auth.service';
import { ResponseEntity } from '../common/entity/response.entity';
import { UserCredential } from './dto/req-credential-body.dto';
import { CredentialResponse } from './dto/res-credential.dto';
import { AllowAny } from './auth.constant';
import { ApiTags } from '@nestjs/swagger';
import { UserCredential } from './dto/req-auth-body.dto';
import { AuthResponse } from './dto/res-auth.dto';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { SwaggerAPI } from '../common/swagger/api.decorator';
import { AuthToken } from './dto/req-auth-token.dto';
import { Response } from 'express';
import { cookieOptions } from '../common/config/cookie.config';
import { ReqToken } from './decorator/token.decorator';

@ApiTags('인증')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@Post('login')
@AllowAny()
@HttpCode(200)
@SwaggerAPI({ name: '로그인', allowAny: true, model: CredentialResponse })
async signIn(@Body() request: UserCredential) {
const response = await this.authService.signInWith(request.toCredentials());
@SwaggerAPI({ name: '로그인', allowAny: true, model: AuthResponse })
async signIn(
@Body() request: UserCredential,
@Res({ passthrough: true }) response: Response,
) {
const result = await this.authService.signInWith(request.toCredentials());
response.cookie('refreshToken', result.refreshToken, cookieOptions);
return ResponseEntity.OK_WITH(
`Successfully login ${request.email}`,
new CredentialResponse(response),
result,
);
}

@Post('register')
@AllowAny()
@SwaggerAPI({ name: '회원가입', allowAny: true })
async signUp(@Body() request: UserCredential) {
await this.authService.signUpWith(request.toCredentials());
Expand All @@ -34,17 +39,37 @@ export class AuthController {
);
}

@Get('refresh')
@ApiBearerAuth('accessToken')
@SwaggerAPI({
name: '토큰 재발급',
model: AuthResponse,
allowAny: true,
})
async refreshSession(
@ReqToken() request: AuthToken,
@Res({ passthrough: true }) response: Response,
) {
const result = await this.authService.refreshSession(
request.refreshToken,
request.accessToken,
);
response.cookie('refreshToken', result.refreshToken, cookieOptions);
return ResponseEntity.OK_WITH('Successfully refresh token', result);
}

@Get('user')
@SwaggerAPI({ name: '유저 정보 조회(임시)', model: CredentialResponse })
@SwaggerAPI({ name: '유저 정보 조회(임시)', model: AuthResponse })
async getUser() {
const response = await this.authService.getCurrentUser();
return ResponseEntity.OK_WITH('Successfully find user', response);
}

@Get('logout')
@SwaggerAPI({ name: '로그아웃' })
async signOut() {
async signOut(@Res({ passthrough: true }) res: Response) {
await this.authService.signOut();
res.clearCookie('refreshToken');
return ResponseEntity.OK('Successfully logout user');
}
}
28 changes: 20 additions & 8 deletions backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,41 @@
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { Session, User, UserResponse } from '@supabase/supabase-js';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { User, UserResponse } from '@supabase/supabase-js';
import { SupabaseService } from '../supabase/supabase.service';
import { CredentialProps } from './auth.interface';
import { AuthResponse } from './dto/res-auth.dto';

@Injectable()
export class AuthService {
private readonly logger = new Logger(AuthService.name);
constructor(private readonly supabaseService: SupabaseService) {}

async signInWith(credentials: CredentialProps): Promise<Session> {
async signInWith(credentials: CredentialProps): Promise<AuthResponse> {
const { data, error } =
await this.supabaseService.client.auth.signInWithPassword(credentials);
this.supabaseService.fail(error);
return data.session;
this.supabaseService.exception(error);
return new AuthResponse(data.session);
}

async signUpWith(credentials: CredentialProps): Promise<void> {
const { data, error } =
await this.supabaseService.client.auth.signUp(credentials);
this.supabaseService.fail(error);
this.supabaseService.exception(error);
if (this.checkUserRole(data.user) === '') {
throw new UnauthorizedException('User already registered');
}
}

async refreshSession(
refreshToken: string,
accessToken: string,
): Promise<AuthResponse> {
const { data, error } = await this.supabaseService.client.auth.setSession({
refresh_token: refreshToken,
access_token: accessToken,
});
this.supabaseService.exception(error);
return new AuthResponse(data.session);
}

checkUserRole(user: User): string {
return user.role;
}
Expand All @@ -34,6 +46,6 @@ export class AuthService {

async signOut(): Promise<void> {
const { error } = await this.supabaseService.client.auth.signOut();
this.supabaseService.fail(error);
this.supabaseService.exception(error);
}
}
6 changes: 6 additions & 0 deletions backend/src/common/config/cookie.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { CookieOptions } from 'express';

export const cookieOptions: CookieOptions = {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
};
3 changes: 2 additions & 1 deletion backend/src/common/swagger/api.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { PageEntity } from '../dto/get-pagination-list.dto';
import { ResponseEntity } from '../entity/response.entity';
import { createSchema } from './api.schema';
import { AllowAny } from '../../auth/decorator/pass-auth.decorator';

export const SwaggerAPI = ({
name,
Expand All @@ -21,7 +22,7 @@ export const SwaggerAPI = ({
isPagination = false,
}: OptionsProps): MethodDecorator => {
const apiAuthorization = allowAny
? []
? [AllowAny()]
: [
ApiUnauthorizedResponse({
description:
Expand Down
2 changes: 2 additions & 0 deletions backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useContainer } from 'class-validator';
import { swaggerConfig } from './common/config/swagger.config';
import { pipeOptions } from './common/config/pipe.config';
import { corsOptions } from './common/config/cors.config';
import * as cookieParser from 'cookie-parser';

async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule, {
Expand All @@ -16,6 +17,7 @@ async function bootstrap(): Promise<void> {
SwaggerModule.setup('/api/swagger', app, document);

app.setGlobalPrefix('api');
app.use(cookieParser());
app.enableCors(corsOptions);
app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));
app.useGlobalPipes(new ValidationPipe(pipeOptions));
Expand Down
5 changes: 3 additions & 2 deletions backend/src/supabase/supabase.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import { ExtractJwt } from 'passport-jwt';
@Injectable({ scope: Scope.REQUEST })
export class SupabaseService {
public client: SupabaseClient;
public anon: SupabaseClient;

constructor(@Inject(REQUEST) private readonly request: Request) {
this.client = createClient<Database>(
process.env.SUPABASE_URL,
process.env.SUPABASE_KEY,
{
auth: {
autoRefreshToken: true,
autoRefreshToken: false,
detectSessionInUrl: false,
persistSession: false,
},
Expand All @@ -29,7 +30,7 @@ export class SupabaseService {
);
}

fail(error: Error) {
exception(error: Error) {
if (error) {
throw error;
}
Expand Down
31 changes: 31 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit bbe3404

Please sign in to comment.