diff --git a/app/modules/member/dependencies.py b/app/modules/member/dependencies.py index 796a7f9..8910657 100644 --- a/app/modules/member/dependencies.py +++ b/app/modules/member/dependencies.py @@ -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("존재하지 않는 사용자입니다.") diff --git a/app/modules/member/router.py b/app/modules/member/router.py index 2b30030..7243397 100644 --- a/app/modules/member/router.py +++ b/app/modules/member/router.py @@ -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 @@ -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에 해당하는 멤버의 상세 정보를 조회합니다.", @@ -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) \ No newline at end of file diff --git a/app/modules/member/schemas.py b/app/modules/member/schemas.py index 82200fe..9d20d2d 100644 --- a/app/modules/member/schemas.py +++ b/app/modules/member/schemas.py @@ -1,5 +1,6 @@ #dto from datetime import date +from uuid import UUID from pydantic import HttpUrl, ConfigDict, EmailStr from sqlmodel import SQLModel, Field @@ -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": "안녕하세요!" } } } @@ -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="생년월일") @@ -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": "안녕하세요!" } } ) @@ -125,4 +128,16 @@ class TokenOut(SQLModel): "token_type": "Bearer" } } - ) \ No newline at end of file + ) + +#refreshToken 요청용 dto +class RefreshTokenIn(SQLModel): + refresh_token: str = Field(description="리프레시 토큰 (JWT)") + + model_config = { + "json_schema_extra": { + "example": { + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + } + } + } \ No newline at end of file diff --git a/app/modules/member/service.py b/app/modules/member/service.py index 031514b..27a5e01 100644 --- a/app/modules/member/service.py +++ b/app/modules/member/service.py @@ -1,3 +1,4 @@ +from base64 import decode from typing import Any from uuid import UUID @@ -5,7 +6,7 @@ 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 @@ -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)}") +