Skip to content

Commit

Permalink
Merge pull request #26 from pseudolab/feature/login-system
Browse files Browse the repository at this point in the history
social 로그인 관련 api
  • Loading branch information
ed-kyu authored Jul 14, 2024
2 parents 4621fa6 + 448c7e8 commit c0d85bd
Show file tree
Hide file tree
Showing 8 changed files with 347 additions and 22 deletions.
48 changes: 40 additions & 8 deletions app/api/auth/routes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from fastapi import APIRouter, Depends, Path
from api.auth.schema import LoginToken, LoginResponse, BingoUser
from fastapi import APIRouter, Depends, Path, Request, HTTPException
from api.auth.schema import LoginToken, LoginResponse, BingoUser, LoginType, LoginUrl
from api.auth.services.social_login import SocialLoginDepends
from api.auth.services.bingo_login import CreateBingoUser, GetBingoUserByName, GetBingoUserById

Expand All @@ -23,23 +23,55 @@ async def bingo_get_user(user_id: int, bingo_user: GetBingoUserById = Depends(Ge


@auth_router.get("/discord/login", description="Discord 로그인 URL 생성")
async def oauth2_login(social_login: SocialLoginDepends) -> LoginResponse:
async def oauth2_login(social_login: SocialLoginDepends) -> LoginUrl:
return await social_login.discord_login()


@auth_router.get("/discord/redirect", description="Discord 로그인 redirect")
async def oauth2_login(login_token: LoginToken, social_login: SocialLoginDepends) -> LoginResponse:
return await social_login.discord_login_redirect(LoginType.discord, login_token.code)
async def oauth2_login(request: Request, social_login: SocialLoginDepends) -> LoginResponse:
code = request.query_params.get("code")
if not code:
raise HTTPException(status_code=400, detail="Missing 'code' parameter")
return await social_login.discord_login_redirect(code)


@auth_router.get("/google/login", description="Google 로그인 URL 생성")
async def oauth2_login(social_login: SocialLoginDepends) -> LoginResponse:
async def oauth2_login(social_login: SocialLoginDepends) -> LoginUrl:
return await social_login.google_login()


@auth_router.get("/google/redirect", description="Google 로그인 redirect")
async def oauth2_login(login_token: LoginToken, social_login: SocialLoginDepends) -> LoginResponse:
return await social_login.google_login(login_token.code)
async def oauth2_login(request: Request, social_login: SocialLoginDepends) -> LoginResponse:
code = request.query_params.get("code")
if not code:
raise HTTPException(status_code=400, detail="Missing 'code' parameter")
return await social_login.google_login_redirect(code)


@auth_router.get("/github/login", description="Github 로그인 URL 생성")
async def oauth2_login(social_login: SocialLoginDepends) -> LoginUrl:
return await social_login.github_login()


@auth_router.get("/github/redirect", description="Github 로그인 redirect")
async def oauth2_login(request: Request, social_login: SocialLoginDepends) -> LoginResponse:
code = request.query_params.get("code")
if not code:
raise HTTPException(status_code=400, detail="Missing 'code' parameter")
return await social_login.github_login_redirect(code)


@auth_router.get("/kakao/login", description="Kakao 로그인 URL 생성")
async def oauth2_login(social_login: SocialLoginDepends) -> LoginUrl:
return await social_login.kakao_login()


@auth_router.get("/kakao/redirect", description="Kakao 로그인 redirect")
async def oauth2_login(request: Request, social_login: SocialLoginDepends) -> LoginResponse:
code = request.query_params.get("code")
if not code:
raise HTTPException(status_code=400, detail="Missing 'code' parameter")
return await social_login.kakao_login_redirect(code)


@auth_router.get("/sign-up", description="회원가입 API")
Expand Down
7 changes: 7 additions & 0 deletions app/api/auth/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ class LoginState(IntEnum):
sign_up = 1


class LoginType:
discord = 0
google = 1
github = 2
kakao = 3


class LoginToken(BaseSchema):
login_type: Optional[LoginState] = Field(description="Social 로그인 종류")
code: Optional[str] = Field(description="각 Social 로그인이 발급해주는 accept token")
Expand Down
31 changes: 31 additions & 0 deletions app/api/auth/services/jwts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import os
import jwt
from datetime import datetime, timedelta
from typing import Union, Tuple

SECRET_KEY = os.getenv("JWT_SECRET_KEY")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
REFRESH_TOKEN_EXPIRE_DAYS = 7


def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None) -> Tuple[str, datetime]:
to_encode = data.copy()
if expires_delta:
expire = datetime.now() + expires_delta
else:
expire = datetime.now() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt, expire


def create_refresh_token(data: dict, expires_delta: Union[timedelta, None] = None) -> Tuple[str, datetime]:
to_encode = data.copy()
if expires_delta:
expire = datetime.now() + expires_delta
else:
expire = datetime.now() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt, expire
213 changes: 205 additions & 8 deletions app/api/auth/services/social_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@
from typing import Annotated
from enum import Enum, auto
from api.auth.schema import LoginState, LoginResponse, LoginUrl
from urllib.parse import parse_qs
from api.auth.services.jwts import create_access_token, create_refresh_token

REDIRECT_URI = "http://localhost:8000/auth/discord/login/redirect"
REDIRECT_URI_DISCORD = os.getenv("REDIRECT_URI_DISCORD")
REDIRECT_URI_GOOGLE = os.getenv("REDIRECT_URI_GOOGLE")
REDIRECT_URI_GITHUB = os.getenv("REDIRECT_URI_GITHUB")
REDIRECT_URI_KAKAO = os.getenv("REDIRECT_URI_KAKAO")


class SocialLogin:
Expand All @@ -25,7 +30,11 @@ def __init__(self, session: AsyncSessionDepends):
self.session = session

async def discord_login(self) -> LoginUrl:
return LoginUrl(url="https://discord.com/api/oauth2/token")
client_id = os.getenv("DISCORD_CLIENT_ID")
redirect_uri = REDIRECT_URI_DISCORD
scope = "identify+email+guilds.join"
login_url = f"https://discord.com/oauth2/authorize?client_id={client_id}&response_type=code&redirect_uri={redirect_uri}&scope={scope}"
return LoginUrl(url=login_url, ok=True, message="Discord login URL generated successfully")

async def discord_login_redirect(self, code: str) -> LoginResponse:
login_state = LoginState.sign_in
Expand All @@ -39,7 +48,7 @@ async def discord_login_redirect(self, code: str) -> LoginResponse:
"client_secret": client_secret,
"grant_type": "authorization_code",
"code": code,
"redirect_uri": REDIRECT_URI,
"redirect_uri": REDIRECT_URI_DISCORD,
"scope": "identify, email",
}
response = await client.post("https://discord.com/api/oauth2/token", headers=headers, data=data)
Expand All @@ -64,24 +73,212 @@ async def discord_login_redirect(self, code: str) -> LoginResponse:

