Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 83 additions & 27 deletions src/routes/accounts.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
from datetime import datetime, timezone
from typing import cast

from fastapi import APIRouter, Depends, status, HTTPException
from fastapi import (
APIRouter,
Depends,
status,
HTTPException,
BackgroundTasks
)
from sqlalchemy import select, delete
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload

from config import get_jwt_auth_manager, get_settings, BaseAppSettings, get_accounts_email_notificator
from config import (
get_settings,
BaseAppSettings,
get_accounts_email_notificator
)
from security.dependencies import get_jwt_auth_manager
from database import (
get_db,
UserModel,
Expand Down Expand Up @@ -67,7 +78,9 @@
)
async def register_user(
user_data: UserRegistrationRequestSchema,
background_tasks: BackgroundTasks,
db: AsyncSession = Depends(get_db),
email_sender: EmailSenderInterface = Depends(get_accounts_email_notificator),
) -> UserRegistrationResponseSchema:
"""
Endpoint for user registration.
Expand Down Expand Up @@ -117,6 +130,13 @@ async def register_user(

activation_token = ActivationTokenModel(user_id=new_user.id)
db.add(activation_token)
activation_link = f"http://127.0.0.1/accounts/activate/?token={activation_token.token}"

background_tasks.add_task(
email_sender.send_activation_email,
new_user.email,
activation_link
)

await db.commit()
await db.refresh(new_user)
Expand Down Expand Up @@ -163,7 +183,9 @@ async def register_user(
)
async def activate_account(
activation_data: UserActivationRequestSchema,
background_tasks: BackgroundTasks,
db: AsyncSession = Depends(get_db),
email_sender: EmailSenderInterface = Depends(get_accounts_email_notificator),
) -> MessageResponseSchema:
"""
Endpoint to activate a user's account.
Expand Down Expand Up @@ -213,12 +235,22 @@ async def activate_account(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User account is already active."
)

user.is_active = True
await db.delete(token_record)
await db.commit()

return MessageResponseSchema(message="User account activated successfully.")
try:
user.is_active = True
await db.delete(token_record)
await db.commit()
background_tasks.add_task(
email_sender.send_activation_complete_email,
user.email
)
except SQLAlchemyError as e:
await db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to activate user account."
) from e
else:
return MessageResponseSchema(message="User account activated successfully.")


@router.post(
Expand All @@ -232,8 +264,10 @@ async def activate_account(
status_code=status.HTTP_200_OK,
)
async def request_password_reset_token(
data: PasswordResetRequestSchema,
reset_request: PasswordResetRequestSchema,
background_tasks: BackgroundTasks,
db: AsyncSession = Depends(get_db),
email_sender: EmailSenderInterface = Depends(get_accounts_email_notificator),
) -> MessageResponseSchema:
"""
Endpoint to request a password reset token.
Expand All @@ -248,7 +282,7 @@ async def request_password_reset_token(
Returns:
MessageResponseSchema: A success message indicating that instructions will be sent.
"""
stmt = select(UserModel).filter_by(email=data.email)
stmt = select(UserModel).filter_by(email=reset_request.email)
result = await db.execute(stmt)
user = result.scalars().first()

Expand All @@ -258,14 +292,27 @@ async def request_password_reset_token(
)

await db.execute(delete(PasswordResetTokenModel).where(PasswordResetTokenModel.user_id == user.id))

reset_token = PasswordResetTokenModel(user_id=cast(int, user.id))
db.add(reset_token)
await db.commit()

return MessageResponseSchema(
message="If you are registered, you will receive an email with instructions."
)
try:
reset_token = PasswordResetTokenModel(user_id=cast(int, user.id))
db.add(reset_token)
await db.commit()
if user and user.is_active:
reset_link = f"http://127.0.0.1/accounts/password-reset/complete/?token={reset_token.token}"
background_tasks.add_task(
email_sender.send_password_reset_email,
user.email,
reset_link
)
except SQLAlchemyError as e:
await db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create password reset token."
) from e
else:
return MessageResponseSchema(
message="If a user with this email exists, a password reset link has been sent."
)


@router.post(
Expand Down Expand Up @@ -312,8 +359,10 @@ async def request_password_reset_token(
},
)
async def reset_password(
data: PasswordResetCompleteRequestSchema,
reset_data: PasswordResetCompleteRequestSchema,
background_tasks: BackgroundTasks,
db: AsyncSession = Depends(get_db),
email_sender: EmailSenderInterface = Depends(get_accounts_email_notificator),
) -> MessageResponseSchema:
"""
Endpoint for resetting a user's password.
Expand All @@ -334,7 +383,7 @@ async def reset_password(
- 400 Bad Request if the email or token is invalid, or the token has expired.
- 500 Internal Server Error if an error occurs during the password reset process.
"""
stmt = select(UserModel).filter_by(email=data.email)
stmt = select(UserModel).filter_by(email=reset_data.email)
result = await db.execute(stmt)
user = result.scalars().first()
if not user or not user.is_active:
Expand All @@ -347,7 +396,7 @@ async def reset_password(
result = await db.execute(stmt)
token_record = result.scalars().first()

if not token_record or token_record.token != data.token:
if not token_record or token_record.token != reset_data.token:
if token_record:
await db.run_sync(lambda s: s.delete(token_record))
await db.commit()
Expand All @@ -366,17 +415,24 @@ async def reset_password(
)

try:
user.password = data.password
user.update_password(reset_data.password)
await db.run_sync(lambda s: s.delete(token_record))
await db.commit()
except SQLAlchemyError:
login_link = "http://127.0.0.1/accounts/login/"
background_tasks.add_task(
email_sender.send_password_reset_complete_email,
user.email,
login_link
)
Comment on lines +422 to +426

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The task description provides an example for this email notification that includes a login_link argument. Including a link to the login page is a good user experience after a password reset. Please add the login_link to this background task as shown in the requirements.


except SQLAlchemyError as e:
await db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="An error occurred while resetting the password."
)

return MessageResponseSchema(message="Password reset successfully.")
detail="Failed to reset password."
) from e
else:
return MessageResponseSchema(message="Password has been reset successfully.")


@router.post(
Expand Down
Loading
Loading