Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""add user profile fields

Revision ID: d87c3bb49953
Revises: j1k2l3m4n567
Create Date: 2026-05-08 06:36:06.259229

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = "d87c3bb49953"
down_revision: Union[str, Sequence[str], None] = "j1k2l3m4n567"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.add_column(
"user",
sa.Column("first_name", sa.String(length=100), nullable=True),
)

op.add_column(
"user",
sa.Column("last_name", sa.String(length=100), nullable=True),
)

op.add_column(
"user",
sa.Column("organization_name", sa.String(length=255), nullable=True),
)


def downgrade() -> None:
op.drop_column("user", "organization_name")
op.drop_column("user", "last_name")
op.drop_column("user", "first_name")
70 changes: 49 additions & 21 deletions backend-api/app/api/v1/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,35 @@ async def read_users_me(user: User = Depends(get_current_user)):
"""Get current authenticated user information."""
return user

#Update User
@users_router.patch("/me", summary="Update my user information", response_model=UserRead)
async def update_users_me(
user_update: UserUpdate,
user: User = Depends(get_current_user),
):
"""Update current authenticated user's profile information."""
from app.db.session import get_async_session

async for session in get_async_session():
db_user = await session.get(User, user.id)

if db_user is None:
raise HTTPException(status_code=404, detail="User not found")

if user_update.first_name is not None:
db_user.first_name = user_update.first_name

if user_update.last_name is not None:
db_user.last_name = user_update.last_name

if user_update.organization_name is not None:
db_user.organization_name = user_update.organization_name

await session.commit()
await session.refresh(db_user)

return db_user


# Change password endpoint
from pydantic import BaseModel
Expand All @@ -51,35 +80,34 @@ async def change_password(
user: User = Depends(get_current_user),
):
"""Change current user's password."""
from app.core.users import get_user_manager
from app.db.session import get_async_session
from fastapi import Request

# Create a mock request object for fastapi-users
request = Request(scope={"type": "http"})
from app.core.users import get_user_manager

async for session in get_async_session():
db_user = await session.get(User, user.id)

if db_user is None:
raise HTTPException(status_code=404, detail="User not found")

async for user_manager in get_user_manager(session):
try:
# Verify current password
verified, updated_password_hash = user_manager.password_helper.verify_and_update(
password_data.current_password, user.hashed_password
)
if not verified:
raise exceptions.InvalidPasswordException()
verified, _ = user_manager.password_helper.verify_and_update(
password_data.current_password,
db_user.hashed_password,
)

# Hash new password
new_hashed_password = user_manager.password_helper.hash(password_data.new_password)
if not verified:
raise HTTPException(
status_code=400,
detail="Current password is incorrect",
)

# Update user password
user.hashed_password = new_hashed_password
await session.commit()
db_user.hashed_password = user_manager.password_helper.hash(
password_data.new_password
)

return {"message": "Password changed successfully"}
await session.commit()

except exceptions.InvalidPasswordException:
from fastapi import HTTPException
raise HTTPException(status_code=400, detail="Invalid current password")
return {"message": "Password changed successfully"}


# Include users router
Expand Down
15 changes: 15 additions & 0 deletions backend-api/app/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,21 @@ class User(SQLAlchemyBaseUserTable[int], Base):
nullable=False
)

first_name: Mapped[str | None] = mapped_column(
String(100),
nullable=True
)

last_name: Mapped[str | None] = mapped_column(
String(100),
nullable=True
)

organization_name: Mapped[str | None] = mapped_column(
String(255),
nullable=True
)

# Inherited from SQLAlchemyBaseUserTable:
# - email: str
# - hashed_password: str
Expand Down
24 changes: 14 additions & 10 deletions backend-api/app/schemas/user.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
from fastapi_users import schemas
from pydantic import EmailStr
from app.models.user import Role


class UserRead(schemas.BaseUser[int]):
"""Schema for reading user data."""
role: Role
first_name: str | None = None
last_name: str | None = None
organization_name: str | None = None


class UserCreate(schemas.BaseUserCreate):
"""Schema for creating a new user."""
role: Role = Role.VIEWER

first_name: str | None = None
last_name: str | None = None
organization_name: str | None = None

class UserRegister(schemas.BaseUserCreate):
"""
Public registration schema.

Intentionally does NOT expose role/is_superuser/etc so a self-registering
user cannot elevate privileges.
"""
class UserRegister(schemas.BaseUserCreate):
first_name: str | None = None
last_name: str | None = None
organization_name: str | None = None


class UserUpdate(schemas.BaseUserUpdate):
"""Schema for updating user data."""
role: Role | None = None

first_name: str | None = None
last_name: str | None = None
organization_name: str | None = None
Loading
Loading