LibreCharge/backend/app/routers/me_v1.py

187 lines
6.4 KiB
Python

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
)