Skip to content

Commit

Permalink
Merge pull request #37 from wafflestudio/change_pw
Browse files Browse the repository at this point in the history
비밀번호 암호화 및 변경 기능 추가
  • Loading branch information
odumag99 authored Jan 23, 2025
2 parents 7fa3b9a + 672f60b commit e43b493
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 17 deletions.
97 changes: 94 additions & 3 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ social-auth-core = "^4.5.4"
aiomysql = "^0.2.0"
python-dotenv = "^1.0.1"
boto3 = "^1.36.2"
bcrypt = "^4.2.1"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
6 changes: 5 additions & 1 deletion snuvote/app/user/dto/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,8 @@ class UserSignupRequest(BaseModel):

class UserSigninRequest(BaseModel):
userid: str
password: str
password: str

class ResetPasswordRequest(BaseModel):
current_password: str
new_password: Annotated[str, AfterValidator(validate_password)]
10 changes: 9 additions & 1 deletion snuvote/app/user/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,12 @@ def __init__(self) -> None:

class BlockedRefreshTokenError(HTTPException):
def __init__(self) -> None:
super().__init__(HTTP_401_UNAUTHORIZED, "Blocked refresh token")
super().__init__(HTTP_401_UNAUTHORIZED, "Blocked refresh token")

class InvalidPasswordError(HTTPException):
def __init__(self) -> None:
super().__init__(HTTP_401_UNAUTHORIZED, "Invalid password")

class InvalidConfirmPasswordError(HTTPException):
def __init__(self) -> None:
super().__init__(HTTP_400_BAD_REQUEST, "Invalid confirm password")
39 changes: 35 additions & 4 deletions snuvote/app/user/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
from fastapi import Depends
from snuvote.database.models import User
from snuvote.app.user.store import UserStore
from snuvote.app.user.errors import InvalidUsernameOrPasswordError, NotAccessTokenError, NotRefreshTokenError, InvalidTokenError, ExpiredTokenError, BlockedRefreshTokenError
from snuvote.app.user.errors import InvalidUsernameOrPasswordError, NotAccessTokenError, NotRefreshTokenError, InvalidTokenError, ExpiredTokenError, BlockedRefreshTokenError, InvalidPasswordError, InvalidConfirmPasswordError

import jwt
from datetime import datetime, timedelta, timezone
from enum import Enum
from uuid import uuid4
from dotenv import load_dotenv
import os
import bcrypt

load_dotenv(dotenv_path = '.env.prod')
SECRET = os.getenv("SECRET_FOR_JWT") # .env.prod에서 불러오기
Expand All @@ -26,12 +27,29 @@ def __init__(self, user_store: Annotated[UserStore, Depends()]) -> None:

#회원가입
def add_user(self, userid: str, password: str, email: str, name: str, college: int) -> User:
return self.user_store.add_user(userid=userid, password=password, email=email, name=name, college=college)
hashed_password = self.hash_password(password)
return self.user_store.add_user(userid=userid, hashed_password=hashed_password, email=email, name=name, college=college)

#아이디로 유저 찾기
def get_user_by_userid(self, userid: str) -> User | None:
return self.user_store.get_user_by_userid(userid)

#비밀번호 해싱하기
def hash_password(self, password:str) -> str:
# 비밀번호를 바이트로 변환
password_bytes = password.encode('utf-8')
# 솔트 생성 및 해싱
hashed_password = bcrypt.hashpw(password_bytes, bcrypt.gensalt())
#바이트를 다시 문자열로 변환
return hashed_password.decode('utf-8')

#비밀번호 검증하기
def verify_password(self, plain_password: str, hashed_password: str) -> bool:
# 저장된 해시를 바이트로 변환
hashed_password_bytes = hashed_password.encode('utf-8')
# 입력된 비밀번호와 비교
return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password_bytes)

