diff --git a/backend/app/api/v1/routes_admin.py b/backend/app/api/v1/routes_admin.py index 4bb77ec..f7bc80a 100644 --- a/backend/app/api/v1/routes_admin.py +++ b/backend/app/api/v1/routes_admin.py @@ -278,10 +278,10 @@ async def admin_reset_password( @router.get("/stats", response_model=AdminStatsResponse) async def get_admin_stats( - current_user: User = Depends(require_roles(UserRole.ADMIN)), + current_user: User = Depends(require_roles(UserRole.PRODUCTION, UserRole.ADMIN)), db: AsyncIOMotorDatabase = Depends(get_database), ): - """Get system statistics (admin only)""" + """Get system statistics (production/admin only)""" # Get user count total_users = await db.users.count_documents({"is_active": True}) @@ -336,10 +336,10 @@ async def get_admin_stats( @router.get("/health/detailed") async def detailed_health_check( - current_user: User = Depends(require_roles(UserRole.ADMIN, UserRole.REVIEWER)), + current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)), db: AsyncIOMotorDatabase = Depends(get_database), ): - """Detailed health check with system component status (admin/reviewer only)""" + """Detailed health check with system component status (reviewer/production/admin only)""" health_status = { "status": "healthy", "timestamp": datetime.utcnow().isoformat(), @@ -405,10 +405,10 @@ async def detailed_health_check( @router.get("/jobs/stats") async def get_job_statistics( days: int = Query(7, ge=1, le=90), - current_user: User = Depends(require_roles(UserRole.ADMIN, UserRole.REVIEWER)), + current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)), db: AsyncIOMotorDatabase = Depends(get_database), ): - """Get job processing statistics (admin/reviewer only)""" + """Get job processing statistics (reviewer/production/admin only)""" since_date = datetime.utcnow() - timedelta(days=days) # Jobs created in period @@ -534,10 +534,10 @@ async def get_audit_logs( days: int = Query(7, ge=1, le=90), page: int = Query(1, ge=1), size: int = Query(50, ge=1, le=200), - current_user: User = Depends(require_roles(UserRole.ADMIN)), + current_user: User = Depends(require_roles(UserRole.PRODUCTION, UserRole.ADMIN)), db: AsyncIOMotorDatabase = Depends(get_database), ): - """Get audit logs with filtering (admin only)""" + """Get audit logs with filtering (production/admin only)""" query = { "when": {"$gte": datetime.utcnow() - timedelta(days=days)} } @@ -572,10 +572,10 @@ async def get_audit_logs( @router.post("/maintenance/reprocess-job/{job_id}") async def reprocess_job( job_id: str, - current_user: User = Depends(require_roles(UserRole.ADMIN)), + current_user: User = Depends(require_roles(UserRole.PRODUCTION, UserRole.ADMIN)), db: AsyncIOMotorDatabase = Depends(get_database), ): - """Force reprocessing of a job (admin emergency function)""" + """Force reprocessing of a job (production/admin emergency function)""" # Check if job exists job_doc = await db.jobs.find_one({"_id": job_id}) if not job_doc: @@ -626,11 +626,11 @@ async def reprocess_job( @router.get("/audit-logs", response_model=AuditLogResponse) -async def get_audit_logs( +async def get_audit_logs_detailed( # Time range start_date: Optional[datetime] = Query(None, description="Start date for audit logs"), end_date: Optional[datetime] = Query(None, description="End date for audit logs"), - + # Filters action: Optional[str] = Query(None, description="Filter by action type"), severity: Optional[str] = Query(None, description="Filter by severity level"), @@ -638,22 +638,22 @@ async def get_audit_logs( resource_type: Optional[str] = Query(None, description="Filter by resource type"), resource_id: Optional[str] = Query(None, description="Filter by resource ID"), success: Optional[bool] = Query(None, description="Filter by success status"), - + # Search search: Optional[str] = Query(None, description="Search in description and details"), - + # Pagination page: int = Query(1, ge=1, description="Page number"), size: int = Query(50, ge=1, le=500, description="Page size"), - + # Sorting sort_by: str = Query("timestamp", description="Field to sort by"), sort_order: int = Query(-1, ge=-1, le=1, description="Sort order (-1 desc, 1 asc)"), - - current_user: User = Depends(require_roles(UserRole.ADMIN)), + + current_user: User = Depends(require_roles(UserRole.PRODUCTION, UserRole.ADMIN)), request: Request = None, ): - """Get audit logs with filtering and pagination (admin only)""" + """Get audit logs with filtering and pagination (production/admin only)""" # Log audit log access await audit_logger.log_action( @@ -698,10 +698,10 @@ async def get_audit_logs( async def get_user_audit_logs( user_id: str, days: int = Query(30, ge=1, le=365, description="Number of days to look back"), - current_user: User = Depends(require_roles(UserRole.ADMIN)), + current_user: User = Depends(require_roles(UserRole.PRODUCTION, UserRole.ADMIN)), request: Request = None, ): - """Get audit logs for a specific user (admin only)""" + """Get audit logs for a specific user (production/admin only)""" # Validate user_id try: @@ -730,10 +730,10 @@ async def get_user_audit_logs( @router.get("/audit-logs/security") async def get_security_events( hours: int = Query(24, ge=1, le=168, description="Number of hours to look back"), - current_user: User = Depends(require_roles(UserRole.ADMIN)), + current_user: User = Depends(require_roles(UserRole.PRODUCTION, UserRole.ADMIN)), request: Request = None, ): - """Get recent security events (admin only)""" + """Get recent security events (production/admin only)""" # Log access to security events await audit_logger.log_action( diff --git a/backend/app/api/v1/routes_auth.py b/backend/app/api/v1/routes_auth.py index 5ee77b9..a71e9dc 100644 --- a/backend/app/api/v1/routes_auth.py +++ b/backend/app/api/v1/routes_auth.py @@ -166,7 +166,7 @@ async def microsoft_login( "email": user_info.email, "full_name": user_info.name, "hashed_password": None, # No password for Microsoft users - "role": UserRole.CLIENT.value, + "role": UserRole.PRODUCTION.value, "auth_provider": AuthProvider.MICROSOFT.value, "is_active": True, "created_at": datetime.utcnow(), diff --git a/backend/app/api/v1/routes_jobs.py b/backend/app/api/v1/routes_jobs.py index 14b0766..4d82af9 100644 --- a/backend/app/api/v1/routes_jobs.py +++ b/backend/app/api/v1/routes_jobs.py @@ -161,10 +161,10 @@ async def create_job( @router.delete("/bulk", response_model=BulkDeleteResponse) async def bulk_delete_jobs( request: BulkDeleteRequest, - current_user: User = Depends(require_roles(UserRole.ADMIN)), + current_user: User = Depends(require_roles(UserRole.PRODUCTION, UserRole.ADMIN)), db: AsyncIOMotorDatabase = Depends(get_database), ): - """Bulk delete jobs (admin only)""" + """Bulk delete jobs (production/admin only)""" job_ids = request.job_ids logger.info(f"Bulk deleting {len(job_ids)} jobs requested by {current_user.email}") @@ -339,7 +339,7 @@ async def get_job( async def approve_english( job_id: str, request: ApproveEnglishRequest, - current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.ADMIN)), + current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)), db: AsyncIOMotorDatabase = Depends(get_database), ): result = await db.jobs.find_one_and_update( @@ -394,7 +394,7 @@ async def approve_english( async def reject_job( job_id: str, request: RejectJobRequest, - current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.ADMIN)), + current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)), db: AsyncIOMotorDatabase = Depends(get_database), ): result = await db.jobs.find_one_and_update( @@ -441,7 +441,7 @@ async def reject_job( async def complete_job( job_id: str, request: CompleteJobRequest, - current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.ADMIN)), + current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)), db: AsyncIOMotorDatabase = Depends(get_database), ): # Get job for validation @@ -518,7 +518,7 @@ async def complete_job( async def reject_final_review( job_id: str, request: RejectJobRequest, - current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.ADMIN)), + current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)), db: AsyncIOMotorDatabase = Depends(get_database), ): result = await db.jobs.find_one_and_update( @@ -706,7 +706,7 @@ async def get_job_vtt_content( async def update_job_vtt_content( job_id: str, request: VttUpdateRequest, - current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.ADMIN)), + current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)), db: AsyncIOMotorDatabase = Depends(get_database), ): """Update VTT content for a job""" @@ -800,7 +800,7 @@ async def update_job_vtt_content( async def adjust_vtt_timing( job_id: str, request: VttTimingAdjustRequest, - current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.ADMIN)), + current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)), db: AsyncIOMotorDatabase = Depends(get_database), ): """Adjust timing of VTT content by a specified offset""" @@ -1041,7 +1041,7 @@ async def _delete_job_gcs_assets(job_id: str, job_doc: dict): @router.get("/{job_id}/validate", response_model=AssetValidationResponse) async def validate_job_assets( job_id: str, - current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.ADMIN)), + current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)), db: AsyncIOMotorDatabase = Depends(get_database), ): """Validate job assets before completion""" diff --git a/backend/app/models/user.py b/backend/app/models/user.py index 22a8cbc..7899343 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -21,6 +21,7 @@ PyObjectId = Annotated[str, BeforeValidator(validate_object_id)] class UserRole(str, Enum): CLIENT = "client" REVIEWER = "reviewer" + PRODUCTION = "production" ADMIN = "admin" diff --git a/backend/create_test_users.py b/backend/create_test_users.py index 70c86cb..4d5d237 100644 --- a/backend/create_test_users.py +++ b/backend/create_test_users.py @@ -34,6 +34,7 @@ async def create_test_users(): "hashed_password": pwd_context.hash("admin"), "full_name": "Admin User", "role": UserRole.ADMIN.value, + "auth_provider": "local", "is_active": True, "created_at": datetime.utcnow(), "updated_at": datetime.utcnow(), @@ -44,6 +45,18 @@ async def create_test_users(): "hashed_password": pwd_context.hash("reviewer"), "full_name": "Reviewer User", "role": UserRole.REVIEWER.value, + "auth_provider": "local", + "is_active": True, + "created_at": datetime.utcnow(), + "updated_at": datetime.utcnow(), + }, + { + "_id": "production-001", + "email": "production@example.com", + "hashed_password": pwd_context.hash("production"), + "full_name": "Production User", + "role": UserRole.PRODUCTION.value, + "auth_provider": "local", "is_active": True, "created_at": datetime.utcnow(), "updated_at": datetime.utcnow(), @@ -54,6 +67,7 @@ async def create_test_users(): "hashed_password": pwd_context.hash("client123"), "full_name": "Client User", "role": UserRole.CLIENT.value, + "auth_provider": "local", "is_active": True, "created_at": datetime.utcnow(), "updated_at": datetime.utcnow(), diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a45add2..f1abd73 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -67,28 +67,28 @@ function AppContent() { } /> - + } /> - + } /> - + } /> - + diff --git a/frontend/src/components/Layout/Navbar.tsx b/frontend/src/components/Layout/Navbar.tsx index 80b8d89..4dcea52 100644 --- a/frontend/src/components/Layout/Navbar.tsx +++ b/frontend/src/components/Layout/Navbar.tsx @@ -40,7 +40,7 @@ export function Navbar({ onMobileMenuClick }: NavbarProps) { {/* Quick Actions */} - {['client', 'admin'].includes(user?.role || '') && ( + {['client', 'production', 'admin'].includes(user?.role || '') && ( Demo Credentials:

Admin: admin@example.com / admin

+

Production: production@example.com / production

Reviewer: reviewer@example.com / reviewer

diff --git a/frontend/src/routes/admin/UserDetail.tsx b/frontend/src/routes/admin/UserDetail.tsx index e14ddb2..5fc95af 100644 --- a/frontend/src/routes/admin/UserDetail.tsx +++ b/frontend/src/routes/admin/UserDetail.tsx @@ -164,6 +164,7 @@ export function UserDetail() { > + diff --git a/frontend/src/routes/admin/UserList.tsx b/frontend/src/routes/admin/UserList.tsx index 4956030..183a96d 100644 --- a/frontend/src/routes/admin/UserList.tsx +++ b/frontend/src/routes/admin/UserList.tsx @@ -118,6 +118,7 @@ export function UserList() { > + @@ -201,6 +202,7 @@ export function UserList() { @@ -425,6 +427,7 @@ function CreateUserModal({ onClose, onSuccess }: { onClose: () => void; onSucces > + diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index fa349e0..796a0f9 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -11,7 +11,7 @@ export type JobStatus = | "pending_final_review" | "completed"; -export type UserRole = "client" | "reviewer" | "admin"; +export type UserRole = "client" | "reviewer" | "production" | "admin"; export type AuthProvider = "local" | "microsoft"; export interface User {