feat: complete external auth V2 migration with advanced features
This commit implements comprehensive external Azure AD authentication with complete task management, file download, and admin monitoring systems. ## Core Features Implemented (80% Complete) ### 1. Token Auto-Refresh Mechanism ✅ - Backend: POST /api/v2/auth/refresh endpoint - Frontend: Auto-refresh 5 minutes before expiration - Auto-retry on 401 errors with seamless token refresh ### 2. File Download System ✅ - Three format support: JSON / Markdown / PDF - Endpoints: GET /api/v2/tasks/{id}/download/{format} - File access control with ownership validation - Frontend download buttons in TaskHistoryPage ### 3. Complete Task Management ✅ Backend Endpoints: - POST /api/v2/tasks/{id}/start - Start task - POST /api/v2/tasks/{id}/cancel - Cancel task - POST /api/v2/tasks/{id}/retry - Retry failed task - GET /api/v2/tasks - List with filters (status, filename, date range) - GET /api/v2/tasks/stats - User statistics Frontend Features: - Status-based action buttons (Start/Cancel/Retry) - Advanced search and filtering (status, filename, date range) - Pagination and sorting - Task statistics dashboard (5 stat cards) ### 4. Admin Monitoring System ✅ (Backend) Admin APIs: - GET /api/v2/admin/stats - System statistics - GET /api/v2/admin/users - User list with stats - GET /api/v2/admin/users/top - User leaderboard - GET /api/v2/admin/audit-logs - Audit log query system - GET /api/v2/admin/audit-logs/user/{id}/summary Admin Features: - Email-based admin check (ymirliu@panjit.com.tw) - Comprehensive system metrics (users, tasks, sessions, activity) - Audit logging service for security tracking ### 5. User Isolation & Security ✅ - Row-level security on all task queries - File access control with ownership validation - Strict user_id filtering on all operations - Session validation and expiry checking - Admin privilege verification ## New Files Created Backend: - backend/app/models/user_v2.py - User model for external auth - backend/app/models/task.py - Task model with user isolation - backend/app/models/session.py - Session management - backend/app/models/audit_log.py - Audit log model - backend/app/services/external_auth_service.py - External API client - backend/app/services/task_service.py - Task CRUD with isolation - backend/app/services/file_access_service.py - File access control - backend/app/services/admin_service.py - Admin operations - backend/app/services/audit_service.py - Audit logging - backend/app/routers/auth_v2.py - V2 auth endpoints - backend/app/routers/tasks.py - Task management endpoints - backend/app/routers/admin.py - Admin endpoints - backend/alembic/versions/5e75a59fb763_*.py - DB migration Frontend: - frontend/src/services/apiV2.ts - Complete V2 API client - frontend/src/types/apiV2.ts - V2 type definitions - frontend/src/pages/TaskHistoryPage.tsx - Task history UI Modified Files: - backend/app/core/deps.py - Added get_current_admin_user_v2 - backend/app/main.py - Registered admin router - frontend/src/pages/LoginPage.tsx - V2 login integration - frontend/src/components/Layout.tsx - User display and logout - frontend/src/App.tsx - Added /tasks route ## Documentation - openspec/changes/.../PROGRESS_UPDATE.md - Detailed progress report ## Pending Items (20%) 1. Database migration execution for audit_logs table 2. Frontend admin dashboard page 3. Frontend audit log viewer ## Testing Status - Manual testing: ✅ Authentication flow verified - Unit tests: ⏳ Pending - Integration tests: ⏳ Pending ## Security Enhancements - ✅ User isolation (row-level security) - ✅ File access control - ✅ Token expiry validation - ✅ Admin privilege verification - ✅ Audit logging infrastructure - ⏳ Token encryption (noted, low priority) - ⏳ Rate limiting (noted, low priority) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
211
backend/app/services/admin_service.py
Normal file
211
backend/app/services/admin_service.py
Normal file
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
Tool_OCR - Admin Service
|
||||
Administrative functions and statistics
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Dict
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, and_
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.models.user_v2 import User
|
||||
from app.models.task import Task, TaskStatus
|
||||
from app.models.session import Session as UserSession
|
||||
from app.models.audit_log import AuditLog
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AdminService:
|
||||
"""Service for administrative operations"""
|
||||
|
||||
# Admin email addresses
|
||||
ADMIN_EMAILS = ["ymirliu@panjit.com.tw"]
|
||||
|
||||
def is_admin(self, email: str) -> bool:
|
||||
"""
|
||||
Check if user is an administrator
|
||||
|
||||
Args:
|
||||
email: User email address
|
||||
|
||||
Returns:
|
||||
True if user is admin
|
||||
"""
|
||||
return email.lower() in [e.lower() for e in self.ADMIN_EMAILS]
|
||||
|
||||
def get_system_statistics(self, db: Session) -> dict:
|
||||
"""
|
||||
Get overall system statistics
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
Dictionary with system stats
|
||||
"""
|
||||
# User statistics
|
||||
total_users = db.query(User).count()
|
||||
active_users = db.query(User).filter(User.is_active == True).count()
|
||||
|
||||
# Count users with logins in last 30 days
|
||||
date_30_days_ago = datetime.utcnow() - timedelta(days=30)
|
||||
active_users_30d = db.query(User).filter(
|
||||
and_(
|
||||
User.last_login >= date_30_days_ago,
|
||||
User.is_active == True
|
||||
)
|
||||
).count()
|
||||
|
||||
# Task statistics
|
||||
total_tasks = db.query(Task).count()
|
||||
tasks_by_status = {}
|
||||
for status in TaskStatus:
|
||||
count = db.query(Task).filter(Task.status == status).count()
|
||||
tasks_by_status[status.value] = count
|
||||
|
||||
# Session statistics
|
||||
active_sessions = db.query(UserSession).filter(
|
||||
UserSession.expires_at > datetime.utcnow()
|
||||
).count()
|
||||
|
||||
# Recent activity (last 7 days)
|
||||
date_7_days_ago = datetime.utcnow() - timedelta(days=7)
|
||||
recent_tasks = db.query(Task).filter(
|
||||
Task.created_at >= date_7_days_ago
|
||||
).count()
|
||||
|
||||
recent_logins = db.query(AuditLog).filter(
|
||||
and_(
|
||||
AuditLog.event_type == "auth_login",
|
||||
AuditLog.created_at >= date_7_days_ago,
|
||||
AuditLog.success == 1
|
||||
)
|
||||
).count()
|
||||
|
||||
return {
|
||||
"users": {
|
||||
"total": total_users,
|
||||
"active": active_users,
|
||||
"active_30d": active_users_30d
|
||||
},
|
||||
"tasks": {
|
||||
"total": total_tasks,
|
||||
"by_status": tasks_by_status,
|
||||
"recent_7d": recent_tasks
|
||||
},
|
||||
"sessions": {
|
||||
"active": active_sessions
|
||||
},
|
||||
"activity": {
|
||||
"logins_7d": recent_logins,
|
||||
"tasks_7d": recent_tasks
|
||||
}
|
||||
}
|
||||
|
||||
def get_user_list(
|
||||
self,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 50
|
||||
) -> tuple[List[Dict], int]:
|
||||
"""
|
||||
Get list of all users with statistics
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
skip: Pagination offset
|
||||
limit: Pagination limit
|
||||
|
||||
Returns:
|
||||
Tuple of (user list, total count)
|
||||
"""
|
||||
# Get total count
|
||||
total = db.query(User).count()
|
||||
|
||||
# Get users
|
||||
users = db.query(User).order_by(User.created_at.desc()).offset(skip).limit(limit).all()
|
||||
|
||||
# Enhance with statistics
|
||||
user_list = []
|
||||
for user in users:
|
||||
# Count user's tasks
|
||||
task_count = db.query(Task).filter(Task.user_id == user.id).count()
|
||||
|
||||
# Count completed tasks
|
||||
completed_tasks = db.query(Task).filter(
|
||||
and_(
|
||||
Task.user_id == user.id,
|
||||
Task.status == TaskStatus.COMPLETED
|
||||
)
|
||||
).count()
|
||||
|
||||
# Count active sessions
|
||||
active_sessions = db.query(UserSession).filter(
|
||||
and_(
|
||||
UserSession.user_id == user.id,
|
||||
UserSession.expires_at > datetime.utcnow()
|
||||
)
|
||||
).count()
|
||||
|
||||
user_list.append({
|
||||
**user.to_dict(),
|
||||
"total_tasks": task_count,
|
||||
"completed_tasks": completed_tasks,
|
||||
"active_sessions": active_sessions,
|
||||
"is_admin": self.is_admin(user.email)
|
||||
})
|
||||
|
||||
return user_list, total
|
||||
|
||||
def get_top_users(
|
||||
self,
|
||||
db: Session,
|
||||
metric: str = "tasks",
|
||||
limit: int = 10
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
Get top users by metric
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
metric: Metric to rank by (tasks, completed_tasks)
|
||||
limit: Number of users to return
|
||||
|
||||
Returns:
|
||||
List of top users with counts
|
||||
"""
|
||||
if metric == "completed_tasks":
|
||||
# Top users by completed tasks
|
||||
results = db.query(
|
||||
User,
|
||||
func.count(Task.id).label("task_count")
|
||||
).join(Task).filter(
|
||||
Task.status == TaskStatus.COMPLETED
|
||||
).group_by(User.id).order_by(
|
||||
func.count(Task.id).desc()
|
||||
).limit(limit).all()
|
||||
else:
|
||||
# Top users by total tasks (default)
|
||||
results = db.query(
|
||||
User,
|
||||
func.count(Task.id).label("task_count")
|
||||
).join(Task).group_by(User.id).order_by(
|
||||
func.count(Task.id).desc()
|
||||
).limit(limit).all()
|
||||
|
||||
return [
|
||||
{
|
||||
"user_id": user.id,
|
||||
"email": user.email,
|
||||
"display_name": user.display_name,
|
||||
"count": count
|
||||
}
|
||||
for user, count in results
|
||||
]
|
||||
|
||||
|
||||
# Singleton instance
|
||||
admin_service = AdminService()
|
||||
Reference in New Issue
Block a user