feat: add linguist role and user management navigation
- Add LINGUIST role to UserRole enum (backend + frontend) - Grant linguists access to QC Review, Final Review, review notes, and VTT editing - Add MongoDB migration to update schema validator with linguist role - Add admin seed: vadymsamoilenko@oliver.agency is promoted to admin on startup - Add User Management sidebar link for admin users - Fix Login.tsx role type cast to use UserRole instead of hardcoded union Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c735ba4bb5
commit
cf761c4bb6
13 changed files with 238 additions and 30 deletions
|
|
@ -246,7 +246,7 @@ async def bulk_delete_jobs(
|
|||
@router.post("/bulk/approve", response_model=BulkApproveResponse)
|
||||
async def bulk_approve_jobs(
|
||||
request: BulkApproveRequest,
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
db: AsyncIOMotorDatabase = Depends(get_database),
|
||||
):
|
||||
"""Bulk approve multiple jobs with optional method selection for accessible video"""
|
||||
|
|
@ -638,7 +638,7 @@ async def get_job(
|
|||
async def approve_source(
|
||||
job_id: str,
|
||||
request: ApproveSourceRequest,
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
db: AsyncIOMotorDatabase = Depends(get_database),
|
||||
):
|
||||
"""Approve the source language version (works for any language)"""
|
||||
|
|
@ -720,7 +720,7 @@ async def approve_source(
|
|||
async def approve_english(
|
||||
job_id: str,
|
||||
request: ApproveEnglishRequest,
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
db: AsyncIOMotorDatabase = Depends(get_database),
|
||||
):
|
||||
"""Legacy endpoint - redirects to approve_source for backwards compatibility"""
|
||||
|
|
@ -736,7 +736,7 @@ async def approve_english(
|
|||
async def reject_job(
|
||||
job_id: str,
|
||||
request: RejectJobRequest,
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
db: AsyncIOMotorDatabase = Depends(get_database),
|
||||
):
|
||||
result = await db.jobs.find_one_and_update(
|
||||
|
|
@ -783,7 +783,7 @@ async def reject_job(
|
|||
async def complete_job(
|
||||
job_id: str,
|
||||
request: CompleteJobRequest,
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
db: AsyncIOMotorDatabase = Depends(get_database),
|
||||
):
|
||||
# Get job for validation
|
||||
|
|
@ -860,7 +860,7 @@ async def complete_job(
|
|||
async def reject_final_review(
|
||||
job_id: str,
|
||||
request: RejectJobRequest,
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
db: AsyncIOMotorDatabase = Depends(get_database),
|
||||
):
|
||||
result = await db.jobs.find_one_and_update(
|
||||
|
|
@ -1171,7 +1171,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.PRODUCTION, UserRole.ADMIN)),
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
db: AsyncIOMotorDatabase = Depends(get_database),
|
||||
):
|
||||
"""Update VTT content for a job. If language is not specified, updates source language content."""
|
||||
|
|
@ -1332,7 +1332,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.PRODUCTION, UserRole.ADMIN)),
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
db: AsyncIOMotorDatabase = Depends(get_database),
|
||||
):
|
||||
"""Adjust timing of VTT content by a specified offset"""
|
||||
|
|
@ -1570,7 +1570,7 @@ async def _delete_job_gcs_assets(job_id: str, job_doc: dict):
|
|||
@router.post("/{job_id}/actions/retry_tts", response_model=JobResponse)
|
||||
async def retry_tts(
|
||||
job_id: str,
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
db: AsyncIOMotorDatabase = Depends(get_database),
|
||||
):
|
||||
"""Retry TTS generation for a job that failed during TTS synthesis.
|
||||
|
|
@ -1666,7 +1666,7 @@ async def retry_tts(
|
|||
@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.PRODUCTION, UserRole.ADMIN)),
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
db: AsyncIOMotorDatabase = Depends(get_database),
|
||||
):
|
||||
"""Validate job assets before completion"""
|
||||
|
|
@ -1695,7 +1695,7 @@ async def validate_job_assets(
|
|||
async def get_accessible_video_edit_state(
|
||||
job_id: str,
|
||||
language: str,
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
db: AsyncIOMotorDatabase = Depends(get_database),
|
||||
):
|
||||
"""Get current pause points, segment metadata, and TTS regeneration queue for QC editing."""
|
||||
|
|
@ -1791,7 +1791,7 @@ async def update_pause_point(
|
|||
language: str,
|
||||
cue_index: int,
|
||||
request: PausePointUpdateRequest,
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
db: AsyncIOMotorDatabase = Depends(get_database),
|
||||
):
|
||||
"""Update a single pause point timing with millisecond precision."""
|
||||
|
|
@ -1879,7 +1879,7 @@ async def queue_tts_regeneration(
|
|||
job_id: str,
|
||||
language: str,
|
||||
request: TTSRegenerationQueueRequest,
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
db: AsyncIOMotorDatabase = Depends(get_database),
|
||||
):
|
||||
"""Queue TTS regeneration for specific cues (uses current AD VTT text)."""
|
||||
|
|
@ -1951,7 +1951,7 @@ async def remove_tts_regeneration(
|
|||
job_id: str,
|
||||
language: str,
|
||||
cue_index: int,
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
db: AsyncIOMotorDatabase = Depends(get_database),
|
||||
):
|
||||
"""Remove a cue from the TTS regeneration queue."""
|
||||
|
|
@ -2003,7 +2003,7 @@ async def trigger_accessible_video_rerender(
|
|||
job_id: str,
|
||||
language: str,
|
||||
request: RerenderAccessibleVideoRequest,
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
db: AsyncIOMotorDatabase = Depends(get_database),
|
||||
):
|
||||
"""
|
||||
|
|
@ -2101,7 +2101,7 @@ async def trigger_accessible_video_rerender(
|
|||
async def update_tts_preferences(
|
||||
job_id: str,
|
||||
request: UpdateTTSPreferencesRequest,
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
db: AsyncIOMotorDatabase = Depends(get_database),
|
||||
):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ router = APIRouter(prefix="/jobs/{job_id}/review-notes", tags=["review-notes"])
|
|||
async def list_review_notes(
|
||||
job_id: str,
|
||||
asset_key: Optional[str] = Query(None, description="Filter notes by asset key"),
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
db: AsyncIOMotorDatabase = Depends(get_database),
|
||||
):
|
||||
"""List all review notes for a job, optionally filtered by asset key."""
|
||||
|
|
@ -57,7 +57,7 @@ async def list_review_notes(
|
|||
async def create_review_note(
|
||||
job_id: str,
|
||||
request: ReviewNoteCreateRequest,
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
db: AsyncIOMotorDatabase = Depends(get_database),
|
||||
):
|
||||
"""Create a new review note for a video asset."""
|
||||
|
|
@ -95,7 +95,7 @@ async def create_review_note(
|
|||
async def get_review_note(
|
||||
job_id: str,
|
||||
note_id: str,
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
db: AsyncIOMotorDatabase = Depends(get_database),
|
||||
):
|
||||
"""Get a single review note by ID."""
|
||||
|
|
@ -114,7 +114,7 @@ async def update_review_note(
|
|||
job_id: str,
|
||||
note_id: str,
|
||||
request: ReviewNoteUpdateRequest,
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
db: AsyncIOMotorDatabase = Depends(get_database),
|
||||
):
|
||||
"""Update a review note. Only the note owner can update."""
|
||||
|
|
@ -150,7 +150,7 @@ async def update_review_note(
|
|||
async def delete_review_note(
|
||||
job_id: str,
|
||||
note_id: str,
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
|
||||
db: AsyncIOMotorDatabase = Depends(get_database),
|
||||
):
|
||||
"""Delete a review note. Only the note owner can delete."""
|
||||
|
|
|
|||
51
backend/app/core/seed.py
Normal file
51
backend/app/core/seed.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
"""Seed utilities for initial data setup."""
|
||||
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from bson import ObjectId
|
||||
|
||||
from .security import get_password_hash
|
||||
|
||||
DEFAULT_ADMIN_EMAIL = "vadymsamoilenko@oliver.agency"
|
||||
|
||||
|
||||
async def seed_default_admin(db) -> None:
|
||||
"""Ensure the default admin user exists and has the admin role.
|
||||
|
||||
Looks up vadymsamoilenko@oliver.agency (case-insensitive).
|
||||
- If found with a non-admin role: promotes to admin.
|
||||
- If not found: creates a local-auth admin account.
|
||||
|
||||
Password is read from DEFAULT_ADMIN_PASSWORD env var (fallback: ChangeMe123!).
|
||||
"""
|
||||
email_pattern = re.compile(f"^{re.escape(DEFAULT_ADMIN_EMAIL)}$", re.IGNORECASE)
|
||||
existing = await db.users.find_one({"email": email_pattern})
|
||||
|
||||
if existing:
|
||||
if existing.get("role") != "admin":
|
||||
await db.users.update_one(
|
||||
{"_id": existing["_id"]},
|
||||
{"$set": {"role": "admin", "updated_at": datetime.utcnow()}},
|
||||
)
|
||||
print(f"✅ Promoted {DEFAULT_ADMIN_EMAIL} to admin role")
|
||||
else:
|
||||
print(f"✅ Default admin {DEFAULT_ADMIN_EMAIL} already exists")
|
||||
return
|
||||
|
||||
password = os.environ.get("DEFAULT_ADMIN_PASSWORD", "ChangeMe123!")
|
||||
user_doc = {
|
||||
"_id": str(ObjectId()),
|
||||
"email": DEFAULT_ADMIN_EMAIL,
|
||||
"hashed_password": get_password_hash(password),
|
||||
"full_name": "Vadym Samoilenko",
|
||||
"role": "admin",
|
||||
"auth_provider": "local",
|
||||
"is_active": True,
|
||||
"created_at": datetime.utcnow(),
|
||||
"updated_at": datetime.utcnow(),
|
||||
}
|
||||
|
||||
await db.users.insert_one(user_doc)
|
||||
print(f"✅ Created default admin: {DEFAULT_ADMIN_EMAIL}")
|
||||
|
|
@ -20,9 +20,10 @@ from .api.v1.routes_websockets import router as websockets_router
|
|||
from .services.websocket import connection_manager
|
||||
from .core.config import settings
|
||||
from .core.secrets_config import initialize_config
|
||||
from .core.database import close_mongo_connection, connect_to_mongo, create_indexes
|
||||
from .core.database import close_mongo_connection, connect_to_mongo, create_indexes, get_database
|
||||
from .core.logging import setup_logging
|
||||
from .core.redis import close_redis_connection, connect_to_redis, get_redis_client
|
||||
from .core.seed import seed_default_admin
|
||||
from .middleware import create_rate_limit_middleware, create_validation_middleware
|
||||
from .telemetry import (
|
||||
app_metrics,
|
||||
|
|
@ -74,6 +75,12 @@ async def lifespan(app: FastAPI):
|
|||
|
||||
await connect_to_mongo()
|
||||
await connect_to_redis()
|
||||
|
||||
try:
|
||||
db = await get_database()
|
||||
await seed_default_admin(db)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not seed default admin: {e}")
|
||||
# await create_indexes() # Temporarily disabled for debugging
|
||||
|
||||
# Start WebSocket connection manager
|
||||
|
|
|
|||
|
|
@ -0,0 +1,137 @@
|
|||
"""Add linguist role to user collection schema validator."""
|
||||
|
||||
from app.migrations.migrator import Migration
|
||||
|
||||
|
||||
class Migration(Migration):
|
||||
"""Update MongoDB schema validator to support linguist role."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.version = "2026-04-16-000000"
|
||||
self.description = "Add linguist role to user schema validator"
|
||||
|
||||
async def up(self) -> None:
|
||||
"""Update the users collection validator."""
|
||||
|
||||
validator = {
|
||||
"$jsonSchema": {
|
||||
"bsonType": "object",
|
||||
"required": ["email", "full_name", "role", "auth_provider", "is_active"],
|
||||
"properties": {
|
||||
"email": {
|
||||
"bsonType": "string",
|
||||
"pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
|
||||
"description": "Must be a valid email address"
|
||||
},
|
||||
"hashed_password": {
|
||||
"bsonType": ["string", "null"],
|
||||
"description": "Hashed password (null for Microsoft users)"
|
||||
},
|
||||
"full_name": {
|
||||
"bsonType": "string",
|
||||
"minLength": 1,
|
||||
"description": "User's full name"
|
||||
},
|
||||
"role": {
|
||||
"enum": ["client", "reviewer", "linguist", "production", "admin"],
|
||||
"description": "User role"
|
||||
},
|
||||
"auth_provider": {
|
||||
"enum": ["local", "microsoft"],
|
||||
"description": "Authentication provider"
|
||||
},
|
||||
"is_active": {
|
||||
"bsonType": "bool",
|
||||
"description": "Whether user account is active"
|
||||
},
|
||||
"created_at": {
|
||||
"bsonType": "date",
|
||||
"description": "Account creation timestamp"
|
||||
},
|
||||
"updated_at": {
|
||||
"bsonType": "date",
|
||||
"description": "Last update timestamp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
await self.db.command({
|
||||
"collMod": "users",
|
||||
"validator": validator,
|
||||
"validationLevel": "moderate",
|
||||
"validationAction": "error"
|
||||
})
|
||||
print(f"✅ Updated users collection validator")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not update validator: {e}")
|
||||
try:
|
||||
await self.db.create_collection(
|
||||
"users",
|
||||
validator=validator,
|
||||
validationLevel="moderate",
|
||||
validationAction="error"
|
||||
)
|
||||
print(f"✅ Created users collection with validator")
|
||||
except Exception as e2:
|
||||
print(f"⚠️ Could not create collection: {e2}")
|
||||
|
||||
print(f"✅ Applied migration {self.version}: {self.description}")
|
||||
|
||||
async def down(self) -> None:
|
||||
"""Revert to validator without linguist role."""
|
||||
|
||||
validator = {
|
||||
"$jsonSchema": {
|
||||
"bsonType": "object",
|
||||
"required": ["email", "full_name", "role", "auth_provider", "is_active"],
|
||||
"properties": {
|
||||
"email": {
|
||||
"bsonType": "string",
|
||||
"pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
|
||||
"description": "Must be a valid email address"
|
||||
},
|
||||
"hashed_password": {
|
||||
"bsonType": ["string", "null"],
|
||||
"description": "Hashed password (null for Microsoft users)"
|
||||
},
|
||||
"full_name": {
|
||||
"bsonType": "string",
|
||||
"minLength": 1,
|
||||
"description": "User's full name"
|
||||
},
|
||||
"role": {
|
||||
"enum": ["client", "reviewer", "production", "admin"],
|
||||
"description": "User role"
|
||||
},
|
||||
"auth_provider": {
|
||||
"enum": ["local", "microsoft"],
|
||||
"description": "Authentication provider"
|
||||
},
|
||||
"is_active": {
|
||||
"bsonType": "bool",
|
||||
"description": "Whether user account is active"
|
||||
},
|
||||
"created_at": {
|
||||
"bsonType": "date",
|
||||
"description": "Account creation timestamp"
|
||||
},
|
||||
"updated_at": {
|
||||
"bsonType": "date",
|
||||
"description": "Last update timestamp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await self.db.command({
|
||||
"collMod": "users",
|
||||
"validator": validator,
|
||||
"validationLevel": "moderate",
|
||||
"validationAction": "error"
|
||||
})
|
||||
|
||||
print(f"⚠️ Rolled back migration {self.version}: {self.description}")
|
||||
print(f"⚠️ WARNING: Linguist role users will fail validation!")
|
||||
|
|
@ -21,6 +21,7 @@ PyObjectId = Annotated[str, BeforeValidator(validate_object_id)]
|
|||
class UserRole(str, Enum):
|
||||
CLIENT = "client"
|
||||
REVIEWER = "reviewer"
|
||||
LINGUIST = "linguist"
|
||||
PRODUCTION = "production"
|
||||
ADMIN = "admin"
|
||||
|
||||
|
|
|
|||
|
|
@ -67,28 +67,28 @@ function AppContent() {
|
|||
} />
|
||||
<Route path="/admin/qc" element={
|
||||
<AuthenticatedRoute>
|
||||
<RoleGate allowedRoles={['reviewer', 'production', 'admin']}>
|
||||
<RoleGate allowedRoles={['reviewer', 'linguist', 'production', 'admin']}>
|
||||
<QCList />
|
||||
</RoleGate>
|
||||
</AuthenticatedRoute>
|
||||
} />
|
||||
<Route path="/admin/qc/:id" element={
|
||||
<AuthenticatedRoute>
|
||||
<RoleGate allowedRoles={['reviewer', 'production', 'admin']}>
|
||||
<RoleGate allowedRoles={['reviewer', 'linguist', 'production', 'admin']}>
|
||||
<QCDetail />
|
||||
</RoleGate>
|
||||
</AuthenticatedRoute>
|
||||
} />
|
||||
<Route path="/admin/final" element={
|
||||
<AuthenticatedRoute>
|
||||
<RoleGate allowedRoles={['reviewer', 'production', 'admin']}>
|
||||
<RoleGate allowedRoles={['reviewer', 'linguist', 'production', 'admin']}>
|
||||
<FinalList />
|
||||
</RoleGate>
|
||||
</AuthenticatedRoute>
|
||||
} />
|
||||
<Route path="/admin/final/:id" element={
|
||||
<AuthenticatedRoute>
|
||||
<RoleGate allowedRoles={['reviewer', 'production', 'admin']}>
|
||||
<RoleGate allowedRoles={['reviewer', 'linguist', 'production', 'admin']}>
|
||||
<FinalDetail />
|
||||
</RoleGate>
|
||||
</AuthenticatedRoute>
|
||||
|
|
|
|||
|
|
@ -38,13 +38,19 @@ export function Sidebar({ onMobileClose }: SidebarProps) {
|
|||
label: 'QC Review',
|
||||
href: '/admin/qc',
|
||||
icon: '🔍',
|
||||
roles: ['reviewer', 'production', 'admin'],
|
||||
roles: ['reviewer', 'linguist', 'production', 'admin'],
|
||||
},
|
||||
{
|
||||
label: 'Final Review',
|
||||
href: '/admin/final',
|
||||
icon: '✅',
|
||||
roles: ['reviewer', 'production', 'admin'],
|
||||
roles: ['reviewer', 'linguist', 'production', 'admin'],
|
||||
},
|
||||
{
|
||||
label: 'User Management',
|
||||
href: '/admin/users',
|
||||
icon: '👥',
|
||||
roles: ['admin'],
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ export function Dashboard() {
|
|||
);
|
||||
|
||||
case 'reviewer':
|
||||
case 'linguist':
|
||||
case 'admin':
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useMsal } from '@azure/msal-react';
|
|||
import { useAuthStore } from '../lib/auth';
|
||||
import { loginRequest } from '../lib/msalConfig';
|
||||
import { apiClient } from '../lib/api';
|
||||
import type { UserRole } from '../types/api';
|
||||
|
||||
export function Login() {
|
||||
const [email, setEmail] = useState('');
|
||||
|
|
@ -54,7 +55,7 @@ export function Login() {
|
|||
id: loginResponse.user_id,
|
||||
email: loginResponse.email,
|
||||
full_name: loginResponse.full_name,
|
||||
role: loginResponse.role as 'client' | 'reviewer' | 'admin',
|
||||
role: loginResponse.role as UserRole,
|
||||
auth_provider: loginResponse.auth_provider,
|
||||
is_active: true,
|
||||
created_at: new Date().toISOString(),
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ export function UserDetail() {
|
|||
>
|
||||
<option value="client">Client</option>
|
||||
<option value="reviewer">Reviewer</option>
|
||||
<option value="linguist">Linguist</option>
|
||||
<option value="production">Production</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ export function UserList() {
|
|||
<option value="admin">Admin</option>
|
||||
<option value="production">Production</option>
|
||||
<option value="reviewer">Reviewer</option>
|
||||
<option value="linguist">Linguist</option>
|
||||
<option value="client">Client</option>
|
||||
</select>
|
||||
</div>
|
||||
|
|
@ -204,6 +205,7 @@ export function UserList() {
|
|||
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' :
|
||||
user.role === 'linguist' ? 'bg-teal-100 text-teal-800' :
|
||||
'bg-green-100 text-green-800'
|
||||
}`}>
|
||||
{user.role}
|
||||
|
|
@ -427,6 +429,7 @@ function CreateUserModal({ onClose, onSuccess }: { onClose: () => void; onSucces
|
|||
>
|
||||
<option value="client">Client</option>
|
||||
<option value="reviewer">Reviewer</option>
|
||||
<option value="linguist">Linguist</option>
|
||||
<option value="production">Production</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export type JobStatus =
|
|||
| "pending_final_review"
|
||||
| "completed";
|
||||
|
||||
export type UserRole = "client" | "reviewer" | "production" | "admin";
|
||||
export type UserRole = "client" | "reviewer" | "linguist" | "production" | "admin";
|
||||
export type AuthProvider = "local" | "microsoft";
|
||||
|
||||
export interface User {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue