-
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 1 commit
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 |
|---|---|---|
|
|
@@ -68,6 +68,7 @@ | |
| async def register_user( | ||
| user_data: UserRegistrationRequestSchema, | ||
| db: AsyncSession = Depends(get_db), | ||
| email_sender: EmailSenderInterface = Depends(get_accounts_email_notificator), | ||
| ) -> UserRegistrationResponseSchema: | ||
| """ | ||
| Endpoint for user registration. | ||
|
|
@@ -79,6 +80,7 @@ async def register_user( | |
| Args: | ||
| user_data (UserRegistrationRequestSchema): The registration details including email and password. | ||
| db (AsyncSession): The asynchronous database session. | ||
| email_sender (EmailSenderInterface): The asynchronous email sender. | ||
|
|
||
| Returns: | ||
| UserRegistrationResponseSchema: The newly created user's details. | ||
|
|
@@ -127,6 +129,13 @@ async def register_user( | |
| detail="An error occurred during user creation." | ||
| ) from e | ||
| else: | ||
| activation_link = "http://127.0.0.1/accounts/activate/" | ||
|
||
|
|
||
| await email_sender.send_activation_email( | ||
| new_user.email, | ||
| activation_link | ||
| ) | ||
|
|
||
| return UserRegistrationResponseSchema.model_validate(new_user) | ||
|
|
||
|
|
||
|
|
@@ -164,6 +173,7 @@ async def register_user( | |
| async def activate_account( | ||
| activation_data: UserActivationRequestSchema, | ||
| 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. | ||
|
|
@@ -176,6 +186,7 @@ async def activate_account( | |
| Args: | ||
| activation_data (UserActivationRequestSchema): Contains the user's email and activation token. | ||
| db (AsyncSession): The asynchronous database session. | ||
| email_sender (EmailSenderInterface): The asynchronous email sender. | ||
|
|
||
| Returns: | ||
| MessageResponseSchema: A response message confirming successful activation. | ||
|
|
@@ -218,6 +229,13 @@ async def activate_account( | |
| await db.delete(token_record) | ||
| await db.commit() | ||
|
|
||
| login_link = "http://127.0.0.1/accounts/login/" | ||
|
|
||
| await email_sender.send_activation_complete_email( | ||
| str(activation_data.email), | ||
| login_link | ||
| ) | ||
|
|
||
| return MessageResponseSchema(message="User account activated successfully.") | ||
|
|
||
|
|
||
|
|
@@ -234,6 +252,7 @@ async def activate_account( | |
| async def request_password_reset_token( | ||
| data: PasswordResetRequestSchema, | ||
| 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. | ||
|
|
@@ -244,6 +263,7 @@ async def request_password_reset_token( | |
| Args: | ||
| data (PasswordResetRequestSchema): The request data containing the user's email. | ||
| db (AsyncSession): The asynchronous database session. | ||
| email_sender (EmailSenderInterface): The asynchronous email sender. | ||
|
|
||
| Returns: | ||
| MessageResponseSchema: A success message indicating that instructions will be sent. | ||
|
|
@@ -263,6 +283,13 @@ async def request_password_reset_token( | |
| db.add(reset_token) | ||
| await db.commit() | ||
|
|
||
| password_reset_complete_link = "http://127.0.0.1/accounts/password-reset-complete/" | ||
|
||
|
|
||
| await email_sender.send_password_reset_email( | ||
| str(data.email), | ||
| password_reset_complete_link | ||
| ) | ||
|
|
||
| return MessageResponseSchema( | ||
| message="If you are registered, you will receive an email with instructions." | ||
| ) | ||
|
|
@@ -314,6 +341,7 @@ async def request_password_reset_token( | |
| async def reset_password( | ||
| data: PasswordResetCompleteRequestSchema, | ||
| 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. | ||
|
|
@@ -325,6 +353,7 @@ async def reset_password( | |
| data (PasswordResetCompleteRequestSchema): The request data containing the user's email, | ||
| token, and new password. | ||
| db (AsyncSession): The asynchronous database session. | ||
| email_sender (EmailSenderInterface): The asynchronous email sender. | ||
|
|
||
| Returns: | ||
| MessageResponseSchema: A response message indicating successful password reset. | ||
|
|
@@ -376,6 +405,13 @@ async def reset_password( | |
| detail="An error occurred while resetting the password." | ||
| ) | ||
|
|
||
| login_link = "http://127.0.0.1/accounts/login/" | ||
|
|
||
| await 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 | ||
| ) | ||
| ) | ||
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,4 +10,125 @@ | |
| validate_birth_date | ||
| ) | ||
|
|
||
| # Write your code here | ||
|
|
||
| class ProfileCreateSchema(BaseModel): | ||
| first_name: str | ||
| last_name: str | ||
| gender: str | ||
| date_of_birth: date | ||
| info: str | ||
| avatar: UploadFile | ||
|
|
||
| @classmethod | ||
| def from_form( | ||
| cls, | ||
| first_name: str = Form(...), | ||
| last_name: str = Form(...), | ||
| gender: str = Form(...), | ||
| date_of_birth: date = Form(...), | ||
| info: str = Form(...), | ||
| avatar: UploadFile = File(...) | ||
| ) -> "ProfileCreateSchema": | ||
| return cls( | ||
| first_name=first_name, | ||
| last_name=last_name, | ||
| gender=gender, | ||
| date_of_birth=date_of_birth, | ||
| info=info, | ||
| avatar=avatar | ||
| ) | ||
|
|
||
| @field_validator("first_name", "last_name") | ||
| @classmethod | ||
| def validate_name_field(cls, name: str) -> str: | ||
| try: | ||
| validate_name(name) | ||
| return name.lower() | ||
| except ValueError as e: | ||
| raise HTTPException( | ||
| status_code=422, | ||
| detail=[{ | ||
| "type": "value_error", | ||
| "loc": ["first_name" if "first_name" in name else "last_name"], | ||
|
||
| "msg": str(e), | ||
| "input": name | ||
| }] | ||
| ) | ||
|
|
||
| @field_validator("avatar") | ||
| @classmethod | ||
| def validate_avatar(cls, avatar: UploadFile) -> UploadFile: | ||
| try: | ||
| validate_image(avatar) | ||
| return avatar | ||
| except ValueError as e: | ||
| raise HTTPException( | ||
| status_code=422, | ||
| detail=[{ | ||
| "type": "value_error", | ||
| "loc": ["avatar"], | ||
| "msg": str(e), | ||
| "input": avatar.filename | ||
| }] | ||
| ) | ||
|
|
||
| @field_validator("gender") | ||
| @classmethod | ||
| def validate_gender(cls, gender: str) -> str: | ||
| try: | ||
| validate_gender(gender) | ||
| return gender | ||
| except ValueError as e: | ||
| raise HTTPException( | ||
| status_code=422, | ||
| detail=[{ | ||
| "type": "value_error", | ||
| "loc": ["gender"], | ||
| "msg": str(e), | ||
| "input": gender | ||
| }] | ||
| ) | ||
|
|
||
| @field_validator("date_of_birth") | ||
| @classmethod | ||
| def validate_date_of_birth(cls, date_of_birth: date) -> date: | ||
| try: | ||
| validate_birth_date(date_of_birth) | ||
| return date_of_birth | ||
| except ValueError as e: | ||
| raise HTTPException( | ||
| status_code=422, | ||
| detail=[{ | ||
| "type": "value_error", | ||
| "loc": ["date_of_birth"], | ||
| "msg": str(e), | ||
| "input": str(date_of_birth) | ||
| }] | ||
| ) | ||
|
|
||
| @field_validator("info") | ||
| @classmethod | ||
| def validate_info(cls, info: str) -> str: | ||
| cleaned_info = info.strip() | ||
| if not cleaned_info: | ||
| raise HTTPException( | ||
| status_code=422, | ||
| detail=[{ | ||
| "type": "value_error", | ||
| "loc": ["info"], | ||
| "msg": "Info field cannot be empty or contain only spaces.", | ||
| "input": info | ||
| }] | ||
| ) | ||
| return cleaned_info | ||
|
|
||
|
|
||
| class ProfileResponseSchema(BaseModel): | ||
| id: int | ||
| user_id: int | ||
| first_name: str | ||
| last_name: str | ||
| gender: str | ||
| date_of_birth: date | ||
| info: str | ||
| avatar: HttpUrl | ||
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.