added production user role and made it default for new MSAL users - production can access everything EXCEPT user management - that's only for admin

This commit is contained in:
michael 2025-10-10 10:07:30 -05:00
parent 665b49c3f1
commit aefd559e68
12 changed files with 61 additions and 41 deletions

View file

@ -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(

View file

@ -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(),

View file

@ -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"""

View file

@ -21,6 +21,7 @@ PyObjectId = Annotated[str, BeforeValidator(validate_object_id)]
class UserRole(str, Enum):
CLIENT = "client"
REVIEWER = "reviewer"
PRODUCTION = "production"
ADMIN = "admin"

View file

@ -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(),

View file

@ -67,28 +67,28 @@ function AppContent() {
} />
<Route path="/admin/qc" element={
<AuthenticatedRoute>
<RoleGate allowedRoles={['reviewer', 'admin']}>
<RoleGate allowedRoles={['reviewer', 'production', 'admin']}>
<QCList />
</RoleGate>
</AuthenticatedRoute>
} />
<Route path="/admin/qc/:id" element={
<AuthenticatedRoute>
<RoleGate allowedRoles={['reviewer', 'admin']}>
<RoleGate allowedRoles={['reviewer', 'production', 'admin']}>
<QCDetail />
</RoleGate>
</AuthenticatedRoute>
} />
<Route path="/admin/final" element={
<AuthenticatedRoute>
<RoleGate allowedRoles={['reviewer', 'admin']}>
<RoleGate allowedRoles={['reviewer', 'production', 'admin']}>
<FinalList />
</RoleGate>
</AuthenticatedRoute>
} />
<Route path="/admin/final/:id" element={
<AuthenticatedRoute>
<RoleGate allowedRoles={['reviewer', 'admin']}>
<RoleGate allowedRoles={['reviewer', 'production', 'admin']}>
<FinalDetail />
</RoleGate>
</AuthenticatedRoute>

View file

@ -40,7 +40,7 @@ export function Navbar({ onMobileMenuClick }: NavbarProps) {
<NotificationMenu />
{/* Quick Actions */}
{['client', 'admin'].includes(user?.role || '') && (
{['client', 'production', 'admin'].includes(user?.role || '') && (
<Link
to="/jobs/new"
className="inline-flex items-center px-4 py-2 bg-gradient-to-r from-blue-500 to-blue-600 text-white text-sm font-medium rounded-lg hover:from-blue-600 hover:to-blue-700 transition-all duration-200 shadow-sm hover:shadow-md"

View file

@ -32,19 +32,19 @@ export function Sidebar({ onMobileClose }: SidebarProps) {
label: 'Upload Video',
href: '/jobs/new',
icon: '📤',
roles: ['client'],
roles: ['client', 'production', 'admin'],
},
{
label: 'QC Review',
href: '/admin/qc',
icon: '🔍',
roles: ['reviewer', 'admin'],
roles: ['reviewer', 'production', 'admin'],
},
{
label: 'Final Review',
href: '/admin/final',
icon: '✅',
roles: ['reviewer', 'admin'],
roles: ['reviewer', 'production', 'admin'],
},
];

View file

@ -242,6 +242,7 @@ export function Login() {
<p className="font-medium">Demo Credentials:</p>
<div className="bg-gray-50 rounded-lg p-3 space-y-1 text-xs">
<p><strong>Admin:</strong> admin@example.com / admin</p>
<p><strong>Production:</strong> production@example.com / production</p>
<p><strong>Reviewer:</strong> reviewer@example.com / reviewer</p>
</div>
<p className="text-xs">

View file

@ -164,6 +164,7 @@ export function UserDetail() {
>
<option value="client">Client</option>
<option value="reviewer">Reviewer</option>
<option value="production">Production</option>
<option value="admin">Admin</option>
</select>
</div>

View file

@ -118,6 +118,7 @@ export function UserList() {
>
<option value="">All Roles</option>
<option value="admin">Admin</option>
<option value="production">Production</option>
<option value="reviewer">Reviewer</option>
<option value="client">Client</option>
</select>
@ -201,6 +202,7 @@ export function UserList() {
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium capitalize ${
user.role === 'admin' ? 'bg-purple-100 text-purple-800' :
user.role === 'production' ? 'bg-orange-100 text-orange-800' :
user.role === 'reviewer' ? 'bg-blue-100 text-blue-800' :
'bg-green-100 text-green-800'
}`}>
@ -425,6 +427,7 @@ function CreateUserModal({ onClose, onSuccess }: { onClose: () => void; onSucces
>
<option value="client">Client</option>
<option value="reviewer">Reviewer</option>
<option value="production">Production</option>
<option value="admin">Admin</option>
</select>
</div>

View file

@ -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 {