Skip to content

Commit 96d83fa

Browse files
committed
fix: create user session asynchronously to prevent latency
1 parent b21aa38 commit 96d83fa

File tree

3 files changed

+73
-42
lines changed

3 files changed

+73
-42
lines changed

api/utils/session_helpers.py

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,38 @@
1-
import requests
2-
3-
from fastapi import Request
1+
import httpx
2+
from fastapi import Request, BackgroundTasks
3+
from sqlalchemy.orm import Session
44

5+
from api.db.database import get_db
56
from api.v1.schemas.session import SessionCreate
7+
from api.v1.services.session import SessionService
68
from api.utils.client_helpers import get_ip_address
79

810

9-
def get_ip_location(ip):
11+
async def get_ip_location(ip):
1012
""" Get IP location.
1113
1214
Args:
1315
ip (str): IP address
1416
"""
1517
country = region = "Unknown"
16-
18+
1719
try:
18-
response = requests.get(f"https://ipinfo.io/{ip}/json/", timeout=10)
19-
if response.status_code != 200:
20-
return f"{region}, {country}"
21-
data = response.json()
22-
region = data.get("region", "Unknown")
23-
country = data.get("country", "Unknown")
24-
except Exception as e:
25-
return f"{region}, {country}"
20+
async with httpx.AsyncClient() as client:
21+
response = await client.get(f"https://ipinfo.io/{ip}/json/", timeout=10)
22+
response.raise_for_status()
23+
data = response.json()
24+
region = data.get("region", "Unknown")
25+
country = data.get("country", "Unknown")
26+
except httpx.RequestError as exc:
27+
print(f"An error occurred while requesting {exc.request.url!r}.")
28+
except httpx.HTTPStatusError as exc:
29+
print(f"Error response {exc.response.status_code} while requesting {exc.request.url!r}.")
30+
except Exception as exc:
31+
print(f"An unexpected error occurred: {exc}")
32+
2633
return f"{region}, {country}"
27-
28-
29-
def get_session_schema_data(request: Request, refresh_token: str = "", expires_at: str = ""):
34+
35+
async def get_session_schema_data(request: Request, refresh_token: str = "", expires_at: str = ""):
3036
"""Get session schema data.
3137
3238
Args:
@@ -35,12 +41,34 @@ def get_session_schema_data(request: Request, refresh_token: str = "", expires_a
3541
expires_at (str): Expiry date
3642
"""
3743
ip = get_ip_address(request)
38-
user_agent= request.headers.get("User-Agent")
3944
return SessionCreate(
4045
ip_address=ip,
41-
location=get_ip_location(ip),
42-
device=user_agent,
46+
location=await get_ip_location(ip),
47+
device=request.headers.get("User-Agent"),
4348
is_revoked=False,
4449
refresh_token=refresh_token,
4550
expires_at=expires_at
4651
)
52+
53+
async def create_session_for_user(
54+
request: Request,
55+
user_id: str,
56+
refresh_token: str = "",
57+
expires_at: str = "",
58+
):
59+
"""Create session for user.
60+
61+
Args:
62+
request (Request): Request object
63+
db: Database session
64+
refresh_token (str): Refresh token
65+
expires_at (str): Expiry date
66+
"""
67+
session_data: SessionCreate = await get_session_schema_data(
68+
request,
69+
refresh_token=refresh_token,
70+
expires_at=expires_at,
71+
)
72+
db = next(get_db())
73+
session_service = SessionService(db)
74+
session_service.create(schema=session_data, user_id=user_id)

api/v1/routes/auth.py

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from api.utils.success_response import auth_response, success_response
2424
from api.utils.send_mail import send_magic_link
2525
from api.utils.settings import settings
26-
from api.utils.session_helpers import get_session_schema_data
26+
from api.utils.session_helpers import create_session_for_user
2727
from api.v1.models import User
2828
from api.v1.schemas.user import Token, UserEmailSender
2929
from api.v1.schemas.user import (
@@ -34,7 +34,7 @@
3434
UserData2,
3535
)
3636
from api.v1.schemas.token import TokenRequest
37-
from api.v1.schemas.session import SessionCreate
37+
# from api.v1.schemas.session import SessionCreate
3838
from api.v1.schemas.user import (MagicLinkRequest,
3939
ChangePasswordSchema,
4040
AuthMeResponse)
@@ -53,7 +53,7 @@
5353
)
5454
from api.v1.services.totp import totp_service
5555
from api.utils.settings import settings
56-
from api.v1.services.session import SessionService
56+
# from api.v1.services.session import SessionService
5757

