-
Notifications
You must be signed in to change notification settings - Fork 265
Solution #256
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Solution #256
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| 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 | ||
|
|
@@ -67,7 +67,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. | ||
|
|
@@ -120,6 +122,15 @@ async def register_user( | |
|
|
||
| await db.commit() | ||
| await db.refresh(new_user) | ||
|
|
||
| activation_link = f"http://127.0.0.1/accounts/activate?token={activation_token.token}&email={new_user.email}" | ||
|
|
||
| background_tasks.add_task( | ||
| email_sender.send_activation_email, | ||
| str(new_user.email), | ||
| activation_link | ||
| ) | ||
|
|
||
| except SQLAlchemyError as e: | ||
| await db.rollback() | ||
| raise HTTPException( | ||
|
|
@@ -163,7 +174,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: | ||
|
Comment on lines
175
to
180
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the registration endpoint, this function should use |
||
| """ | ||
| Endpoint to activate a user's account. | ||
|
|
@@ -218,6 +231,14 @@ async def activate_account( | |
| await db.delete(token_record) | ||
| await db.commit() | ||
|
|
||
| login_link = "http://127.0.0.1/accounts/login/" | ||
|
|
||
| background_tasks.add_task( | ||
| email_sender.send_activation_complete_email, | ||
| str(activation_data.email), | ||
| login_link | ||
| ) | ||
|
|
||
| return MessageResponseSchema(message="User account activated successfully.") | ||
|
|
||
|
|
||
|
|
@@ -233,7 +254,9 @@ async def activate_account( | |
| ) | ||
| async def request_password_reset_token( | ||
| data: PasswordResetRequestSchema, | ||
| background_tasks: BackgroundTasks, | ||
| db: AsyncSession = Depends(get_db), | ||
| email_sender: EmailSenderInterface = Depends(get_accounts_email_notificator) | ||
| ) -> MessageResponseSchema: | ||
|
Comment on lines
255
to
260
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function should use |
||
| """ | ||
| Endpoint to request a password reset token. | ||
|
|
@@ -263,6 +286,14 @@ async def request_password_reset_token( | |
| db.add(reset_token) | ||
| await db.commit() | ||
|
|
||
| reset_link = f"http://127.0.0.1/accounts/password-reset?token={reset_token.token}&email={data.email}" | ||
|
|
||
| background_tasks.add_task( | ||
| email_sender.send_password_reset_email, | ||
| str(data.email), | ||
| reset_link | ||
| ) | ||
|
|
||
| return MessageResponseSchema( | ||
| message="If you are registered, you will receive an email with instructions." | ||
| ) | ||
|
|
@@ -313,7 +344,9 @@ async def request_password_reset_token( | |
| ) | ||
| async def reset_password( | ||
| data: PasswordResetCompleteRequestSchema, | ||
| background_tasks: BackgroundTasks, | ||
| db: AsyncSession = Depends(get_db), | ||
| email_sender: EmailSenderInterface = Depends(get_accounts_email_notificator) | ||
| ) -> MessageResponseSchema: | ||
|
Comment on lines
345
to
350
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The password reset completion email should be sent in the background. Please inject |
||
| """ | ||
| Endpoint for resetting a user's password. | ||
|
|
@@ -376,6 +409,14 @@ async def reset_password( | |
| detail="An error occurred while resetting the password." | ||
| ) | ||
|
|
||
| login_link = "http://127.0.0.1/accounts/login/" | ||
|
|
||
| background_tasks.add_task( | ||
| email_sender.send_password_reset_complete_email, | ||
| str(data.email), | ||
| login_link | ||
| ) | ||
|
|
||
| return MessageResponseSchema(message="Password reset successfully.") | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,137 @@ | ||
| from fastapi import APIRouter | ||
| from typing import cast | ||
|
|
||
| from fastapi import APIRouter, Depends, HTTPException, status | ||
| from pydantic import HttpUrl | ||
| from sqlalchemy import select | ||
| from sqlalchemy.ext.asyncio import AsyncSession | ||
|
|
||
| from config import get_s3_storage_client, get_jwt_auth_manager | ||
| from database import get_db | ||
| from database.models.accounts import UserModel, UserProfileModel, GenderEnum, UserGroupModel, UserGroupEnum | ||
| from exceptions import BaseSecurityError, S3FileUploadError | ||
| from schemas.profiles import ProfileCreateSchema, ProfileResponseSchema | ||
| from security.interfaces import JWTAuthManagerInterface | ||
| from security.http import get_token | ||
| from storages import S3StorageInterface | ||
|
|
||
|
|
||
| router = APIRouter() | ||
|
|
||
| # Write your code here | ||
|
|
||
| @router.post( | ||
| "/users/{user_id}/profile/", | ||
| response_model=ProfileResponseSchema, | ||
| summary="Create user profile", | ||
| status_code=status.HTTP_201_CREATED | ||
| ) | ||
| async def create_profile( | ||
| user_id: int, | ||
| token: str = Depends(get_token), | ||
| jwt_manager: JWTAuthManagerInterface = Depends(get_jwt_auth_manager), | ||
| db: AsyncSession = Depends(get_db), | ||
| s3_client: S3StorageInterface = Depends(get_s3_storage_client), | ||
| profile_data: ProfileCreateSchema = Depends(ProfileCreateSchema.from_form) | ||
| ) -> ProfileResponseSchema: | ||
| """ | ||
| Creates a user profile. | ||
|
|
||
| Steps: | ||
| - Validate user authentication token. | ||
| - Check if the user already has a profile. | ||
| - Upload avatar to S3 storage. | ||
| - Store profile details in the database. | ||
|
|
||
| Args: | ||
| user_id (int): The ID of the user for whom the profile is being created. | ||
| token (str): The authentication token. | ||
| jwt_manager (JWTAuthManagerInterface): JWT manager for decoding tokens. | ||
| db (AsyncSession): The asynchronous database session. | ||
| s3_client (S3StorageInterface): The asynchronous S3 storage client. | ||
| profile_data (ProfileCreateSchema): The profile data from the form. | ||
|
|
||
| Returns: | ||
| ProfileResponseSchema: The created user profile details. | ||
|
|
||
| Raises: | ||
| HTTPException: If authentication fails, if the user is not found or inactive, | ||
| or if the profile already exists, or if S3 upload fails. | ||
| """ | ||
| try: | ||
| payload = jwt_manager.decode_access_token(token) | ||
| token_user_id = payload.get("user_id") | ||
| except BaseSecurityError as e: | ||
| raise HTTPException( | ||
| status_code=status.HTTP_401_UNAUTHORIZED, | ||
| detail=str(e) | ||
| ) | ||
|
|
||
| if user_id != token_user_id: | ||
| stmt = ( | ||
| select(UserGroupModel) | ||
| .join(UserModel) | ||
| .where(UserModel.id == token_user_id) | ||
| ) | ||
| result = await db.execute(stmt) | ||
| user_group = result.scalars().first() | ||
| if not user_group or user_group.name == UserGroupEnum.USER: | ||
| raise HTTPException( | ||
| status_code=status.HTTP_403_FORBIDDEN, | ||
| detail="You don't have permission to edit this profile." | ||
| ) | ||
|
|
||
| stmt = select(UserModel).where(UserModel.id == user_id) | ||
| result = await db.execute(stmt) | ||
| user = result.scalars().first() | ||
| if not user or not user.is_active: | ||
| raise HTTPException( | ||
| status_code=status.HTTP_401_UNAUTHORIZED, | ||
| detail="User not found or not active." | ||
| ) | ||
|
|
||
| stmt_profile = select(UserProfileModel).where(UserProfileModel.user_id == user.id) | ||
| result_profile = await db.execute(stmt_profile) | ||
| existing_profile = result_profile.scalars().first() | ||
| if existing_profile: | ||
| raise HTTPException( | ||
| status_code=status.HTTP_400_BAD_REQUEST, | ||
| detail="User already has a profile." | ||
| ) | ||
|
|
||
| avatar_bytes = await profile_data.avatar.read() | ||
| avatar_key = f"avatars/{user.id}_{profile_data.avatar.filename}" | ||
|
|
||
| try: | ||
| await s3_client.upload_file(file_name=avatar_key, file_data=avatar_bytes) | ||
| except S3FileUploadError as e: | ||
| print(f"Error uploading avatar to S3: {e}") | ||
| raise HTTPException( | ||
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | ||
| detail="Failed to upload avatar. Please try again later." | ||
| ) | ||
|
|
||
| new_profile = UserProfileModel( | ||
| user_id=cast(int, user.id), | ||
| first_name=profile_data.first_name, | ||
| last_name=profile_data.last_name, | ||
| gender=cast(GenderEnum, profile_data.gender), | ||
| date_of_birth=profile_data.date_of_birth, | ||
| info=profile_data.info, | ||
| avatar=avatar_key | ||
| ) | ||
|
|
||
| db.add(new_profile) | ||
| await db.commit() | ||
| await db.refresh(new_profile) | ||
|
|
||
| avatar_url = await s3_client.get_file_url(new_profile.avatar) | ||
|
|
||
| return ProfileResponseSchema( | ||
| id=new_profile.id, | ||
| user_id=new_profile.user_id, | ||
| first_name=new_profile.first_name, | ||
| last_name=new_profile.last_name, | ||
| gender=new_profile.gender, | ||
| date_of_birth=new_profile.date_of_birth, | ||
| info=new_profile.info, | ||
| avatar=cast(HttpUrl, avatar_url) | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,4 +16,4 @@ | |
| UserLoginRequestSchema, | ||
| TokenRefreshRequestSchema, | ||
| TokenRefreshResponseSchema | ||
| ) | ||
| ) | ||
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the task requirements, email notifications must be sent asynchronously. Please add
background_tasks: BackgroundTasksto the function parameters and usebackground_tasks.add_task()later to send the email, instead of calling it directly withawait.