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
14
.env.example
14
.env.example
@ -59,6 +59,20 @@ MAX_REQUEST_SIZE_MB=100
|
|||||||
# Request timeout in seconds
|
# Request timeout in seconds
|
||||||
REQUEST_TIMEOUT_SECONDS=300
|
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 ==============
|
# ============== Monitoring ==============
|
||||||
# Log level: DEBUG, INFO, WARNING, ERROR
|
# Log level: DEBUG, INFO, WARNING, ERROR
|
||||||
LOG_LEVEL=INFO
|
LOG_LEVEL=INFO
|
||||||
|
|||||||
271
README.md
271
README.md
@ -1,6 +1,6 @@
|
|||||||
# 📄 Document Translation API
|
# 📄 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
|
## ✨ Features
|
||||||
|
|
||||||
@ -12,6 +12,7 @@ A powerful Python API for translating complex structured documents (Excel, Word,
|
|||||||
| **WebLLM** | Browser | Runs entirely in browser using WebGPU |
|
| **WebLLM** | Browser | Runs entirely in browser using WebGPU |
|
||||||
| **DeepL** | Cloud | High-quality translations (API key required) |
|
| **DeepL** | Cloud | High-quality translations (API key required) |
|
||||||
| **LibreTranslate** | Self-hosted | Open-source alternative |
|
| **LibreTranslate** | Self-hosted | Open-source alternative |
|
||||||
|
| **OpenAI** | Cloud | GPT-4o/4o-mini with vision support |
|
||||||
|
|
||||||
### 📊 Excel Translation (.xlsx)
|
### 📊 Excel Translation (.xlsx)
|
||||||
- ✅ Translates all cell content and sheet names
|
- ✅ 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
|
- ✅ Image text extraction with text boxes added below images
|
||||||
- ✅ Keeps layering order and positions
|
- ✅ Keeps layering order and positions
|
||||||
|
|
||||||
### 🧠 LLM Features (Ollama/WebLLM)
|
### 🧠 LLM Features (Ollama/WebLLM/OpenAI)
|
||||||
- ✅ **Custom System Prompts**: Provide context for better translations
|
- ✅ **Custom System Prompts**: Provide context for better translations
|
||||||
- ✅ **Technical Glossary**: Define term mappings (e.g., `batterie=coil`)
|
- ✅ **Technical Glossary**: Define term mappings (e.g., `batterie=coil`)
|
||||||
- ✅ **Presets**: HVAC, IT, Legal, Medical terminology
|
- ✅ **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
|
## 🚀 Quick Start
|
||||||
|
|
||||||
@ -60,9 +69,15 @@ python main.py
|
|||||||
|
|
||||||
The API starts on `http://localhost:8000`
|
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
|
## 📚 API Documentation
|
||||||
|
|
||||||
@ -71,7 +86,9 @@ Open `http://localhost:8000/static/index.html` for the full-featured web interfa
|
|||||||
|
|
||||||
## 🔧 API Endpoints
|
## 🔧 API Endpoints
|
||||||
|
|
||||||
### POST /translate
|
### Translation
|
||||||
|
|
||||||
|
#### POST /translate
|
||||||
Translate a document with full customization.
|
Translate a document with full customization.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -81,28 +98,63 @@ curl -X POST "http://localhost:8000/translate" \
|
|||||||
-F "provider=ollama" \
|
-F "provider=ollama" \
|
||||||
-F "ollama_model=gemma3:12b" \
|
-F "ollama_model=gemma3:12b" \
|
||||||
-F "translate_images=true" \
|
-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 |
|
#### GET /health
|
||||||
|-----------|------|---------|-------------|
|
Comprehensive health check with system status.
|
||||||
| `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 /ollama/models
|
```json
|
||||||
List available Ollama models.
|
{
|
||||||
|
"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
|
#### GET /metrics
|
||||||
Configure Ollama settings.
|
System metrics and statistics.
|
||||||
|
|
||||||
### GET /health
|
#### GET /rate-limit/status
|
||||||
Health check endpoint.
|
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
|
## 🌐 Supported Languages
|
||||||
|
|
||||||
@ -120,22 +172,47 @@ Health check endpoint.
|
|||||||
### Environment Variables (.env)
|
### Environment Variables (.env)
|
||||||
|
|
||||||
```env
|
```env
|
||||||
# Translation Service
|
# ============== Translation Services ==============
|
||||||
TRANSLATION_SERVICE=google
|
TRANSLATION_SERVICE=google
|
||||||
|
DEEPL_API_KEY=your_deepl_api_key_here
|
||||||
|
|
||||||
# Ollama Configuration
|
# Ollama Configuration
|
||||||
OLLAMA_BASE_URL=http://localhost:11434
|
OLLAMA_BASE_URL=http://localhost:11434
|
||||||
OLLAMA_MODEL=llama3.2
|
OLLAMA_MODEL=llama3
|
||||||
|
OLLAMA_VISION_MODEL=llava
|
||||||
|
|
||||||
# DeepL API Key (optional)
|
# ============== File Limits ==============
|
||||||
DEEPL_API_KEY=your_api_key_here
|
|
||||||
|
|
||||||
# File Limits
|
|
||||||
MAX_FILE_SIZE_MB=50
|
MAX_FILE_SIZE_MB=50
|
||||||
|
|
||||||
# Directories
|
# ============== Rate Limiting (SaaS) ==============
|
||||||
UPLOAD_DIR=./uploads
|
RATE_LIMIT_ENABLED=true
|
||||||
OUTPUT_DIR=./outputs
|
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
|
### Ollama Setup
|
||||||
@ -179,6 +256,73 @@ vanne 3 voies=3-way valve
|
|||||||
- ⚖️ **Legal**: Legal documents
|
- ⚖️ **Legal**: Legal documents
|
||||||
- 🏥 **Medical**: Healthcare terminology
|
- 🏥 **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
|
## 🔌 MCP Integration
|
||||||
|
|
||||||
This API can be used as an MCP (Model Context Protocol) server for AI assistants.
|
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 |
|
### Security Checklist
|
||||||
|------|-------------|
|
- [ ] Change `ADMIN_PASSWORD` or set `ADMIN_PASSWORD_HASH`
|
||||||
| `translate_document` | Translate a document file |
|
- [ ] Set `CORS_ORIGINS` to your frontend domain
|
||||||
| `list_ollama_models` | Get available Ollama models |
|
- [ ] Enable `ENABLE_HSTS=true` if using HTTPS
|
||||||
| `get_supported_languages` | List supported language codes |
|
- [ ] Configure rate limits appropriately
|
||||||
| `configure_translation` | Set translation provider and options |
|
- [ ] 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
|
## 📝 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,
|
Cloud,
|
||||||
BookText,
|
BookText,
|
||||||
Upload,
|
Upload,
|
||||||
|
Shield,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@ -43,6 +44,15 @@ const navigation = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const adminNavigation = [
|
||||||
|
{
|
||||||
|
name: "Admin Dashboard",
|
||||||
|
href: "/admin",
|
||||||
|
icon: Shield,
|
||||||
|
description: "System monitoring (login required)",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
@ -85,6 +95,37 @@ export function Sidebar() {
|
|||||||
</Tooltip>
|
</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>
|
</nav>
|
||||||
|
|
||||||
{/* User section at bottom */}
|
{/* User section at bottom */}
|
||||||
|
|||||||
147
main.py
147
main.py
@ -3,16 +3,20 @@ Document Translation API
|
|||||||
FastAPI application for translating complex documents while preserving formatting
|
FastAPI application for translating complex documents while preserving formatting
|
||||||
SaaS-ready with rate limiting, validation, and robust error handling
|
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.responses import FileResponse, JSONResponse
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import secrets
|
||||||
|
import hashlib
|
||||||
|
import time
|
||||||
|
|
||||||
from config import config
|
from config import config
|
||||||
from translators import excel_translator, word_translator, pptx_translator
|
from translators import excel_translator, word_translator, pptx_translator
|
||||||
@ -31,6 +35,57 @@ logging.basicConfig(
|
|||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
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
|
# Initialize SaaS components
|
||||||
rate_limit_config = RateLimitConfig(
|
rate_limit_config = RateLimitConfig(
|
||||||
requests_per_minute=int(os.getenv("RATE_LIMIT_PER_MINUTE", "30")),
|
requests_per_minute=int(os.getenv("RATE_LIMIT_PER_MINUTE", "30")),
|
||||||
@ -773,6 +828,87 @@ async def reconstruct_document(
|
|||||||
|
|
||||||
# ============== SaaS Management Endpoints ==============
|
# ============== 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")
|
@app.get("/metrics")
|
||||||
async def get_metrics():
|
async def get_metrics():
|
||||||
"""Get system metrics and statistics for monitoring"""
|
"""Get system metrics and statistics for monitoring"""
|
||||||
@ -815,8 +951,8 @@ async def get_rate_limit_status(request: Request):
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/admin/cleanup/trigger")
|
@app.post("/admin/cleanup/trigger")
|
||||||
async def trigger_cleanup():
|
async def trigger_cleanup(is_admin: bool = Depends(require_admin)):
|
||||||
"""Trigger manual cleanup of expired files"""
|
"""Trigger manual cleanup of expired files (requires admin auth)"""
|
||||||
try:
|
try:
|
||||||
cleaned = await cleanup_manager.cleanup_expired()
|
cleaned = await cleanup_manager.cleanup_expired()
|
||||||
return {
|
return {
|
||||||
@ -830,8 +966,8 @@ async def trigger_cleanup():
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/admin/files/tracked")
|
@app.get("/admin/files/tracked")
|
||||||
async def get_tracked_files():
|
async def get_tracked_files(is_admin: bool = Depends(require_admin)):
|
||||||
"""Get list of currently tracked files"""
|
"""Get list of currently tracked files (requires admin auth)"""
|
||||||
tracked = cleanup_manager.get_tracked_files()
|
tracked = cleanup_manager.get_tracked_files()
|
||||||
return {
|
return {
|
||||||
"count": len(tracked),
|
"count": len(tracked),
|
||||||
@ -842,4 +978,3 @@ async def get_tracked_files():
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
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