From 1abbb0ec27886108551d4dc7463b30678d4764cb Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Thu, 4 Jul 2024 10:48:30 +0545 Subject: [PATCH 1/5] userprofile db table --- src/backend/app/db/db_models.py | 19 +++++++ .../app/migrations/versions/62a16e505bc3_.py | 49 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 src/backend/app/migrations/versions/62a16e505bc3_.py diff --git a/src/backend/app/db/db_models.py b/src/backend/app/db/db_models.py index 98df433b..c6bb9fb0 100644 --- a/src/backend/app/db/db_models.py +++ b/src/backend/app/db/db_models.py @@ -264,3 +264,22 @@ class GroundControlPoint(Base): pixel_y = cast(int, Column(SmallInteger)) reference_point = cast(WKBElement, Column(Geometry("POLYGON", srid=4326))) created_at = cast(datetime, Column(DateTime, default=timestamp)) + + +class DbUserProfile(Base): + __tablename__ = "user_profile" + user_id = cast(str, Column(String, ForeignKey("users.id"), primary_key=True)) + phone_number = cast(str, Column(String)) + country = cast(str, Column(String)) + city = cast(str, Column(String)) + + # for project creator + organization_name = cast(str, Column(String, nullable=True)) # + organization_address = cast(str, Column(String, nullable=True)) # + job_title = cast(str, Column(String, nullable=True)) # + + notify_for_projects_within_km = cast(int, Column(SmallInteger, nullable=True)) + experience_years = cast(int, Column(SmallInteger, nullable=True)) + drone_you_own = cast(str, Column(String, nullable=True)) + certified_drone_operator = cast(bool, Column(Boolean, default=False)) + certificate = cast(bytes, Column(LargeBinary, nullable=True)) diff --git a/src/backend/app/migrations/versions/62a16e505bc3_.py b/src/backend/app/migrations/versions/62a16e505bc3_.py new file mode 100644 index 00000000..638ee263 --- /dev/null +++ b/src/backend/app/migrations/versions/62a16e505bc3_.py @@ -0,0 +1,49 @@ +""" + +Revision ID: 62a16e505bc3 +Revises: bf26afac872d +Create Date: 2024-07-04 04:58:26.406871 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "62a16e505bc3" +down_revision: Union[str, None] = "bf26afac872d" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "user_profile", + sa.Column("user_id", sa.String(), nullable=False), + sa.Column("phone_number", sa.String(), nullable=True), + sa.Column("country", sa.String(), nullable=True), + sa.Column("city", sa.String(), nullable=True), + sa.Column("organization_name", sa.String(), nullable=True), + sa.Column("organization_address", sa.String(), nullable=True), + sa.Column("job_title", sa.String(), nullable=True), + sa.Column("notify_for_projects_within_km", sa.SmallInteger(), nullable=True), + sa.Column("experience_years", sa.SmallInteger(), nullable=True), + sa.Column("drone_you_own", sa.String(), nullable=True), + sa.Column("certified_drone_operator", sa.Boolean(), nullable=True), + sa.Column("certificate", sa.LargeBinary(), nullable=True), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.id"], + ), + sa.PrimaryKeyConstraint("user_id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("user_profile") + # ### end Alembic commands ### From e907e9dd8c8c47ac79c5917af34c52fb0a5685a8 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Thu, 4 Jul 2024 13:06:11 +0545 Subject: [PATCH 2/5] api to create user profile --- src/backend/app/db/db_models.py | 6 +- src/backend/app/users/user_crud.py | 94 +++++++++++++++++++++------ src/backend/app/users/user_routes.py | 49 ++++++++++++-- src/backend/app/users/user_schemas.py | 13 ++++ 4 files changed, 134 insertions(+), 28 deletions(-) diff --git a/src/backend/app/db/db_models.py b/src/backend/app/db/db_models.py index c6bb9fb0..2fef8647 100644 --- a/src/backend/app/db/db_models.py +++ b/src/backend/app/db/db_models.py @@ -274,9 +274,9 @@ class DbUserProfile(Base): city = cast(str, Column(String)) # for project creator - organization_name = cast(str, Column(String, nullable=True)) # - organization_address = cast(str, Column(String, nullable=True)) # - job_title = cast(str, Column(String, nullable=True)) # + organization_name = cast(str, Column(String, nullable=True)) + organization_address = cast(str, Column(String, nullable=True)) + job_title = cast(str, Column(String, nullable=True)) notify_for_projects_within_km = cast(int, Column(SmallInteger, nullable=True)) experience_years = cast(int, Column(SmallInteger, nullable=True)) diff --git a/src/backend/app/users/user_crud.py b/src/backend/app/users/user_crud.py index f43517cc..8f3a53f0 100644 --- a/src/backend/app/users/user_crud.py +++ b/src/backend/app/users/user_crud.py @@ -3,10 +3,8 @@ from app.config import settings from typing import Any from passlib.context import CryptContext -from sqlalchemy.orm import Session from app.db import db_models -from app.users.user_schemas import UserCreate, AuthUser -from sqlalchemy import text +from app.users.user_schemas import UserCreate, AuthUser, ProfileUpdate from databases import Database from fastapi import HTTPException from app.models.enums import UserRole @@ -62,36 +60,34 @@ def get_password_hash(password: str) -> str: return pwd_context.hash(password) -def get_user_by_email(db: Session, email: str): - query = text(f"SELECT * FROM users WHERE email_address = '{email}' LIMIT 1;") - result = db.execute(query) - data = result.fetchone() - return data +async def get_user_by_id(db: Database, id: str): + query = "SELECT * FROM users WHERE id = :id LIMIT 1;" + result = await db.fetch_one(query, {"id": id}) + return result -async def get_user_email(db: Database, email: str): - query = f"SELECT * FROM users WHERE email_address = '{email}' LIMIT 1;" - result = await db.fetch_one(query) +async def get_userprofile_by_userid(db: Database, user_id: str): + query = "SELECT * FROM user_profile WHERE user_id = :user_id LIMIT 1;" + result = await db.fetch_one(query, {"user_id": user_id}) return result -async def get_user_username(db: Database, username: str): - query = f"SELECT * FROM users WHERE username = '{username}' LIMIT 1;" - result = await db.fetch_one(query=query) +async def get_user_by_email(db: Database, email: str): + query = "SELECT * FROM users WHERE email_address = :email LIMIT 1;" + result = await db.fetch_one(query, {"email": email}) return result -def get_user_by_username(db: Session, username: str): - query = text(f"SELECT * FROM users WHERE username = '{username}' LIMIT 1;") - result = db.execute(query) - data = result.fetchone() - return data +async def get_user_by_username(db: Database, username: str): + query = "SELECT * FROM users WHERE username = :username LIMIT 1;" + result = await db.fetch_one(query, {"username": username}) + return result async def authenticate( db: Database, username: str, password: str ) -> db_models.DbUser | None: - db_user = await get_user_username(db, username) + db_user = await get_user_by_username(db, username) if not db_user: return None if not verify_password(password, db_user["password"]): @@ -162,3 +158,61 @@ async def get_or_create_user( ) from e else: raise HTTPException(status_code=400, detail=str(e)) from e + + +async def update_user_profile( + db: Database, user_id: int, profile_update: ProfileUpdate +): + """ + Update user profile in the database. + Args: + db (Database): Database connection object. + user_id (int): ID of the user whose profile is being updated. + profile_update (ProfileUpdate): Instance of ProfileUpdate containing fields to update. + Returns: + bool: True if update operation succeeds. + Raises: + Any exceptions thrown during database operations. + """ + + try: + print("notification = ", profile_update.notify_for_projects_within_km) + + profile_query = """ + INSERT INTO user_profile (user_id, phone_number, country, city, organization_name, organization_address, job_title, notify_for_projects_within_km, + experience_years, drone_you_own, certified_drone_operator) + VALUES (:user_id, :phone_number, :country, :city, :organization_name, :organization_address, :job_title, :notify_for_projects_within_km , + :experience_years, :drone_you_own, :certified_drone_operator) + ON CONFLICT (user_id) + DO UPDATE SET + phone_number = :phone_number, + country = :country, + city = :city, + organization_name = :organization_name, + organization_address = :organization_address, + job_title = :job_title, + notify_for_projects_within_km = :notify_for_projects_within_km, + experience_years = :experience_years, + drone_you_own = :drone_you_own, + certified_drone_operator = :certified_drone_operator; + """ + + await db.execute( + profile_query, + { + "user_id": user_id, + "phone_number": profile_update.phone_number, + "country": profile_update.country, + "city": profile_update.city, + "organization_name": profile_update.organization_name, + "organization_address": profile_update.organization_address, + "job_title": profile_update.job_title, + "notify_for_projects_within_km": profile_update.notify_for_projects_within_km, + "experience_years": profile_update.experience_years, + "drone_you_own": profile_update.drone_you_own, + "certified_drone_operator": profile_update.certified_drone_operator, + }, + ) + return True + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) from e diff --git a/src/backend/app/users/user_routes.py b/src/backend/app/users/user_routes.py index 96c00767..ff16ea5a 100644 --- a/src/backend/app/users/user_routes.py +++ b/src/backend/app/users/user_routes.py @@ -1,13 +1,20 @@ from typing import Any from datetime import timedelta -from fastapi import APIRouter, HTTPException, Depends +from fastapi import APIRouter, Response, HTTPException, Depends from typing import Annotated from fastapi.security import OAuth2PasswordRequestForm -from app.users.user_schemas import Token, UserPublic, UserRegister -from app.users.user_deps import CurrentUser +from app.users.user_schemas import ( + Token, + UserPublic, + UserRegister, + ProfileUpdate, + AuthUser, +) +from app.users.user_deps import CurrentUser, login_required from app.config import settings from app.users import user_crud from app.db import database +from app.models.enums import HTTPStatus from databases import Database @@ -48,13 +55,13 @@ async def register_user( """ Create new user without the need to be logged in. """ - user = await user_crud.get_user_email(db, user_in.email_address) + user = await user_crud.get_user_by_email(db, user_in.email_address) if user: raise HTTPException( status_code=400, detail="The user with this email already exists in the system", ) - user = await user_crud.get_user_username(db, user_in.username) + user = await user_crud.get_user_by_username(db, user_in.username) if user: raise HTTPException( status_code=400, @@ -83,3 +90,35 @@ def read_user_me(current_user: CurrentUser) -> Any: Get current user. """ return current_user + + +@router.post("/{user_id}/profile") +async def update_user_profile( + user_id: str, + profile_update: ProfileUpdate, + db: Database = Depends(database.encode_db), + user_data: AuthUser = Depends(login_required), +): + """ + Update user profile based on provided user_id and profile_update data. + Args: + user_id (int): The ID of the user whose profile is being updated. + profile_update (UserProfileUpdate): Updated profile data to apply. + Returns: + dict: Updated user profile information. + Raises: + HTTPException: If user with given user_id is not found in the database. + """ + + user = await user_crud.get_user_by_id(db, user_id) + if user.id != user_id: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="You are not authorized to update profile", + ) + + if not user: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="User not found") + + user = await user_crud.update_user_profile(db, user_id, profile_update) + return Response(status_code=HTTPStatus.OK) diff --git a/src/backend/app/users/user_schemas.py b/src/backend/app/users/user_schemas.py index 309f16da..40195c0a 100644 --- a/src/backend/app/users/user_schemas.py +++ b/src/backend/app/users/user_schemas.py @@ -72,3 +72,16 @@ def password_complexity(cls, v: str, info: ValidationInfo): class UserCreate(UserBase): password: str + + +class ProfileUpdate(BaseModel): + phone_number: Optional[str] + country: Optional[str] + city: Optional[str] + organization_name: Optional[str] + organization_address: Optional[str] + job_title: Optional[str] + notify_for_projects_within_km: Optional[int] + drone_you_own: Optional[str] + experience_years: Optional[int] + certified_drone_operator: Optional[bool] From 030a1e3637ec4e9cc17c9bad9b3bba99b2ef5822 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Thu, 4 Jul 2024 13:07:15 +0545 Subject: [PATCH 3/5] fix: user id check in update profile api --- src/backend/app/users/user_routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/app/users/user_routes.py b/src/backend/app/users/user_routes.py index ff16ea5a..4cbe03ac 100644 --- a/src/backend/app/users/user_routes.py +++ b/src/backend/app/users/user_routes.py @@ -111,7 +111,7 @@ async def update_user_profile( """ user = await user_crud.get_user_by_id(db, user_id) - if user.id != user_id: + if user_data.id != user_id: raise HTTPException( status_code=HTTPStatus.FORBIDDEN, detail="You are not authorized to update profile", From 1d7882cfc08773689818c0ff1865dbf27cd83f41 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Thu, 4 Jul 2024 13:23:29 +0545 Subject: [PATCH 4/5] has_user_profile boolean passed into my-info api --- src/backend/app/users/oauth_routes.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/backend/app/users/oauth_routes.py b/src/backend/app/users/oauth_routes.py index 8984eea2..5dd8c653 100644 --- a/src/backend/app/users/oauth_routes.py +++ b/src/backend/app/users/oauth_routes.py @@ -63,4 +63,9 @@ async def my_data( ): """Read access token and get user details from Google""" - return await user_crud.get_or_create_user(db, user_data) + user_info = await user_crud.get_or_create_user(db, user_data) + has_user_profile = await user_crud.get_userprofile_by_userid(db, user_info.id) + + user_info_dict = user_info.model_dump() + user_info_dict["has_user_profile"] = bool(has_user_profile) + return user_info_dict From 14c4e09e152d96733959afe35f06dc73b163f976 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Thu, 4 Jul 2024 13:25:36 +0545 Subject: [PATCH 5/5] user id type updated in AuthUser schema --- src/backend/app/users/user_schemas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/app/users/user_schemas.py b/src/backend/app/users/user_schemas.py index 40195c0a..9cadc25f 100644 --- a/src/backend/app/users/user_schemas.py +++ b/src/backend/app/users/user_schemas.py @@ -6,7 +6,7 @@ class AuthUser(BaseModel): """The user model returned from Google OAuth2.""" - id: int + id: str email: EmailStr img_url: Optional[str] = None