feat: implement user authentication module
- Backend (FastAPI): - External API authentication (pj-auth-api.vercel.app) - JWT token validation with Redis session storage - RBAC with department isolation - User, Role, Department models with pjctrl_ prefix - Alembic migrations with project-specific version table - Complete test coverage (13 tests) - Frontend (React + Vite): - AuthContext for state management - Login page with error handling - Protected route component - Dashboard with user info display - OpenSpec: - 7 capability specs defined - add-user-auth change archived 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
3
backend/app/api/departments/__init__.py
Normal file
3
backend/app/api/departments/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from app.api.departments import router
|
||||
|
||||
__all__ = ["router"]
|
||||
152
backend/app/api/departments/router.py
Normal file
152
backend/app/api/departments/router.py
Normal file
@@ -0,0 +1,152 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.models.department import Department
|
||||
from app.models.user import User
|
||||
from app.schemas.department import DepartmentCreate, DepartmentUpdate, DepartmentResponse
|
||||
from app.middleware.auth import require_permission, require_system_admin
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=List[DepartmentResponse])
|
||||
async def list_departments(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_permission("users.read")),
|
||||
):
|
||||
"""
|
||||
List all departments.
|
||||
"""
|
||||
departments = db.query(Department).offset(skip).limit(limit).all()
|
||||
return departments
|
||||
|
||||
|
||||
@router.get("/{department_id}", response_model=DepartmentResponse)
|
||||
async def get_department(
|
||||
department_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_permission("users.read")),
|
||||
):
|
||||
"""
|
||||
Get a specific department by ID.
|
||||
"""
|
||||
department = db.query(Department).filter(Department.id == department_id).first()
|
||||
if not department:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Department not found",
|
||||
)
|
||||
return department
|
||||
|
||||
|
||||
@router.post("", response_model=DepartmentResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_department(
|
||||
department_data: DepartmentCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_system_admin),
|
||||
):
|
||||
"""
|
||||
Create a new department. Requires system admin.
|
||||
"""
|
||||
# Check if parent exists if specified
|
||||
if department_data.parent_id:
|
||||
parent = db.query(Department).filter(
|
||||
Department.id == department_data.parent_id
|
||||
).first()
|
||||
if not parent:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Parent department not found",
|
||||
)
|
||||
|
||||
department = Department(**department_data.model_dump())
|
||||
db.add(department)
|
||||
db.commit()
|
||||
db.refresh(department)
|
||||
return department
|
||||
|
||||
|
||||
@router.patch("/{department_id}", response_model=DepartmentResponse)
|
||||
async def update_department(
|
||||
department_id: str,
|
||||
department_update: DepartmentUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_system_admin),
|
||||
):
|
||||
"""
|
||||
Update a department. Requires system admin.
|
||||
"""
|
||||
department = db.query(Department).filter(Department.id == department_id).first()
|
||||
if not department:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Department not found",
|
||||
)
|
||||
|
||||
# Check if new parent exists if specified
|
||||
update_data = department_update.model_dump(exclude_unset=True)
|
||||
if "parent_id" in update_data and update_data["parent_id"]:
|
||||
# Prevent circular reference
|
||||
if update_data["parent_id"] == department_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Department cannot be its own parent",
|
||||
)
|
||||
|
||||
parent = db.query(Department).filter(
|
||||
Department.id == update_data["parent_id"]
|
||||
).first()
|
||||
if not parent:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Parent department not found",
|
||||
)
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(department, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(department)
|
||||
return department
|
||||
|
||||
|
||||
@router.delete("/{department_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_department(
|
||||
department_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_system_admin),
|
||||
):
|
||||
"""
|
||||
Delete a department. Requires system admin.
|
||||
"""
|
||||
department = db.query(Department).filter(Department.id == department_id).first()
|
||||
if not department:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Department not found",
|
||||
)
|
||||
|
||||
# Check if department has users
|
||||
user_count = db.query(User).filter(User.department_id == department_id).count()
|
||||
if user_count > 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Cannot delete department with {user_count} users",
|
||||
)
|
||||
|
||||
# Check if department has children
|
||||
child_count = db.query(Department).filter(
|
||||
Department.parent_id == department_id
|
||||
).count()
|
||||
if child_count > 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Cannot delete department with {child_count} child departments",
|
||||
)
|
||||
|
||||
db.delete(department)
|
||||
db.commit()
|
||||
Reference in New Issue
Block a user