# 이메일로 유저 가입 유무 체크
find_user = await User.get_user_by_email(self.session, email)
access_token_jwt = ""
refresh_token_jwt = ""
if not find_user:
message = "회원 가입이 필요합니다."
login_state = LoginState.sign_up
else:
user_id = find_user.user_id
access_token_jwt, access_expires = create_access_token(data={"sub": user_id})
refresh_token_jwt, refresh_expires = create_refresh_token(data={"sub": user_id})
await User.update_tokens(
self.session, user_id, access_token_jwt, refresh_token_jwt, access_expires, refresh_expires
)

# DB 체크해서 로그인, 회원가입 상태 체크
return LoginResponse(
ok=True,
message=message,
login_state=login_state,
access_token="",
refresh_token="",
access_token=access_token_jwt,
refresh_token=refresh_token_jwt,
)

async def google_login(self) -> LoginUrl:
return LoginUrl(url="")
client_id = os.getenv("GOOGLE_CLIENT_ID")
redirect_uri = REDIRECT_URI_GOOGLE
scope = "https://www.googleapis.com/auth/userinfo.email"
# scope = "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile"
login_url = f"https://accounts.google.com/o/oauth2/auth?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&scope={scope}"
return LoginUrl(url=login_url, ok=True, message="Google login URL generated successfully")