5858
auth = APIRouter(prefix="/auth", tags=["Authentication"])
5959

@@ -79,14 +79,9 @@ def register(
7979
# Create user account
8080
user = user_service.create(db=db, schema=user_schema)
8181

82-
8382
verification_token = user_service.create_verification_token(user.id)
8483
verification_link = f"{base_url}/api/v1/auth/verify-email?token={verification_token}"
8584

86-
access_token = user_service.create_access_token(user_id=user.id)
87-
refresh_token = user_service.create_refresh_token(user_id=user.id)
88-
cta_link = "https://anchor-python.teams.hng.tech/about-us"
89-
9085
# create an organization for the user
9186
org = CreateUpdateOrganisation(
9287
name=f"{user.email}'s Organisation", email=user.email
@@ -98,15 +93,18 @@ def register(
9893
access_token = user_service.create_access_token(user_id=user.id)
9994
refresh_token = user_service.create_refresh_token(user_id=user.id)
10095
cta_link = f"{settings.ANCHOR_PYTHON_BASE_URL}/about-us"
96+
97+
# create session for user
10198
expires = dt.datetime.now(dt.timezone.utc) + (dt.timedelta(
10299
days=settings.JWT_REFRESH_EXPIRY) - dt.timedelta(seconds=1)
103100
)
104-
session_schema: SessionCreate = get_session_schema_data(
105-
request,
101+
background_tasks.add_task(
102+
create_session_for_user,
103+
request=request,
104+
user_id=user.id,
106105
refresh_token=refresh_token,
107-
expires_at=expires)
108-
session_service = SessionService(db)
109-
session_service.create(db=db, schema=session_schema, user_id=user.id)
106+
expires_at=expires
107+
)
110108

111109
# Send email in the background
112110
background_tasks.add_task(
@@ -257,15 +255,18 @@ def login(request: Request, login_request: LoginRequest, background_tasks: Backg
257255
# Generate access and refresh tokens
258256
access_token = user_service.create_access_token(user_id=user.id)
259257
refresh_token = user_service.create_refresh_token(user_id=user.id)
258+
259+
# create session for user
260260
expires = dt.datetime.now(dt.timezone.utc) + (dt.timedelta(
261261
days=settings.JWT_REFRESH_EXPIRY) - dt.timedelta(seconds=1)
262262
)
263-
session_schema: SessionCreate = get_session_schema_data(
264-
request,
263+
background_tasks.add_task(
264+
create_session_for_user,
265+
request=request,
266+
user_id=user.id,
265267
refresh_token=refresh_token,
266-
expires_at=expires)
267-
session_service = SessionService(db)
268-
session_service.create(db=db, schema=session_schema, user_id=user.id)
268+
expires_at=expires
269+
)
269270

270271
# Background task for email notification
271272
logger.info(f"Queueing login notification for {user.email} in the background...")

api/v1/services/session.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class SessionService:
1010
"""Session service functionality."""
1111

1212
def __init__(self, db: Session):
13+
"""Initialize the service."""
1314
self.db = db
1415

1516
def is_revoked_or_expired(self, refresh_token: str):
@@ -19,6 +20,8 @@ def is_revoked_or_expired(self, refresh_token: str):
1920
return True
2021
if isinstance(session.expires_at, str):
2122
session.expires_at = datetime.fromisoformat(session.expires_at)
23+
24+
session.expires_at = session.expires_at.astimezone(timezone.utc)
2225
current_time = datetime.now(timezone.utc)
2326
if session.is_revoked or (session.expires_at < current_time):
2427
return True
@@ -46,16 +49,15 @@ def fetch_by_ip_and_user_agent(self, ip_address: str, user_agent: str):
4649
).all()
4750
return sessions
4851

49-
def create(self, db: Session, schema: SessionCreate, user_id: str):
52+
def create(self, schema: SessionCreate, user_id: str):
5053
"""Create a new session."""
5154
sessions = self.fetch_by_ip_and_user_agent(schema.ip_address, schema.device)
5255
if sessions:
53-
print(sessions)
5456
self.revoke_sessions(sessions)
5557
new_session = UserSession(**schema.model_dump(), user_id=user_id)
56-
db.add(new_session)
57-
db.commit()
58-
db.refresh(new_session)
58+
self.db.add(new_session)
59+
self.db.commit()
60+
self.db.refresh(new_session)
5961
return new_session
6062

6163
def fetch_all(self, user_id):

0 commit comments

Comments
 (0)