diff --git a/.env.dist b/.env.dist index b4167bd..09a246c 100644 --- a/.env.dist +++ b/.env.dist @@ -3,4 +3,6 @@ POSTGRES_PASSWORD= POSTGRES_DB= POSTGRES_HOST= POSTGRES_PORT= -DATABASE_URL= \ No newline at end of file +DATABASE_URL= +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= \ No newline at end of file diff --git a/app/controller/Users.py b/app/controller/Users.py index 8e49ff5..f5157bc 100644 --- a/app/controller/Users.py +++ b/app/controller/Users.py @@ -20,3 +20,8 @@ def handle_create_user(self, user_data: dict): "message": "User created successfully", "status": status.HTTP_201_CREATED, } + + def handle_login(self, auth_code: str): + user, first_login = self.users_service.login(auth_code) + code = status.HTTP_201_CREATED if first_login else status.HTTP_200_OK + return {"message": user, "status": code} diff --git a/app/docker/tablas.sql b/app/docker/tablas.sql index b15132c..4abc24b 100644 --- a/app/docker/tablas.sql +++ b/app/docker/tablas.sql @@ -1,12 +1,16 @@ CREATE SCHEMA IF NOT EXISTS dev; CREATE TABLE IF NOT EXISTS dev.users ( - id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL + id SERIAL PRIMARY KEY, + name VARCHAR(255), + email VARCHAR(255) UNIQUE NOT NULL, + gender VARCHAR(20), + photo VARCHAR(255) ); INSERT INTO - dev.users (name) -VALUES ('Agus'), - ('Pach'), - ('Sofi'), - ('Violeta'); \ No newline at end of file + dev.users (name, email) +VALUES ('Agus', 'agus@fi.uba.ar'), + ('Pach', 'pach@fi.uba.ar'), + ('Sofi', 'sofi@fi.uba.ar'), + ('Violeta', 'violeta@fi.uba.ar'); \ No newline at end of file diff --git a/app/exceptions/LoginException.py b/app/exceptions/LoginException.py new file mode 100644 index 0000000..61caf7e --- /dev/null +++ b/app/exceptions/LoginException.py @@ -0,0 +1,8 @@ +from fastapi import HTTPException, status +from typing import Optional + + +class AuthenticationError(HTTPException): + def __init__(self, message: Optional[str] = "Could not authenticate"): + status_code = status.HTTP_401_UNAUTHORIZED + super().__init__(status_code=status_code, detail=message) diff --git a/app/main.py b/app/main.py index dcdef49..8d14472 100644 --- a/app/main.py +++ b/app/main.py @@ -2,8 +2,8 @@ from controller.Users import UsersController from service.Users import UsersService from repository.Users import UsersRepository -# from repository.UsersLocal import UsersRepository from schemas.Schemas import CreateUserSchema +from schemas.Schemas import LoginRequest app = FastAPI() @@ -30,3 +30,8 @@ async def get_all_users(): @app.post("/users") async def create_user(user_data: CreateUserSchema): return users_controller.handle_create_user(user_data.dict()) + + +@app.post("/login") +def login_with_google(request: LoginRequest): + return users_controller.handle_login(request.auth_code) diff --git a/app/models/users.py b/app/models/users.py index 34e79b0..074df62 100644 --- a/app/models/users.py +++ b/app/models/users.py @@ -6,4 +6,7 @@ class User(Base): __tablename__ = "users" __table_args__ = {'schema': 'dev'} id = Column(Integer, primary_key=True, index=True, autoincrement=True) - name = Column(String, nullable=False) + name = Column(String, nullable=True) + email = Column(String, nullable=False) + gender = Column(String, nullable=True) + photo = Column(String, nullable=True) diff --git a/app/repository/Users.py b/app/repository/Users.py index 4f7ba00..3af1b17 100644 --- a/app/repository/Users.py +++ b/app/repository/Users.py @@ -1,6 +1,7 @@ from sqlalchemy import create_engine, engine from sqlalchemy.orm import Session import os +from typing import Optional from models.users import User @@ -31,15 +32,31 @@ def get_user(self, user_id: int): user = self.session.query(User).filter_by(id=user_id).first() return user.__dict__ if user else None + def get_user_by_email(self, email: str): + user = self.session.query(User).filter_by(email=email).first() + return user.__dict__ if user else None + def get_all_users(self): users = self.session.query(User).all() return self.__parse_result(users) - def create_user(self, name: str): - new_user = User(name=name) + def create_user(self, email: str, + name: Optional[str] = None, + gender: Optional[str] = None, + photo: Optional[str] = None): + user_data = {'email': email} + + if name is not None: + user_data['name'] = name + if gender is not None: + user_data['gender'] = gender + if photo is not None: + user_data['photo'] = photo + + new_user = User(**user_data) self.session.add(new_user) self.session.commit() - return + return new_user.__dict__ def __parse_result(self, result): if not result: diff --git a/app/schemas/Schemas.py b/app/schemas/Schemas.py index 1d978e4..8bbcd77 100644 --- a/app/schemas/Schemas.py +++ b/app/schemas/Schemas.py @@ -4,7 +4,14 @@ class UserSchema(BaseModel): id: int name: str + email: str + gender: str + photo: str class CreateUserSchema(BaseModel): - name: str + email: str + + +class LoginRequest(BaseModel): + auth_code: str diff --git a/app/service/Users.py b/app/service/Users.py index 42db1bc..4913001 100644 --- a/app/service/Users.py +++ b/app/service/Users.py @@ -1,6 +1,8 @@ from exceptions.UserException import UserNotFound +from exceptions.LoginException import AuthenticationError from repository.Users import UsersRepository -# from repository.UsersLocal import UsersRepository +import requests +import os class UsersService: @@ -17,5 +19,53 @@ def get_all_users(self): return self.user_repository.get_all_users() def create_user(self, user_data: dict): - name = user_data.get("name") - return self.user_repository.create_user(name) + email = user_data.get("email") + return self.user_repository.create_user(email) + + def login(self, auth_code: str): + access_token = self._get_access_token(auth_code) + if access_token is None: + raise AuthenticationError("Authentication code is invalid") + + user_info = self._get_user_info(access_token) + user = self.user_repository.get_user_by_email(user_info["email"]) + + if user is not None: + return user, False + + self.user_repository.create_user(**user_info) + user = self.user_repository.get_user_by_email(user_info["email"]) + return user, True + + def _get_access_token(self, authorization_code): + token_url = "https://oauth2.googleapis.com/token" + payload = { + "client_id": os.environ["GOOGLE_CLIENT_ID"], + "client_secret": os.environ["GOOGLE_CLIENT_SECRET"], + "code": authorization_code, + "grant_type": "authorization_code", + "redirect_uri": "http://localhost:8000/auth/google/callback" + } + response = requests.post(token_url, data=payload) + if response.status_code == 200: + return response.json().get("access_token") + else: + return None + + def _get_user_info(self, access_token): + user_info_url = "https://www.googleapis.com/oauth2/v2/userinfo" + headers = {"Authorization": f"Bearer {access_token}"} + params = {"fields": "id,email,name,picture,gender"} + response = requests.get(user_info_url, headers=headers, params=params) + + if response.status_code != 200: + raise AuthenticationError() + + user_data = {'email': response.json().get("email")} + if response.json().get("gender") is not None: + user_data['gender'] = response.json().get("gender") + if response.json().get("name") is not None: + user_data['name'] = response.json().get("name") + if response.json().get("picture") is not None: + user_data['photo'] = response.json().get("picture") + return user_data diff --git a/requirements.txt b/requirements.txt index eef42db..6ddea78 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ uvicorn python-dotenv flake8 psycopg2-binary -SQLAlchemy \ No newline at end of file +SQLAlchemy +requests_oauthlib