Skip to content

Commit c9cee48

Browse files
authored
로컬 스토리지에서 쿠키로 로그인 로직 변경 (#117)
* fix: gitignore 수정 * feat: 관리자 로그인 페이지 만들기 * feat: 로그인 기능 구현 * feat: guard를 통한 어드민 엔드포인트 보호 * feat: 평문 비교가 아닌 해시로 비교 하도록 변경 * feat: ProtectedRoutes로 라우트 접근 보호 컴포넌트 추가 * fix: Bearer 토큰에 관한 설정 진행 * feat: axios interceptor로 어드민 api 요청에 관한 액세스에 토큰 추가 기능 구현 * feat: 서버에서 쿠키 생성 로직 추가 * feat: admin guard에 쿠키 파싱 진행 * feat: 클라이언트에서 쿠키를 이용한 로그인 방법으로 리팩토링 진행
1 parent 6f6619a commit c9cee48

File tree

10 files changed

+733
-681
lines changed

10 files changed

+733
-681
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ server/.yarn
1818

1919
server/music*
2020

21+
package-lock.json
22+
2123
# Runtime data
2224
pids
2325
*.pid

client/src/components/ProtectedRoute.tsx

+26-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,34 @@
11
import { Navigate, useLocation } from 'react-router-dom';
2+
import { useEffect, useState } from 'react';
3+
import axios from 'axios';
24

35
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
4-
const token = localStorage.getItem('authorization');
6+
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
57
const location = useLocation();
68

7-
if (!token) {
9+
useEffect(() => {
10+
const checkAuth = async () => {
11+
try {
12+
await axios.get(
13+
`${import.meta.env.VITE_API_URL}/api/admin/verify-token`,
14+
{
15+
withCredentials: true,
16+
},
17+
);
18+
setIsAuthenticated(true);
19+
} catch (error) {
20+
setIsAuthenticated(false);
21+
}
22+
};
23+
24+
checkAuth();
25+
}, []);
26+
27+
if (isAuthenticated === null) {
28+
return <div>Loading...</div>;
29+
}
30+
31+
if (!isAuthenticated) {
832
return <Navigate to="/admin/login" state={{ from: location }} replace />;
933
}
1034

client/src/pages/AdminLoginPage/ui/AdminLoginPage.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ export function AdminLoginPage() {
1212
e.preventDefault();
1313

1414
try {
15-
const { data } = await axios.post('/api/admin/login', { adminKey });
16-
localStorage.setItem('authorization', `Bearer ${data.token}`);
15+
await axios.post(
16+
`${import.meta.env.VITE_API_URL}/api/admin/login`,
17+
{ adminKey },
18+
{ withCredentials: true },
19+
);
1720
navigate('/admin');
1821
} catch (error) {
1922
console.error('Login error:', error);

client/src/shared/api/axios.ts

+2-11
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,8 @@
11
import axios from 'axios';
22

33
const adminApi = axios.create({
4-
baseURL: '/api',
5-
});
6-
7-
adminApi.interceptors.request.use((config) => {
8-
const token = localStorage.getItem('authorization');
9-
if (token) {
10-
config.headers.Authorization = token.startsWith('Bearer ')
11-
? token
12-
: `Bearer ${token}`;
13-
}
14-
return config;
4+
baseURL: `${import.meta.env.VITE_API_URL}/api`,
5+
withCredentials: true,
156
});
167

178
export const albumAPI = {

server/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"cache-manager": "5.2.3",
4343
"class-transformer": "^0.5.1",
4444
"class-validator": "^0.14.1",
45+
"cookie-parser": "^1.4.7",
4546
"express": "^4.21.1",
4647
"fluent-ffmpeg": "^2.1.3",
4748
"hls-server": "^1.5.0",
@@ -65,6 +66,7 @@
6566
"@nestjs/testing": "^10.0.0",
6667
"@types/bcrypt": "^5",
6768
"@types/cache-manager": "^4.0.6",
69+
"@types/cookie-parser": "^1.4.8",
6870
"@types/express": "^5.0.0",
6971
"@types/multer": "^1.4.12",
7072
"@types/node": "^22.9.0",

server/src/admin/admin.controller.ts

+20-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import {
22
Body,
33
Controller,
4+
Get,
45
Post,
6+
Res,
57
UploadedFiles,
68
UseGuards,
79
UseInterceptors,
@@ -18,6 +20,8 @@ import { RoomService } from '@/room/room.service';
1820
import { AdminGuard } from './admin.guard';
1921
import { plainToInstance } from 'class-transformer';
2022
import { MissingSongFiles } from '@/common/exceptions/domain/song/missing-song-files.exception';
23+
import { Response } from 'express';
24+
import { ConfigService } from '@nestjs/config';
2125

2226
export interface UploadedFiles {
2327
albumCover?: Express.Multer.File;
@@ -28,19 +32,32 @@ export interface UploadedFiles {
2832
@Controller('admin')
2933
export class AdminController {
3034
constructor(
35+
private configService: ConfigService,
3136
private readonly adminService: AdminService,
3237
private readonly musicProcessingService: MusicProcessingSevice,
3338
private readonly albumRepository: AlbumRepository,
3439
private readonly roomService: RoomService,
3540
) {}
3641

3742
@Post('login')
38-
async login(@Body() body: { adminKey: string }) {
39-
return this.adminService.login(body.adminKey);
43+
async login(
44+
@Body() body: { adminKey: string },
45+
@Res({ passthrough: true }) response: Response,
46+
) {
47+
const result = await this.adminService.login(body.adminKey);
48+
49+
response.cookie('admin_token', result.token, {
50+
httpOnly: true,
51+
secure: true,
52+
sameSite: 'strict',
53+
maxAge: parseInt(this.configService.get('TOKEN_EXPIRATION')) * 1000,
54+
});
55+
56+
return { message: 'Login successful' };
4057
}
4158

4259
@UseGuards(AdminGuard)
43-
@Post('verify-token')
60+
@Get('verify-token')
4461
async verifyAdminToken() {
4562
return { valid: true };
4663
}

server/src/admin/admin.guard.ts

+1-11
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,15 @@ import {
55
UnauthorizedException,
66
} from '@nestjs/common';
77
import { JwtService } from '@nestjs/jwt';
8-
import { Observable } from 'rxjs';
9-
import { Request } from 'express';
108

119
@Injectable()
1210
export class AdminGuard implements CanActivate {
1311
constructor(private jwtService: JwtService) {}
1412

1513
async canActivate(context: ExecutionContext): Promise<boolean> {
1614
const request = context.switchToHttp().getRequest();
15+
const token = request.cookies['admin_token'];
1716

18-
const token = this.extractTokenFromHeader(request);
1917
if (!token) {
2018
throw new UnauthorizedException('Client does not have token');
2119
}
@@ -32,12 +30,4 @@ export class AdminGuard implements CanActivate {
3230
throw new UnauthorizedException('Invalid token');
3331
}
3432
}
35-
36-
private extractTokenFromHeader(request: Request): string {
37-
if (!request.headers.authorization) {
38-
return undefined;
39-
}
40-
const [check, token] = request.headers.authorization.split(' ');
41-
return check === 'Bearer' ? token : undefined;
42-
}
4333
}

server/src/admin/admin.service.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ export class AdminService {
2323
private jwtService: JwtService,
2424
) {
2525
this.s3 = new AWS.S3({
26-
endpoint: new AWS.Endpoint('https://kr.object.ncloudstorage.com'),
27-
region: 'kr-standard',
26+
endpoint: this.configService.get<string>('S3_END_POINT'),
27+
region: this.configService.get<string>('S3_REGION'),
2828
credentials: {
2929
accessKeyId: this.configService.get<string>('S3_ACCESS_KEY'),
3030
secretAccessKey: this.configService.get<string>('S3_SECRET_KEY'),
@@ -39,16 +39,18 @@ export class AdminService {
3939
if (!isValid) {
4040
throw new UnauthorizedException('Invalid admin key');
4141
}
42+
const expiration = parseInt(this.configService.get('TOKEN_EXPIRATION'));
43+
const now = Math.floor(Date.now() / 1000);
4244

4345
const payload = {
4446
role: 'admin',
45-
iat: Math.floor(Date.now() / 1000),
46-
exp: Math.floor(Date.now() / 1000) + 60 * 60,
47+
iat: now,
48+
exp: now + expiration,
4749
};
4850

4951
return {
5052
token: await this.jwtService.signAsync(payload),
51-
expiresIn: 3600,
53+
expiresIn: expiration,
5254
};
5355
}
5456

server/src/main.ts

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { NestExpressApplication } from '@nestjs/platform-express';
66
import { join } from 'path';
77
import { ValidationPipe } from '@nestjs/common';
88
import { GlobalExceptionFilter } from '@/common/exceptions/global-exception.filter';
9+
import cookieParser from 'cookie-parser';
910

1011
async function bootstrap() {
1112
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
@@ -15,6 +16,7 @@ async function bootstrap() {
1516
app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));
1617
app.setGlobalPrefix('api');
1718
app.useGlobalPipes(new ValidationPipe());
19+
app.use(cookieParser());
1820

1921
const config = new DocumentBuilder()
2022
.setTitle('inear')

0 commit comments

Comments
 (0)