diff --git a/src/backend/app/db/db_models.py b/src/backend/app/db/db_models.py index 03951131..d2929a0d 100644 --- a/src/backend/app/db/db_models.py +++ b/src/backend/app/db/db_models.py @@ -37,14 +37,12 @@ class DbUser(Base): __tablename__ = "users" id = cast(str, Column(String, primary_key=True)) - username = cast(str, Column(String, nullable=False, unique=True)) + email_address = cast(str, Column(String, nullable=False, unique=True)) password = cast(str, Column(String)) + name = cast(str, Column(String)) is_active = cast(bool, Column(Boolean, default=False)) is_superuser = cast(bool, Column(Boolean, default=False)) profile_img = cast(str, Column(String, nullable=True)) - name = cast(str, Column(String)) - email_address = cast(str, Column(String, nullable=False, unique=True)) - role = cast(UserRole, Column(Enum(UserRole), default=UserRole.DRONE_PILOT)) date_registered = cast(datetime, Column(DateTime, default=timestamp)) @@ -73,7 +71,6 @@ class DbTask(Base): ) project_task_index = cast(int, Column(Integer)) outline = cast(WKBElement, Column(Geometry("POLYGON", srid=4326))) - task_status = cast(TaskStatus, Column(Enum(TaskStatus), default=TaskStatus.READY)) class DbProject(Base): @@ -269,6 +266,7 @@ class GroundControlPoint(Base): class DbUserProfile(Base): __tablename__ = "user_profile" user_id = cast(str, Column(String, ForeignKey("users.id"), primary_key=True)) + role = cast(UserRole, Column(Enum(UserRole), default=UserRole.DRONE_PILOT)) phone_number = cast(str, Column(String)) country = cast(str, Column(String)) city = cast(str, Column(String)) diff --git a/src/backend/app/migrations/versions/3293e9c98b2a_.py b/src/backend/app/migrations/versions/3293e9c98b2a_.py new file mode 100644 index 00000000..6470009f --- /dev/null +++ b/src/backend/app/migrations/versions/3293e9c98b2a_.py @@ -0,0 +1,67 @@ +""" + +Revision ID: 3293e9c98b2a +Revises: 06668eb5d14a +Create Date: 2024-07-10 04:52:41.412683 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = "3293e9c98b2a" +down_revision: Union[str, None] = "06668eb5d14a" +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.drop_column("tasks", "task_status") + op.add_column( + "user_profile", + sa.Column( + "role", + sa.Enum("PROJECT_CREATOR", "DRONE_PILOT", name="userrole"), + nullable=True, + ), + ) + op.drop_column("users", "role") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "users", + sa.Column( + "role", + postgresql.ENUM("PROJECT_CREATOR", "DRONE_PILOT", "BOTH", name="userrole"), + autoincrement=False, + nullable=True, + ), + ) + op.drop_column("user_profile", "role") + op.add_column( + "tasks", + sa.Column( + "task_status", + postgresql.ENUM( + "READY", + "LOCKED_FOR_MAPPING", + "MAPPED", + "LOCKED_FOR_VALIDATION", + "VALIDATED", + "INVALIDATED", + "BAD", + "SPLIT", + name="taskstatus", + ), + autoincrement=False, + nullable=True, + ), + ) + # ### end Alembic commands ### diff --git a/src/backend/app/migrations/versions/88ae62ec8876_.py b/src/backend/app/migrations/versions/88ae62ec8876_.py new file mode 100644 index 00000000..94758111 --- /dev/null +++ b/src/backend/app/migrations/versions/88ae62ec8876_.py @@ -0,0 +1,35 @@ +""" + +Revision ID: 88ae62ec8876 +Revises: 3293e9c98b2a +Create Date: 2024-07-10 05:21:58.749137 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "88ae62ec8876" +down_revision: Union[str, None] = "3293e9c98b2a" +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.drop_constraint("users_username_key", "users", type_="unique") + op.drop_column("users", "username") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "users", + sa.Column("username", sa.VARCHAR(), autoincrement=False, nullable=False), + ) + op.create_unique_constraint("users_username_key", "users", ["username"]) + # ### end Alembic commands ### diff --git a/src/backend/app/models/enums.py b/src/backend/app/models/enums.py index 41563192..a4943433 100644 --- a/src/backend/app/models/enums.py +++ b/src/backend/app/models/enums.py @@ -113,7 +113,6 @@ class DroneType(IntEnum): class UserRole(IntEnum, Enum): PROJECT_CREATOR = 1 DRONE_PILOT = 2 - BOTH = 3 class State(int, Enum): diff --git a/src/backend/app/users/user_crud.py b/src/backend/app/users/user_crud.py index 28fbbf1f..06da4bec 100644 --- a/src/backend/app/users/user_crud.py +++ b/src/backend/app/users/user_crud.py @@ -7,7 +7,6 @@ from app.users.user_schemas import AuthUser, ProfileUpdate from databases import Database from fastapi import HTTPException -from app.models.enums import UserRole pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") @@ -103,10 +102,10 @@ async def get_or_create_user( try: update_sql = """ INSERT INTO users ( - id, username, email_address, profile_img, role + id, name, email_address, profile_img, is_active, is_superuser, date_registered ) VALUES ( - :user_id, :username, :email_address, :profile_img, :role + :user_id, :name, :email_address, :profile_img, False, False, now() ) ON CONFLICT (id) DO UPDATE SET profile_img = :profile_img; @@ -116,10 +115,9 @@ async def get_or_create_user( update_sql, { "user_id": str(user_data.id), - "username": user_data.email, # FIXME: remove this + "name": user_data.name, "email_address": user_data.email, "profile_img": user_data.img_url, - "role": UserRole.DRONE_PILOT.name, }, ) return user_data @@ -154,12 +152,13 @@ async def update_user_profile( try: profile_query = """ - INSERT INTO user_profile (user_id, phone_number, country, city, organization_name, organization_address, job_title, notify_for_projects_within_km, + INSERT INTO user_profile (user_id, role, 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 , + VALUES (:user_id, :role, :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 + role = :role, phone_number = :phone_number, country = :country, city = :city, @@ -176,6 +175,7 @@ async def update_user_profile( profile_query, { "user_id": user_id, + "role": profile_update.role, "phone_number": profile_update.phone_number, "country": profile_update.country, "city": profile_update.city, @@ -188,6 +188,22 @@ async def update_user_profile( "certified_drone_operator": profile_update.certified_drone_operator, }, ) + + # If password is provided, update the users table + if profile_update.password: + password_update_query = """ + UPDATE users + SET password = :password + WHERE id = :user_id; + """ + await db.execute( + password_update_query, + { + "password": get_password_hash(profile_update.password), + "user_id": user_id, + }, + ) + return True except Exception as e: raise HTTPException(status_code=400, detail=str(e)) from e diff --git a/src/backend/app/users/user_schemas.py b/src/backend/app/users/user_schemas.py index 9cadc25f..92e19f14 100644 --- a/src/backend/app/users/user_schemas.py +++ b/src/backend/app/users/user_schemas.py @@ -1,6 +1,7 @@ from pydantic import BaseModel, EmailStr, ValidationInfo, Field from pydantic.functional_validators import field_validator from typing import Optional +from app.models.enums import UserRole class AuthUser(BaseModel): @@ -8,6 +9,7 @@ class AuthUser(BaseModel): id: str email: EmailStr + name: str img_url: Optional[str] = None @@ -75,13 +77,15 @@ class UserCreate(UserBase): 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] + phone_number: Optional[str] = None + country: Optional[str] = None + city: Optional[str] = None + organization_name: Optional[str] = None + organization_address: Optional[str] = None + job_title: Optional[str] = None + notify_for_projects_within_km: Optional[int] = None + drone_you_own: Optional[str] = None + experience_years: Optional[int] = None + certified_drone_operator: Optional[bool] = False + role: Optional[UserRole] = UserRole.DRONE_PILOT.name + password: Optional[str] = None