Skip to content

Commit

Permalink
social_user is created in login/create_user && get endpoint changed: …
Browse files Browse the repository at this point in the history
…can now filter by lists of ids by query param.
  • Loading branch information
fjpacheco committed Apr 18, 2024
1 parent 160167a commit c1c28a8
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 64 deletions.
9 changes: 7 additions & 2 deletions .env.dist
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Users service
LOGGING_LEVEL=
PORT=
JWT_SECRET=

# Postgres
POSTGRES_USER=
Expand All @@ -14,4 +13,10 @@ POSTGRES_SCHEMA=
# Google
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REDIRECT_URI=
GOOGLE_REDIRECT_URI=

# JWT System
JWT_SECRET=

# External services
SOCIAL_SERVICE_URL=
75 changes: 48 additions & 27 deletions app/controller/Users.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from fastapi import status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from service.Social import SocialService
from service.Users import UsersService


Expand All @@ -12,43 +13,63 @@ def handle_get_user(self, user_id: int):
user = self.users_service.get_user(user_id)
return JSONResponse(
status_code=status.HTTP_200_OK,
content=jsonable_encoder({
"message": user,
"status": status.HTTP_200_OK,
})
content=jsonable_encoder(
{
"message": user,
"status": status.HTTP_200_OK,
}
),
)

def handle_get_all_users(self):
users = self.users_service.get_all_users()
return JSONResponse(
status_code=status.HTTP_200_OK,
content=jsonable_encoder({
"message": users,
"status": status.HTTP_200_OK,
})
)
def handle_get_all_users(self, ids: list = None):
if ids:
users = self.users_service.get_users_by_ids(ids)
return JSONResponse(
status_code=status.HTTP_200_OK,
content=jsonable_encoder(
{
"message": users,
"status": status.HTTP_200_OK,
}
),
)
else:
users = self.users_service.get_all_users()
return JSONResponse(
status_code=status.HTTP_200_OK,
content=jsonable_encoder(
{
"message": users,
"status": status.HTTP_200_OK,
}
),
)

def handle_create_user(self, user_data: dict):
self.users_service.create_user(user_data)
async def handle_create_user(self, user_data: dict):
result = self.users_service.create_user(user_data)
await SocialService.create_social_user(user_id=result.id)
return JSONResponse(
status_code=status.HTTP_201_CREATED,
content=jsonable_encoder({
"message": "User created successfully",
"status": status.HTTP_201_CREATED,
})
content=jsonable_encoder(
{
"message": "User created successfully",
"status": status.HTTP_201_CREATED,
}
),
)

def handle_login(self, auth_code: str):
async def handle_login(self, auth_code: str):
user, jwt_token = self.users_service.login(auth_code)
await SocialService.create_social_user(user_id=user.get("id"))
return JSONResponse(
status_code=status.HTTP_200_OK,
content=jsonable_encoder({
"message": user,
"status": status.HTTP_200_OK,
}),
headers={
"x-access-token": f"{jwt_token}"
},
content=jsonable_encoder(
{
"message": user,
"status": status.HTTP_200_OK,
}
),
headers={"x-access-token": f"{jwt_token}"},
)

def handle_update_user(self, user_id: int, update_data: dict):
Expand Down
17 changes: 17 additions & 0 deletions app/exceptions/InternalServerErrorException.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from fastapi import HTTPException, status
from typing import Optional


class InternalServerErrorException(HTTPException):
def __init__(
self,
microservice: Optional[str] = "User service",
detail: Optional[str] = None,
):
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
message = f"An error occurred in the {microservice}"

if detail:
message += f": {detail.lower()}"

super().__init__(status_code=status_code, detail=message)
3 changes: 1 addition & 2 deletions app/exceptions/UserException.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ class InvalidData(HTTPException):
def __init__(self):
status_code = status.HTTP_400_BAD_REQUEST
super().__init__(
status_code=status_code,
detail="Invalid user data was provided"
status_code=status_code, detail="Invalid user data was provided"
)


Expand Down
19 changes: 12 additions & 7 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
from repository.Users import UsersRepository
from schemas.Schemas import CreateUserSchema, UpdateUserSchema
from schemas.Schemas import LoginRequest

from typing import List, Annotated, Union
from fastapi import FastAPI, Query

app = FastAPI()
users_repository = UsersRepository()
Expand All @@ -23,18 +24,22 @@ def get_users(user_id: int):


@app.get("/users")
def get_all_users():
return users_controller.handle_get_all_users()
def get_all_users(ids: Annotated[Union[List[str], None], Query()] = None):
if ids:
ids = ids[0].split(",")
return users_controller.handle_get_all_users(ids)
else:
return users_controller.handle_get_all_users()


@app.post("/users")
def create_user(user_data: CreateUserSchema):
return users_controller.handle_create_user(user_data.dict())
async def create_user(user_data: CreateUserSchema):
return await 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)
async def login_with_google(request: LoginRequest):
return await users_controller.handle_login(request.auth_code)


@app.patch("/users/{user_id}")
Expand Down
4 changes: 1 addition & 3 deletions app/models/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@

class User(Base):
__tablename__ = "users"
__table_args__ = {"schema": environ.get(
"POSTGRES_SCHEMA",
"users_service")}
__table_args__ = {"schema": environ.get("POSTGRES_SCHEMA", "users_service")}
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
name = Column(String, nullable=True)
email = Column(String, nullable=False)
Expand Down
14 changes: 7 additions & 7 deletions app/repository/Users.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@


