Skip to content

Commit 4b8d139

Browse files
authored
Merge pull request #600 from gnuboard/fix/error
fix, feat: 회원가입 및 REST API 이슈 및 오류사항 수정
2 parents 7f74668 + 206c8ce commit 4b8d139

25 files changed

+542
-270
lines changed

api/settings.py

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class ApiSettings(BaseSettings):
2121

2222
AUTH_ALGORITHM: str = "HS256" # JWT 알고리즘
2323
AUTH_ISSUER: str = "g6_rest_api" # JWT 발급자
24+
AUTH_AUDIENCE: str = "g6_rest_api" # JWT 대상자
2425

2526
ACCESS_TOKEN_EXPIRE_MINUTES: float = 30
2627
REFRESH_TOKEN_EXPIRE_MINUTES: float = 60 * 24 * 14 # 14 days

api/v1/auth/jwt.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,10 @@ def create_token(token_type: TokenType, data: dict = None) -> str:
5454
exp = datetime.now() + timedelta(minutes=expires_minute)
5555
to_encode.update({
5656
"iss": api_settings.AUTH_ISSUER,
57-
"iat": iat.timestamp(),
58-
"exp": exp.timestamp()})
57+
"aud": api_settings.AUTH_AUDIENCE,
58+
"nbf": int(iat.timestamp()),
59+
"iat": int(iat.timestamp()),
60+
"exp": int(exp.timestamp())})
5961

6062
return encode(to_encode, secret_key, algorithm=api_settings.AUTH_ALGORITHM)
6163

@@ -85,13 +87,14 @@ def decode_token(token: str, secret_key: str) -> dict:
8587
token,
8688
secret_key,
8789
algorithms=[api_settings.AUTH_ALGORITHM],
90+
audience=api_settings.AUTH_AUDIENCE,
8891
)
8992
return TokenPayload(**payload)
9093
except ExpiredSignatureError as e:
91-
http_exception.detail = "Token has expired"
94+
http_exception.detail = f"Token has expired. {e}"
9295
raise http_exception from e
9396
except InvalidTokenError as e:
94-
http_exception.detail = "Could not validate credentials"
97+
http_exception.detail = f"Could not validate credentials. {e}"
9598
raise http_exception from e
9699
except Exception as e:
97100
http_exception.detail = str(e)

api/v1/dependencies/member.py

+30-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import Optional
33
from typing_extensions import Annotated
44

5-
from fastapi import Depends, HTTPException, status
5+
from fastapi import Body, Depends, HTTPException, Path, status
66

77
from core.models import Member
88

@@ -12,6 +12,8 @@
1212
from api.v1.service.member import MemberServiceAPI, ValidateMemberAPI
1313
from api.v1.models.auth import TokenPayload
1414
from api.v1.models.member import CreateMember, UpdateMember
15+
from lib.common import is_none_datetime
16+
from lib.pbkdf2 import validate_password
1517

1618

