Skip to content

Commit 9488e12

Browse files
authored
Merge pull request #9 from Teampling/feat/2-member-jwt
Feat: 회원 access 토큰 재발급 구현 및 오류 수정
2 parents d00e893 + 1370f9c commit 9488e12

File tree

4 files changed

+82
-25
lines changed

4 files changed

+82
-25
lines changed

app/modules/member/dependencies.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ async def get_current_member(
3838
if payload.get("type") != "access":
3939
raise AppError.unauthorized("엑세스 토큰이 아닙니다.")
4040

41-
member = await service.get(UUID(member_id))
41+
member = await service.get(UUID(member_id), include_deleted=True)
4242

4343
if member is None:
4444
raise AppError.unauthorized("존재하지 않는 사용자입니다.")

app/modules/member/router.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from fastapi.security import OAuth2PasswordRequestForm
77

88
from app.modules.member.dependencies import CurrentMemberDep, MemberServiceDep
9-
from app.modules.member.schemas import MemberCreateIn, MemberOut, MemberUpdateIn, TokenOut
9+
from app.modules.member.schemas import MemberCreateIn, MemberOut, MemberUpdateIn, TokenOut, RefreshTokenIn
1010
from app.shared.schemas import ApiResponse, PageOut
1111

1212

@@ -57,7 +57,7 @@ async def list_members(
5757
#summary: Swagger에서 보여줄 간단한 API 설명
5858
#description: Swagger에서 보여줄 상세한 API 설명
5959
@router.get(
60-
path="",
60+
path="/{member_id}",
6161
response_model=ApiResponse[MemberOut],
6262
summary="멤버 단건 조회",
6363
description="member ID에 해당하는 멤버의 상세 정보를 조회합니다.",
@@ -185,11 +185,29 @@ async def restore_member(
185185
data=MemberOut.model_validate(restored)
186186
)
187187

188-
@router.post("/login")
188+
@router.post(
189+
path="/login",
190+
response_model=TokenOut,
191+
summary="로그인",
192+
description="이메일과 비밀번호를 통해 로그인 합니다."
193+
)
189194
async def login_member(
190195
service: MemberServiceDep,
191196
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
192197
):
193198
tokens = await service.login(form_data.username, form_data.password)
194199

200+
return TokenOut(**tokens)
201+
202+
@router.post(
203+
path="/reissue",
204+
response_model=TokenOut,
205+
summary="refresh 토큰 재발급",
206+
description="refresh 토큰으로 새로운 access 토큰과 refresh 토큰을 재발급합니다."
207+
)
208+
async def reissue_refresh_token(
209+
service: MemberServiceDep,
210+
data: RefreshTokenIn,
211+
):
212+
tokens = await service.reissue(data.refresh_token)
195213
return TokenOut(**tokens)

app/modules/member/schemas.py

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#dto
22
from datetime import date
3+
from uuid import UUID
34

45
from pydantic import HttpUrl, ConfigDict, EmailStr
56
from sqlmodel import SQLModel, Field
@@ -35,17 +36,17 @@ class MemberCreateIn(SQLModel):
3536
model_config = {
3637
"json_schema_extra": {
3738
"example": {
38-
"email": "test@example.com",
39+
"email": "test@naver.com",
3940
"password": "test1234!",
40-
"name": "홍길동",
41-
"birth": "1999-01-01",
41+
"name": "송시월",
42+
"birth": "2001-05-21",
4243
"gender": True,
4344
"phone_num": "01012345678",
44-
"nickname": "길동이",
45-
"organization": "한국대학교",
46-
"dept": "산업디자인과",
45+
"nickname": "쏴리쏭",
46+
"organization": "한성대학교",
47+
"dept": "컴퓨터공학과",
4748
"profile_url": "https://example.com/profile.jpg",
48-
"detail": "안녕하세요~"
49+
"detail": "안녕하세요!"
4950
}
5051
}
5152
}
@@ -66,21 +67,22 @@ class MemberUpdateIn(SQLModel):
6667
"json_schema_extra": {
6768
"example": {
6869
"password": "test1234!",
69-
"name": "홍길동",
70-
"birth": "1999-01-01",
70+
"name": "송시월",
71+
"birth": "2001-05-21",
7172
"gender": True,
7273
"phone_num": "01012345678",
73-
"nickname": "길동이",
74-
"organization": "한국대학교",
75-
"dept": "산업디자인과",
74+
"nickname": "쏴리쏭",
75+
"organization": "한성대학교",
76+
"dept": "컴퓨터공학과",
7677
"profile_url": "https://example.com/profile.jpg",
77-
"detail": "안녕하세요~"
78+
"detail": "안녕하세요!"
7879
}
7980
}
8081
}
8182

8283
#응답
8384
class MemberOut(SQLModel):
85+
id: UUID = Field(description="회원 ID")
8486
email: EmailStr = Field(description="회원 이메일")
8587
name: str = Field(description="이름")
8688
birth: date = Field(description="생년월일")
@@ -97,15 +99,16 @@ class MemberOut(SQLModel):
9799
from_attributes=True,
98100
json_schema_extra={
99101
"example": {
102+
"id": "3e1672cf-8d99-4b1c-9b5e-9c3ece11b089",
100103
"email": "test@example.com",
101-
"name": "홍길동",
102-
"birth": "1999-01-01",
104+
"name": "송시월",
105+
"birth": "2001-05-21",
103106
"gender": True,
104-
"nickname": "길동이",
105-
"organization": "한국대학교",
106-
"dept": "산업디자인과",
107+
"nickname": "쏴리쏭",
108+
"organization": "한성대학교",
109+
"dept": "컴퓨터공학과",
107110
"profile_url": "https://example.com/profile.jpg",
108-
"detail": "안녕하세요~"
111+
"detail": "안녕하세요!"
109112
}
110113
}
111114
)
@@ -125,4 +128,16 @@ class TokenOut(SQLModel):
125128
"token_type": "Bearer"
126129
}
127130
}
128-
)
131+
)
132+
133+
#refreshToken 요청용 dto
134+
class RefreshTokenIn(SQLModel):
135+
refresh_token: str = Field(description="리프레시 토큰 (JWT)")
136+
137+
model_config = {
138+
"json_schema_extra": {
139+
"example": {
140+
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
141+
}
142+
}
143+
}