class UsersRepository:
db_url = environ.get(
"DATABASE_URL").replace(
"postgres://",
"postgresql://",
1)
db_url = environ.get("DATABASE_URL").replace("postgres://", "postgresql://", 1)

engine = create_engine(db_url)

Expand All @@ -39,10 +35,14 @@ 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):
def get_all_users(self, ids: list = None):
users = self.session.query(User).all()
return self.__parse_result(users)

def get_users_by_ids(self, ids: list):
users = self.session.query(User).filter(User.id.in_(ids)).all()
return self.__parse_result(users)

def create_user(
self,
email: str,
Expand All @@ -52,7 +52,7 @@ def create_user(
nickname: Optional[str] = None,
biography: Optional[str] = None,
location: Optional[dict] = None,
birthdate: Optional[date] = None
birthdate: Optional[date] = None,
) -> User:
user_data = {"email": email}

Expand Down
40 changes: 40 additions & 0 deletions app/service/Social.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import logging
from httpx import AsyncClient, Response, AsyncHTTPTransport
from os import environ

from exceptions.InternalServerErrorException import InternalServerErrorException
from exceptions.UserException import InvalidData

logger = logging.getLogger("social")
logger.setLevel("DEBUG")

SOCIAL_SERVICE_URL = environ["SOCIAL_SERVICE_URL"]

# Heroku dyno plan has a limit of 30 seconds...
# so, assign 3 retries of 10 seconds each :)
# https://devcenter.heroku.com/articles/request-timeout
NUMBER_OF_RETRIES = 3
TIMEOUT = 10


class SocialService:
@staticmethod
async def post(path: str, body: dict) -> Response:
async with AsyncClient(
transport=AsyncHTTPTransport(retries=NUMBER_OF_RETRIES), timeout=TIMEOUT
) as client:
url = SOCIAL_SERVICE_URL + path
response = await client.post(url, json=body)
return response

@staticmethod
async def create_social_user(user_id: int):
try:
response = await SocialService.post(f"/social/users", body={"id": user_id})
if response.status_code == 201:
return
else:
raise InvalidData()
except Exception as e:
print(f"Unexpected error: {e}")
raise InternalServerErrorException("Social service")
34 changes: 18 additions & 16 deletions app/service/Users.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ def get_user(self, user_id: int):
def get_all_users(self):
return self.user_repository.get_all_users()

def get_users_by_ids(self, ids: list):
return self.user_repository.get_users_by_ids(ids)

def create_user(self, user_data: dict):
if not self._validate_location(user_data.get("location")):
raise InvalidData()
Expand All @@ -31,12 +34,14 @@ def update_user(self, user_id: int, update_data: dict):
# es el propio usuario editando sus datos y no permitir
# que un usuario edite los de un tercero
self.get_user(user_id)
filtered_update_data = {k: v for k, v in update_data.items()
if v is not None}
if 'photo' in filtered_update_data:
photo_url = filtered_update_data['photo']
if not re.match(r'^https?://(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,6}'
r'(?:/[^/#?]+)+(?:\?.*)?$', photo_url):
filtered_update_data = {k: v for k, v in update_data.items() if v is not None}
if "photo" in filtered_update_data:
photo_url = filtered_update_data["photo"]
if not re.match(
r"^https?://(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,6}"
r"(?:/[^/#?]+)+(?:\?.*)?$",
photo_url,
):
raise InvalidURL("Invalid photo URL")
self.user_repository.edit_user(user_id, filtered_update_data)

Expand All @@ -53,9 +58,7 @@ def login(self, auth_code: str):
user = self.user_repository.get_user_by_email(user_info["email"])
user_id = user.get("id")
payload = {"user_id": user_id}
jwt_token = jwt.encode(payload,
os.environ["JWT_SECRET"],
algorithm="HS256")
jwt_token = jwt.encode(payload, os.environ["JWT_SECRET"], algorithm="HS256")
return user, jwt_token

def _get_access_token(self, authorization_code):
Expand All @@ -65,7 +68,7 @@ def _get_access_token(self, authorization_code):
"client_secret": os.environ["GOOGLE_CLIENT_SECRET"],
"code": authorization_code,
"grant_type": "authorization_code",
"redirect_uri": os.environ["GOOGLE_REDIRECT_URI"]
"redirect_uri": os.environ["GOOGLE_REDIRECT_URI"],
}
response = requests.post(token_url, data=payload)
if response.status_code == 200:
Expand All @@ -82,18 +85,17 @@ def _get_user_info(self, access_token):
if response.status_code != 200:
raise AuthenticationError()

user_data = {'email': response.json().get("email")}
user_data = {"email": response.json().get("email")}
if response.json().get("gender") is not None:
user_data['gender'] = response.json().get("gender")
user_data["gender"] = response.json().get("gender")
if response.json().get("name") is not None:
user_data['name'] = response.json().get("name")
user_data["name"] = response.json().get("name")
if response.json().get("picture") is not None:
user_data['photo'] = response.json().get("picture")
user_data["photo"] = response.json().get("picture")
return user_data

def _validate_location(self, location):
if "lat" in location and "long" in location:
if -90 <= location["lat"] <= 90 and \
-180 <= location["long"] <= 180:
if -90 <= location["lat"] <= 90 and -180 <= location["long"] <= 180:
return True
return False
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ psycopg2-binary
SQLAlchemy
requests_oauthlib
pyjwt
httpx

0 comments on commit c1c28a8

Please sign in to comment.