1719
async def get_current_member(
@@ -73,8 +75,10 @@ def validate_create_data(
7375
):
7476
"""회원 가입시 회원 정보의 유효성을 검사합니다."""
7577
validate.valid_id(data.mb_id)
78+
validate.valid_name(data.mb_name)
7679
validate.valid_nickname(data.mb_nick)
7780
validate.valid_email(data.mb_email)
81+
validate.valid_recommend(data.mb_recommend, data.mb_id)
7882

7983
return data
8084

@@ -104,3 +108,28 @@ def validate_update_data(
104108
del data.mb_open_date
105109

106110
return data
111+
112+
113+
114+
def validate_certify_email_member(
115+
member_service: Annotated[MemberServiceAPI, Depends()],
116+
mb_id: Annotated[str, Path(..., title="회원 아이디", description="회원 아이디")],
117+
password: Annotated[str, Body(..., title="비밀번호", description="회원 비밀번호")],
118+
):
119+
"""
120+
인증 이메일 변경시 회원 정보의 유효성을 검사합니다.
121+
"""
122+
member = member_service.fetch_member_by_id(mb_id)
123+
if not validate_password(password, member.mb_password):
124+
raise HTTPException(
125+
status_code=400,
126+
detail="비밀번호가 올바르지 않습니다.",
127+
)
128+
129+
if not is_none_datetime(member.mb_email_certify):
130+
raise HTTPException(
131+
status_code=409,
132+
detail="이미 메일인증을 진행한 회원입니다.",
133+
)
134+
135+
return member

api/v1/models/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class Tags(Enum):
3535
"""API 태그를 정의합니다."""
3636
AUTH = "인증"
3737
BOARD = "게시판"
38+
CAPTCHA = "캡차"
3839
GROUP = "게시판그룹"
3940
CONFIG = "환경설정"
4041
CONTENT = "컨텐츠"

api/v1/models/auth.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class TokenPayload(BaseModel):
1818
iss: str = None
1919
sub: str = None
2020
aud: str = None
21-
exp: float = None
21+
exp: int = None
2222
nbf: int = None
23-
iat: float = None
23+
iat: int = None
2424
jti: str = None

api/v1/models/member.py

+26-4
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ class CreateMember(BaseModel):
1717

1818
mb_id: str = Body(..., min_length=3, max_length=20, pattern=r"^[a-zA-Z0-9_]+$",
1919
title="아이디", description="3~20자의 영문, 숫자, _만 사용 가능합니다.")
20-
mb_password: str = Body(..., title="비밀번호")
21-
mb_password_re: str = Body(..., title="비밀번호 확인")
20+
mb_password: str = Body(..., title="비밀번호", min_length=4, max_length=20)
21+
mb_password_re: str = Body(..., title="비밀번호 확인", min_length=4, max_length=20)
2222
mb_nick: str = Body(..., title="닉네임")
2323
mb_name: str = Body(..., title="이름")
2424
mb_sex: str = Body("", pattern=r"^[mf]?$", title="성별")
@@ -86,8 +86,8 @@ class UpdateMember(BaseModel):
8686
# 추가 필드 허용
8787
model_config = ConfigDict(extra='allow')
8888

89-
mb_password: str = Body(None, title="비밀번호")
90-
mb_password_re: str = Body(None, title="비밀번호 확인")
89+
mb_password: str = Body(None, title="비밀번호", min_length=4, max_length=20)
90+
mb_password_re: str = Body(None, title="비밀번호 확인", min_length=4, max_length=20)
9191
mb_nick: str = Body(None, title="닉네임")
9292
mb_sex: str = Body(None, pattern=r"^[mf]?$", title="성별")
9393
mb_email: EmailStr = Body(..., title="이메일", description="이메일 형식에 맞게 입력해주세요.")
@@ -106,6 +106,17 @@ class UpdateMember(BaseModel):
106106
mb_sms: int = Body(None, title="SMS 수신 여부")
107107
mb_open: int = Body(None, title="회원정보 공개 여부")
108108

109+
mb_1: str
110+
mb_2: str
111+
mb_3: str
112+
mb_4: str
113+
mb_5: str
114+
mb_6: str
115+
mb_7: str
116+
mb_8: str
117+
mb_9: str
118+
mb_10: str
119+
109120
@field_validator('mb_zip', mode='after')
110121
@classmethod
111122
def divide_zip(cls, v: str) -> str:
@@ -160,6 +171,17 @@ class MemberResponse(BaseModel):
160171
mb_icon_path: str
161172
mb_image_path: str
162173

174+
mb_1: str
175+
mb_2: str
176+
mb_3: str
177+
mb_4: str
178+
mb_5: str
179+
mb_6: str
180+
mb_7: str
181+
mb_8: str
182+
mb_9: str
183+
mb_10: str
184+
163185
@model_validator(mode='before')
164186
def init_fields(self) -> 'MemberResponse':
165187
"""

api/v1/models/response.py

+6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ class MessageResponse(BaseModel):
77
"""메시지 응답 모델 (API Docs)"""
88
message: str
99

10+
response_400 = {
11+
status.HTTP_400_BAD_REQUEST: {
12+
"model": MessageResponse,
13+
"description": "잘못된 요청"
14+
}
15+
}
1016

1117
response_401 = {
1218
status.HTTP_401_UNAUTHORIZED: {

api/v1/routers/__init__.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from api.v1.dependencies.current_connect import set_current_connect
66
from api.v1.models import Tags
77
from api.v1.routers import (
8-
auth, autosave, board, board_good, board_new, config, content,
8+
auth, autosave, board, board_good, board_new, captcha, config, content,
99
current_connect, faq, member, memo, menu, newwin, point, poll, popular,
1010
qa, scrap, search, visit, group
1111
)
@@ -17,11 +17,12 @@
1717
Depends(set_current_connect)])
1818
router.include_router(auth.router, tags=[Tags.AUTH])
1919
router.include_router(board.router, prefix="/boards", tags=[Tags.BOARD])
20-
router.include_router(group.router, prefix="/groups", tags=[Tags.GROUP])
20+
router.include_router(captcha.router, tags=[Tags.CAPTCHA])
2121
router.include_router(config.router, tags=[Tags.CONFIG])
2222
router.include_router(content.router, tags=[Tags.CONTENT])
2323
router.include_router(current_connect.router, tags=[Tags.CURRENT_CONNECT])
2424
router.include_router(faq.router, tags=[Tags.FAQ])
25+
router.include_router(group.router, prefix="/groups", tags=[Tags.GROUP])
2526
router.include_router(member.router, tags=[Tags.MEMBER])
2627
router.include_router(memo.router, prefix="/member", tags=[Tags.MEMO])
2728
router.include_router(menu.router, tags=[Tags.MENU])

api/v1/routers/auth.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ async def login_for_access_token(
3434
access_token, access_token_expire_at = _create_token_and_expiration(
3535
TokenType.ACCESS, member.mb_id)
3636
refresh_token, refresh_token_expire_at = _create_token_and_expiration(
37-
TokenType.REFRESH)
37+
TokenType.REFRESH, member.mb_id)
3838

3939
# 기존 Refresh Token 삭제
4040
db.execute(
@@ -78,7 +78,7 @@ async def refresh_access_token(
7878
access_token, access_token_expire_at = _create_token_and_expiration(
7979
TokenType.ACCESS, member_refresh_token.mb_id)
8080
refresh_token, refresh_token_expire_at = _create_token_and_expiration(
81-
TokenType.REFRESH)
81+
TokenType.REFRESH, member_refresh_token.mb_id)
8282

8383
# 데이터베이스의 refresh_token 갱신
8484
member_refresh_token.updated_at = datetime.now()

api/v1/routers/captcha.py

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""캡차 API Router"""
2+
from typing_extensions import Annotated
3+
from fastapi import APIRouter, Body, HTTPException, Request
4+
5+
from api.v1.models.response import (
6+
MessageResponse, response_400, response_404, response_422
7+
)
8+
from lib.captcha import get_current_captcha_cls
9+
10+
router = APIRouter(prefix="/captcha")
11+
12+
13+
@router.post("/recaptcha/verify",
14+
summary="구글 reCAPTCHA 유효성 검사",
15+
responses={**response_400, **response_404, **response_422})
16+
async def recaptcha_verify(
17+
request: Request,
18+
recaptcha_response: Annotated[str, Body()] = None,
19+
) -> MessageResponse:
20+
"""
21+
구글 reCAPTCHA 유효성 검사
22+
23+
#### Request Body
24+
- recaptcha_response: 구글 reCAPTCHA 응답 토큰
25+
"""
26+
config = request.state.config
27+
28+
captcha_cls = get_current_captcha_cls(config)
29+
if not captcha_cls:
30+
raise HTTPException(status_code=404, detail="사용할 수 있는 캡차가 없습니다.")
31+
32+
captcha = captcha_cls(config)
33+
if captcha and (not await captcha.verify(recaptcha_response)):
34+
raise HTTPException(status_code=400, detail="캡차가 올바르지 않습니다.")
35+
36+
return {"message": "캡차가 올바릅니다."}

api/v1/routers/member.py

+42-8
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,21 @@
11
"""회원 관련 API Router"""
22
from datetime import datetime
3+
import secrets
34
from typing_extensions import Annotated
45

56
from fastapi import (
6-
APIRouter, BackgroundTasks, Depends, File, Form, Path, Query,
7+
APIRouter, BackgroundTasks, Body, Depends, File, Form, Path, Query,
78
Request, status, UploadFile
89
)
910
from sqlalchemy import delete
1011

11-
from api.v1.service.point import PointServiceAPI
1212
from bbs.social import SocialAuthService
1313
from core.database import db_session
1414
from core.models import Member
15-
from lib.mail import send_password_reset_mail, send_register_mail
15+
from lib.mail import send_password_reset_mail, send_register_admin_mail, send_register_mail
1616

1717
from api.v1.dependencies.member import (
18-
get_current_member, validate_create_data, validate_update_data
19-
)
20-
from api.v1.service.member import (
21-
MemberServiceAPI,
22-
MemberImageServiceAPI as ImageService
18+
get_current_member, validate_certify_email_member, validate_create_data, validate_update_data
2319
)
2420
from api.v1.models import MemberRefreshToken
2521
from api.v1.models.member import (
@@ -30,6 +26,12 @@
3026
from api.v1.models.response import (
3127
MessageResponse, response_401, response_403, response_404, response_409, response_422
3228
)
29+
from api.v1.service.member import (
30+
MemberServiceAPI,
31+
MemberImageServiceAPI as ImageService,
32+
ValidateMemberAPI
33+
)
34+
from api.v1.service.point import PointServiceAPI
3335

3436
router = APIRouter()
3537

@@ -70,6 +72,7 @@ async def create_member(
7072

7173
# 회원가입메일 발송 처리(백그라운드)
7274
background_tasks.add_task(send_register_mail, request, member)
75+
background_tasks.add_task(send_register_admin_mail, request, member)
7376

7477
message = "회원가입이 완료되었습니다."
7578
if member.mb_email_certify2:
@@ -83,6 +86,37 @@ async def create_member(
8386
}
8487

8588

89+
@router.put("/members/{mb_id}/email-certification/change",
90+
summary="인증 이메일 변경",
91+
responses={**response_403, **response_409, **response_422})
92+
async def certificate_email_change(
93+
request: Request,
94+
db: db_session,
95+
member_vaildate: Annotated[ValidateMemberAPI, Depends()],
96+
member: Annotated[Member, Depends(validate_certify_email_member)],
97+
email: Annotated[str, Body(..., title="이메일", description="변경할 이메일")],
98+
) -> MessageResponse:
99+
"""
100+
메일인증을 처리하지 않은 회원의 메일을 변경하고 인증메일을 재전송합니다.
101+
102+
#### Request Body
103+
- email: 변경할 이메일 주소
104+
- password: 회원 비밀번호
105+
"""
106+
member_vaildate.valid_email(email, member.mb_id)
107+
108+
# 이메일 및 인증코드 변경
109+
member.mb_email = email
110+
member.mb_email_certify2 = secrets.token_hex(16)
111+
db.commit()
112+
db.refresh(member)
113+
114+
# 인증메일 재전송
115+
await send_register_mail(request, member)
116+
117+
return {"message": f"{email} 주소로 인증 메일을 재전송 했습니다."}
118+
119+
86120
@router.put("/members/{mb_id}/email-certification",
87121
summary="회원가입 메일인증 처리")
88122
async def certificate_email(

0 commit comments

Comments
 (0)