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:
2025-09-14 21:04:07 +02:00
parent 49c813778b
commit c086f64363
48 changed files with 6992 additions and 126 deletions
+203
View File
@@ -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