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:
Sepehr 2025-11-30 19:33:59 +01:00
parent 500502440c
commit 54d85f0b34
5 changed files with 845 additions and 86 deletions

View File

@ -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
View File

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

View 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>
);
}

View File

@ -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
View File

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