From 6fce634fb6d2f13f6cd16fc3191eb23cddfa5c7f Mon Sep 17 00:00:00 2001 From: 2020-13600 Date: Sun, 19 Jan 2025 01:07:54 +0900 Subject: [PATCH 1/4] social_login create --- instaclone/app/auth/store.py | 43 +++++++++++++++++ instaclone/app/auth/views.py | 64 ++++++++++++++++++++++++++ instaclone/common/errors.py | 10 +++- instaclone/database/google_settings.py | 16 +++++++ 4 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 instaclone/app/auth/store.py create mode 100644 instaclone/app/auth/views.py create mode 100644 instaclone/database/google_settings.py diff --git a/instaclone/app/auth/store.py b/instaclone/app/auth/store.py new file mode 100644 index 0000000..42821b9 --- /dev/null +++ b/instaclone/app/auth/store.py @@ -0,0 +1,43 @@ +from sqlalchemy.future import select +from datetime import datetime +from instaclone.app.user.models import User +from instaclone.database.connection import SESSION +from sqlalchemy.exc import SQLAlchemyError +from instaclone.common.errors import CommentServerError + +async def get_or_create_user_from_google(user_info: dict): + try: + # User DB - email 기반 사용자 정보 조회 + async with SESSION() as session: + stmt = select(User).filter_by(email=user_info["email"]) + result = await session.execute(stmt) + user = result.scalars().first() + + # 유저가 새로 생성된 경우 + if not user: + user = User( + username=user_info.get("name"), + email=user_info.get("email"), + full_name=user_info.get("name"), + password="default", + phone_number=99999999999, + creation_date=datetime.today().date(), + profile_image=user_info.get("picture"), + ) + session.add(user) + await session.commit() + return { + "user": user, + "is_created": True + } + + # 유저가 이미 있는 경우 + return { + "user": user, + "is_created": False + } + + except SQLAlchemyError as e: + # 예외가 발생하면 롤백 처리하고, 커스텀 예외 던지기 + await SESSION.rollback() + raise CommentServerError(f"Failed to create or retrieve user: {str(e)}", e) from e \ No newline at end of file diff --git a/instaclone/app/auth/views.py b/instaclone/app/auth/views.py new file mode 100644 index 0000000..14a5db2 --- /dev/null +++ b/instaclone/app/auth/views.py @@ -0,0 +1,64 @@ +import os +import httpx +from fastapi import FastAPI, Depends, APIRouter, HTTPException +from fastapi.responses import RedirectResponse +from fastapi import Query +from instaclone.database.google_settings import GOOGLE_SETTINGS +from instaclone.app.auth.store import get_or_create_user_from_google + +GOOGLE_CLIENT_ID = GOOGLE_SETTINGS.client_id +GOOGLE_CLIENT_SECRET = GOOGLE_SETTINGS.client_secret +GOOGLE_REDIRECT_URI = "http://localhost:8000/auth/callback" # 설정한 리디렉션 URI + +google_oauth_router = APIRouter() + +@google_oauth_router.get("/login") +async def login(): + # Google OAuth 인증 URL 생성 + google_auth_url = f"https://accounts.google.com/o/oauth2/v2/auth?client_id={GOOGLE_CLIENT_ID}&redirect_uri={GOOGLE_REDIRECT_URI}&response_type=code&scope=email%20profile" + return RedirectResponse(google_auth_url) + + + +@google_oauth_router.get("/callback") +async def callback(code: str = Query(..., description="Google Authorization Code")): + # Google API에 액세스 토큰 요청 + token_url = "https://oauth2.googleapis.com/token" + data = { + "code": code, + "client_id": GOOGLE_CLIENT_ID, + "client_secret": GOOGLE_CLIENT_SECRET, + "redirect_uri": GOOGLE_REDIRECT_URI, + "grant_type": "authorization_code" + } + + async with httpx.AsyncClient() as client: + response = await client.post(token_url, data=data) + if response.status_code != 200: + raise HTTPException(status_code=400, detail="Failed to retrieve access token") + + tokens = response.json() + access_token = tokens.get("access_token") + id_token = tokens.get("id_token") + + # Access token을 사용해 Google 사용자 정보 요청 + user_info_url = "https://www.googleapis.com/oauth2/v3/userinfo" + headers = {"Authorization": f"Bearer {access_token}"} + + async with httpx.AsyncClient() as client: + user_info_response = await client.get(user_info_url, headers=headers) + if user_info_response.status_code != 200: + raise HTTPException(status_code=400, detail="Failed to retrieve user info") + + user_info = user_info_response.json() + + response = await get_or_create_user_from_google(user_info) + + # user와 is_created를 각각 받음 + user = response['user'] + is_created = response['is_created'] + return { + "user_info": user_info, + "user_id": user.user_id, + "is_created": is_created + } \ No newline at end of file diff --git a/instaclone/common/errors.py b/instaclone/common/errors.py index be34446..12027f1 100644 --- a/instaclone/common/errors.py +++ b/instaclone/common/errors.py @@ -1,5 +1,5 @@ from fastapi import HTTPException -from starlette.status import HTTP_400_BAD_REQUEST, HTTP_401_UNAUTHORIZED +from starlette.status import HTTP_400_BAD_REQUEST, HTTP_401_UNAUTHORIZED, HTTP_500_INTERNAL_SERVER_ERROR class InstacloneHttpException(HTTPException): @@ -26,4 +26,10 @@ def __init__(self) -> None: class BlockedTokenError(HTTPException): def __init__(self) -> None: - super().__init__(HTTP_401_UNAUTHORIZED, "The token has been blocked.") \ No newline at end of file + super().__init__(HTTP_401_UNAUTHORIZED, "The token has been blocked.") + +class CommentServerError(HTTPException): + def __init__(self, message: str, original_exception=None) -> None: + # HTTPException 초기화 + super().__init__(HTTP_500_INTERNAL_SERVER_ERROR, message) + self.original_exception = original_exception \ No newline at end of file diff --git a/instaclone/database/google_settings.py b/instaclone/database/google_settings.py new file mode 100644 index 0000000..4120193 --- /dev/null +++ b/instaclone/database/google_settings.py @@ -0,0 +1,16 @@ +# instaclone/settings/google_settings.py +from pydantic_settings import BaseSettings +from instaclone.settings import SETTINGS + +class GoogleSettings(BaseSettings): + client_id: str = "" + client_secret: str = "" + + class Config: + case_sensitive = False + env_prefix = "GOOGLE_" + env_file = SETTINGS.env_file + extra = "allow" # extra 필드 허용 + +# GoogleSettings 인스턴스를 따로 초기화 +GOOGLE_SETTINGS = GoogleSettings() From 314ad467df100815f699ae9c0c195068bffec530 Mon Sep 17 00:00:00 2001 From: 2020-13600 Date: Sun, 19 Jan 2025 02:52:16 +0900 Subject: [PATCH 2/4] social_login updated --- instaclone/app/auth/views.py | 2 ++ instaclone/database/settings.py | 1 + 2 files changed, 3 insertions(+) diff --git a/instaclone/app/auth/views.py b/instaclone/app/auth/views.py index 14a5db2..954775d 100644 --- a/instaclone/app/auth/views.py +++ b/instaclone/app/auth/views.py @@ -60,5 +60,7 @@ async def callback(code: str = Query(..., description="Google Authorization Code return { "user_info": user_info, "user_id": user.user_id, + "username": user.username, + "user_password": user.password, "is_created": is_created } \ No newline at end of file diff --git a/instaclone/database/settings.py b/instaclone/database/settings.py index b507b1d..99678de 100644 --- a/instaclone/database/settings.py +++ b/instaclone/database/settings.py @@ -19,6 +19,7 @@ def url(self) -> str: case_sensitive=False, env_prefix="DB_", env_file=SETTINGS.env_file, + extra = "allow" ) From 615df9835c569688af4d75e416f6b88f19aa4049 Mon Sep 17 00:00:00 2001 From: 2020-13600 Date: Sun, 19 Jan 2025 18:04:41 +0900 Subject: [PATCH 3/4] user model updated --- instaclone/app/auth/store.py | 1 + instaclone/app/user/models.py | 6 ++++-- ..._db_initialized.py => c7b13a1ab89d_db_initialized.py} | 9 +++++---- 3 files changed, 10 insertions(+), 6 deletions(-) rename instaclone/database/alembic/versions/{d9846c7e8030_db_initialized.py => c7b13a1ab89d_db_initialized.py} (96%) diff --git a/instaclone/app/auth/store.py b/instaclone/app/auth/store.py index 42821b9..abd9e50 100644 --- a/instaclone/app/auth/store.py +++ b/instaclone/app/auth/store.py @@ -23,6 +23,7 @@ async def get_or_create_user_from_google(user_info: dict): phone_number=99999999999, creation_date=datetime.today().date(), profile_image=user_info.get("picture"), + social=True ) session.add(user) await session.commit() diff --git a/instaclone/app/user/models.py b/instaclone/app/user/models.py index 406e0cb..21fa027 100644 --- a/instaclone/app/user/models.py +++ b/instaclone/app/user/models.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING, List from pydantic import EmailStr -from sqlalchemy import String, BigInteger, Date, ForeignKey +from sqlalchemy import String, BigInteger, Date, Boolean from sqlalchemy.orm import Mapped, mapped_column, relationship from instaclone.database.common import Base @@ -26,11 +26,13 @@ class User(Base): # email email: Mapped[EmailStr] = mapped_column(String(100), unique=True) # phone_number : 010XXXXXXXX - phone_number: Mapped[str] = mapped_column(String(11), unique=True) + phone_number: Mapped[str] = mapped_column(String(11), unique=True, nullable=True) # creation_date : YYYY-MM-DD creation_date: Mapped[Date] = mapped_column(Date) # profile_image : file path string profile_image: Mapped[str] = mapped_column(String(100)) + # social : bool + social: Mapped[bool] = mapped_column(Boolean, default=False, nullable=True) # gender gender: Mapped[str] = mapped_column(String(10), nullable=True) diff --git a/instaclone/database/alembic/versions/d9846c7e8030_db_initialized.py b/instaclone/database/alembic/versions/c7b13a1ab89d_db_initialized.py similarity index 96% rename from instaclone/database/alembic/versions/d9846c7e8030_db_initialized.py rename to instaclone/database/alembic/versions/c7b13a1ab89d_db_initialized.py index 8c62a4c..5359ebf 100644 --- a/instaclone/database/alembic/versions/d9846c7e8030_db_initialized.py +++ b/instaclone/database/alembic/versions/c7b13a1ab89d_db_initialized.py @@ -1,8 +1,8 @@ """DB initialized -Revision ID: d9846c7e8030 +Revision ID: c7b13a1ab89d Revises: -Create Date: 2025-01-17 13:23:06.067010 +Create Date: 2025-01-19 17:17:18.641285 """ from typing import Sequence, Union @@ -12,7 +12,7 @@ # revision identifiers, used by Alembic. -revision: str = 'd9846c7e8030' +revision: str = 'c7b13a1ab89d' down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -31,9 +31,10 @@ def upgrade() -> None: sa.Column('password', sa.String(length=128), nullable=False), sa.Column('full_name', sa.String(length=30), nullable=False), sa.Column('email', sa.String(length=100), nullable=False), - sa.Column('phone_number', sa.String(length=11), nullable=False), + sa.Column('phone_number', sa.String(length=11), nullable=True), sa.Column('creation_date', sa.Date(), nullable=False), sa.Column('profile_image', sa.String(length=100), nullable=False), + sa.Column('social', sa.Boolean(), nullable=True), sa.Column('gender', sa.String(length=10), nullable=True), sa.Column('birthday', sa.Date(), nullable=True), sa.Column('introduce', sa.String(length=100), nullable=True), From 7271f8d15681fef4eecdd564c4dd6cf32965fdc6 Mon Sep 17 00:00:00 2001 From: 2020-13600 Date: Tue, 21 Jan 2025 00:25:43 +0900 Subject: [PATCH 4/4] router added --- instaclone/main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/instaclone/main.py b/instaclone/main.py index 510edd3..d2813d3 100644 --- a/instaclone/main.py +++ b/instaclone/main.py @@ -3,14 +3,17 @@ from fastapi.staticfiles import StaticFiles from instaclone.api import api_router +from instaclone.app.auth.views import google_oauth_router app = FastAPI() app.include_router(api_router, prefix="/api") +app.include_router(google_oauth_router, prefix='/auth') origins = [ "https://d3l72zsyuz0duc.cloudfront.net/", - "http://localhost:5173" + "http://localhost:5173", + "http://localhost:8000" ] app.add_middleware(