async def google_login_redirect(self, code: str) -> LoginResponse:
pass
login_state = LoginState.sign_in
message = "로그인 성공"
client_id = os.getenv("GOOGLE_CLIENT_ID")
client_secret = os.getenv("GOOGLE_CLIENT_SECRET")
async with AsyncClient() as client:
headers = {"Content-Type": "application/x-www-form-urlencoded"}
data = {
"client_id": client_id,
"client_secret": client_secret,
"grant_type": "authorization_code",
"code": code,
"redirect_uri": REDIRECT_URI_GOOGLE,
}
response = await client.post("https://oauth2.googleapis.com/token", headers=headers, data=data)
if not response.is_success:
raise HTTPException(
status_code=500, detail=f"Error in getting token or user data from Google API: {response.json()}"
)

res_data = response.json()
access_token = res_data.get("access_token")
headers = {"Authorization": f"Bearer {access_token}"}
response = await client.get("https://www.googleapis.com/oauth2/v1/userinfo", headers=headers)
if not response.is_success:
raise HTTPException(
status_code=500, detail=f"Error in getting user data from Google API: {response.json()}"
)

user_data = response.json()
email = user_data.get("email")

find_user = await User.get_user_by_email(self.session, email)
access_token_jwt = ""
refresh_token_jwt = ""
if not find_user:
message = "회원 가입이 필요합니다."
login_state = LoginState.sign_up
else:
user_id = find_user.user_id
access_token_jwt, access_expires = create_access_token(data={"sub": user_id})
refresh_token_jwt, refresh_expires = create_refresh_token(data={"sub": user_id})
await User.update_tokens(
self.session, user_id, access_token_jwt, refresh_token_jwt, access_expires, refresh_expires
)

return LoginResponse(
ok=True,
message=message,
login_state=login_state,
access_token=access_token_jwt,
refresh_token=refresh_token_jwt,
)

async def github_login(self) -> LoginUrl:
client_id = os.getenv("GITHUB_CLIENT_ID")
redirect_uri = REDIRECT_URI_GITHUB
scope = "user:email"
login_url = (
f"https://github.com/login/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&scope={scope}"
)
return LoginUrl(url=login_url, ok=True, message="GitHub login URL generated successfully")

async def github_login_redirect(self, code: str) -> LoginResponse:
login_state = LoginState.sign_in
message = "로그인 성공"
client_id = os.getenv("GITHUB_CLIENT_ID")
client_secret = os.getenv("GITHUB_CLIENT_SECRET")
async with AsyncClient() as client:
headers = {"Content-Type": "application/json"}
data = {
"client_id": client_id,
"client_secret": client_secret,
"code": code,
"redirect_uri": REDIRECT_URI_GITHUB,
}
response = await client.post("https://github.com/login/oauth/access_token", headers=headers, json=data)
if not response.is_success:
raise HTTPException(
status_code=500, detail=f"Error in getting token or user data from GitHub API: {response.content}"
)

res_data = parse_qs(response.content.decode())
access_token = res_data.get("access_token")[0]
headers = {"Authorization": f"Bearer {access_token}"}
response = await client.get("https://api.github.com/user", headers=headers)
if not response.is_success:
raise HTTPException(
status_code=500, detail=f"Error in getting user data from GitHub API: {response.json()}"
)

user_data = response.json()
email = user_data.get("email")

