feat: Add admin dashboard with authentication - Admin login/logout with Bearer token authentication - Secure admin dashboard page in frontend - Real-time system monitoring (memory, disk, translations) - Rate limits and cleanup service monitoring - Protected admin endpoints - Updated README with full SaaS documentation
This commit is contained in:
parent
500502440c
commit
54d85f0b34
16
.env.example
16
.env.example
@ -59,6 +59,20 @@ MAX_REQUEST_SIZE_MB=100
|
||||
# Request timeout in seconds
|
||||
REQUEST_TIMEOUT_SECONDS=300
|
||||
|
||||
# ============== Admin Authentication ==============
|
||||
# Admin username
|
||||
ADMIN_USERNAME=admin
|
||||
|
||||
# Admin password (change in production!)
|
||||
ADMIN_PASSWORD=changeme123
|
||||
|
||||
# Or use SHA256 hash of password (more secure)
|
||||
# Generate with: python -c "import hashlib; print(hashlib.sha256(b'your_password').hexdigest())"
|
||||
# ADMIN_PASSWORD_HASH=
|
||||
|
||||
# Token secret for session management (auto-generated if not set)
|
||||
# ADMIN_TOKEN_SECRET=
|
||||
|
||||
# ============== Monitoring ==============
|
||||
# Log level: DEBUG, INFO, WARNING, ERROR
|
||||
LOG_LEVEL=INFO
|
||||
@ -67,4 +81,4 @@ LOG_LEVEL=INFO
|
||||
ENABLE_REQUEST_LOGGING=true
|
||||
|
||||
# Memory usage threshold (percentage)
|
||||
MAX_MEMORY_PERCENT=80
|
||||
MAX_MEMORY_PERCENT=80
|
||||
271
README.md
271
README.md
@ -1,6 +1,6 @@
|
||||
# 📄 Document Translation API
|
||||
|
||||
A powerful Python API for translating complex structured documents (Excel, Word, PowerPoint) while **strictly preserving** the original formatting, layout, and embedded media.
|
||||
A powerful SaaS-ready Python API for translating complex structured documents (Excel, Word, PowerPoint) while **strictly preserving** the original formatting, layout, and embedded media.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
@ -12,6 +12,7 @@ A powerful Python API for translating complex structured documents (Excel, Word,
|
||||
| **WebLLM** | Browser | Runs entirely in browser using WebGPU |
|
||||
| **DeepL** | Cloud | High-quality translations (API key required) |
|
||||
| **LibreTranslate** | Self-hosted | Open-source alternative |
|
||||
| **OpenAI** | Cloud | GPT-4o/4o-mini with vision support |
|
||||
|
||||
### 📊 Excel Translation (.xlsx)
|
||||
- ✅ Translates all cell content and sheet names
|
||||
@ -32,11 +33,19 @@ A powerful Python API for translating complex structured documents (Excel, Word,
|
||||
- ✅ Image text extraction with text boxes added below images
|
||||
- ✅ Keeps layering order and positions
|
||||
|
||||
### 🧠 LLM Features (Ollama/WebLLM)
|
||||
### 🧠 LLM Features (Ollama/WebLLM/OpenAI)
|
||||
- ✅ **Custom System Prompts**: Provide context for better translations
|
||||
- ✅ **Technical Glossary**: Define term mappings (e.g., `batterie=coil`)
|
||||
- ✅ **Presets**: HVAC, IT, Legal, Medical terminology
|
||||
- ✅ **Vision Models**: Translate text within images (gemma3, qwen3-vl, llava)
|
||||
- ✅ **Vision Models**: Translate text within images (gemma3, qwen3-vl, gpt-4o)
|
||||
|
||||
### 🏢 SaaS-Ready Features
|
||||
- 🚦 **Rate Limiting**: Per-client IP with token bucket and sliding window algorithms
|
||||
- 🔒 **Security Headers**: CSP, XSS protection, HSTS support
|
||||
- 🧹 **Auto Cleanup**: Automatic file cleanup with TTL tracking
|
||||
- 📊 **Monitoring**: Health checks, metrics, and system status
|
||||
- 🔐 **Admin Dashboard**: Secure admin panel with authentication
|
||||
- 📝 **Request Logging**: Structured logging with unique request IDs
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
@ -60,9 +69,15 @@ python main.py
|
||||
|
||||
The API starts on `http://localhost:8000`
|
||||
|
||||
### Web Interface
|
||||
### Frontend Setup
|
||||
|
||||
Open `http://localhost:8000/static/index.html` for the full-featured web interface.
|
||||
```powershell
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Frontend runs on `http://localhost:3000`
|
||||
|
||||
## 📚 API Documentation
|
||||
|
||||
@ -71,7 +86,9 @@ Open `http://localhost:8000/static/index.html` for the full-featured web interfa
|
||||
|
||||
## 🔧 API Endpoints
|
||||
|
||||
### POST /translate
|
||||
### Translation
|
||||
|
||||
#### POST /translate
|
||||
Translate a document with full customization.
|
||||
|
||||
```bash
|
||||
@ -81,28 +98,63 @@ curl -X POST "http://localhost:8000/translate" \
|
||||
-F "provider=ollama" \
|
||||
-F "ollama_model=gemma3:12b" \
|
||||
-F "translate_images=true" \
|
||||
-F "system_prompt=You are translating HVAC documents. Use: batterie=coil, CTA=AHU"
|
||||
-F "system_prompt=You are translating HVAC documents."
|
||||
```
|
||||
|
||||
### Parameters
|
||||
### Monitoring
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `file` | File | required | Document to translate (.xlsx, .docx, .pptx) |
|
||||
| `target_language` | string | required | Target language code (en, fr, es, fa, etc.) |
|
||||
| `provider` | string | google | Translation provider (google, ollama, webllm, deepl, libre) |
|
||||
| `ollama_model` | string | llama3.2 | Ollama model name |
|
||||
| `translate_images` | bool | false | Extract and translate image text with vision |
|
||||
| `system_prompt` | string | "" | Custom instructions and glossary for LLM |
|
||||
#### GET /health
|
||||
Comprehensive health check with system status.
|
||||
|
||||
### GET /ollama/models
|
||||
List available Ollama models.
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"translation_service": "google",
|
||||
"memory": {"system_percent": 34.1, "system_available_gb": 61.7},
|
||||
"disk": {"total_files": 0, "total_size_mb": 0},
|
||||
"cleanup_service": {"is_running": true}
|
||||
}
|
||||
```
|
||||
|
||||
### POST /ollama/configure
|
||||
Configure Ollama settings.
|
||||
#### GET /metrics
|
||||
System metrics and statistics.
|
||||
|
||||
### GET /health
|
||||
Health check endpoint.
|
||||
#### GET /rate-limit/status
|
||||
Current rate limit status for the requesting client.
|
||||
|
||||
### Admin Endpoints (Authentication Required)
|
||||
|
||||
#### POST /admin/login
|
||||
Login to admin dashboard.
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/admin/login" \
|
||||
-F "username=admin" \
|
||||
-F "password=your_password"
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"token": "your_bearer_token",
|
||||
"expires_in": 86400
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /admin/dashboard
|
||||
Get comprehensive dashboard data (requires Bearer token).
|
||||
|
||||
```bash
|
||||
curl "http://localhost:8000/admin/dashboard" \
|
||||
-H "Authorization: Bearer your_token"
|
||||
```
|
||||
|
||||
#### POST /admin/cleanup/trigger
|
||||
Manually trigger file cleanup.
|
||||
|
||||
#### GET /admin/files/tracked
|
||||
List currently tracked files.
|
||||
|
||||
## 🌐 Supported Languages
|
||||
|
||||
@ -120,22 +172,47 @@ Health check endpoint.
|
||||
### Environment Variables (.env)
|
||||
|
||||
```env
|
||||
# Translation Service
|
||||
# ============== Translation Services ==============
|
||||
TRANSLATION_SERVICE=google
|
||||
DEEPL_API_KEY=your_deepl_api_key_here
|
||||
|
||||
# Ollama Configuration
|
||||
OLLAMA_BASE_URL=http://localhost:11434
|
||||
OLLAMA_MODEL=llama3.2
|
||||
OLLAMA_MODEL=llama3
|
||||
OLLAMA_VISION_MODEL=llava
|
||||
|
||||
# DeepL API Key (optional)
|
||||
DEEPL_API_KEY=your_api_key_here
|
||||
|
||||
# File Limits
|
||||
# ============== File Limits ==============
|
||||
MAX_FILE_SIZE_MB=50
|
||||
|
||||
# Directories
|
||||
UPLOAD_DIR=./uploads
|
||||
OUTPUT_DIR=./outputs
|
||||
# ============== Rate Limiting (SaaS) ==============
|
||||
RATE_LIMIT_ENABLED=true
|
||||
RATE_LIMIT_PER_MINUTE=30
|
||||
RATE_LIMIT_PER_HOUR=200
|
||||
TRANSLATIONS_PER_MINUTE=10
|
||||
TRANSLATIONS_PER_HOUR=50
|
||||
MAX_CONCURRENT_TRANSLATIONS=5
|
||||
|
||||
# ============== Cleanup Service ==============
|
||||
CLEANUP_ENABLED=true
|
||||
CLEANUP_INTERVAL_MINUTES=15
|
||||
FILE_TTL_MINUTES=60
|
||||
INPUT_FILE_TTL_MINUTES=30
|
||||
OUTPUT_FILE_TTL_MINUTES=120
|
||||
|
||||
# ============== Security ==============
|
||||
ENABLE_HSTS=false
|
||||
CORS_ORIGINS=*
|
||||
|
||||
# ============== Admin Authentication ==============
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=changeme123 # Change in production!
|
||||
# Or use a SHA256 hash:
|
||||
# ADMIN_PASSWORD_HASH=your_sha256_hash
|
||||
|
||||
# ============== Monitoring ==============
|
||||
LOG_LEVEL=INFO
|
||||
ENABLE_REQUEST_LOGGING=true
|
||||
MAX_MEMORY_PERCENT=80
|
||||
```
|
||||
|
||||
### Ollama Setup
|
||||
@ -179,6 +256,73 @@ vanne 3 voies=3-way valve
|
||||
- ⚖️ **Legal**: Legal documents
|
||||
- 🏥 **Medical**: Healthcare terminology
|
||||
|
||||
## <20> Admin Dashboard
|
||||
|
||||
Access the admin dashboard at `/admin` in the frontend. Features:
|
||||
|
||||
- **System Status**: Health, uptime, and issues
|
||||
- **Memory & Disk Monitoring**: Real-time usage stats
|
||||
- **Translation Statistics**: Total translations, success rate
|
||||
- **Rate Limit Management**: View active clients and limits
|
||||
- **Cleanup Service**: Monitor and trigger manual cleanup
|
||||
|
||||
### Default Credentials
|
||||
- **Username**: admin
|
||||
- **Password**: changeme123
|
||||
|
||||
⚠️ **Change the default password in production!**
|
||||
|
||||
## 🏗️ Project Structure
|
||||
|
||||
```
|
||||
Translate/
|
||||
├── main.py # FastAPI application with SaaS features
|
||||
├── config.py # Configuration with SaaS settings
|
||||
├── requirements.txt # Dependencies
|
||||
├── mcp_server.py # MCP server implementation
|
||||
├── middleware/ # SaaS middleware
|
||||
│ ├── __init__.py
|
||||
│ ├── rate_limiting.py # Rate limiting with token bucket
|
||||
│ ├── validation.py # Input validation
|
||||
│ ├── security.py # Security headers & logging
|
||||
│ └── cleanup.py # Auto cleanup service
|
||||
├── services/
|
||||
│ └── translation_service.py # Translation providers
|
||||
├── translators/
|
||||
│ ├── excel_translator.py # Excel with image support
|
||||
│ ├── word_translator.py # Word with image support
|
||||
│ └── pptx_translator.py # PowerPoint with image support
|
||||
├── frontend/ # Next.js frontend
|
||||
│ ├── src/
|
||||
│ │ ├── app/
|
||||
│ │ │ ├── page.tsx # Main translation page
|
||||
│ │ │ ├── admin/ # Admin dashboard
|
||||
│ │ │ └── settings/ # Settings pages
|
||||
│ │ └── components/
|
||||
│ └── package.json
|
||||
├── static/
|
||||
│ └── webllm.html # WebLLM standalone interface
|
||||
├── uploads/ # Temporary uploads (auto-cleaned)
|
||||
└── outputs/ # Translated files (auto-cleaned)
|
||||
```
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
### Backend
|
||||
- **FastAPI**: Modern async web framework
|
||||
- **openpyxl**: Excel manipulation
|
||||
- **python-docx**: Word documents
|
||||
- **python-pptx**: PowerPoint presentations
|
||||
- **deep-translator**: Google/DeepL/Libre translation
|
||||
- **psutil**: System monitoring
|
||||
- **python-magic**: File type validation
|
||||
|
||||
### Frontend
|
||||
- **Next.js 15**: React framework
|
||||
- **Tailwind CSS**: Styling
|
||||
- **Lucide Icons**: Icon library
|
||||
- **WebLLM**: Browser-based LLM
|
||||
|
||||
## 🔌 MCP Integration
|
||||
|
||||
This API can be used as an MCP (Model Context Protocol) server for AI assistants.
|
||||
@ -203,56 +347,27 @@ Add to your VS Code `settings.json` or `.vscode/mcp.json`:
|
||||
}
|
||||
```
|
||||
|
||||
### MCP Tools Available
|
||||
## 🚀 Production Deployment
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `translate_document` | Translate a document file |
|
||||
| `list_ollama_models` | Get available Ollama models |
|
||||
| `get_supported_languages` | List supported language codes |
|
||||
| `configure_translation` | Set translation provider and options |
|
||||
### Security Checklist
|
||||
- [ ] Change `ADMIN_PASSWORD` or set `ADMIN_PASSWORD_HASH`
|
||||
- [ ] Set `CORS_ORIGINS` to your frontend domain
|
||||
- [ ] Enable `ENABLE_HSTS=true` if using HTTPS
|
||||
- [ ] Configure rate limits appropriately
|
||||
- [ ] Set up log rotation for `logs/` directory
|
||||
- [ ] Use a reverse proxy (nginx/traefik) for HTTPS
|
||||
|
||||
## 🏗️ Project Structure
|
||||
### Docker Deployment (Coming Soon)
|
||||
|
||||
```dockerfile
|
||||
FROM python:3.11-slim
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install -r requirements.txt
|
||||
COPY . .
|
||||
EXPOSE 8000
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
```
|
||||
Translate/
|
||||
├── main.py # FastAPI application
|
||||
├── config.py # Configuration
|
||||
├── requirements.txt # Dependencies
|
||||
├── mcp_server.py # MCP server implementation
|
||||
├── services/
|
||||
│ └── translation_service.py # Translation providers
|
||||
├── translators/
|
||||
│ ├── excel_translator.py # Excel with image support
|
||||
│ ├── word_translator.py # Word with image support
|
||||
│ └── pptx_translator.py # PowerPoint with image support
|
||||
├── utils/
|
||||
│ ├── file_handler.py # File operations
|
||||
│ └── exceptions.py # Custom exceptions
|
||||
├── static/
|
||||
│ └── index.html # Web interface
|
||||
├── uploads/ # Temporary uploads
|
||||
└── outputs/ # Translated files
|
||||
```
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
1. Start the API: `python main.py`
|
||||
2. Open: http://localhost:8000/static/index.html
|
||||
3. Configure Ollama model
|
||||
4. Upload a document
|
||||
5. Select target language and provider
|
||||
6. Click "Translate Document"
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
- **FastAPI**: Modern async web framework
|
||||
- **openpyxl**: Excel manipulation
|
||||
- **python-docx**: Word documents
|
||||
- **python-pptx**: PowerPoint presentations
|
||||
- **deep-translator**: Google/DeepL/Libre translation
|
||||
- **requests**: Ollama API communication
|
||||
- **Uvicorn**: ASGI server
|
||||
|
||||
## 📝 License
|
||||
|
||||
@ -264,4 +379,4 @@ Contributions welcome! Please submit a Pull Request.
|
||||
|
||||
---
|
||||
|
||||
**Built with ❤️ using Python, FastAPI, and Ollama**
|
||||
**Built with ❤️ using Python, FastAPI, Next.js, and Ollama**
|
||||
|
||||
454
frontend/src/app/admin/page.tsx
Normal file
454
frontend/src/app/admin/page.tsx
Normal file
@ -0,0 +1,454 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Shield, LogOut, RefreshCw, Trash2, Activity, HardDrive, Cpu, Clock, Users, FileText, AlertTriangle, CheckCircle } from "lucide-react";
|
||||
|
||||
interface DashboardData {
|
||||
timestamp: string;
|
||||
uptime: string;
|
||||
status: string;
|
||||
issues: string[];
|
||||
system: {
|
||||
memory: {
|
||||
process_rss_mb: number;
|
||||
system_total_gb: number;
|
||||
system_available_gb: number;
|
||||
system_percent: number;
|
||||
};
|
||||
disk: {
|
||||
total_files: number;
|
||||
total_size_mb: number;
|
||||
usage_percent: number;
|
||||
};
|
||||
};
|
||||
translations: {
|
||||
total: number;
|
||||
errors: number;
|
||||
success_rate: number;
|
||||
};
|
||||
cleanup: {
|
||||
files_cleaned_total: number;
|
||||
bytes_freed_total_mb: number;
|
||||
cleanup_runs: number;
|
||||
tracked_files_count: number;
|
||||
is_running: boolean;
|
||||
};
|
||||
rate_limits: {
|
||||
total_requests: number;
|
||||
total_translations: number;
|
||||
active_clients: number;
|
||||
config: {
|
||||
requests_per_minute: number;
|
||||
translations_per_minute: number;
|
||||
};
|
||||
};
|
||||
config: {
|
||||
max_file_size_mb: number;
|
||||
supported_extensions: string[];
|
||||
translation_service: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function AdminPage() {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [loginError, setLoginError] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [dashboard, setDashboard] = useState<DashboardData | null>(null);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
|
||||
|
||||
// Check if already authenticated
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem("admin_token");
|
||||
if (token) {
|
||||
verifyToken(token);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const verifyToken = async (token: string) => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/admin/verify`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
if (response.ok) {
|
||||
setIsAuthenticated(true);
|
||||
fetchDashboard(token);
|
||||
} else {
|
||||
localStorage.removeItem("admin_token");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Token verification failed:", error);
|
||||
localStorage.removeItem("admin_token");
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoginError("");
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("username", username);
|
||||
formData.append("password", password);
|
||||
|
||||
const response = await fetch(`${API_URL}/admin/login`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
localStorage.setItem("admin_token", data.token);
|
||||
setIsAuthenticated(true);
|
||||
fetchDashboard(data.token);
|
||||
} else {
|
||||
setLoginError(data.detail || "Login failed");
|
||||
}
|
||||
} catch (error) {
|
||||
setLoginError("Connection error. Is the backend running?");
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
const token = localStorage.getItem("admin_token");
|
||||
if (token) {
|
||||
try {
|
||||
await fetch(`${API_URL}/admin/logout`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Logout error:", error);
|
||||
}
|
||||
}
|
||||
localStorage.removeItem("admin_token");
|
||||
setIsAuthenticated(false);
|
||||
setDashboard(null);
|
||||
};
|
||||
|
||||
const fetchDashboard = async (token?: string) => {
|
||||
const authToken = token || localStorage.getItem("admin_token");
|
||||
if (!authToken) return;
|
||||
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/admin/dashboard`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setDashboard(data);
|
||||
} else if (response.status === 401) {
|
||||
handleLogout();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch dashboard:", error);
|
||||
}
|
||||
setIsRefreshing(false);
|
||||
};
|
||||
|
||||
const triggerCleanup = async () => {
|
||||
const token = localStorage.getItem("admin_token");
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/admin/cleanup/trigger`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
alert(`Cleanup completed: ${data.files_cleaned} files removed`);
|
||||
fetchDashboard();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Cleanup failed:", error);
|
||||
alert("Cleanup failed");
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-refresh every 30 seconds
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
const interval = setInterval(() => fetchDashboard(), 30000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<div className="bg-zinc-800/50 backdrop-blur rounded-2xl p-8 w-full max-w-md border border-zinc-700/50">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="p-3 bg-blue-500/20 rounded-xl">
|
||||
<Shield className="w-8 h-8 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Admin Access</h1>
|
||||
<p className="text-zinc-400 text-sm">Login to access the dashboard</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-300 mb-2">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-zinc-900/50 border border-zinc-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="admin"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-300 mb-2">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-zinc-900/50 border border-zinc-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{loginError && (
|
||||
<div className="p-3 bg-red-500/20 border border-red-500/50 rounded-xl text-red-400 text-sm">
|
||||
{loginError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-xl transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? "Logging in..." : "Login"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-3 bg-blue-500/20 rounded-xl">
|
||||
<Shield className="w-8 h-8 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Admin Dashboard</h1>
|
||||
<p className="text-zinc-400">System monitoring and management</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => fetchDashboard()}
|
||||
disabled={isRefreshing}
|
||||
className="p-3 bg-zinc-800 hover:bg-zinc-700 rounded-xl transition-colors disabled:opacity-50"
|
||||
title="Refresh"
|
||||
>
|
||||
<RefreshCw className={`w-5 h-5 ${isRefreshing ? "animate-spin" : ""}`} />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center gap-2 px-4 py-3 bg-red-600/20 hover:bg-red-600/30 text-red-400 rounded-xl transition-colors"
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{dashboard && (
|
||||
<>
|
||||
{/* Status Banner */}
|
||||
<div className={`p-4 rounded-xl flex items-center gap-3 ${
|
||||
dashboard.status === "healthy"
|
||||
? "bg-green-500/20 border border-green-500/30"
|
||||
: "bg-yellow-500/20 border border-yellow-500/30"
|
||||
}`}>
|
||||
{dashboard.status === "healthy" ? (
|
||||
<CheckCircle className="w-6 h-6 text-green-400" />
|
||||
) : (
|
||||
<AlertTriangle className="w-6 h-6 text-yellow-400" />
|
||||
)}
|
||||
<div>
|
||||
<span className={`font-medium ${dashboard.status === "healthy" ? "text-green-400" : "text-yellow-400"}`}>
|
||||
System {dashboard.status.charAt(0).toUpperCase() + dashboard.status.slice(1)}
|
||||
</span>
|
||||
{dashboard.issues.length > 0 && (
|
||||
<p className="text-sm text-zinc-400">{dashboard.issues.join(", ")}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2 text-sm text-zinc-400">
|
||||
<Clock className="w-4 h-4" />
|
||||
Uptime: {dashboard.uptime}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Total Requests */}
|
||||
<div className="bg-zinc-800/50 backdrop-blur rounded-xl p-5 border border-zinc-700/50">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="p-2 bg-blue-500/20 rounded-lg">
|
||||
<Activity className="w-5 h-5 text-blue-400" />
|
||||
</div>
|
||||
<span className="text-zinc-400 text-sm">Total Requests</span>
|
||||
</div>
|
||||
<div className="text-3xl font-bold">{dashboard.rate_limits.total_requests.toLocaleString()}</div>
|
||||
<div className="text-sm text-zinc-500 mt-1">
|
||||
{dashboard.rate_limits.active_clients} active clients
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Translations */}
|
||||
<div className="bg-zinc-800/50 backdrop-blur rounded-xl p-5 border border-zinc-700/50">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="p-2 bg-green-500/20 rounded-lg">
|
||||
<FileText className="w-5 h-5 text-green-400" />
|
||||
</div>
|
||||
<span className="text-zinc-400 text-sm">Translations</span>
|
||||
</div>
|
||||
<div className="text-3xl font-bold">{dashboard.translations.total.toLocaleString()}</div>
|
||||
<div className="text-sm text-zinc-500 mt-1">
|
||||
{dashboard.translations.success_rate}% success rate
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Memory Usage */}
|
||||
<div className="bg-zinc-800/50 backdrop-blur rounded-xl p-5 border border-zinc-700/50">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="p-2 bg-purple-500/20 rounded-lg">
|
||||
<Cpu className="w-5 h-5 text-purple-400" />
|
||||
</div>
|
||||
<span className="text-zinc-400 text-sm">Memory Usage</span>
|
||||
</div>
|
||||
<div className="text-3xl font-bold">{dashboard.system.memory.system_percent}%</div>
|
||||
<div className="text-sm text-zinc-500 mt-1">
|
||||
{dashboard.system.memory.system_available_gb.toFixed(1)} GB available
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Disk Usage */}
|
||||
<div className="bg-zinc-800/50 backdrop-blur rounded-xl p-5 border border-zinc-700/50">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="p-2 bg-orange-500/20 rounded-lg">
|
||||
<HardDrive className="w-5 h-5 text-orange-400" />
|
||||
</div>
|
||||
<span className="text-zinc-400 text-sm">Tracked Files</span>
|
||||
</div>
|
||||
<div className="text-3xl font-bold">{dashboard.cleanup.tracked_files_count}</div>
|
||||
<div className="text-sm text-zinc-500 mt-1">
|
||||
{dashboard.system.disk.total_size_mb.toFixed(1)} MB total
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detailed Panels */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Rate Limits */}
|
||||
<div className="bg-zinc-800/50 backdrop-blur rounded-xl p-6 border border-zinc-700/50">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<Users className="w-5 h-5 text-blue-400" />
|
||||
Rate Limits Configuration
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center py-2 border-b border-zinc-700/50">
|
||||
<span className="text-zinc-400">Requests per minute</span>
|
||||
<span className="font-medium">{dashboard.rate_limits.config.requests_per_minute}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b border-zinc-700/50">
|
||||
<span className="text-zinc-400">Translations per minute</span>
|
||||
<span className="font-medium">{dashboard.rate_limits.config.translations_per_minute}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b border-zinc-700/50">
|
||||
<span className="text-zinc-400">Max file size</span>
|
||||
<span className="font-medium">{dashboard.config.max_file_size_mb} MB</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2">
|
||||
<span className="text-zinc-400">Translation service</span>
|
||||
<span className="font-medium capitalize">{dashboard.config.translation_service}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cleanup Service */}
|
||||
<div className="bg-zinc-800/50 backdrop-blur rounded-xl p-6 border border-zinc-700/50">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold flex items-center gap-2">
|
||||
<Trash2 className="w-5 h-5 text-orange-400" />
|
||||
Cleanup Service
|
||||
</h3>
|
||||
<button
|
||||
onClick={triggerCleanup}
|
||||
className="px-3 py-1.5 bg-orange-600/20 hover:bg-orange-600/30 text-orange-400 text-sm rounded-lg transition-colors"
|
||||
>
|
||||
Trigger Cleanup
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center py-2 border-b border-zinc-700/50">
|
||||
<span className="text-zinc-400">Service status</span>
|
||||
<span className={`font-medium ${dashboard.cleanup.is_running ? "text-green-400" : "text-red-400"}`}>
|
||||
{dashboard.cleanup.is_running ? "Running" : "Stopped"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b border-zinc-700/50">
|
||||
<span className="text-zinc-400">Files cleaned</span>
|
||||
<span className="font-medium">{dashboard.cleanup.files_cleaned_total}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b border-zinc-700/50">
|
||||
<span className="text-zinc-400">Space freed</span>
|
||||
<span className="font-medium">{dashboard.cleanup.bytes_freed_total_mb.toFixed(2)} MB</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2">
|
||||
<span className="text-zinc-400">Cleanup runs</span>
|
||||
<span className="font-medium">{dashboard.cleanup.cleanup_runs}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Info */}
|
||||
<div className="text-center text-sm text-zinc-500 pt-4">
|
||||
Last updated: {new Date(dashboard.timestamp).toLocaleString()} • Auto-refresh every 30 seconds
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -8,6 +8,7 @@ import {
|
||||
Cloud,
|
||||
BookText,
|
||||
Upload,
|
||||
Shield,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
@ -43,6 +44,15 @@ const navigation = [
|
||||
},
|
||||
];
|
||||
|
||||
const adminNavigation = [
|
||||
{
|
||||
name: "Admin Dashboard",
|
||||
href: "/admin",
|
||||
icon: Shield,
|
||||
description: "System monitoring (login required)",
|
||||
},
|
||||
];
|
||||
|
||||
export function Sidebar() {
|
||||
const pathname = usePathname();
|
||||
|
||||
@ -85,6 +95,37 @@ export function Sidebar() {
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Admin Section */}
|
||||
<div className="mt-4 pt-4 border-t border-zinc-800">
|
||||
<p className="px-3 mb-2 text-xs font-medium text-zinc-600 uppercase tracking-wider">Admin</p>
|
||||
{adminNavigation.map((item) => {
|
||||
const isActive = pathname === item.href;
|
||||
const Icon = item.icon;
|
||||
|
||||
return (
|
||||
<Tooltip key={item.name}>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
|
||||
isActive
|
||||
? "bg-blue-500/10 text-blue-400"
|
||||
: "text-zinc-500 hover:bg-zinc-800 hover:text-zinc-300"
|
||||
)}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
<span>{item.name}</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>{item.description}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* User section at bottom */}
|
||||
|
||||
149
main.py
149
main.py
@ -3,16 +3,20 @@ Document Translation API
|
||||
FastAPI application for translating complex documents while preserving formatting
|
||||
SaaS-ready with rate limiting, validation, and robust error handling
|
||||
"""
|
||||
from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Request, Depends
|
||||
from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Request, Depends, Header
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
import hashlib
|
||||
import time
|
||||
|
||||
from config import config
|
||||
from translators import excel_translator, word_translator, pptx_translator
|
||||
@ -31,6 +35,57 @@ logging.basicConfig(
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ============== Admin Authentication ==============
|
||||
ADMIN_USERNAME = os.getenv("ADMIN_USERNAME", "admin")
|
||||
ADMIN_PASSWORD_HASH = os.getenv("ADMIN_PASSWORD_HASH", "") # SHA256 hash of password
|
||||
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "changeme123") # Default password (change in production!)
|
||||
ADMIN_TOKEN_SECRET = os.getenv("ADMIN_TOKEN_SECRET", secrets.token_hex(32))
|
||||
|
||||
# Store active admin sessions (token -> expiry timestamp)
|
||||
admin_sessions: dict = {}
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
"""Hash password with SHA256"""
|
||||
return hashlib.sha256(password.encode()).hexdigest()
|
||||
|
||||
def verify_admin_password(password: str) -> bool:
|
||||
"""Verify admin password"""
|
||||
if ADMIN_PASSWORD_HASH:
|
||||
return hash_password(password) == ADMIN_PASSWORD_HASH
|
||||
return password == ADMIN_PASSWORD
|
||||
|
||||
def create_admin_token() -> str:
|
||||
"""Create a new admin session token"""
|
||||
token = secrets.token_urlsafe(32)
|
||||
# Token expires in 24 hours
|
||||
admin_sessions[token] = time.time() + (24 * 60 * 60)
|
||||
return token
|
||||
|
||||
def verify_admin_token(token: str) -> bool:
|
||||
"""Verify admin token is valid and not expired"""
|
||||
if token not in admin_sessions:
|
||||
return False
|
||||
if time.time() > admin_sessions[token]:
|
||||
del admin_sessions[token]
|
||||
return False
|
||||
return True
|
||||
|
||||
async def require_admin(authorization: Optional[str] = Header(None)) -> bool:
|
||||
"""Dependency to require admin authentication"""
|
||||
if not authorization:
|
||||
raise HTTPException(status_code=401, detail="Authorization header required")
|
||||
|
||||
# Expect "Bearer <token>"
|
||||
parts = authorization.split(" ")
|
||||
if len(parts) != 2 or parts[0].lower() != "bearer":
|
||||
raise HTTPException(status_code=401, detail="Invalid authorization format. Use: Bearer <token>")
|
||||
|
||||
token = parts[1]
|
||||
if not verify_admin_token(token):
|
||||
raise HTTPException(status_code=401, detail="Invalid or expired token")
|
||||
|
||||
return True
|
||||
|
||||
# Initialize SaaS components
|
||||
rate_limit_config = RateLimitConfig(
|
||||
requests_per_minute=int(os.getenv("RATE_LIMIT_PER_MINUTE", "30")),
|
||||
@ -773,6 +828,87 @@ async def reconstruct_document(
|
||||
|
||||
# ============== SaaS Management Endpoints ==============
|
||||
|
||||
@app.post("/admin/login")
|
||||
async def admin_login(
|
||||
username: str = Form(...),
|
||||
password: str = Form(...)
|
||||
):
|
||||
"""
|
||||
Admin login endpoint
|
||||
Returns a bearer token for authenticated admin access
|
||||
"""
|
||||
if username != ADMIN_USERNAME:
|
||||
logger.warning(f"Failed admin login attempt with username: {username}")
|
||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||
|
||||
if not verify_admin_password(password):
|
||||
logger.warning(f"Failed admin login attempt - wrong password")
|
||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||
|
||||
token = create_admin_token()
|
||||
logger.info(f"Admin login successful")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"token": token,
|
||||
"expires_in": 86400, # 24 hours in seconds
|
||||
"message": "Login successful"
|
||||
}
|
||||
|
||||
|
||||
@app.post("/admin/logout")
|
||||
async def admin_logout(authorization: Optional[str] = Header(None)):
|
||||
"""Logout and invalidate admin token"""
|
||||
if authorization:
|
||||
parts = authorization.split(" ")
|
||||
if len(parts) == 2 and parts[0].lower() == "bearer":
|
||||
token = parts[1]
|
||||
if token in admin_sessions:
|
||||
del admin_sessions[token]
|
||||
logger.info("Admin logout successful")
|
||||
|
||||
return {"status": "success", "message": "Logged out"}
|
||||
|
||||
|
||||
@app.get("/admin/verify")
|
||||
async def verify_admin_session(is_admin: bool = Depends(require_admin)):
|
||||
"""Verify admin token is still valid"""
|
||||
return {"status": "valid", "authenticated": True}
|
||||
|
||||
|
||||
@app.get("/admin/dashboard")
|
||||
async def get_admin_dashboard(is_admin: bool = Depends(require_admin)):
|
||||
"""Get comprehensive admin dashboard data"""
|
||||
health_status = await health_checker.check_health()
|
||||
cleanup_stats = cleanup_manager.get_stats()
|
||||
rate_limit_stats = rate_limit_manager.get_stats()
|
||||
tracked_files = cleanup_manager.get_tracked_files()
|
||||
|
||||
return {
|
||||
"timestamp": health_status.get("timestamp"),
|
||||
"uptime": health_status.get("uptime_human"),
|
||||
"status": health_status.get("status"),
|
||||
"issues": health_status.get("issues", []),
|
||||
"system": {
|
||||
"memory": health_status.get("memory", {}),
|
||||
"disk": health_status.get("disk", {}),
|
||||
},
|
||||
"translations": health_status.get("translations", {}),
|
||||
"cleanup": {
|
||||
**cleanup_stats,
|
||||
"tracked_files_count": len(tracked_files)
|
||||
},
|
||||
"rate_limits": rate_limit_stats,
|
||||
"config": {
|
||||
"max_file_size_mb": config.MAX_FILE_SIZE_MB,
|
||||
"supported_extensions": list(config.SUPPORTED_EXTENSIONS),
|
||||
"translation_service": config.TRANSLATION_SERVICE,
|
||||
"rate_limit_per_minute": rate_limit_config.requests_per_minute,
|
||||
"translations_per_minute": rate_limit_config.translations_per_minute
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.get("/metrics")
|
||||
async def get_metrics():
|
||||
"""Get system metrics and statistics for monitoring"""
|
||||
@ -815,8 +951,8 @@ async def get_rate_limit_status(request: Request):
|
||||
|
||||
|
||||
@app.post("/admin/cleanup/trigger")
|
||||
async def trigger_cleanup():
|
||||
"""Trigger manual cleanup of expired files"""
|
||||
async def trigger_cleanup(is_admin: bool = Depends(require_admin)):
|
||||
"""Trigger manual cleanup of expired files (requires admin auth)"""
|
||||
try:
|
||||
cleaned = await cleanup_manager.cleanup_expired()
|
||||
return {
|
||||
@ -830,8 +966,8 @@ async def trigger_cleanup():
|
||||
|
||||
|
||||
@app.get("/admin/files/tracked")
|
||||
async def get_tracked_files():
|
||||
"""Get list of currently tracked files"""
|
||||
async def get_tracked_files(is_admin: bool = Depends(require_admin)):
|
||||
"""Get list of currently tracked files (requires admin auth)"""
|
||||
tracked = cleanup_manager.get_tracked_files()
|
||||
return {
|
||||
"count": len(tracked),
|
||||
@ -841,5 +977,4 @@ async def get_tracked_files():
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
||||
|
||||
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
||||
Loading…
x
Reference in New Issue
Block a user