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
24 changes: 10 additions & 14 deletions backend/script/reset_dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,7 @@ async def reset_dev():
print("Populating tables...")
async with AsyncSessionLocal() as session:
with open(
str(
Path(__file__).parent.parent.parent
/ "frontend"
/ "shared"
/ "mock_data.json"
),
str(Path(__file__).parent.parent.parent / "frontend" / "shared" / "mock_data.json"),
"r",
) as f:
data = json.load(f)
Expand Down Expand Up @@ -118,11 +113,9 @@ async def reset_dev():
session.add(account)
await session.flush()

student = StudentEntity.from_model(
student = StudentEntity.from_data(
StudentData(
contact_preference=ContactPreference(
student_data["contact_preference"]
),
contact_preference=ContactPreference(student_data["contact_preference"]),
phone_number=student_data["phone_number"],
last_registered=parse_date(student_data.get("last_registered")),
),
Expand Down Expand Up @@ -150,17 +143,20 @@ async def reset_dev():
session.add(location)

for party_data in data["parties"]:
party_datetime = parse_date(party_data["party_datetime"])
assert party_datetime is not None, f"party_datetime required for party {party_data['id']}"

party = PartyEntity(
party_datetime=parse_date(party_data["party_datetime"]),
party_datetime=party_datetime,
location_id=party_data["location_id"],
contact_one_id=party_data["contact_one_id"],
contact_two_first_name=party_data["contact_two"]["first_name"],
contact_two_last_name=party_data["contact_two"]["last_name"],
contact_two_email=party_data["contact_two"]["email"],
contact_two_phone_number=party_data["contact_two"]["phone_number"],
contact_two_contact_preference=party_data["contact_two"][
"contact_preference"
],
contact_two_contact_preference=ContactPreference(
party_data["contact_two"]["contact_preference"]
),
)
session.add(party)

Expand Down
42 changes: 21 additions & 21 deletions backend/src/core/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from fastapi import Depends, Request
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from src.core.exceptions import CredentialsException, ForbiddenException
from src.modules.account.account_model import Account, AccountRole
from src.modules.police.police_model import PoliceAccount
from src.modules.account.account_model import AccountDto, AccountRole
from src.modules.police.police_model import PoliceAccountDto

StringRole = Literal["student", "admin", "staff", "police"]

Expand All @@ -20,7 +20,7 @@ async def __call__(self, request: Request):
bearer_scheme = HTTPBearer401()


def mock_authenticate(role: AccountRole) -> Account | None:
def mock_authenticate(role: AccountRole) -> AccountDto | None:
"""Mock authentication function. Replace with real authentication logic."""
role_to_id = {
AccountRole.ADMIN: 1,
Expand All @@ -32,7 +32,7 @@ def mock_authenticate(role: AccountRole) -> Account | None:
AccountRole.ADMIN: "222222222",
AccountRole.STAFF: "333333333",
}
return Account(
return AccountDto(
id=role_to_id[role],
email="[email protected]",
first_name="Test",
Expand All @@ -44,7 +44,7 @@ def mock_authenticate(role: AccountRole) -> Account | None:

async def authenticate_user(
authorization: HTTPAuthorizationCredentials = Depends(bearer_scheme),
) -> Account:
) -> AccountDto:
"""
Middleware to authenticate user from Bearer token.
Expects token to be one of: "student", "admin", "staff" for mock authentication.
Expand Down Expand Up @@ -74,13 +74,13 @@ def authenticate_by_role(*roles: StringRole):

async def _authenticate(
authorization: HTTPAuthorizationCredentials = Depends(bearer_scheme),
) -> Account | PoliceAccount:
) -> AccountDto | PoliceAccountDto:
token = authorization.credentials.lower()

# Check if police token and police is allowed
if token == "police":
if "police" in roles:
return PoliceAccount(email="[email protected]")
return PoliceAccountDto(email="[email protected]")
else:
raise ForbiddenException(detail="Insufficient privileges")

Expand All @@ -102,38 +102,38 @@ async def _authenticate(


async def authenticate_admin(
account: Account | PoliceAccount = Depends(authenticate_by_role("admin")),
) -> Account:
if not isinstance(account, Account):
account: AccountDto | PoliceAccountDto = Depends(authenticate_by_role("admin")),
) -> AccountDto:
if not isinstance(account, AccountDto):
raise ForbiddenException(detail="Insufficient privileges")
return account


async def authenticate_staff_or_admin(
account: Account | PoliceAccount = Depends(authenticate_by_role("staff", "admin")),
) -> Account:
if not isinstance(account, Account):
account: AccountDto | PoliceAccountDto = Depends(authenticate_by_role("staff", "admin")),
) -> AccountDto:
if not isinstance(account, AccountDto):
raise ForbiddenException(detail="Insufficient privileges")
return account


async def authenticate_student_or_admin(
account: Account | PoliceAccount = Depends(authenticate_by_role("student", "admin")),
) -> Account:
if not isinstance(account, Account):
account: AccountDto | PoliceAccountDto = Depends(authenticate_by_role("student", "admin")),
) -> AccountDto:
if not isinstance(account, AccountDto):
raise ForbiddenException(detail="Insufficient privileges")
return account


async def authenticate_student(
account: Account | PoliceAccount = Depends(authenticate_by_role("student")),
) -> Account:
if not isinstance(account, Account):
account: AccountDto | PoliceAccountDto = Depends(authenticate_by_role("student")),
) -> AccountDto:
if not isinstance(account, AccountDto):
raise ForbiddenException(detail="Insufficient privileges")
return account


async def authenticate_police_or_admin(
account: Account | PoliceAccount = Depends(authenticate_by_role("police", "admin")),
) -> PoliceAccount | Account:
account: AccountDto | PoliceAccountDto = Depends(authenticate_by_role("police", "admin")),
) -> PoliceAccountDto | AccountDto:
return account
8 changes: 4 additions & 4 deletions backend/src/modules/account/account_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from sqlalchemy import CheckConstraint, Enum, Integer, String
from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
from src.core.database import EntityBase
from src.modules.account.account_model import Account, AccountData, AccountRole
from src.modules.account.account_model import AccountData, AccountDto, AccountRole


class AccountEntity(MappedAsDataclass, EntityBase):
Expand All @@ -24,7 +24,7 @@ class AccountEntity(MappedAsDataclass, EntityBase):
role: Mapped[AccountRole] = mapped_column(Enum(AccountRole), nullable=False)

@classmethod
def from_model(cls, data: "AccountData") -> Self:
def from_data(cls, data: "AccountData") -> Self:
return cls(
email=data.email,
first_name=data.first_name,
Expand All @@ -33,8 +33,8 @@ def from_model(cls, data: "AccountData") -> Self:
role=AccountRole(data.role),
)

def to_model(self) -> "Account":
return Account(
def to_dto(self) -> "AccountDto":
return AccountDto(
id=self.id,
email=self.email,
first_name=self.first_name,
Expand Down
2 changes: 1 addition & 1 deletion backend/src/modules/account/account_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class AccountData(BaseModel):
role: AccountRole


class Account(BaseModel):
class AccountDto(BaseModel):
"""DTO for Account responses."""

id: int
Expand Down
20 changes: 10 additions & 10 deletions backend/src/modules/account/account_router.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from fastapi import APIRouter, Depends, Query
from src.core.authentication import authenticate_admin
from src.modules.account.account_model import Account, AccountData, AccountRole
from src.modules.account.account_model import AccountData, AccountDto, AccountRole
from src.modules.account.account_service import AccountService
from src.modules.police.police_model import PoliceAccount, PoliceAccountUpdate
from src.modules.police.police_model import PoliceAccountDto, PoliceAccountUpdate
from src.modules.police.police_service import PoliceService

account_router = APIRouter(prefix="/api/accounts", tags=["accounts"])
Expand All @@ -12,19 +12,19 @@
async def get_police_credentials(
police_service: PoliceService = Depends(),
_=Depends(authenticate_admin),
) -> PoliceAccount:
) -> PoliceAccountDto:
police_entity = await police_service.get_police()
return PoliceAccount(email=police_entity.email)
return PoliceAccountDto(email=police_entity.email)


@account_router.put("/police")
async def update_police_credentials(
data: PoliceAccountUpdate,
police_service: PoliceService = Depends(),
_=Depends(authenticate_admin),
) -> PoliceAccount:
) -> PoliceAccountDto:
police_entity = await police_service.update_police(data.email, data.password)
return PoliceAccount(email=police_entity.email)
return PoliceAccountDto(email=police_entity.email)


@account_router.get("")
Expand All @@ -34,7 +34,7 @@ async def list_accounts(
),
account_service: AccountService = Depends(),
_=Depends(authenticate_admin),
) -> list[Account]:
) -> list[AccountDto]:
return await account_service.get_accounts_by_roles(role)


Expand All @@ -43,7 +43,7 @@ async def create_account(
data: AccountData,
account_service: AccountService = Depends(),
_=Depends(authenticate_admin),
) -> Account:
) -> AccountDto:
return await account_service.create_account(data)


Expand All @@ -53,7 +53,7 @@ async def update_account(
data: AccountData,
account_service: AccountService = Depends(),
_=Depends(authenticate_admin),
) -> Account:
) -> AccountDto:
return await account_service.update_account(account_id, data)


Expand All @@ -62,5 +62,5 @@ async def delete_account(
account_id: int,
account_service: AccountService = Depends(),
_=Depends(authenticate_admin),
) -> Account:
) -> AccountDto:
return await account_service.delete_account(account_id)
32 changes: 17 additions & 15 deletions backend/src/modules/account/account_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from src.core.database import get_session
from src.core.exceptions import ConflictException, NotFoundException
from src.modules.account.account_entity import AccountEntity, AccountRole
from src.modules.account.account_model import Account, AccountData
from src.modules.account.account_model import AccountData, AccountDto


class AccountNotFoundException(NotFoundException):
Expand Down Expand Up @@ -45,30 +45,32 @@ async def _get_account_entity_by_email(self, email: str) -> AccountEntity:
raise AccountByEmailNotFoundException(email)
return account

async def get_accounts(self) -> list[Account]:
async def get_accounts(self) -> list[AccountDto]:
result = await self.session.execute(select(AccountEntity))
accounts = result.scalars().all()
return [account.to_model() for account in accounts]
return [account.to_dto() for account in accounts]

async def get_accounts_by_roles(self, roles: list[AccountRole] | None = None) -> list[Account]:
async def get_accounts_by_roles(
self, roles: list[AccountRole] | None = None
) -> list[AccountDto]:
if not roles:
return await self.get_accounts()

result = await self.session.execute(
select(AccountEntity).where(AccountEntity.role.in_(roles))
)
accounts = result.scalars().all()
return [account.to_model() for account in accounts]
return [account.to_dto() for account in accounts]

async def get_account_by_id(self, account_id: int) -> Account:
async def get_account_by_id(self, account_id: int) -> AccountDto:
account_entity = await self._get_account_entity_by_id(account_id)
return account_entity.to_model()
return account_entity.to_dto()

async def get_account_by_email(self, email: str) -> Account:
async def get_account_by_email(self, email: str) -> AccountDto:
account_entity = await self._get_account_entity_by_email(email)
return account_entity.to_model()
return account_entity.to_dto()

async def create_account(self, data: AccountData) -> Account:
async def create_account(self, data: AccountData) -> AccountDto:
try:
await self._get_account_entity_by_email(data.email)
# If we get here, account exists
Expand All @@ -91,9 +93,9 @@ async def create_account(self, data: AccountData) -> Account:
# handle race condition where another session inserted the same email
raise AccountConflictException(data.email)
await self.session.refresh(new_account)
return new_account.to_model()
return new_account.to_dto()

async def update_account(self, account_id: int, data: AccountData) -> Account:
async def update_account(self, account_id: int, data: AccountData) -> AccountDto:
account_entity = await self._get_account_entity_by_id(account_id)

if data.email != account_entity.email:
Expand All @@ -118,11 +120,11 @@ async def update_account(self, account_id: int, data: AccountData) -> Account:
except IntegrityError:
raise AccountConflictException(data.email)
await self.session.refresh(account_entity)
return account_entity.to_model()
return account_entity.to_dto()

async def delete_account(self, account_id: int) -> Account:
async def delete_account(self, account_id: int) -> AccountDto:
account_entity = await self._get_account_entity_by_id(account_id)
account = account_entity.to_model()
account = account_entity.to_dto()
await self.session.delete(account_entity)
await self.session.commit()
return account
8 changes: 4 additions & 4 deletions backend/src/modules/complaint/complaint_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from sqlalchemy import DateTime, ForeignKey, Integer, String
from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column, relationship
from src.core.database import EntityBase
from src.modules.complaint.complaint_model import Complaint, ComplaintData
from src.modules.complaint.complaint_model import ComplaintData, ComplaintDto

if TYPE_CHECKING:
from src.modules.location.location_entity import LocationEntity
Expand All @@ -26,16 +26,16 @@ class ComplaintEntity(MappedAsDataclass, EntityBase):
)

@classmethod
def from_model(cls, data: ComplaintData) -> Self:
def from_data(cls, data: ComplaintData) -> Self:
return cls(
location_id=data.location_id,
complaint_datetime=data.complaint_datetime,
description=data.description,
)

def to_model(self) -> Complaint:
def to_dto(self) -> ComplaintDto:
"""Convert entity to model."""
return Complaint(
return ComplaintDto(
id=self.id,
location_id=self.location_id,
complaint_datetime=self.complaint_datetime,
Expand Down
2 changes: 1 addition & 1 deletion backend/src/modules/complaint/complaint_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class ComplaintData(BaseModel):
description: str = ""


class Complaint(ComplaintData):
class ComplaintDto(ComplaintData):
"""Output DTO for a complaint."""

id: int
Loading