backend/src/auth/auth.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { AuthController } from './auth.controller';
describe('AuthController', () => {
// let controller: AuthController;
beforeEach(async () => {
// const module: TestingModule = await Test.createTestingModule({
// controllers: [AuthController],
// }).compile();
// controller = module.get<AuthController>(AuthController);
});
it('should be defined', () => {
// expect(controller).toBeDefined();
});
});
backend/src/auth/auth.controller.ts
import {
BadRequestException,
Controller,
Get,
Logger,
Post,
Query,
Redirect,
Req,
Res,
} from '@nestjs/common';
import axios from 'axios';
import { Request, Response } from 'express';
import { UserService } from 'src/user/user.service';
import { AuthService } from './auth.service';
import { ConfigService } from '@nestjs/config';
import { ApiTags } from '@nestjs/swagger';
import * as domain from "domain";
@ApiTags('Auth')
@Controller('api/v1/auth')
export class AuthController {
private readonly origin: string;
private readonly client_id: string;
private readonly redirect_uri: string;
private readonly logger = new Logger(AuthController.name);
private readonly accessTokenPath = '/api/v1';
private readonly refreshTokenPath = '/api/v1/auth/redirect';
constructor(
private readonly config: ConfigService,
private readonly userService: UserService,
private readonly authService: AuthService,
) {
this.origin = this.config.get<string>('ORIGIN');
this.client_id = this.config.get<string>('REST_API');
this.redirect_uri = `${this.origin}/api/v1/auth/redirect`;
}
@Get('authorize')
@Redirect()
authorize(@Query('scope') scope: string) {
const scopeParam = scope ? `&scope=${scope}` : '';
return {
url: `https://kauth.kakao.com/oauth/authorize?client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&response_type=code${scopeParam}`,
};
}
@Get('redirect')
async redirect(@Query('code') code: string, @Res() res: Response) {
const data = {
grant_type: 'authorization_code',
client_id: this.client_id,
redirect_uri: this.redirect_uri,
code: code,
};
try {
const accessTokenResponse = await axios.post(
'<https://kauth.kakao.com/oauth/token>',
data,
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
},
);
const kakaoUserInfoResponse = await axios.post(
'<https://kapi.kakao.com/v2/user/me>',
{},
{
headers: {
Authorization: 'Bearer ' + accessTokenResponse.data.access_token,
'Content-Type': 'application/x-www-form-urlencoded',
},
},
);
const kakaoId = kakaoUserInfoResponse.data.id;
const nickname = kakaoUserInfoResponse.data.properties.nickname;
let user = await this.userService.getUserByKakaoId(kakaoId);
if (!user) {
user = await this.userService.createUserWithKakaoIdAndUsername(
kakaoId,
nickname,
);
}
const {accessToken, refreshToken} = await this.authService.createTokens(
user.id,
);
this.setTokens(
res,
accessToken,
refreshToken,
'here-there-fe.vercel.app',
'<https://here-there-fe.vercel.app/boards>',
);
} catch {
throw new BadRequestException();
}
}
@Get('redirect/refresh')
async refresh(@Req() req: Request, @Res() res: Response) {
const {accessToken, refreshToken} = await this.authService.refreshTokens(
req.cookies['refreshToken'],
);
this.setTokens(res, accessToken, refreshToken, 'here-there-fe.vercel.app');
}
@Post('logout')
async logout(@Res() res: Response) {
res.clearCookie('accessToken', {path: this.accessTokenPath});
res.clearCookie('refreshToken', {path: this.refreshTokenPath});
res.sendStatus(204);
}
// 카카오 로그인 테스트 용도, 삭제 예정
@Get()
getTestHtml(@Res() res: Response): void {
res.header('Content-Type', 'text/html');
res.send(`
<html>
<body>
<a href="/api/v1/auth/authorize">
<img src="//k.kakaocdn.net/14/dn/btqCn0WEmI3/nijroPfbpCa4at5EIsjyf0/o.jpg" width="222"/>
</a>
</body>
</html>
`);
}
private setTokens(
res: Response,
accessToken: string,
refreshToken: string,
domain: string,
redirectUrl?: string,
) {
// 쿠키 설정 로그 추가
this.logger.log(`Setting accessToken cookie: ${accessToken}`);
this.logger.log(`Setting refreshToken cookie: ${refreshToken}`);
this.logger.log(`Cookie domain: ${domain}`);
this.logger.log(`Redirect URL: ${redirectUrl}`);
res.cookie('accessToken', accessToken, {
httpOnly: true,
secure: true,
sameSite: 'none',
//domain,
path: this.accessTokenPath,
});
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'none',
//domain,
path: this.refreshTokenPath,
});
this.logger.log(`Tokens issued - Access Token: ${accessToken}, Refresh Token: ${refreshToken}`);
if (redirectUrl) {
res.redirect(302, redirectUrl);
} else {
res.sendStatus(204);
}
}
}
backend/src/auth/auth.decorator.ts
import { ExecutionContext, createParamDecorator } from '@nestjs/common';
export const Token = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const token = request.token;
console.log('Decorator token:', token);
return data ? token?.[data] : token;
},
);
backend/src/auth/auth.guard.spec.ts
import { AuthGuard } from './auth.guard';
describe('AuthGuard', () => {
it('should be defined', () => {
expect(new AuthGuard(null, null)).toBeDefined();
});
});
backend/src/auth/auth.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromCookie(request);
if (!token) {
console.log('토큰이 제공되지 않았습니다.');
throw new UnauthorizedException('토큰이 제공되지 않았습니다.');
}
try {
const payload = await this.jwtService.verifyAsync(token);
//console.log('토큰 페이로드:', payload);
request['token'] = payload;
} catch (err) {
console.log('유효하지 않은 토큰:', err);
throw new UnauthorizedException('유효하지 않은 토큰');
}
return true;
}
private extractTokenFromCookie(request: Request): string | undefined {
return request.cookies['accessToken'];
}
}
backend/src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { UserModule } from '../user/user.module';
import { AuthGuard } from './auth.guard';
@Module({
imports: [
ConfigModule,
JwtModule.registerAsync({
global: true,
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: {
expiresIn: configService.get<string>('JWT_EXPIRATION_TIME'),
},
}),
}),
UserModule,
],
controllers: [AuthController],
providers: [AuthService, AuthGuard],
exports: [AuthGuard, JwtModule],
})
export class AuthModule {}
backend/src/auth/auth.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let service: AuthService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
}).compile();
service = module.get<AuthService>(AuthService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
backend/src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AuthService {
constructor(
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
) {}
private async generateTokens(payload: any) {
const refreshTokenExpirationTime = this.configService.get<string>(
'REFRESH_TOKEN_EXPIRATION_TIME',
);
const accessToken = await this.jwtService.signAsync(payload);
const refreshToken = refreshTokenExpirationTime
? await this.jwtService.signAsync(payload, {
expiresIn: refreshTokenExpirationTime,
})
: undefined;
return { accessToken, refreshToken };
}
async createTokens(
id: number,
): Promise<{ accessToken: string; refreshToken: string }> {
const payload = { sub: id };
return this.generateTokens(payload);
}
async refreshTokens(refreshToken: string) {
const payload = await this.jwtService.verifyAsync(refreshToken);
const newPayload = { sub: payload['sub'] };
return this.generateTokens(newPayload);
}
}