c086f64363
- 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
204 lines
5.7 KiB
Python
204 lines
5.7 KiB
Python
# 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
|