from datetime import UTC, datetime, timedelta from uuid import UUID from fastapi import APIRouter, HTTPException from fastapi.params import Depends from sqlalchemy import select from sqlalchemy.orm import Session as DbSession from app.database import get_db from app.schemas.session import Session from app.schemas.auth_token import AccessToken from app.schemas.user import PasswordUpdate, UserUpdate, User from app.security.jwt_bearer import JWTBearer from app.services import session_service, user_service from app.util.errors import InvalidStateError, NotFoundError from app.schemas.dashboard import DashboardResponse, DashboardStats from app.models.transaction import Transaction as DbTransaction from app.schemas.transaction import TransactionStatus router = APIRouter(prefix="/me", tags=["Me (v1)"]) @router.get(path="", response_model=User) async def get_myself( db: DbSession = Depends(get_db), token: AccessToken = Depends(JWTBearer()) ): """ Get the currently authenticated user. """ user = await user_service.get_user(db=db, id=UUID(token.subject)) if not user: raise HTTPException(status_code=404, detail="user_not_found") else: return user @router.patch(path="", response_model=User) async def update_myself( user_update: UserUpdate, db: DbSession = Depends(get_db), token: AccessToken = Depends(JWTBearer()), ): """ Update the currently authenticated user. Changing the email address automatically marks it as not verified and starts a new verification workflow. """ try: return await user_service.update_user( db, UUID(token.subject), user_update ) except NotFoundError: raise HTTPException(status_code=404, detail="user_not_found") @router.post(path="/password", response_model=list[None]) async def change_password( update: PasswordUpdate, db: DbSession = Depends(get_db), token: AccessToken = Depends(JWTBearer()), ): """ Change the password of the currently authenticated user. """ try: await user_service.change_user_password( db=db, id=UUID(token.subject), update=update ) return list() except NotFoundError: raise HTTPException(status_code=404, detail="user_not_found") except InvalidStateError: raise HTTPException(status_code=409, detail="incorrect_password") @router.get(path="/sessions", response_model=list[Session]) async def get_user_sessions( db: DbSession = Depends(get_db), token: AccessToken = Depends(JWTBearer()) ): """ List the active sessions of the currently authenticated user. """ return await session_service.get_sessions_by_user( db=db, user_id=UUID(token.subject) ) @router.delete(path="/sessions", response_model=list[None]) async def clear_user_sessions( db: DbSession = Depends(get_db), token: AccessToken = Depends(JWTBearer()) ): """ Clear all sessions of the currently authenticated user. """ await session_service.remove_all_sessions_for_user( db=db, user_id=UUID(token.subject), ) return list() @router.delete(path="/sessions/{session_id}", response_model=list[None]) async def delete_user_session( session_id: UUID, db: DbSession = Depends(get_db), token: AccessToken = Depends(JWTBearer()), ): """ Invalidate a specific session of the currently authenticated user. """ try: await session_service.remove_session_for_user( db=db, id=session_id, user_id=UUID(token.subject), ) except NotFoundError: raise HTTPException(status_code=404, detail="session_not_found") return list() @router.get(path="/dashboard", response_model=DashboardResponse) async def get_dashboard( db: DbSession = Depends(get_db), token: AccessToken = Depends(JWTBearer()), ): """ Get dashboard information for the currently authenticated user. """ # Currently ongoing transaction stmt_current_transaction = select(DbTransaction).where(DbTransaction.user_id == token.subject).where(DbTransaction.status == TransactionStatus.ONGOING) current_transaction = db.execute(stmt_current_transaction).scalars().first() # Common base query stmt_base = select(DbTransaction).where(DbTransaction.user_id == token.subject).where( DbTransaction.status == TransactionStatus.ENDED).order_by(DbTransaction.ended_at.desc()) # 5 most recent transactions stmt_transactions_recent = stmt_base.limit(5) recent_transactions = db.execute(stmt_transactions_recent).scalars().all() # Calculate beginning of the current and previous month current_date = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=None) beginning_current_month = current_date.replace(day=1) beginning_previous_month = beginning_current_month.replace( year=(beginning_current_month.year - 1 if beginning_current_month.month == 1 else beginning_current_month.year), month=(12 if beginning_current_month.month == 1 else beginning_current_month.month - 1) ) # Transactions for total calculations stmt_transactions = stmt_base.where(DbTransaction.ended_at >= beginning_previous_month) transactions = db.execute(stmt_transactions).scalars().all() # Current month totals count = 0 energy_total = 0 cost_total = 0 # Previous month totals count_previous = 0 energy_total_previous = 0 cost_total_previous = 0 # Calculate totals for trans in transactions: trans_energy = trans.meter_end - trans.meter_start trans_cost = trans_energy * trans.price if trans.ended_at >= beginning_current_month: # Current month energy_total += trans_energy cost_total += trans_cost count += 1 else: # Previous month energy_total_previous += trans_energy cost_total_previous += trans_cost count_previous += 1 return DashboardResponse( stats=DashboardStats( transaction_count=count, transaction_count_previous=count_previous, transaction_energy_total=energy_total, transaction_energy_total_previous=energy_total_previous, transaction_cost_total=cost_total, transaction_cost_total_previous=cost_total_previous ), current_transaction=current_transaction, recent_transactions=recent_transactions )