find_user = await User.get_user_by_email(self.session, email)
access_token_jwt = ""
refresh_token_jwt = ""
if not find_user:
message = "회원 가입이 필요합니다."
login_state = LoginState.sign_up
else:
user_id = find_user.user_id
access_token_jwt, access_expires = create_access_token(data={"sub": user_id})
refresh_token_jwt, refresh_expires = create_refresh_token(data={"sub": user_id})
await User.update_tokens(
self.session, user_id, access_token_jwt, refresh_token_jwt, access_expires, refresh_expires
)

return LoginResponse(
ok=True,
message=message,
login_state=login_state,
access_token=access_token_jwt,
refresh_token=refresh_token_jwt,
)

async def kakao_login(self) -> LoginUrl:
client_id = os.getenv("KAKAO_CLIENT_ID")
redirect_uri = REDIRECT_URI_KAKAO
scope = "account_email"
login_url = f"https://kauth.kakao.com/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&scope={scope}"
return LoginUrl(url=login_url, ok=True, message="Kakao login URL generated successfully")

async def kakao_login_redirect(self, code: str) -> LoginResponse:
login_state = LoginState.sign_in
message = "로그인 성공"
client_id = os.getenv("KAKAO_CLIENT_ID")
client_secret = os.getenv("KAKAO_CLIENT_SECRET")
async with AsyncClient() as client:
headers = {"Content-Type": "application/x-www-form-urlencoded"}
data = {
"grant_type": "authorization_code",
"client_id": client_id,
"client_secret": client_secret,
"redirect_uri": REDIRECT_URI_KAKAO,
"code": code,
}
response = await client.post("https://kauth.kakao.com/oauth/token", headers=headers, data=data)
if not response.is_success:
raise HTTPException(
status_code=500, detail=f"Error in getting token or user data from Kakao API: {response.json()}"
)

res_data = response.json()
access_token = res_data.get("access_token")
headers = {"Authorization": f"Bearer {access_token}"}
response = await client.get("https://kapi.kakao.com/v2/user/me", headers=headers)
if not response.is_success:
raise HTTPException(
status_code=500, detail=f"Error in getting user data from Kakao API: {response.json()}"
)

user_data = response.json()
kakao_account = user_data.get("kakao_account")
email = kakao_account.get("email")

find_user = await User.get_user_by_email(self.session, email)
access_token_jwt = ""
refresh_token_jwt = ""
if not find_user:
message = "회원 가입이 필요합니다."
login_state = LoginState.sign_up
else:
user_id = find_user.user_id
access_token_jwt, access_expires = create_access_token(data={"sub": user_id})
refresh_token_jwt, refresh_expires = create_refresh_token(data={"sub": user_id})
await User.update_tokens(
self.session, user_id, access_token_jwt, refresh_token_jwt, access_expires, refresh_expires
)

return LoginResponse(
ok=True,
message=message,
login_state=login_state,
access_token=access_token_jwt,
refresh_token=refresh_token_jwt,
)


class SignUp:
Expand Down
15 changes: 14 additions & 1 deletion app/config/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,17 @@ DISCORD_CLIENT_ID=DISCORD_CLIENT_ID
DISCORD_CLIENT_SECRET=DISCORD_CLIENT_SECRET

GOOGLE_CLIENT_ID=GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET=GOOGLE_CLIENT_SECRET
GOOGLE_CLIENT_SECRET=GOOGLE_CLIENT_SECRET

GITHUB_CLIENT_ID=GITHUB_CLIENT_ID
GITHUB_CLIENT_SECRET=GITHUB_CLIENT_SECRET

KAKAO_CLIENT_ID=KAKAO_CLIENT_ID
KAKAO_CLIENT_SECRET=KAKAO_CLIENT_SECRET

REDIRECT_URI_DISCORD=REDIRECT_URI_DISCORD
REDIRECT_URI_GOOGLE=REDIRECT_URI_GOOGLE
REDIRECT_URI_GITHUB=REDIRECT_URI_GITHUB
REDIRECT_URI_KAKAO=REDIRECT_URI_KAKAO

JWT_SECRET_KEY=JWT_SECRET_KEY
Loading

0 comments on commit c0d85bd

Please sign in to comment.