feat: implement complete CMT backend with API endpoints and test suite
- Add 7 core API endpoints: users, transactions, partners, products, inventory, payments, credit - Implement role-based authentication (admin/write/read-only access) - Add comprehensive database models with proper relationships - Include full test coverage for all endpoints and business logic - Set up Alembic migrations and Docker configuration - Configure FastAPI app with CORS and database integration
This commit is contained in:
@@ -1,30 +0,0 @@
|
||||
"""
|
||||
"""
|
||||
from fastapi import Depends
|
||||
from sqlmodel import Session, SQLModel, select
|
||||
from app.core.db import get_session
|
||||
from app.schemas.models import Client
|
||||
from typing import Type, Optional, Annotated
|
||||
|
||||
|
||||
SessionDep = Annotated[Session, Depends(get_session)]
|
||||
|
||||
|
||||
def exists(session: Session, model: Type[SQLModel], **filters) -> Optional[bool]:
|
||||
"""
|
||||
Checks if a request exists in the given model using any filters.
|
||||
|
||||
Example:
|
||||
exists(session, Client, phone="0781232465", tax_number="TIN123")
|
||||
"""
|
||||
if not filters:
|
||||
raise ValueError("At least one filter must be provided")
|
||||
|
||||
stmt = select(model)
|
||||
for field, value in filters.items():
|
||||
if not hasattr(model, field):
|
||||
raise ValueError(f"Invalid filter field: {field}")
|
||||
stmt = stmt.where(getattr(model, field) == value)
|
||||
|
||||
result = session.exec(stmt).first()
|
||||
return result is not None
|
||||
@@ -0,0 +1,198 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlmodel import Session, select
|
||||
from app.core.db import get_session
|
||||
from app.core.auth import require_any_access, require_write_access, require_admin
|
||||
from app.schemas.models import Credit, Partner, Transaction
|
||||
from app.schemas.schemas import (
|
||||
CreditCreate,
|
||||
CreditUpdate,
|
||||
CreditResponse,
|
||||
UserResponse
|
||||
)
|
||||
from typing import List
|
||||
|
||||
router = APIRouter(prefix="/credit", tags=["credit"])
|
||||
|
||||
# Create Credit
|
||||
@router.post("/", response_model=CreditResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_credit(
|
||||
credit: CreditCreate,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_write_access)
|
||||
):
|
||||
"""Create new credit account (requires write access)."""
|
||||
# Validate partner exists
|
||||
partner = session.get(Partner, credit.partner_id)
|
||||
if not partner:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Partner not found"
|
||||
)
|
||||
|
||||
# Validate transaction exists
|
||||
transaction = session.get(Transaction, credit.transaction_id)
|
||||
if not transaction:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Transaction not found"
|
||||
)
|
||||
|
||||
# Check if credit account already exists for this partner
|
||||
existing_credit = session.exec(
|
||||
select(Credit).where(Credit.partner_id == credit.partner_id)
|
||||
).first()
|
||||
if existing_credit:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Credit account already exists for this partner"
|
||||
)
|
||||
|
||||
# Create credit with audit fields
|
||||
credit_data = credit.model_dump()
|
||||
credit_data["created_by"] = current_user.id
|
||||
credit_data["updated_by"] = current_user.id
|
||||
|
||||
db_credit = Credit(**credit_data)
|
||||
|
||||
session.add(db_credit)
|
||||
session.commit()
|
||||
session.refresh(db_credit)
|
||||
return db_credit
|
||||
|
||||
# Read all Credit accounts
|
||||
@router.get("/", response_model=List[CreditResponse])
|
||||
def read_credits(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_any_access)
|
||||
):
|
||||
"""Get all credit accounts (requires authentication)."""
|
||||
credits = session.exec(
|
||||
select(Credit).offset(skip).limit(limit)
|
||||
).all()
|
||||
return credits
|
||||
|
||||
# Read Credit by partner
|
||||
@router.get("/partner/{partner_id}", response_model=CreditResponse)
|
||||
def read_credit_by_partner(
|
||||
partner_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_any_access)
|
||||
):
|
||||
"""Get credit account for a specific partner (requires authentication)."""
|
||||
# Validate partner exists
|
||||
partner = session.get(Partner, partner_id)
|
||||
if not partner:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Partner not found"
|
||||
)
|
||||
|
||||
credit = session.exec(
|
||||
select(Credit).where(Credit.partner_id == partner_id)
|
||||
).first()
|
||||
|
||||
if not credit:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Credit account not found for this partner"
|
||||
)
|
||||
|
||||
return credit
|
||||
|
||||
# Read single Credit by ID
|
||||
@router.get("/{credit_id}", response_model=CreditResponse)
|
||||
def read_credit_by_id(
|
||||
credit_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_any_access)
|
||||
):
|
||||
"""Get specific credit account by ID (requires authentication)."""
|
||||
credit = session.get(Credit, credit_id)
|
||||
if not credit:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Credit account not found"
|
||||
)
|
||||
return credit
|
||||
|
||||
# Update Credit
|
||||
@router.put("/{credit_id}", response_model=CreditResponse)
|
||||
def update_credit(
|
||||
credit_id: int,
|
||||
credit: CreditUpdate,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_write_access)
|
||||
):
|
||||
"""Update specific credit account (requires write access)."""
|
||||
db_credit = session.get(Credit, credit_id)
|
||||
if not db_credit:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Credit account not found"
|
||||
)
|
||||
|
||||
update_data = credit.model_dump(exclude_unset=True)
|
||||
|
||||
# Validate partner if being updated
|
||||
if "partner_id" in update_data:
|
||||
partner = session.get(Partner, update_data["partner_id"])
|
||||
if not partner:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Partner not found"
|
||||
)
|
||||
|
||||
# Check for duplicate partner (excluding current record)
|
||||
existing_credit = session.exec(
|
||||
select(Credit).where(
|
||||
Credit.partner_id == update_data["partner_id"],
|
||||
Credit.id != credit_id
|
||||
)
|
||||
).first()
|
||||
if existing_credit:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Credit account already exists for this partner"
|
||||
)
|
||||
|
||||
# Validate transaction if being updated
|
||||
if "transaction_id" in update_data:
|
||||
transaction = session.get(Transaction, update_data["transaction_id"])
|
||||
if not transaction:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Transaction not found"
|
||||
)
|
||||
|
||||
# Track who updated
|
||||
update_data["updated_by"] = current_user.id
|
||||
|
||||
# Update credit
|
||||
for key, value in update_data.items():
|
||||
setattr(db_credit, key, value)
|
||||
|
||||
session.add(db_credit)
|
||||
session.commit()
|
||||
session.refresh(db_credit)
|
||||
return db_credit
|
||||
|
||||
# Delete Credit
|
||||
@router.delete("/{credit_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_credit(
|
||||
credit_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_admin)
|
||||
):
|
||||
"""Delete specific credit account (admin only)."""
|
||||
credit = session.get(Credit, credit_id)
|
||||
if not credit:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Credit account not found"
|
||||
)
|
||||
|
||||
session.delete(credit)
|
||||
session.commit()
|
||||
return None
|
||||
@@ -0,0 +1,175 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlmodel import Session, select
|
||||
from app.core.db import get_session
|
||||
from app.core.auth import require_any_access, require_write_access, require_admin
|
||||
from app.schemas.models import Inventory, Product
|
||||
from app.schemas.schemas import (
|
||||
InventoryCreate,
|
||||
InventoryUpdate,
|
||||
InventoryResponse,
|
||||
UserResponse
|
||||
)
|
||||
from typing import List
|
||||
|
||||
router = APIRouter(prefix="/inventory", tags=["inventory"])
|
||||
|
||||
# Create Inventory
|
||||
@router.post("/", response_model=InventoryResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_inventory(
|
||||
inventory: InventoryCreate,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_write_access)
|
||||
):
|
||||
"""Create new inventory entry (requires write access)."""
|
||||
# Validate product exists
|
||||
product = session.get(Product, inventory.product_id)
|
||||
if not product:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Product not found"
|
||||
)
|
||||
|
||||
# Check if inventory already exists for this product
|
||||
existing_inventory = session.exec(
|
||||
select(Inventory).where(Inventory.product_id == inventory.product_id)
|
||||
).first()
|
||||
if existing_inventory:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Inventory entry already exists for this product"
|
||||
)
|
||||
|
||||
# Create inventory
|
||||
inventory_data = inventory.model_dump()
|
||||
db_inventory = Inventory(**inventory_data)
|
||||
|
||||
session.add(db_inventory)
|
||||
session.commit()
|
||||
session.refresh(db_inventory)
|
||||
return db_inventory
|
||||
|
||||
# Read all Inventory
|
||||
@router.get("/", response_model=List[InventoryResponse])
|
||||
def read_inventory(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_any_access)
|
||||
):
|
||||
"""Get all inventory entries (requires authentication)."""
|
||||
inventory = session.exec(
|
||||
select(Inventory).offset(skip).limit(limit)
|
||||
).all()
|
||||
return inventory
|
||||
|
||||
# Read Inventory by product
|
||||
@router.get("/product/{product_id}", response_model=InventoryResponse)
|
||||
def read_inventory_by_product(
|
||||
product_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_any_access)
|
||||
):
|
||||
"""Get inventory for a specific product (requires authentication)."""
|
||||
# Validate product exists
|
||||
product = session.get(Product, product_id)
|
||||
if not product:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Product not found"
|
||||
)
|
||||
|
||||
inventory = session.exec(
|
||||
select(Inventory).where(Inventory.product_id == product_id)
|
||||
).first()
|
||||
|
||||
if not inventory:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Inventory not found for this product"
|
||||
)
|
||||
|
||||
return inventory
|
||||
|
||||
# Read single Inventory by ID
|
||||
@router.get("/{inventory_id}", response_model=InventoryResponse)
|
||||
def read_inventory_by_id(
|
||||
inventory_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_any_access)
|
||||
):
|
||||
"""Get specific inventory entry by ID (requires authentication)."""
|
||||
inventory = session.get(Inventory, inventory_id)
|
||||
if not inventory:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Inventory entry not found"
|
||||
)
|
||||
return inventory
|
||||
|
||||
# Update Inventory
|
||||
@router.put("/{inventory_id}", response_model=InventoryResponse)
|
||||
def update_inventory(
|
||||
inventory_id: int,
|
||||
inventory: InventoryUpdate,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_write_access)
|
||||
):
|
||||
"""Update specific inventory entry (requires write access)."""
|
||||
db_inventory = session.get(Inventory, inventory_id)
|
||||
if not db_inventory:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Inventory entry not found"
|
||||
)
|
||||
|
||||
update_data = inventory.model_dump(exclude_unset=True)
|
||||
|
||||
# Validate product if being updated
|
||||
if "product_id" in update_data:
|
||||
product = session.get(Product, update_data["product_id"])
|
||||
if not product:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Product not found"
|
||||
)
|
||||
|
||||
# Check for duplicate product (excluding current record)
|
||||
existing_inventory = session.exec(
|
||||
select(Inventory).where(
|
||||
Inventory.product_id == update_data["product_id"],
|
||||
Inventory.id != inventory_id
|
||||
)
|
||||
).first()
|
||||
if existing_inventory:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Inventory entry already exists for this product"
|
||||
)
|
||||
|
||||
# Update inventory
|
||||
for key, value in update_data.items():
|
||||
setattr(db_inventory, key, value)
|
||||
|
||||
session.add(db_inventory)
|
||||
session.commit()
|
||||
session.refresh(db_inventory)
|
||||
return db_inventory
|
||||
|
||||
# Delete Inventory
|
||||
@router.delete("/{inventory_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_inventory(
|
||||
inventory_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_admin)
|
||||
):
|
||||
"""Delete specific inventory entry (admin only)."""
|
||||
inventory = session.get(Inventory, inventory_id)
|
||||
if not inventory:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Inventory entry not found"
|
||||
)
|
||||
|
||||
session.delete(inventory)
|
||||
session.commit()
|
||||
return None
|
||||
@@ -0,0 +1,126 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlmodel import Session, select
|
||||
from app.core.db import get_session
|
||||
from app.core.auth import require_any_access, require_write_access, require_admin
|
||||
from app.schemas.models import Partner
|
||||
from app.schemas.schemas import (
|
||||
PartnerCreate,
|
||||
PartnerUpdate,
|
||||
PartnerResponse,
|
||||
UserResponse
|
||||
)
|
||||
from typing import List
|
||||
|
||||
router = APIRouter(prefix="/partners", tags=["partners"])
|
||||
|
||||
# Create Partner
|
||||
@router.post("/", response_model=PartnerResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_partner(
|
||||
partner: PartnerCreate,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_write_access)
|
||||
):
|
||||
"""Create a new partner (requires write access)."""
|
||||
# Check if TIN number already exists
|
||||
statement = select(Partner).where(Partner.tin_number == partner.tin_number)
|
||||
existing_partner = session.exec(statement).first()
|
||||
if existing_partner:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Partner with this TIN number already exists"
|
||||
)
|
||||
|
||||
# Create new partner
|
||||
partner_data = partner.model_dump()
|
||||
db_partner = Partner(**partner_data)
|
||||
|
||||
session.add(db_partner)
|
||||
session.commit()
|
||||
session.refresh(db_partner)
|
||||
return db_partner
|
||||
|
||||
# Read all Partners
|
||||
@router.get("/", response_model=List[PartnerResponse])
|
||||
def read_partners(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_any_access)
|
||||
):
|
||||
"""Get all partners (requires authentication)."""
|
||||
partners = session.exec(select(Partner).offset(skip).limit(limit)).all()
|
||||
return partners
|
||||
|
||||
# Read single Partner by ID
|
||||
@router.get("/{partner_id}", response_model=PartnerResponse)
|
||||
def read_partner(
|
||||
partner_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_any_access)
|
||||
):
|
||||
"""Get specific partner by ID (requires authentication)."""
|
||||
partner = session.get(Partner, partner_id)
|
||||
if not partner:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Partner not found"
|
||||
)
|
||||
return partner
|
||||
|
||||
# Update Partner
|
||||
@router.put("/{partner_id}", response_model=PartnerResponse)
|
||||
def update_partner(
|
||||
partner_id: int,
|
||||
partner: PartnerUpdate,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_write_access)
|
||||
):
|
||||
"""Update specific partner (requires write access)."""
|
||||
db_partner = session.get(Partner, partner_id)
|
||||
if not db_partner:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Partner not found"
|
||||
)
|
||||
|
||||
# Check for TIN number conflicts if updating TIN
|
||||
update_data = partner.model_dump(exclude_unset=True)
|
||||
if "tin_number" in update_data:
|
||||
statement = select(Partner).where(
|
||||
Partner.tin_number == update_data["tin_number"],
|
||||
Partner.id != partner_id
|
||||
)
|
||||
existing_partner = session.exec(statement).first()
|
||||
if existing_partner:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Partner with this TIN number already exists"
|
||||
)
|
||||
|
||||
# Update partner
|
||||
for key, value in update_data.items():
|
||||
setattr(db_partner, key, value)
|
||||
|
||||
session.add(db_partner)
|
||||
session.commit()
|
||||
session.refresh(db_partner)
|
||||
return db_partner
|
||||
|
||||
# Delete Partner
|
||||
@router.delete("/{partner_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_partner(
|
||||
partner_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_admin)
|
||||
):
|
||||
"""Delete specific partner (admin only)."""
|
||||
partner = session.get(Partner, partner_id)
|
||||
if not partner:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Partner not found"
|
||||
)
|
||||
|
||||
session.delete(partner)
|
||||
session.commit()
|
||||
return None
|
||||
@@ -0,0 +1,155 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlmodel import Session, select
|
||||
from app.core.db import get_session
|
||||
from app.core.auth import require_any_access, require_write_access, require_admin
|
||||
from app.schemas.models import Payment, Transaction
|
||||
from app.schemas.schemas import (
|
||||
PaymentCreate,
|
||||
PaymentUpdate,
|
||||
PaymentResponse,
|
||||
UserResponse
|
||||
)
|
||||
from typing import List
|
||||
|
||||
router = APIRouter(prefix="/payments", tags=["payments"])
|
||||
|
||||
# Create Payment
|
||||
@router.post("/", response_model=PaymentResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_payment(
|
||||
payment: PaymentCreate,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_write_access)
|
||||
):
|
||||
"""Create new payment (requires write access)."""
|
||||
# Validate transaction exists
|
||||
transaction = session.get(Transaction, payment.transaction_id)
|
||||
if not transaction:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Transaction not found"
|
||||
)
|
||||
|
||||
# Create payment with audit fields
|
||||
payment_data = payment.model_dump()
|
||||
payment_data["created_by"] = current_user.id
|
||||
payment_data["updated_by"] = current_user.id
|
||||
|
||||
db_payment = Payment(**payment_data)
|
||||
|
||||
session.add(db_payment)
|
||||
session.commit()
|
||||
session.refresh(db_payment)
|
||||
return db_payment
|
||||
|
||||
# Read all Payments
|
||||
@router.get("/", response_model=List[PaymentResponse])
|
||||
def read_payments(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_any_access)
|
||||
):
|
||||
"""Get all payments (requires authentication)."""
|
||||
payments = session.exec(
|
||||
select(Payment).offset(skip).limit(limit)
|
||||
).all()
|
||||
return payments
|
||||
|
||||
# Read Payments by transaction
|
||||
@router.get("/transaction/{transaction_id}", response_model=List[PaymentResponse])
|
||||
def read_payments_by_transaction(
|
||||
transaction_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_any_access)
|
||||
):
|
||||
"""Get payments for a specific transaction (requires authentication)."""
|
||||
# Validate transaction exists
|
||||
transaction = session.get(Transaction, transaction_id)
|
||||
if not transaction:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Transaction not found"
|
||||
)
|
||||
|
||||
statement = select(Payment).where(
|
||||
Payment.transaction_id == transaction_id
|
||||
).offset(skip).limit(limit)
|
||||
|
||||
payments = session.exec(statement).all()
|
||||
return payments
|
||||
|
||||
# Read single Payment by ID
|
||||
@router.get("/{payment_id}", response_model=PaymentResponse)
|
||||
def read_payment_by_id(
|
||||
payment_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_any_access)
|
||||
):
|
||||
"""Get specific payment by ID (requires authentication)."""
|
||||
payment = session.get(Payment, payment_id)
|
||||
if not payment:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Payment not found"
|
||||
)
|
||||
return payment
|
||||
|
||||
# Update Payment
|
||||
@router.put("/{payment_id}", response_model=PaymentResponse)
|
||||
def update_payment(
|
||||
payment_id: int,
|
||||
payment: PaymentUpdate,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_write_access)
|
||||
):
|
||||
"""Update specific payment (requires write access)."""
|
||||
db_payment = session.get(Payment, payment_id)
|
||||
if not db_payment:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Payment not found"
|
||||
)
|
||||
|
||||
update_data = payment.model_dump(exclude_unset=True)
|
||||
|
||||
# Validate transaction if being updated
|
||||
if "transaction_id" in update_data:
|
||||
transaction = session.get(Transaction, update_data["transaction_id"])
|
||||
if not transaction:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Transaction not found"
|
||||
)
|
||||
|
||||
# Track who updated
|
||||
update_data["updated_by"] = current_user.id
|
||||
|
||||
# Update payment
|
||||
for key, value in update_data.items():
|
||||
setattr(db_payment, key, value)
|
||||
|
||||
session.add(db_payment)
|
||||
session.commit()
|
||||
session.refresh(db_payment)
|
||||
return db_payment
|
||||
|
||||
# Delete Payment
|
||||
@router.delete("/{payment_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_payment(
|
||||
payment_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_admin)
|
||||
):
|
||||
"""Delete specific payment (admin only)."""
|
||||
payment = session.get(Payment, payment_id)
|
||||
if not payment:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Payment not found"
|
||||
)
|
||||
|
||||
session.delete(payment)
|
||||
session.commit()
|
||||
return None
|
||||
@@ -0,0 +1,166 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlmodel import Session, select
|
||||
from app.core.db import get_session
|
||||
from app.core.auth import require_any_access, require_write_access, require_admin
|
||||
from app.schemas.models import Product
|
||||
from app.schemas.schemas import (
|
||||
ProductCreate,
|
||||
ProductUpdate,
|
||||
ProductResponse,
|
||||
UserResponse
|
||||
)
|
||||
from typing import List
|
||||
|
||||
router = APIRouter(prefix="/products", tags=["products"])
|
||||
|
||||
# Create Product
|
||||
@router.post("/", response_model=ProductResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_product(
|
||||
product: ProductCreate,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_write_access)
|
||||
):
|
||||
"""Create a new product (requires write access)."""
|
||||
# Check if product code already exists
|
||||
statement = select(Product).where(Product.product_code == product.product_code)
|
||||
existing_product = session.exec(statement).first()
|
||||
if existing_product:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Product with this code already exists"
|
||||
)
|
||||
|
||||
# Check if product name already exists
|
||||
statement = select(Product).where(Product.product_name == product.product_name)
|
||||
existing_product = session.exec(statement).first()
|
||||
if existing_product:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Product with this name already exists"
|
||||
)
|
||||
|
||||
# Create new product
|
||||
product_data = product.model_dump()
|
||||
db_product = Product(**product_data)
|
||||
|
||||
session.add(db_product)
|
||||
session.commit()
|
||||
session.refresh(db_product)
|
||||
return db_product
|
||||
|
||||
# Read all Products
|
||||
@router.get("/", response_model=List[ProductResponse])
|
||||
def read_products(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_any_access)
|
||||
):
|
||||
"""Get all products (requires authentication)."""
|
||||
products = session.exec(select(Product).offset(skip).limit(limit)).all()
|
||||
return products
|
||||
|
||||
# Read single Product by ID
|
||||
@router.get("/{product_id}", response_model=ProductResponse)
|
||||
def read_product(
|
||||
product_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_any_access)
|
||||
):
|
||||
"""Get specific product by ID (requires authentication)."""
|
||||
product = session.get(Product, product_id)
|
||||
if not product:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Product not found"
|
||||
)
|
||||
return product
|
||||
|
||||
# Read Product by code
|
||||
@router.get("/code/{product_code}", response_model=ProductResponse)
|
||||
def read_product_by_code(
|
||||
product_code: str,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_any_access)
|
||||
):
|
||||
"""Get specific product by code (requires authentication)."""
|
||||
statement = select(Product).where(Product.product_code == product_code)
|
||||
product = session.exec(statement).first()
|
||||
if not product:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Product not found"
|
||||
)
|
||||
return product
|
||||
|
||||
# Update Product
|
||||
@router.put("/{product_id}", response_model=ProductResponse)
|
||||
def update_product(
|
||||
product_id: int,
|
||||
product: ProductUpdate,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_write_access)
|
||||
):
|
||||
"""Update specific product (requires write access)."""
|
||||
db_product = session.get(Product, product_id)
|
||||
if not db_product:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Product not found"
|
||||
)
|
||||
|
||||
update_data = product.model_dump(exclude_unset=True)
|
||||
|
||||
# Check for product code conflicts if updating code
|
||||
if "product_code" in update_data:
|
||||
statement = select(Product).where(
|
||||
Product.product_code == update_data["product_code"],
|
||||
Product.id != product_id
|
||||
)
|
||||
existing_product = session.exec(statement).first()
|
||||
if existing_product:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Product with this code already exists"
|
||||
)
|
||||
|
||||
# Check for product name conflicts if updating name
|
||||
if "product_name" in update_data:
|
||||
statement = select(Product).where(
|
||||
Product.product_name == update_data["product_name"],
|
||||
Product.id != product_id
|
||||
)
|
||||
existing_product = session.exec(statement).first()
|
||||
if existing_product:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Product with this name already exists"
|
||||
)
|
||||
|
||||
# Update product
|
||||
for key, value in update_data.items():
|
||||
setattr(db_product, key, value)
|
||||
|
||||
session.add(db_product)
|
||||
session.commit()
|
||||
session.refresh(db_product)
|
||||
return db_product
|
||||
|
||||
# Delete Product
|
||||
@router.delete("/{product_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_product(
|
||||
product_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_admin)
|
||||
):
|
||||
"""Delete specific product (admin only)."""
|
||||
product = session.get(Product, product_id)
|
||||
if not product:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Product not found"
|
||||
)
|
||||
|
||||
session.delete(product)
|
||||
session.commit()
|
||||
return None
|
||||
@@ -0,0 +1,197 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlmodel import Session, select
|
||||
from app.core.db import get_session
|
||||
from app.core.auth import require_any_access, require_write_access, require_admin
|
||||
from app.schemas.models import Transaction_details, Partner, Product
|
||||
from app.schemas.schemas import (
|
||||
TransactionDetailsCreate,
|
||||
TransactionDetailsUpdate,
|
||||
TransactionDetailsResponse,
|
||||
UserResponse
|
||||
)
|
||||
from typing import List
|
||||
|
||||
router = APIRouter(prefix="/transaction-details", tags=["transaction-details"])
|
||||
|
||||
# Create Transaction Details
|
||||
@router.post("/", response_model=TransactionDetailsResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_transaction_details(
|
||||
transaction_details: TransactionDetailsCreate,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_write_access)
|
||||
):
|
||||
"""Create new transaction details (requires write access)."""
|
||||
# Validate partner exists
|
||||
partner = session.get(Partner, transaction_details.partner_id)
|
||||
if not partner:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Partner not found"
|
||||
)
|
||||
|
||||
# Validate product exists
|
||||
product = session.get(Product, transaction_details.product_id)
|
||||
if not product:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Product not found"
|
||||
)
|
||||
|
||||
# Create transaction details with audit fields
|
||||
transaction_details_data = transaction_details.model_dump()
|
||||
transaction_details_data["created_by"] = current_user.id
|
||||
transaction_details_data["updated_by"] = current_user.id
|
||||
|
||||
db_transaction_details = Transaction_details(**transaction_details_data)
|
||||
|
||||
session.add(db_transaction_details)
|
||||
session.commit()
|
||||
session.refresh(db_transaction_details)
|
||||
return db_transaction_details
|
||||
|
||||
# Read all Transaction Details
|
||||
@router.get("/", response_model=List[TransactionDetailsResponse])
|
||||
def read_transaction_details(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_any_access)
|
||||
):
|
||||
"""Get all transaction details (requires authentication)."""
|
||||
transaction_details = session.exec(
|
||||
select(Transaction_details).offset(skip).limit(limit)
|
||||
).all()
|
||||
return transaction_details
|
||||
|
||||
# Read Transaction Details by partner
|
||||
@router.get("/partner/{partner_id}", response_model=List[TransactionDetailsResponse])
|
||||
def read_transaction_details_by_partner(
|
||||
partner_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_any_access)
|
||||
):
|
||||
"""Get transaction details for a specific partner (requires authentication)."""
|
||||
# Validate partner exists
|
||||
partner = session.get(Partner, partner_id)
|
||||
if not partner:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Partner not found"
|
||||
)
|
||||
|
||||
statement = select(Transaction_details).where(
|
||||
Transaction_details.partner_id == partner_id
|
||||
).offset(skip).limit(limit)
|
||||
|
||||
transaction_details = session.exec(statement).all()
|
||||
return transaction_details
|
||||
|
||||
# Read Transaction Details by product
|
||||
@router.get("/product/{product_id}", response_model=List[TransactionDetailsResponse])
|
||||
def read_transaction_details_by_product(
|
||||
product_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_any_access)
|
||||
):
|
||||
"""Get transaction details for a specific product (requires authentication)."""
|
||||
# Validate product exists
|
||||
product = session.get(Product, product_id)
|
||||
if not product:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Product not found"
|
||||
)
|
||||
|
||||
statement = select(Transaction_details).where(
|
||||
Transaction_details.product_id == product_id
|
||||
).offset(skip).limit(limit)
|
||||
|
||||
transaction_details = session.exec(statement).all()
|
||||
return transaction_details
|
||||
|
||||
# Read single Transaction Details by ID
|
||||
@router.get("/{transaction_details_id}", response_model=TransactionDetailsResponse)
|
||||
def read_transaction_details_by_id(
|
||||
transaction_details_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_any_access)
|
||||
):
|
||||
"""Get specific transaction details by ID (requires authentication)."""
|
||||
transaction_details = session.get(Transaction_details, transaction_details_id)
|
||||
if not transaction_details:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Transaction details not found"
|
||||
)
|
||||
return transaction_details
|
||||
|
||||
# Update Transaction Details
|
||||
@router.put("/{transaction_details_id}", response_model=TransactionDetailsResponse)
|
||||
def update_transaction_details(
|
||||
transaction_details_id: int,
|
||||
transaction_details: TransactionDetailsUpdate,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_write_access)
|
||||
):
|
||||
"""Update specific transaction details (requires write access)."""
|
||||
db_transaction_details = session.get(Transaction_details, transaction_details_id)
|
||||
if not db_transaction_details:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Transaction details not found"
|
||||
)
|
||||
|
||||
update_data = transaction_details.model_dump(exclude_unset=True)
|
||||
|
||||
# Validate partner if being updated
|
||||
if "partner_id" in update_data:
|
||||
partner = session.get(Partner, update_data["partner_id"])
|
||||
if not partner:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Partner not found"
|
||||
)
|
||||
|
||||
# Validate product if being updated
|
||||
if "product_id" in update_data:
|
||||
product = session.get(Product, update_data["product_id"])
|
||||
if not product:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Product not found"
|
||||
)
|
||||
|
||||
# Track who updated
|
||||
update_data["updated_by"] = current_user.id
|
||||
|
||||
# Update transaction details
|
||||
for key, value in update_data.items():
|
||||
setattr(db_transaction_details, key, value)
|
||||
|
||||
session.add(db_transaction_details)
|
||||
session.commit()
|
||||
session.refresh(db_transaction_details)
|
||||
return db_transaction_details
|
||||
|
||||
# Delete Transaction Details
|
||||
@router.delete("/{transaction_details_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_transaction_details(
|
||||
transaction_details_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_admin)
|
||||
):
|
||||
"""Delete specific transaction details (admin only)."""
|
||||
transaction_details = session.get(Transaction_details, transaction_details_id)
|
||||
if not transaction_details:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Transaction details not found"
|
||||
)
|
||||
|
||||
session.delete(transaction_details)
|
||||
session.commit()
|
||||
return None
|
||||
@@ -0,0 +1,88 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlmodel import Session, select
|
||||
from app.core.db import get_session
|
||||
from app.core.auth import require_any_access, require_write_access, get_current_active_user
|
||||
from app.schemas.models import Transaction
|
||||
from app.schemas.schemas import TransactionCreate, TransactionUpdate, TransactionResponse, UserResponse
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
router = APIRouter(prefix="/transactions", tags=["transactions"])
|
||||
|
||||
# Create Transaction
|
||||
@router.post("/", response_model=TransactionResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_transaction(
|
||||
transaction: TransactionCreate,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_write_access)
|
||||
):
|
||||
# Set created_by and updated_by to current user
|
||||
transaction_data = transaction.model_dump(exclude_unset=True)
|
||||
transaction_data["created_by"] = current_user.id
|
||||
transaction_data["updated_by"] = current_user.id
|
||||
|
||||
db_transaction = Transaction(**transaction_data)
|
||||
session.add(db_transaction)
|
||||
session.commit()
|
||||
session.refresh(db_transaction)
|
||||
return db_transaction
|
||||
|
||||
# Read all Transactions
|
||||
@router.get("/", response_model=List[TransactionResponse])
|
||||
def read_transactions(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_any_access)
|
||||
):
|
||||
transactions = session.exec(select(Transaction).offset(skip).limit(limit)).all()
|
||||
return transactions
|
||||
|
||||
# Read single Transaction by ID
|
||||
@router.get("/{transaction_id}", response_model=TransactionResponse)
|
||||
def read_transaction(
|
||||
transaction_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_any_access)
|
||||
):
|
||||
transaction = session.get(Transaction, transaction_id)
|
||||
if not transaction:
|
||||
raise HTTPException(status_code=404, detail="Transaction not found")
|
||||
return transaction
|
||||
|
||||
# Update Transaction
|
||||
@router.put("/{transaction_id}", response_model=TransactionResponse)
|
||||
def update_transaction(
|
||||
transaction_id: int,
|
||||
transaction: TransactionUpdate,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_write_access)
|
||||
):
|
||||
db_transaction = session.get(Transaction, transaction_id)
|
||||
if not db_transaction:
|
||||
raise HTTPException(status_code=404, detail="Transaction not found")
|
||||
|
||||
update_data = transaction.model_dump(exclude_unset=True)
|
||||
update_data["updated_by"] = current_user.id # Track who updated
|
||||
|
||||
for key, value in update_data.items():
|
||||
setattr(db_transaction, key, value)
|
||||
|
||||
session.add(db_transaction)
|
||||
session.commit()
|
||||
session.refresh(db_transaction)
|
||||
return db_transaction
|
||||
|
||||
# Delete Transaction
|
||||
@router.delete("/{transaction_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_transaction(
|
||||
transaction_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_write_access)
|
||||
):
|
||||
transaction = session.get(Transaction, transaction_id)
|
||||
if not transaction:
|
||||
raise HTTPException(status_code=404, detail="Transaction not found")
|
||||
session.delete(transaction)
|
||||
session.commit()
|
||||
return None
|
||||
@@ -0,0 +1,203 @@
|
||||
# backend/app/api/v1/users.py
|
||||
from datetime import timedelta
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer
|
||||
from sqlmodel import Session, select
|
||||
from app.core.db import get_session
|
||||
from app.core.auth import (
|
||||
authenticate_user,
|
||||
create_access_token,
|
||||
get_password_hash,
|
||||
get_current_active_user,
|
||||
get_token_expiration_minutes,
|
||||
require_admin,
|
||||
require_write_access,
|
||||
require_any_access
|
||||
)
|
||||
from app.schemas.models import User
|
||||
from app.schemas.schemas import (
|
||||
UserCreate,
|
||||
UserUpdate,
|
||||
UserLogin,
|
||||
Token,
|
||||
UserResponse
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["users"])
|
||||
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
def login(user_credentials: UserLogin, session: Session = Depends(get_session)):
|
||||
"""Authenticate user and return JWT token with role-based expiration."""
|
||||
user = authenticate_user(session, user_credentials.username, user_credentials.password)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# Get role-based expiration time
|
||||
expire_minutes = get_token_expiration_minutes(user.role)
|
||||
access_token_expires = timedelta(minutes=expire_minutes)
|
||||
|
||||
# Create token with user data
|
||||
access_token = create_access_token(
|
||||
data={
|
||||
"sub": user.username,
|
||||
"user_id": user.id,
|
||||
"role": user.role.value
|
||||
},
|
||||
expires_delta=access_token_expires
|
||||
)
|
||||
|
||||
return Token(
|
||||
access_token=access_token,
|
||||
token_type="bearer",
|
||||
expires_in=expire_minutes * 60, # Convert to seconds
|
||||
user=UserResponse(
|
||||
id=user.id,
|
||||
username=user.username,
|
||||
role=user.role
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
def get_current_user_info(current_user: User = Depends(get_current_active_user)):
|
||||
"""Get current user information from token."""
|
||||
return UserResponse(
|
||||
id=current_user.id,
|
||||
username=current_user.username,
|
||||
role=current_user.role
|
||||
)
|
||||
|
||||
|
||||
@router.get("/", response_model=list[UserResponse])
|
||||
def get_all_users(
|
||||
session: Session = Depends(get_session),
|
||||
current_user: User = Depends(require_any_access),
|
||||
skip: int = 0,
|
||||
limit: int = 100
|
||||
):
|
||||
"""Get all users (requires any authenticated role)."""
|
||||
statement = select(User).offset(skip).limit(limit)
|
||||
users = session.exec(statement).all()
|
||||
return [
|
||||
UserResponse(id=user.id, username=user.username, role=user.role)
|
||||
for user in users
|
||||
]
|
||||
|
||||
|
||||
@router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_user(
|
||||
user: UserCreate,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Create a new user (admin only)."""
|
||||
# Check if username already exists
|
||||
statement = select(User).where(User.username == user.username)
|
||||
existing_user = session.exec(statement).first()
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Username already registered"
|
||||
)
|
||||
|
||||
# Create new user with hashed password
|
||||
hashed_password = get_password_hash(user.password)
|
||||
db_user = User(
|
||||
username=user.username,
|
||||
password_hash=hashed_password,
|
||||
role=user.role
|
||||
)
|
||||
|
||||
session.add(db_user)
|
||||
session.commit()
|
||||
session.refresh(db_user)
|
||||
|
||||
return UserResponse(
|
||||
id=db_user.id,
|
||||
username=db_user.username,
|
||||
role=db_user.role
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserResponse)
|
||||
def get_user(
|
||||
user_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: User = Depends(require_any_access)
|
||||
):
|
||||
"""Get specific user by ID (requires authentication)."""
|
||||
user = session.get(User, user_id)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
return UserResponse(
|
||||
id=user.id,
|
||||
username=user.username,
|
||||
role=user.role
|
||||
)
|
||||
|
||||
|
||||
# Update to handle user self-updating password
|
||||
@router.put("/{user_id}", response_model=UserResponse)
|
||||
def update_user(
|
||||
user_id: int,
|
||||
user_update: UserUpdate,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: UserResponse = Depends(require_admin)
|
||||
):
|
||||
"""Update specific user (admin only)."""
|
||||
user = session.get(User, user_id)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
# Update only provided fields
|
||||
update_data = user_update.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(user, key, value)
|
||||
|
||||
session.add(user)
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
|
||||
return UserResponse(
|
||||
id=user.id,
|
||||
username=user.username,
|
||||
role=user.role
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_user(
|
||||
user_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Delete specific user (admin only)."""
|
||||
user = session.get(User, user_id)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
# Prevent self-deletion
|
||||
if user.id == current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Cannot delete your own account"
|
||||
)
|
||||
|
||||
session.delete(user)
|
||||
session.commit()
|
||||
return None
|
||||
Reference in New Issue
Block a user