app/modules/member/service.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
from base64 import decode
12
from typing import Any
23
from uuid import UUID
34

45
from sqlalchemy.exc import IntegrityError
56
from sqlalchemy.ext.asyncio import AsyncSession
67

78
from app.core.exceptions import AppError
8-
from app.core.security import password_hash, verify_password, create_access_token, create_refresh_token
9+
from app.core.security import password_hash, verify_password, create_access_token, create_refresh_token, decode_token
910
from app.modules.member.models import Member
1011
from app.modules.member.repository import MemberRepository
1112
from app.modules.member.schemas import MemberCreateIn, MemberUpdateIn
@@ -261,3 +262,26 @@ async def login(self, email: str, password: str) -> dict[str, str]:
261262
"refresh_token": create_refresh_token(data=str(member.id)),
262263
}
263264

265+
#accessToken 재발급 함수
266+
async def reissue(self, refresh_token: str) -> dict[str, str]:
267+
try:
268+
payload = decode_token(refresh_token)
269+
member_id: str | None = payload.get("sub")
270+
token_type: str | None = payload.get("type")
271+
272+
if member_id is None or token_type != "refresh":
273+
raise AppError.unauthorized("유효하지 않은 refresh 토큰입니다.")
274+
275+
member = await self.get(UUID(member_id))
276+
if not member or member.is_deleted:
277+
raise AppError.unauthorized("존재하지 않거나 삭제된 사용자입니다.")
278+
279+
return {
280+
"access_token": create_access_token(data=str(member.id)),
281+
"refresh_token": create_refresh_token(data=str(member.id)),
282+
}
283+
except ValueError as e:
284+
raise AppError.unauthorized(str(e))
285+
except Exception as e:
286+
raise AppError.unauthorized(f"토큰 재발급 과정에서 오류가 발생했습니다.: {str(e)}")
287+

0 commit comments

Comments
 (0)