Skip to content

Commit

Permalink
Han 38: login with google (#2)
Browse files Browse the repository at this point in the history
## Linked ticket in Jira
- https://hanagotchi.atlassian.net/browse/HAN-38

## Describe your changes, marking the new features and possible risks
- Se agrega un endpoint para login con google
- Desde el front se envia un codigo de autenticacion que va a ser
utilizado desde el back para obtener informacion del usuario y crearlo
en la db de ser necesario.
- Se le pega a "https://oauth2.googleapis.com/token" con el codigo que
paso el front para obtener un token
- Se le pega a "https://www.googleapis.com/oauth2/v2/userinfo" con el
token para obtener la informacion del usuario. Pueden ser los
parametros: email, name, picture, y otros
(https://any-api.com/googleapis_com/oauth2/docs/userinfo/oauth2_userinfo_get)
pero depende de la cuenta del usuario.

## Pruebas
Para probar en endpoint hay que generar el token que nos pasaria el
front. Es un token **single use**. Los pasos son:
- Acceder a esta url
https://accounts.google.com/o/oauth2/auth?client_id=242595357606-8j31hrog4d785563t3f03qc01hpevr84.apps.googleusercontent.com&redirect_uri=http://localhost:8000/auth/google/callback&scope=openid%20email&response_type=code&access_type=offline
- Loguearse con la cuenta fiuba. Solo nuestras cuentas fiuba estan
autorizadas.
- Agarrar el query param _code_ de la url a la que te redirecciona
- Desencodearlo (https://www.url-encode-decode.com/)
- Ponerlo en el body de la request
  • Loading branch information
feijooso authored Feb 27, 2024
2 parents 5e62a72 + 12c4efb commit 18bd2e5
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 17 deletions.
4 changes: 3 additions & 1 deletion .env.dist
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ POSTGRES_PASSWORD=
POSTGRES_DB=
POSTGRES_HOST=
POSTGRES_PORT=
DATABASE_URL=
DATABASE_URL=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
5 changes: 5 additions & 0 deletions app/controller/Users.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
16 changes: 10 additions & 6 deletions app/docker/tablas.sql
Original file line number Diff line number Diff line change
@@ -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');
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');
8 changes: 8 additions & 0 deletions app/exceptions/LoginException.py
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 6 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
5 changes: 4 additions & 1 deletion app/models/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
23 changes: 20 additions & 3 deletions app/repository/Users.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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:
Expand Down
9 changes: 8 additions & 1 deletion app/schemas/Schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
56 changes: 53 additions & 3 deletions app/service/Users.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ uvicorn
python-dotenv
flake8
psycopg2-binary
SQLAlchemy
SQLAlchemy
requests_oauthlib

0 comments on commit 18bd2e5

Please sign in to comment.