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 app/modules/member/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ async def get_current_member(
if payload.get("type") != "access":
raise AppError.unauthorized("엑세스 토큰이 아닙니다.")

member = await service.get(UUID(member_id))
member = await service.get(UUID(member_id), include_deleted=True)

if member is None:
raise AppError.unauthorized("존재하지 않는 사용자입니다.")
Expand Down
24 changes: 21 additions & 3 deletions app/modules/member/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from fastapi.security import OAuth2PasswordRequestForm

from app.modules.member.dependencies import CurrentMemberDep, MemberServiceDep
from app.modules.member.schemas import MemberCreateIn, MemberOut, MemberUpdateIn, TokenOut
from app.modules.member.schemas import MemberCreateIn, MemberOut, MemberUpdateIn, TokenOut, RefreshTokenIn
from app.shared.schemas import ApiResponse, PageOut


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

@router.post("/login")
@router.post(
path="/login",
response_model=TokenOut,
summary="로그인",
description="이메일과 비밀번호를 통해 로그인 합니다."
)
async def login_member(
service: MemberServiceDep,
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
):
tokens = await service.login(form_data.username, form_data.password)

return TokenOut(**tokens)

@router.post(
path="/reissue",
response_model=TokenOut,
summary="refresh 토큰 재발급",
description="refresh 토큰으로 새로운 access 토큰과 refresh 토큰을 재발급합니다."
)
async def reissue_refresh_token(
service: MemberServiceDep,
data: RefreshTokenIn,
):
tokens = await service.reissue(data.refresh_token)
return TokenOut(**tokens)
55 changes: 35 additions & 20 deletions app/modules/member/schemas.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#dto
from datetime import date
from uuid import UUID

from pydantic import HttpUrl, ConfigDict, EmailStr
from sqlmodel import SQLModel, Field
Expand Down Expand Up @@ -35,17 +36,17 @@ class MemberCreateIn(SQLModel):
model_config = {
"json_schema_extra": {
"example": {
"email": "test@example.com",
"email": "test@naver.com",
"password": "test1234!",
"name": "홍길동",
"birth": "1999-01-01",
"name": "송시월",
"birth": "2001-05-21",
"gender": True,
"phone_num": "01012345678",
"nickname": "길동이",
"organization": "한국대학교",
"dept": "산업디자인과",
"nickname": "쏴리쏭",
"organization": "한성대학교",
"dept": "컴퓨터공학과",
"profile_url": "https://example.com/profile.jpg",
"detail": "안녕하세요~"
"detail": "안녕하세요!"
}
}
}
Expand All @@ -66,21 +67,22 @@ class MemberUpdateIn(SQLModel):
"json_schema_extra": {
"example": {
"password": "test1234!",
"name": "홍길동",
"birth": "1999-01-01",
"name": "송시월",
"birth": "2001-05-21",
"gender": True,
"phone_num": "01012345678",
"nickname": "길동이",
"organization": "한국대학교",
"dept": "산업디자인과",
"nickname": "쏴리쏭",
"organization": "한성대학교",
"dept": "컴퓨터공학과",
"profile_url": "https://example.com/profile.jpg",
"detail": "안녕하세요~"
"detail": "안녕하세요!"
}
}
}

#응답
class MemberOut(SQLModel):
id: UUID = Field(description="회원 ID")
email: EmailStr = Field(description="회원 이메일")
name: str = Field(description="이름")
birth: date = Field(description="생년월일")
Expand All @@ -97,15 +99,16 @@ class MemberOut(SQLModel):
from_attributes=True,
json_schema_extra={
"example": {
"id": "3e1672cf-8d99-4b1c-9b5e-9c3ece11b089",
"email": "test@example.com",
"name": "홍길동",
"birth": "1999-01-01",
"name": "송시월",
"birth": "2001-05-21",
"gender": True,
"nickname": "길동이",
"organization": "한국대학교",
"dept": "산업디자인과",
"nickname": "쏴리쏭",
"organization": "한성대학교",
"dept": "컴퓨터공학과",
"profile_url": "https://example.com/profile.jpg",
"detail": "안녕하세요~"
"detail": "안녕하세요!"
}
}
)
Expand All @@ -125,4 +128,16 @@ class TokenOut(SQLModel):
"token_type": "Bearer"
}
}
)
)

#refreshToken 요청용 dto
class RefreshTokenIn(SQLModel):
refresh_token: str = Field(description="리프레시 토큰 (JWT)")

model_config = {
"json_schema_extra": {
"example": {
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
}
26 changes: 25 additions & 1 deletion app/modules/member/service.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from base64 import decode
from typing import Any
from uuid import UUID

from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession

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

#accessToken 재발급 함수
async def reissue(self, refresh_token: str) -> dict[str, str]:
try:
payload = decode_token(refresh_token)
member_id: str | None = payload.get("sub")
token_type: str | None = payload.get("type")

if member_id is None or token_type != "refresh":
raise AppError.unauthorized("유효하지 않은 refresh 토큰입니다.")

member = await self.get(UUID(member_id))
if not member or member.is_deleted:
raise AppError.unauthorized("존재하지 않거나 삭제된 사용자입니다.")

return {
"access_token": create_access_token(data=str(member.id)),
"refresh_token": create_refresh_token(data=str(member.id)),
}
except ValueError as e:
raise AppError.unauthorized(str(e))
except Exception as e:
raise AppError.unauthorized(f"토큰 재발급 과정에서 오류가 발생했습니다.: {str(e)}")

Loading