Skip to content

Commit

Permalink
Merge pull request #38 from wafflestudio/naver_social_login
Browse files Browse the repository at this point in the history
네이버 계정 연동 및 로그인 기능 추가
  • Loading branch information
odumag99 authored Jan 24, 2025
2 parents e43b493 + 66e7923 commit 81abba6
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 63 deletions.
104 changes: 48 additions & 56 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 @@ -21,6 +21,7 @@ aiomysql = "^0.2.0"
python-dotenv = "^1.0.1"
boto3 = "^1.36.2"
bcrypt = "^4.2.1"
httpx = "^0.28.1"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
14 changes: 13 additions & 1 deletion snuvote/app/user/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,16 @@ def __init__(self) -> None:

class InvalidConfirmPasswordError(HTTPException):
def __init__(self) -> None:
super().__init__(HTTP_400_BAD_REQUEST, "Invalid confirm password")
super().__init__(HTTP_400_BAD_REQUEST, "Invalid confirm password")

class NaverApiError(HTTPException):
def __init__(self) -> None:
super().__init__(HTTP_401_UNAUTHORIZED, "Naver API error")

class InvalidNaverTokenError(HTTPException):
def __init__(self) -> None:
super().__init__(HTTP_401_UNAUTHORIZED, "Invalid Naver token")

class NotLinkedNaverAccountError(HTTPException):
def __init__(self) -> None:
super().__init__(HTTP_401_UNAUTHORIZED, "Not linked Naver account")
43 changes: 42 additions & 1 deletion snuvote/app/user/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
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, InvalidPasswordError, InvalidConfirmPasswordError
from snuvote.app.user.errors import InvalidUsernameOrPasswordError, NotAccessTokenError, NotRefreshTokenError, InvalidTokenError, ExpiredTokenError, BlockedRefreshTokenError, InvalidPasswordError, NaverApiError, InvalidNaverTokenError

import jwt
from datetime import datetime, timedelta, timezone
Expand All @@ -13,6 +13,8 @@
import os
import bcrypt

import httpx

load_dotenv(dotenv_path = '.env.prod')
SECRET = os.getenv("SECRET_FOR_JWT") # .env.prod에서 불러오기

Expand Down Expand Up @@ -145,4 +147,43 @@ def reset_password(self, user:User, current_password:str, new_password:str) -> N
#새 비밀번호 해싱하기
hashed_new_password = self.hash_password(new_password)
return self.user_store.reset_password(userid=user.userid, new_password=hashed_new_password)

# 네이버 access_token 이용해 User의 네이버 고유 식별 id 가져오기
async def get_naver_id_with_naver_access_token(self, access_token: str) -> str:

url = "https://openapi.naver.com/v1/nid/me" # 네이버 프로필 조회 API
headers = {"Authorization": f"Bearer {access_token}"}

async with httpx.AsyncClient() as client:
response = await client.get(url, headers=headers)
if response.status_code != 200:
if response.status_code == 401 and response.json().get("resultcode") == "024": # "024": 네이버 인증 실패 에러 코드 https://developers.naver.com/docs/login/profile/profile.md
raise InvalidNaverTokenError()
else:
raise NaverApiError()

data = response.json()
naver_id = data.get("response", {}).get("id") # 회원의 네이버 고유 식별 id

if naver_id is None:
raise NaverApiError()

return naver_id


# 네이버 계정과 연동
async def link_with_naver(self, user:User, naver_access_token: str) -> None:
naver_id = await self.get_naver_id_with_naver_access_token(naver_access_token) # 네이버 access_token 이용해 User의 네이버 고유 식별 id 가져오기
self.user_store.link_with_naver(user.userid, naver_id) # User의 네이버 고유 식별 id 등록

# 네이버 access_token 이용해 로그인
async def signin_with_naver_access_token(self, naver_access_token: str):

naver_id = await self.get_naver_id_with_naver_access_token(naver_access_token)
user = self.user_store.get_user_by_naver_id(naver_id)