#토큰 생성
def issue_tokens(self, userid: str) -> tuple[str, str]:
access_payload = {
Expand All @@ -53,7 +71,7 @@ def issue_tokens(self, userid: str) -> tuple[str, str]:
#처음 로그인
def signin(self, userid: str, password: str) -> tuple[str, str]:
user = self.get_user_by_userid(userid)
if user is None or user.password != password:
if user is None or not self.verify_password(password, user.hashed_password):
raise InvalidUsernameOrPasswordError()
return self.issue_tokens(userid)

Expand Down Expand Up @@ -114,4 +132,17 @@ def block_refresh_token(self, refresh_token: str) -> None:
def reissue_tokens(self, refresh_token: str) -> tuple[str, str]:
userid = self.validate_refresh_token(refresh_token)
self.block_refresh_token(refresh_token)
return self.issue_tokens(userid)
return self.issue_tokens(userid)


#비밀번호 변경
def reset_password(self, user:User, current_password:str, new_password:str) -> None:

#현재 비밀번호를 틀린 경우
if not self.verify_password(current_password, user.hashed_password):
raise InvalidPasswordError()

#새 비밀번호 해싱하기
hashed_new_password = self.hash_password(new_password)
return self.user_store.reset_password(userid=user.userid, new_password=hashed_new_password)

17 changes: 12 additions & 5 deletions snuvote/app/user/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ def __init__(self, session: Annotated[Session, Depends(get_db_session)]) -> None
self.session = session

#회원가입하기
def add_user(self, userid: str, password: str, email: str, name: str, college: int) -> User:
def add_user(self, userid: str, hashed_password: str, email: str, name: str, college: int) -> User:
if self.get_user_by_userid(userid):
raise UserIdAlreadyExistsError()

if self.get_user_by_email(email):
raise EmailAlreadyExistsError()

user = User(userid=userid, password=password, email=email, name=name, college=college)
user = User(userid=userid, hashed_password=hashed_password, email=email, name=name, college=college)
self.session.add(user)
self.session.commit()
self.session.flush()

return user

Expand All @@ -40,7 +40,7 @@ def get_user_by_email(self, email: str) -> User | None:
def block_refresh_token(self, token_id: str, expires_at: datetime) -> None:
blocked_refresh_token = BlockedRefreshToken(token_id=token_id, expires_at=expires_at)
self.session.add(blocked_refresh_token)
self.session.commit()
self.session.flush()

#리프레쉬토큰 만료 체크하기
def is_refresh_token_blocked(self, token_id: int) -> bool:
Expand All @@ -49,4 +49,11 @@ def is_refresh_token_blocked(self, token_id: int) -> bool:
select(BlockedRefreshToken).where(BlockedRefreshToken.token_id == token_id)
)
is not None
)
)

#비밀번호 변경하기
def reset_password(self, userid:str, new_password:str) -> None:
user = self.get_user_by_userid(userid)
print(userid, user)
user.hashed_password = new_password
self.session.flush()
18 changes: 16 additions & 2 deletions snuvote/app/user/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from fastapi import APIRouter, Depends, HTTPException, Header
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from starlette.status import HTTP_200_OK, HTTP_201_CREATED, HTTP_401_UNAUTHORIZED
from snuvote.app.user.dto.requests import UserSignupRequest, UserSigninRequest
from snuvote.app.user.dto.requests import UserSignupRequest, UserSigninRequest, ResetPasswordRequest
from snuvote.app.user.dto.responses import UserSigninResponse, UserInfoResponse
from snuvote.database.models import User
from snuvote.app.user.service import UserService
Expand Down Expand Up @@ -63,4 +63,18 @@ def refresh(
def get_me(
user: Annotated[User, Depends(login_with_access_token)]
):
return UserInfoResponse.from_user(user)
return UserInfoResponse.from_user(user)

#비밀번호 변경하기
@user_router.patch("/reset_pw", status_code=HTTP_200_OK)
def reset_password(
user: Annotated[User, Depends(login_with_access_token)],
reset_password_request: ResetPasswordRequest,
user_service: Annotated[UserService, Depends()]
):

user_service.reset_password(
user, reset_password_request.current_password, reset_password_request.new_password
)

return "Success"
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""User DB에서 password를 hashed_password로 교체
Revision ID: 49abd7e4c043
Revises: 9e2715d6e171
Create Date: 2025-01-23 23:11:56.190187
"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql

# revision identifiers, used by Alembic.
revision: str = '49abd7e4c043'
down_revision: Union[str, None] = '9e2715d6e171'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:

# password Column을 hashed_password로 교체
op.alter_column(
'user', # table name
'password', # original column name
new_column_name='hashed_password',
type_=sa.String(60),
existing_type=sa.String(20),
existing_nullable=False
)

# 기존 회원들의 hashed_password를 특정값으로 교체
# op.execute(
# "UPDATE user SET hashed_password = 'hashed_password' ")


def downgrade() -> None:
op.alter_column(
'user',
'hashed_password',
new_column_name = 'password',
type = sa.String(20),
existing_type = sa.String(60),
existing_nullable = False
)
2 changes: 1 addition & 1 deletion snuvote/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class User(Base):
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
userid: Mapped[str] = mapped_column(String(20), unique=True, index=True)
email: Mapped[str] = mapped_column(String(100), unique=True, index=True)
password: Mapped[str] = mapped_column(String(20))
hashed_password: Mapped[str] = mapped_column(String(60))
name: Mapped[str] = mapped_column(String(20))
college: Mapped[int] = mapped_column(Integer)

Expand Down

0 comments on commit e43b493

Please sign in to comment.