Skip to content

Commit

Permalink
Merge pull request #24 from hotosm/user-profile
Browse files Browse the repository at this point in the history
Update User Profile After Google OAuth Signup
  • Loading branch information
nrjadkry authored Jul 4, 2024
2 parents af7d4ad + 14c4e09 commit 804f325
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 27 deletions.
19 changes: 19 additions & 0 deletions src/backend/app/db/db_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
49 changes: 49 additions & 0 deletions src/backend/app/migrations/versions/62a16e505bc3_.py
Original file line number Diff line number Diff line change
@@ -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 ###
7 changes: 6 additions & 1 deletion src/backend/app/users/oauth_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
94 changes: 74 additions & 20 deletions src/backend/app/users/user_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"]):
Expand Down Expand Up @@ -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
49 changes: 44 additions & 5 deletions src/backend/app/users/user_routes.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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_data.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)
15 changes: 14 additions & 1 deletion src/backend/app/users/user_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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]

0 comments on commit 804f325

Please sign in to comment.