return self.issue_tokens(user.userid)




23 changes: 20 additions & 3 deletions snuvote/app/user/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from datetime import datetime

from fastapi import Depends
from snuvote.app.user.errors import EmailAlreadyExistsError, UserUnsignedError, UserIdAlreadyExistsError
from snuvote.database.models import User, BlockedRefreshToken
from snuvote.app.user.errors import EmailAlreadyExistsError, UserIdAlreadyExistsError, NotLinkedNaverAccountError, UserNotFoundError
from snuvote.database.models import User, BlockedRefreshToken, NaverUser

from snuvote.database.connection import get_db_session
from sqlalchemy import select, delete
Expand Down Expand Up @@ -54,6 +54,23 @@ def is_refresh_token_blocked(self, token_id: int) -> bool:
#비밀번호 변경하기
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()

# 네이버 고유 식별 id 등록
def link_with_naver(self, userid: str, naver_id: str):
user = self.get_user_by_userid(userid)
new_naveruser = NaverUser(user_id=user.id, naver_id=naver_id)
self.session.add(new_naveruser)
self.session.flush()

def get_user_by_naver_id(self, naver_id: str) -> User:
user_id = self.session.scalar(select(NaverUser.user_id).where(NaverUser.naver_id == naver_id))
if user_id is None:
raise NotLinkedNaverAccountError()

user = self.session.scalar(select(User).where(User.id == user_id))
if not user:
raise UserNotFoundError()

return user
25 changes: 23 additions & 2 deletions snuvote/app/user/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Header
from fastapi import APIRouter, Depends, HTTPException, Header, Body
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, ResetPasswordRequest
Expand Down Expand Up @@ -77,4 +77,25 @@ def reset_password(
user, reset_password_request.current_password, reset_password_request.new_password
)

return "Success"
return "Success"

# 네이버 계정과 연동
@user_router.post("/link/naver", status_code=HTTP_201_CREATED)
async def link_with_naver(
user: Annotated[User, Depends(login_with_access_token)],
user_service: Annotated[UserService, Depends()],
access_token: Annotated[str, Body(...)]
):
await user_service.link_with_naver(user, access_token)

return "Success"

# 네이버 계정으로 로그인
@user_router.post("/signin/naver", status_code=HTTP_200_OK)
async def signin_with_naver_access_token(
user_service: Annotated[UserService, Depends()],
naver_access_token: Annotated[str, Body(...)]
):
access_token, refresh_token = await user_service.signin_with_naver_access_token(naver_access_token)

return UserSigninResponse(access_token=access_token, refresh_token=refresh_token)
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""DB에 NaverUser Table 추가 후 보완
Revision ID: e837333e71e1
Revises: 49abd7e4c043
Create Date: 2025-01-24 09:25:35.188730
"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


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


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('naver_user',
sa.Column('id', sa.BigInteger(), nullable=False),
sa.Column('user_id', sa.BigInteger(), nullable=False),
sa.Column('naver_id', sa.String(length=50), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_naver_user_naver_id'), 'naver_user', ['naver_id'], unique=True)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_naver_user_naver_id'), table_name='naver_user')
op.drop_table('naver_user')
# ### end Alembic commands ###
12 changes: 12 additions & 0 deletions snuvote/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ class User(Base):

comments: Mapped[Optional[List["Comment"]]] = relationship("Comment", back_populates="writer")

naver_user: Mapped[Optional["NaverUser"]] = relationship("NaverUser", back_populates="user", uselist=False)

class NaverUser(Base):
__tablename__ = "naver_user"

id: Mapped[int] = mapped_column(BigInteger, primary_key=True)

user_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("user.id"))
user: Mapped["User"] = relationship("User", back_populates="naver_user", uselist=False)

naver_id: Mapped[str] = mapped_column(String(50), unique=True, index=True)

class Vote(Base):
__tablename__ = "vote"

Expand Down

0 comments on commit 81abba6

Please sign in to comment.