Add FastAPI application, API endpoints, Celery workers, and Docker configuration
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
+356
@@ -0,0 +1,356 @@
|
|||||||
|
# POP3 Forwarder SaaS - Multi-Tenant Architecture
|
||||||
|
|
||||||
|
This document describes the new multi-tenant SaaS architecture for the POP3/IMAP email forwarder.
|
||||||
|
|
||||||
|
## 🎯 Project Overview
|
||||||
|
|
||||||
|
The project has been transformed from a single-user Docker application into a full-featured multi-tenant SaaS platform with:
|
||||||
|
|
||||||
|
- **Multi-user support** with subscription tiers
|
||||||
|
- **Google OAuth2 authentication**
|
||||||
|
- **RESTful API** for all operations
|
||||||
|
- **Web dashboard** (frontend to be implemented)
|
||||||
|
- **Subscription management** with Stripe integration
|
||||||
|
- **POP3 and IMAP protocol support**
|
||||||
|
- **Auto-detection** of mail server settings
|
||||||
|
- **Encrypted credential storage**
|
||||||
|
- **Background job processing** with Celery
|
||||||
|
- **Multi-channel notifications** with Apprise
|
||||||
|
|
||||||
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
pop_puller_to_gmail/
|
||||||
|
├── backend/ # FastAPI backend application
|
||||||
|
│ ├── app/
|
||||||
|
│ │ ├── api/ # API endpoints
|
||||||
|
│ │ │ └── v1/
|
||||||
|
│ │ │ ├── endpoints/ # Individual route modules
|
||||||
|
│ │ │ └── api.py # Router aggregation
|
||||||
|
│ │ ├── core/ # Core configuration
|
||||||
|
│ │ │ ├── config.py # Settings management
|
||||||
|
│ │ │ ├── database.py # Database connection
|
||||||
|
│ │ │ ├── security.py # Security utilities
|
||||||
|
│ │ │ └── deps.py # FastAPI dependencies
|
||||||
|
│ │ ├── models/ # Data models
|
||||||
|
│ │ │ ├── database_models.py # SQLAlchemy models
|
||||||
|
│ │ │ └── schemas.py # Pydantic schemas
|
||||||
|
│ │ ├── services/ # Business logic
|
||||||
|
│ │ │ ├── auth_service.py # OAuth authentication
|
||||||
|
│ │ │ └── mail_processor.py # Email processing
|
||||||
|
│ │ ├── workers/ # Celery background tasks
|
||||||
|
│ │ ├── utils/ # Utility functions
|
||||||
|
│ │ └── main.py # FastAPI application
|
||||||
|
│ ├── alembic/ # Database migrations
|
||||||
|
│ ├── tests/ # Test suite
|
||||||
|
│ ├── requirements.txt # Python dependencies
|
||||||
|
│ ├── Dockerfile # Docker configuration
|
||||||
|
│ └── .env.example # Environment template
|
||||||
|
├── frontend/ # React/Next.js frontend (to be implemented)
|
||||||
|
├── docker-compose.new.yml # Docker Compose for all services
|
||||||
|
├── pop3_forwarder.py # Legacy single-user script
|
||||||
|
└── README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Docker and Docker Compose
|
||||||
|
- PostgreSQL 15+
|
||||||
|
- Redis 7+
|
||||||
|
- Python 3.11+ (for local development)
|
||||||
|
- Node.js 18+ (for frontend development)
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
1. **Clone and navigate to repository**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/christianlouis/pop_puller_to_gmail.git
|
||||||
|
cd pop_puller_to_gmail
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configure backend environment**
|
||||||
|
```bash
|
||||||
|
cp backend/.env.example backend/.env
|
||||||
|
# Edit backend/.env with your settings
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Start services with Docker Compose**
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.new.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Run database migrations**
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.new.yml exec backend alembic upgrade head
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Access the application**
|
||||||
|
- API: http://localhost:8000
|
||||||
|
- API Documentation: http://localhost:8000/api/docs
|
||||||
|
- Frontend: http://localhost:3000 (when implemented)
|
||||||
|
|
||||||
|
## 🔑 Key Features
|
||||||
|
|
||||||
|
### 1. Multi-Tenant User Management
|
||||||
|
|
||||||
|
- **User Registration**: Email/password and Google OAuth2
|
||||||
|
- **Subscription Tiers**: Free, Basic, Pro, Enterprise
|
||||||
|
- **Account Limits**: Based on subscription tier
|
||||||
|
- **Secure Storage**: Encrypted credentials with Fernet encryption
|
||||||
|
|
||||||
|
### 2. Mail Account Management
|
||||||
|
|
||||||
|
- **Protocols**: POP3, POP3+SSL, IMAP, IMAP+SSL
|
||||||
|
- **Auto-Detection**: Automatic server configuration for common providers
|
||||||
|
- **Provider Presets**: Gmail, Outlook, GMX, WEB.de, T-Online, Yahoo
|
||||||
|
- **Connection Testing**: Test before saving
|
||||||
|
- **Per-Account Settings**: Check interval, max emails, forwarding destination
|
||||||
|
|
||||||
|
### 3. Email Processing
|
||||||
|
|
||||||
|
- **Background Jobs**: Celery workers for async processing
|
||||||
|
- **Scheduled Checks**: Configurable intervals per account
|
||||||
|
- **Smart Forwarding**: Preserves metadata, handles MIME types
|
||||||
|
- **Error Handling**: Automatic retries with exponential backoff
|
||||||
|
- **Statistics**: Track success/failure rates, last check times
|
||||||
|
|
||||||
|
### 4. Subscription Management
|
||||||
|
|
||||||
|
- **Stripe Integration**: Secure payment processing
|
||||||
|
- **Tier-Based Limits**: Automatic enforcement
|
||||||
|
- **Upgrade/Downgrade**: Self-service subscription changes
|
||||||
|
- **Webhook Handling**: Real-time subscription updates
|
||||||
|
|
||||||
|
### 5. Notifications
|
||||||
|
|
||||||
|
- **Multi-Channel**: Email, Telegram, Webhook, Slack, Discord
|
||||||
|
- **Apprise Integration**: 70+ notification services
|
||||||
|
- **Smart Alerting**: Threshold-based notifications
|
||||||
|
- **Per-User Configuration**: Custom notification preferences
|
||||||
|
|
||||||
|
### 6. Security
|
||||||
|
|
||||||
|
- **JWT Authentication**: Secure API access
|
||||||
|
- **Encrypted Credentials**: All POP3/IMAP passwords encrypted at rest
|
||||||
|
- **OAuth2**: Google Sign-In support
|
||||||
|
- **Audit Logging**: Complete audit trail
|
||||||
|
- **RBAC**: Role-based access control
|
||||||
|
- **Rate Limiting**: Per-user and per-tier limits
|
||||||
|
|
||||||
|
## 🗄️ Database Schema
|
||||||
|
|
||||||
|
### Core Tables
|
||||||
|
|
||||||
|
- **users**: User accounts, OAuth info, subscription data
|
||||||
|
- **mail_accounts**: POP3/IMAP account configurations
|
||||||
|
- **processing_runs**: Email processing batch records
|
||||||
|
- **processing_logs**: Detailed processing logs
|
||||||
|
- **notification_configs**: User notification settings
|
||||||
|
- **subscription_plans**: Available subscription tiers
|
||||||
|
- **mail_server_presets**: Known provider configurations
|
||||||
|
- **audit_logs**: Security and compliance audit trail
|
||||||
|
|
||||||
|
## 🔌 API Endpoints
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- `POST /api/v1/auth/register` - Register new user
|
||||||
|
- `POST /api/v1/auth/login` - Login with email/password
|
||||||
|
- `POST /api/v1/auth/google` - Google OAuth2 login
|
||||||
|
- `GET /api/v1/auth/google/authorize-url` - Get OAuth URL
|
||||||
|
|
||||||
|
### Users
|
||||||
|
- `GET /api/v1/users/me` - Get current user profile
|
||||||
|
- `PUT /api/v1/users/me` - Update user profile
|
||||||
|
|
||||||
|
### Mail Accounts
|
||||||
|
- `POST /api/v1/mail-accounts` - Create mail account
|
||||||
|
- `GET /api/v1/mail-accounts` - List user's accounts
|
||||||
|
- `GET /api/v1/mail-accounts/{id}` - Get account details
|
||||||
|
- `PUT /api/v1/mail-accounts/{id}` - Update account
|
||||||
|
- `DELETE /api/v1/mail-accounts/{id}` - Delete account
|
||||||
|
- `POST /api/v1/mail-accounts/test` - Test connection
|
||||||
|
- `POST /api/v1/mail-accounts/auto-detect` - Auto-detect settings
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
- `POST /api/v1/notifications` - Create notification config
|
||||||
|
- `GET /api/v1/notifications` - List notification configs
|
||||||
|
|
||||||
|
### Subscriptions
|
||||||
|
- `GET /api/v1/subscriptions/plans` - List available plans
|
||||||
|
- `GET /api/v1/subscriptions/current` - Get current subscription
|
||||||
|
|
||||||
|
### Admin
|
||||||
|
- `GET /api/v1/admin/stats` - System statistics (admin only)
|
||||||
|
|
||||||
|
See full API documentation at `/api/docs` when running.
|
||||||
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Key configuration options in `backend/.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Database
|
||||||
|
DATABASE_URL=postgresql+asyncpg://user:pass@host:port/db
|
||||||
|
|
||||||
|
# Security
|
||||||
|
SECRET_KEY=your-secret-key-min-32-chars
|
||||||
|
ENCRYPTION_KEY=your-encryption-key
|
||||||
|
|
||||||
|
# OAuth
|
||||||
|
GOOGLE_CLIENT_ID=your-client-id
|
||||||
|
GOOGLE_CLIENT_SECRET=your-client-secret
|
||||||
|
|
||||||
|
# Stripe (optional)
|
||||||
|
STRIPE_API_KEY=sk_test_...
|
||||||
|
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||||
|
|
||||||
|
# Limits
|
||||||
|
TIER_FREE_MAX_ACCOUNTS=1
|
||||||
|
TIER_BASIC_MAX_ACCOUNTS=5
|
||||||
|
TIER_PRO_MAX_ACCOUNTS=20
|
||||||
|
TIER_ENTERPRISE_MAX_ACCOUNTS=100
|
||||||
|
|
||||||
|
# Processing
|
||||||
|
CHECK_INTERVAL_MINUTES=5
|
||||||
|
MAX_EMAILS_PER_RUN=50
|
||||||
|
THROTTLE_EMAILS_PER_MINUTE=10
|
||||||
|
```
|
||||||
|
|
||||||
|
### Subscription Tiers
|
||||||
|
|
||||||
|
| Tier | Max Accounts | Price | Features |
|
||||||
|
|------|--------------|-------|----------|
|
||||||
|
| Free | 1 | $0/mo | Basic email forwarding |
|
||||||
|
| Basic | 5 | $9/mo | Multiple accounts, Priority support |
|
||||||
|
| Pro | 20 | $29/mo | Advanced features, API access |
|
||||||
|
| Enterprise | 100 | $99/mo | White-label, SLA, Dedicated support |
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run tests
|
||||||
|
cd backend
|
||||||
|
pytest
|
||||||
|
|
||||||
|
# With coverage
|
||||||
|
pytest --cov=app --cov-report=html
|
||||||
|
|
||||||
|
# Run specific test file
|
||||||
|
pytest tests/test_auth.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 Deployment
|
||||||
|
|
||||||
|
### Production Deployment
|
||||||
|
|
||||||
|
1. **Set production environment variables**
|
||||||
|
2. **Use production database** (PostgreSQL with backups)
|
||||||
|
3. **Configure Redis** for caching and job queue
|
||||||
|
4. **Set up SSL/TLS** with reverse proxy (nginx/traefik)
|
||||||
|
5. **Enable monitoring** (Prometheus, Grafana)
|
||||||
|
6. **Configure logging** (structured JSON logs)
|
||||||
|
|
||||||
|
### Kubernetes Deployment
|
||||||
|
|
||||||
|
Coming soon: Kubernetes manifests and Helm charts.
|
||||||
|
|
||||||
|
## 🔄 Migration from Legacy Version
|
||||||
|
|
||||||
|
To migrate from the single-user `pop3_forwarder.py`:
|
||||||
|
|
||||||
|
1. **Export existing configuration** from `.env` file
|
||||||
|
2. **Create user account** via API or admin panel
|
||||||
|
3. **Add mail accounts** using the API:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/api/v1/mail-accounts \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d @account.json
|
||||||
|
```
|
||||||
|
4. **Verify processing** in the dashboard
|
||||||
|
5. **Stop legacy container** once confirmed working
|
||||||
|
|
||||||
|
## 🛠️ Development
|
||||||
|
|
||||||
|
### Local Development Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend
|
||||||
|
cd backend
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Run database
|
||||||
|
docker-compose -f docker-compose.new.yml up postgres redis -d
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
alembic upgrade head
|
||||||
|
|
||||||
|
# Start development server
|
||||||
|
uvicorn app.main:app --reload --port 8000
|
||||||
|
|
||||||
|
# Frontend (to be implemented)
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Migrations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create new migration
|
||||||
|
alembic revision --autogenerate -m "Description"
|
||||||
|
|
||||||
|
# Apply migrations
|
||||||
|
alembic upgrade head
|
||||||
|
|
||||||
|
# Rollback migration
|
||||||
|
alembic downgrade -1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Additional Documentation
|
||||||
|
|
||||||
|
- [API Documentation](http://localhost:8000/api/docs) - Interactive API docs
|
||||||
|
- [CONTRIBUTING.md](CONTRIBUTING.md) - Contribution guidelines
|
||||||
|
- [ROADMAP.md](ROADMAP.md) - Future development plans
|
||||||
|
- [SECURITY.md](SECURITY.md) - Security policies
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
Contributions welcome! Please:
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Make your changes with tests
|
||||||
|
4. Submit a pull request
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
MIT License - See [LICENSE](LICENSE) file
|
||||||
|
|
||||||
|
## 🆘 Support
|
||||||
|
|
||||||
|
- **Issues**: https://github.com/christianlouis/pop_puller_to_gmail/issues
|
||||||
|
- **Discussions**: https://github.com/christianlouis/pop_puller_to_gmail/discussions
|
||||||
|
- **Email**: support@example.com
|
||||||
|
|
||||||
|
## 🙏 Acknowledgments
|
||||||
|
|
||||||
|
Built with:
|
||||||
|
- [FastAPI](https://fastapi.tiangolo.com/) - Modern Python web framework
|
||||||
|
- [SQLAlchemy](https://www.sqlalchemy.org/) - Database ORM
|
||||||
|
- [Celery](https://docs.celeryq.dev/) - Distributed task queue
|
||||||
|
- [Stripe](https://stripe.com/) - Payment processing
|
||||||
|
- [Apprise](https://github.com/caronc/apprise) - Notification service
|
||||||
|
- [React](https://react.dev/) - Frontend framework
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status**: 🚧 Active Development - Phase 1 Complete
|
||||||
|
|
||||||
|
For questions or feedback, please open an issue on GitHub.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Database Configuration
|
||||||
|
DATABASE_URL=postgresql+asyncpg://postgres:password@localhost:5432/pop3_forwarder
|
||||||
|
|
||||||
|
# Security
|
||||||
|
SECRET_KEY=change-this-to-a-secure-random-secret-key-minimum-32-characters
|
||||||
|
ENCRYPTION_KEY=change-this-to-a-secure-encryption-key-for-credentials
|
||||||
|
ALGORITHM=HS256
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||||
|
REFRESH_TOKEN_EXPIRE_DAYS=7
|
||||||
|
|
||||||
|
# OAuth2 - Google
|
||||||
|
GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
|
||||||
|
GOOGLE_CLIENT_SECRET=your-google-client-secret
|
||||||
|
GOOGLE_REDIRECT_URI=http://localhost:3000/auth/callback/google
|
||||||
|
|
||||||
|
# CORS Origins (comma-separated)
|
||||||
|
CORS_ORIGINS=http://localhost:3000,http://localhost:8000
|
||||||
|
|
||||||
|
# Stripe Payment (Optional)
|
||||||
|
STRIPE_API_KEY=sk_test_your_stripe_api_key
|
||||||
|
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret
|
||||||
|
STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key
|
||||||
|
|
||||||
|
# Subscription Tiers Limits
|
||||||
|
TIER_FREE_MAX_ACCOUNTS=1
|
||||||
|
TIER_BASIC_MAX_ACCOUNTS=5
|
||||||
|
TIER_PRO_MAX_ACCOUNTS=20
|
||||||
|
TIER_ENTERPRISE_MAX_ACCOUNTS=100
|
||||||
|
|
||||||
|
# Email Processing
|
||||||
|
MAX_EMAILS_PER_RUN=50
|
||||||
|
CHECK_INTERVAL_MINUTES=5
|
||||||
|
THROTTLE_EMAILS_PER_MINUTE=10
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_URL=redis://localhost:6379/0
|
||||||
|
CELERY_BROKER_URL=redis://localhost:6379/0
|
||||||
|
CELERY_RESULT_BACKEND=redis://localhost:6379/0
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
|
||||||
|
# Admin Account (created on first startup)
|
||||||
|
ADMIN_EMAIL=admin@example.com
|
||||||
|
ADMIN_PASSWORD=change-this-secure-password
|
||||||
|
|
||||||
|
# Application
|
||||||
|
APP_NAME=POP3 Forwarder SaaS
|
||||||
|
APP_VERSION=2.0.0
|
||||||
|
DEBUG=false
|
||||||
|
HOST=0.0.0.0
|
||||||
|
PORT=8000
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
gcc \
|
||||||
|
postgresql-client \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy requirements
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN useradd -m -u 1000 appuser && \
|
||||||
|
chown -R appuser:appuser /app
|
||||||
|
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Default command (can be overridden)
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Alembic configuration for database migrations
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# Path to migration scripts
|
||||||
|
script_location = alembic
|
||||||
|
|
||||||
|
# Template used to generate migration files
|
||||||
|
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# Timezone for migration timestamps
|
||||||
|
timezone = UTC
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""API v1 package"""
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
API v1 router aggregation.
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app.api.v1.endpoints import auth, users, mail_accounts, notifications, subscriptions, admin
|
||||||
|
|
||||||
|
api_router = APIRouter()
|
||||||
|
|
||||||
|
# Include all endpoint routers
|
||||||
|
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
|
||||||
|
api_router.include_router(users.router, prefix="/users", tags=["Users"])
|
||||||
|
api_router.include_router(mail_accounts.router, prefix="/mail-accounts", tags=["Mail Accounts"])
|
||||||
|
api_router.include_router(notifications.router, prefix="/notifications", tags=["Notifications"])
|
||||||
|
api_router.include_router(subscriptions.router, prefix="/subscriptions", tags=["Subscriptions"])
|
||||||
|
api_router.include_router(admin.router, prefix="/admin", tags=["Admin"])
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""API v1 endpoints package"""
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"""Admin endpoints"""
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select, func
|
||||||
|
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.deps import get_current_superuser
|
||||||
|
from app.models.database_models import User, MailAccount, ProcessingRun
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/stats")
|
||||||
|
async def get_admin_stats(
|
||||||
|
current_user: User = Depends(get_current_superuser),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Get overall system statistics (admin only)"""
|
||||||
|
|
||||||
|
# Count users
|
||||||
|
user_count = await db.execute(select(func.count(User.id)))
|
||||||
|
total_users = user_count.scalar()
|
||||||
|
|
||||||
|
# Count accounts
|
||||||
|
account_count = await db.execute(select(func.count(MailAccount.id)))
|
||||||
|
total_accounts = account_count.scalar()
|
||||||
|
|
||||||
|
# Count processing runs
|
||||||
|
run_count = await db.execute(select(func.count(ProcessingRun.id)))
|
||||||
|
total_runs = run_count.scalar()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_users": total_users,
|
||||||
|
"total_mail_accounts": total_accounts,
|
||||||
|
"total_processing_runs": total_runs
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
"""
|
||||||
|
Authentication endpoints (login, register, OAuth).
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.security import verify_password, get_password_hash
|
||||||
|
from app.models.database_models import User, SubscriptionTier
|
||||||
|
from app.models.schemas import (
|
||||||
|
Token, UserCreate, UserResponse, GoogleAuthRequest
|
||||||
|
)
|
||||||
|
from app.services.auth_service import oauth_service
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def register(
|
||||||
|
user_in: UserCreate,
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Register a new user with email and password"""
|
||||||
|
|
||||||
|
# Check if user exists
|
||||||
|
result = await db.execute(
|
||||||
|
select(User).where(User.email == user_in.email)
|
||||||
|
)
|
||||||
|
existing_user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if existing_user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Email already registered"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create new user
|
||||||
|
user = User(
|
||||||
|
email=user_in.email,
|
||||||
|
full_name=user_in.full_name,
|
||||||
|
hashed_password=get_password_hash(user_in.password) if user_in.password else None,
|
||||||
|
subscription_tier=SubscriptionTier.FREE,
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
|
||||||
|
db.add(user)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(user)
|
||||||
|
|
||||||
|
logger.info(f"New user registered: {user.email}")
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/login", response_model=Token)
|
||||||
|
async def login(
|
||||||
|
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Login with email and password"""
|
||||||
|
|
||||||
|
# Get user
|
||||||
|
result = await db.execute(
|
||||||
|
select(User).where(User.email == form_data.username)
|
||||||
|
)
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not user or not user.hashed_password:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Incorrect email or password",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify password
|
||||||
|
if not verify_password(form_data.password, user.hashed_password):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Incorrect email or password",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if user is active
|
||||||
|
if not user.is_active:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="User account is inactive"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update last login
|
||||||
|
user.last_login_at = datetime.utcnow()
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
# Create tokens
|
||||||
|
tokens = oauth_service.create_tokens_for_user(user)
|
||||||
|
|
||||||
|
logger.info(f"User logged in: {user.email}")
|
||||||
|
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/google", response_model=Token)
|
||||||
|
async def google_oauth(
|
||||||
|
auth_request: GoogleAuthRequest,
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Authenticate with Google OAuth2.
|
||||||
|
Exchange authorization code for access token and user info.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Get user info from Google
|
||||||
|
user_info = await oauth_service.get_google_user_info(
|
||||||
|
code=auth_request.code,
|
||||||
|
redirect_uri=auth_request.redirect_uri
|
||||||
|
)
|
||||||
|
|
||||||
|
if not user_info.get('verified_email'):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Email not verified with Google"
|
||||||
|
)
|
||||||
|
|
||||||
|
email = user_info['email']
|
||||||
|
google_id = user_info['google_id']
|
||||||
|
|
||||||
|
# Check if user exists
|
||||||
|
result = await db.execute(
|
||||||
|
select(User).where(
|
||||||
|
(User.email == email) | (User.google_id == google_id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if user:
|
||||||
|
# Update Google ID if not set
|
||||||
|
if not user.google_id:
|
||||||
|
user.google_id = google_id
|
||||||
|
user.oauth_provider = "google"
|
||||||
|
|
||||||
|
# Update last login
|
||||||
|
user.last_login_at = datetime.utcnow()
|
||||||
|
|
||||||
|
logger.info(f"Existing user logged in with Google: {user.email}")
|
||||||
|
else:
|
||||||
|
# Create new user
|
||||||
|
user = User(
|
||||||
|
email=email,
|
||||||
|
full_name=user_info.get('full_name'),
|
||||||
|
google_id=google_id,
|
||||||
|
oauth_provider="google",
|
||||||
|
subscription_tier=SubscriptionTier.FREE,
|
||||||
|
is_active=True,
|
||||||
|
last_login_at=datetime.utcnow()
|
||||||
|
)
|
||||||
|
db.add(user)
|
||||||
|
|
||||||
|
logger.info(f"New user registered with Google: {user.email}")
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(user)
|
||||||
|
|
||||||
|
# Create tokens
|
||||||
|
tokens = oauth_service.create_tokens_for_user(user)
|
||||||
|
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/google/authorize-url")
|
||||||
|
async def get_google_authorize_url(redirect_uri: str):
|
||||||
|
"""Get Google OAuth2 authorization URL"""
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
auth_url = (
|
||||||
|
f"https://accounts.google.com/o/oauth2/v2/auth?"
|
||||||
|
f"client_id={settings.GOOGLE_CLIENT_ID}&"
|
||||||
|
f"response_type=code&"
|
||||||
|
f"scope=openid%20email%20profile&"
|
||||||
|
f"redirect_uri={redirect_uri}&"
|
||||||
|
f"access_type=offline"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"authorization_url": auth_url}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
"""Mail account management endpoints"""
|
||||||
|
from typing import List
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select, desc
|
||||||
|
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.deps import get_current_active_user
|
||||||
|
from app.core.security import encrypt_credential, decrypt_credential
|
||||||
|
from app.models.database_models import User, MailAccount
|
||||||
|
from app.models.schemas import (
|
||||||
|
MailAccountCreate, MailAccountResponse, MailAccountUpdate,
|
||||||
|
MailAccountTestRequest, MailAccountTestResponse,
|
||||||
|
MailAccountAutoDetectRequest, MailAccountAutoDetectResponse
|
||||||
|
)
|
||||||
|
from app.services.mail_processor import MailProcessor, MailServerAutoDetect
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=MailAccountResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_mail_account(
|
||||||
|
account_in: MailAccountCreate,
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Create a new mail account"""
|
||||||
|
|
||||||
|
# Check subscription limits
|
||||||
|
result = await db.execute(
|
||||||
|
select(MailAccount).where(MailAccount.user_id == current_user.id)
|
||||||
|
)
|
||||||
|
existing_accounts = result.scalars().all()
|
||||||
|
|
||||||
|
tier_limits = {
|
||||||
|
"free": settings.TIER_FREE_MAX_ACCOUNTS,
|
||||||
|
"basic": settings.TIER_BASIC_MAX_ACCOUNTS,
|
||||||
|
"pro": settings.TIER_PRO_MAX_ACCOUNTS,
|
||||||
|
"enterprise": settings.TIER_ENTERPRISE_MAX_ACCOUNTS,
|
||||||
|
}
|
||||||
|
|
||||||
|
max_accounts = tier_limits.get(current_user.subscription_tier.value, 1)
|
||||||
|
|
||||||
|
if len(existing_accounts) >= max_accounts:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_402_PAYMENT_REQUIRED,
|
||||||
|
detail=f"Account limit reached. Upgrade your subscription to add more accounts."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Encrypt password
|
||||||
|
encrypted_password = encrypt_credential(account_in.password)
|
||||||
|
|
||||||
|
# Create account
|
||||||
|
account = MailAccount(
|
||||||
|
user_id=current_user.id,
|
||||||
|
name=account_in.name,
|
||||||
|
email_address=account_in.email_address,
|
||||||
|
protocol=account_in.protocol,
|
||||||
|
host=account_in.host,
|
||||||
|
port=account_in.port,
|
||||||
|
use_ssl=account_in.use_ssl,
|
||||||
|
use_tls=account_in.use_tls,
|
||||||
|
username=account_in.username,
|
||||||
|
encrypted_password=encrypted_password,
|
||||||
|
forward_to=account_in.forward_to,
|
||||||
|
is_enabled=account_in.is_enabled,
|
||||||
|
check_interval_minutes=account_in.check_interval_minutes,
|
||||||
|
max_emails_per_check=account_in.max_emails_per_check,
|
||||||
|
delete_after_forward=account_in.delete_after_forward
|
||||||
|
)
|
||||||
|
|
||||||
|
db.add(account)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(account)
|
||||||
|
|
||||||
|
return account
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=List[MailAccountResponse])
|
||||||
|
async def list_mail_accounts(
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""List all mail accounts for current user"""
|
||||||
|
result = await db.execute(
|
||||||
|
select(MailAccount)
|
||||||
|
.where(MailAccount.user_id == current_user.id)
|
||||||
|
.order_by(desc(MailAccount.created_at))
|
||||||
|
)
|
||||||
|
accounts = result.scalars().all()
|
||||||
|
return accounts
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{account_id}", response_model=MailAccountResponse)
|
||||||
|
async def get_mail_account(
|
||||||
|
account_id: int,
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Get a specific mail account"""
|
||||||
|
result = await db.execute(
|
||||||
|
select(MailAccount).where(
|
||||||
|
MailAccount.id == account_id,
|
||||||
|
MailAccount.user_id == current_user.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
account = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not account:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Mail account not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
return account
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{account_id}", response_model=MailAccountResponse)
|
||||||
|
async def update_mail_account(
|
||||||
|
account_id: int,
|
||||||
|
account_update: MailAccountUpdate,
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Update a mail account"""
|
||||||
|
result = await db.execute(
|
||||||
|
select(MailAccount).where(
|
||||||
|
MailAccount.id == account_id,
|
||||||
|
MailAccount.user_id == current_user.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
account = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not account:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Mail account not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update fields
|
||||||
|
update_data = account_update.dict(exclude_unset=True)
|
||||||
|
|
||||||
|
if "password" in update_data:
|
||||||
|
update_data["encrypted_password"] = encrypt_credential(update_data.pop("password"))
|
||||||
|
|
||||||
|
for field, value in update_data.items():
|
||||||
|
setattr(account, field, value)
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(account)
|
||||||
|
|
||||||
|
return account
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{account_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def delete_mail_account(
|
||||||
|
account_id: int,
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Delete a mail account"""
|
||||||
|
result = await db.execute(
|
||||||
|
select(MailAccount).where(
|
||||||
|
MailAccount.id == account_id,
|
||||||
|
MailAccount.user_id == current_user.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
account = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not account:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Mail account not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.delete(account)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/test", response_model=MailAccountTestResponse)
|
||||||
|
async def test_mail_connection(
|
||||||
|
test_request: MailAccountTestRequest,
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
):
|
||||||
|
"""Test connection to mail server"""
|
||||||
|
|
||||||
|
# Create temporary account for testing
|
||||||
|
temp_account = MailAccount(
|
||||||
|
user_id=current_user.id,
|
||||||
|
name="test",
|
||||||
|
email_address="test@test.com",
|
||||||
|
protocol=test_request.protocol,
|
||||||
|
host=test_request.host,
|
||||||
|
port=test_request.port,
|
||||||
|
use_ssl=test_request.use_ssl,
|
||||||
|
use_tls=test_request.use_tls,
|
||||||
|
username=test_request.username,
|
||||||
|
encrypted_password="", # Not used for test
|
||||||
|
forward_to="test@test.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
processor = MailProcessor(temp_account, test_request.password)
|
||||||
|
success, message = await processor.test_connection()
|
||||||
|
|
||||||
|
return MailAccountTestResponse(
|
||||||
|
success=success,
|
||||||
|
message=message
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/auto-detect", response_model=MailAccountAutoDetectResponse)
|
||||||
|
async def auto_detect_mail_settings(
|
||||||
|
detect_request: MailAccountAutoDetectRequest,
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
):
|
||||||
|
"""Auto-detect mail server settings for an email address"""
|
||||||
|
|
||||||
|
suggestions = MailServerAutoDetect.detect(detect_request.email_address)
|
||||||
|
|
||||||
|
return MailAccountAutoDetectResponse(
|
||||||
|
success=len(suggestions) > 0,
|
||||||
|
suggestions=suggestions
|
||||||
|
)
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
"""Notification configuration endpoints"""
|
||||||
|
from typing import List
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.deps import get_current_active_user
|
||||||
|
from app.models.database_models import User, NotificationConfig
|
||||||
|
from app.models.schemas import (
|
||||||
|
NotificationConfigCreate, NotificationConfigResponse, NotificationConfigUpdate
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=NotificationConfigResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_notification_config(
|
||||||
|
config_in: NotificationConfigCreate,
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Create notification configuration"""
|
||||||
|
config = NotificationConfig(
|
||||||
|
user_id=current_user.id,
|
||||||
|
**config_in.dict()
|
||||||
|
)
|
||||||
|
db.add(config)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(config)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=List[NotificationConfigResponse])
|
||||||
|
async def list_notification_configs(
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""List all notification configurations"""
|
||||||
|
result = await db.execute(
|
||||||
|
select(NotificationConfig).where(NotificationConfig.user_id == current_user.id)
|
||||||
|
)
|
||||||
|
return result.scalars().all()
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"""Subscription and payment endpoints"""
|
||||||
|
from typing import List
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.deps import get_current_active_user
|
||||||
|
from app.models.database_models import User, SubscriptionPlan
|
||||||
|
from app.models.schemas import SubscriptionPlanResponse
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/plans", response_model=List[SubscriptionPlanResponse])
|
||||||
|
async def list_subscription_plans(
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""List all available subscription plans"""
|
||||||
|
result = await db.execute(
|
||||||
|
select(SubscriptionPlan).where(SubscriptionPlan.is_active == True)
|
||||||
|
)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/current")
|
||||||
|
async def get_current_subscription(
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
"""Get current user's subscription details"""
|
||||||
|
return {
|
||||||
|
"tier": current_user.subscription_tier,
|
||||||
|
"status": current_user.subscription_status,
|
||||||
|
"expires_at": current_user.subscription_expires_at
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"""User management endpoints"""
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.deps import get_current_active_user
|
||||||
|
from app.models.database_models import User
|
||||||
|
from app.models.schemas import UserResponse, UserDetailResponse, UserUpdate
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/me", response_model=UserDetailResponse)
|
||||||
|
async def get_current_user_profile(
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
"""Get current user profile"""
|
||||||
|
return current_user
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/me", response_model=UserDetailResponse)
|
||||||
|
async def update_current_user_profile(
|
||||||
|
user_update: UserUpdate,
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Update current user profile"""
|
||||||
|
if user_update.email:
|
||||||
|
current_user.email = user_update.email
|
||||||
|
if user_update.full_name:
|
||||||
|
current_user.full_name = user_update.full_name
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(current_user)
|
||||||
|
return current_user
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
"""
|
||||||
|
Main FastAPI application.
|
||||||
|
"""
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.api.v1.api import api_router
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=getattr(logging, settings.LOG_LEVEL.upper()),
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def create_application() -> FastAPI:
|
||||||
|
"""Create and configure FastAPI application"""
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title=settings.APP_NAME,
|
||||||
|
version=settings.APP_VERSION,
|
||||||
|
description="Multi-tenant POP3/IMAP to Gmail forwarder with subscription management",
|
||||||
|
docs_url="/api/docs",
|
||||||
|
redoc_url="/api/redoc",
|
||||||
|
openapi_url="/api/openapi.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
# CORS middleware
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=settings.CORS_ORIGINS,
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Include API router
|
||||||
|
app.include_router(api_router, prefix=settings.API_V1_PREFIX)
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
"""Root endpoint"""
|
||||||
|
return {
|
||||||
|
"message": "POP3 Forwarder SaaS API",
|
||||||
|
"version": settings.APP_VERSION,
|
||||||
|
"docs": "/api/docs"
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
"""Health check endpoint for container orchestration"""
|
||||||
|
return {"status": "healthy"}
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup_event():
|
||||||
|
"""Run on application startup"""
|
||||||
|
logger.info(f"Starting {settings.APP_NAME} v{settings.APP_VERSION}")
|
||||||
|
logger.info(f"Debug mode: {settings.DEBUG}")
|
||||||
|
logger.info(f"API documentation: /api/docs")
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def shutdown_event():
|
||||||
|
"""Run on application shutdown"""
|
||||||
|
logger.info("Shutting down application")
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
app = create_application()
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
"""
|
||||||
|
OAuth2 authentication service for Google and other providers.
|
||||||
|
"""
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
import httpx
|
||||||
|
from authlib.integrations.starlette_client import OAuth
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.core.security import create_access_token, create_refresh_token
|
||||||
|
from app.models.database_models import User
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthService:
|
||||||
|
"""OAuth2 authentication service"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.oauth = OAuth()
|
||||||
|
self._register_google()
|
||||||
|
|
||||||
|
def _register_google(self):
|
||||||
|
"""Register Google OAuth2 provider"""
|
||||||
|
if settings.GOOGLE_CLIENT_ID and settings.GOOGLE_CLIENT_SECRET:
|
||||||
|
self.oauth.register(
|
||||||
|
name='google',
|
||||||
|
client_id=settings.GOOGLE_CLIENT_ID,
|
||||||
|
client_secret=settings.GOOGLE_CLIENT_SECRET,
|
||||||
|
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
|
||||||
|
client_kwargs={'scope': 'openid email profile'}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_google_user_info(self, code: str, redirect_uri: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Exchange Google authorization code for user information.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code: Authorization code from Google
|
||||||
|
redirect_uri: Redirect URI used in OAuth flow
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with user information (email, name, google_id)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Exchange code for token
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
token_response = await client.post(
|
||||||
|
'https://oauth2.googleapis.com/token',
|
||||||
|
data={
|
||||||
|
'code': code,
|
||||||
|
'client_id': settings.GOOGLE_CLIENT_ID,
|
||||||
|
'client_secret': settings.GOOGLE_CLIENT_SECRET,
|
||||||
|
'redirect_uri': redirect_uri,
|
||||||
|
'grant_type': 'authorization_code'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if token_response.status_code != 200:
|
||||||
|
logger.error(f"Google token exchange failed: {token_response.text}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Failed to exchange authorization code"
|
||||||
|
)
|
||||||
|
|
||||||
|
token_data = token_response.json()
|
||||||
|
access_token = token_data.get('access_token')
|
||||||
|
|
||||||
|
if not access_token:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="No access token received"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get user info
|
||||||
|
user_info_response = await client.get(
|
||||||
|
'https://www.googleapis.com/oauth2/v2/userinfo',
|
||||||
|
headers={'Authorization': f'Bearer {access_token}'}
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_info_response.status_code != 200:
|
||||||
|
logger.error(f"Google user info fetch failed: {user_info_response.text}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Failed to get user information"
|
||||||
|
)
|
||||||
|
|
||||||
|
user_info = user_info_response.json()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'email': user_info.get('email'),
|
||||||
|
'full_name': user_info.get('name'),
|
||||||
|
'google_id': user_info.get('id'),
|
||||||
|
'picture': user_info.get('picture'),
|
||||||
|
'verified_email': user_info.get('verified_email', False)
|
||||||
|
}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"OAuth error: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="OAuth authentication failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_tokens_for_user(user: User) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Create access and refresh tokens for a user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user: User database model
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with access_token, refresh_token, and token_type
|
||||||
|
"""
|
||||||
|
access_token = create_access_token(data={"sub": user.id})
|
||||||
|
refresh_token = create_refresh_token(data={"sub": user.id})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"access_token": access_token,
|
||||||
|
"refresh_token": refresh_token,
|
||||||
|
"token_type": "bearer"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Global OAuth service instance
|
||||||
|
oauth_service = OAuthService()
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""
|
||||||
|
Celery application for background email processing tasks.
|
||||||
|
"""
|
||||||
|
from celery import Celery
|
||||||
|
from celery.schedules import crontab
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Create Celery app
|
||||||
|
celery_app = Celery(
|
||||||
|
"pop3_forwarder",
|
||||||
|
broker=settings.CELERY_BROKER_URL,
|
||||||
|
backend=settings.CELERY_RESULT_BACKEND,
|
||||||
|
include=["app.workers.tasks"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Celery configuration
|
||||||
|
celery_app.conf.update(
|
||||||
|
task_serializer="json",
|
||||||
|
accept_content=["json"],
|
||||||
|
result_serializer="json",
|
||||||
|
timezone="UTC",
|
||||||
|
enable_utc=True,
|
||||||
|
task_track_started=True,
|
||||||
|
task_time_limit=30 * 60, # 30 minutes
|
||||||
|
task_soft_time_limit=25 * 60, # 25 minutes
|
||||||
|
worker_prefetch_multiplier=1,
|
||||||
|
worker_max_tasks_per_child=1000,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Periodic tasks schedule
|
||||||
|
celery_app.conf.beat_schedule = {
|
||||||
|
"process-all-mail-accounts": {
|
||||||
|
"task": "app.workers.tasks.process_all_enabled_accounts",
|
||||||
|
"schedule": crontab(minute="*/5"), # Every 5 minutes
|
||||||
|
},
|
||||||
|
"cleanup-old-logs": {
|
||||||
|
"task": "app.workers.tasks.cleanup_old_logs",
|
||||||
|
"schedule": crontab(hour=3, minute=0), # Daily at 3 AM
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@celery_app.task(bind=True)
|
||||||
|
def debug_task(self):
|
||||||
|
"""Debug task to test Celery"""
|
||||||
|
logger.info(f"Request: {self.request!r}")
|
||||||
|
return "Celery is working!"
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
"""
|
||||||
|
Celery tasks for background email processing.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import List
|
||||||
|
from celery import Task
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from app.workers.celery_app import celery_app
|
||||||
|
from app.core.database import async_session_maker
|
||||||
|
from app.core.security import decrypt_credential
|
||||||
|
from app.models.database_models import MailAccount, ProcessingRun, ProcessingLog, AccountStatus
|
||||||
|
from app.services.mail_processor import MailProcessor
|
||||||
|
from sqlalchemy import select, and_
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncTask(Task):
|
||||||
|
"""Base task class that handles async operations"""
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
"""Run async task in event loop"""
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
return loop.run_until_complete(self.run(*args, **kwargs))
|
||||||
|
|
||||||
|
|
||||||
|
@celery_app.task(base=AsyncTask, name="app.workers.tasks.process_mail_account")
|
||||||
|
async def process_mail_account(account_id: int):
|
||||||
|
"""
|
||||||
|
Process a single mail account - fetch and forward emails.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
account_id: ID of mail account to process
|
||||||
|
"""
|
||||||
|
async with async_session_maker() as db:
|
||||||
|
try:
|
||||||
|
# Get account
|
||||||
|
result = await db.execute(
|
||||||
|
select(MailAccount).where(MailAccount.id == account_id)
|
||||||
|
)
|
||||||
|
account = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not account or not account.is_enabled:
|
||||||
|
logger.warning(f"Account {account_id} not found or disabled")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create processing run
|
||||||
|
run = ProcessingRun(
|
||||||
|
mail_account_id=account.id,
|
||||||
|
started_at=datetime.utcnow(),
|
||||||
|
status="running"
|
||||||
|
)
|
||||||
|
db.add(run)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(run)
|
||||||
|
|
||||||
|
# Decrypt password
|
||||||
|
password = decrypt_credential(account.encrypted_password)
|
||||||
|
|
||||||
|
# Create processor
|
||||||
|
processor = MailProcessor(account, password)
|
||||||
|
|
||||||
|
# Fetch emails
|
||||||
|
emails = await processor.fetch_emails(account.max_emails_per_check)
|
||||||
|
|
||||||
|
run.emails_fetched = len(emails)
|
||||||
|
|
||||||
|
# Forward emails
|
||||||
|
emails_forwarded = 0
|
||||||
|
emails_failed = 0
|
||||||
|
|
||||||
|
# TODO: Get SMTP config from user settings or environment
|
||||||
|
smtp_config = {
|
||||||
|
"host": "smtp.gmail.com",
|
||||||
|
"port": 587,
|
||||||
|
"username": "smtp_user@gmail.com", # Should come from config
|
||||||
|
"password": "smtp_password", # Should come from config
|
||||||
|
"use_tls": True
|
||||||
|
}
|
||||||
|
|
||||||
|
for email_data in emails:
|
||||||
|
try:
|
||||||
|
success = await MailProcessor.forward_email(
|
||||||
|
email_data,
|
||||||
|
account.name,
|
||||||
|
account.forward_to,
|
||||||
|
smtp_config
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
emails_forwarded += 1
|
||||||
|
else:
|
||||||
|
emails_failed += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error forwarding email: {e}")
|
||||||
|
emails_failed += 1
|
||||||
|
|
||||||
|
# Update run
|
||||||
|
run.emails_forwarded = emails_forwarded
|
||||||
|
run.emails_failed = emails_failed
|
||||||
|
run.completed_at = datetime.utcnow()
|
||||||
|
run.duration_seconds = (run.completed_at - run.started_at).total_seconds()
|
||||||
|
run.status = "completed" if emails_failed == 0 else "partial_failure"
|
||||||
|
|
||||||
|
# Update account
|
||||||
|
account.total_emails_processed += emails_forwarded
|
||||||
|
account.total_emails_failed += emails_failed
|
||||||
|
account.last_check_at = datetime.utcnow()
|
||||||
|
|
||||||
|
if emails_failed == 0:
|
||||||
|
account.last_successful_check_at = datetime.utcnow()
|
||||||
|
account.status = AccountStatus.ACTIVE
|
||||||
|
else:
|
||||||
|
account.status = AccountStatus.ERROR
|
||||||
|
account.last_error_at = datetime.utcnow()
|
||||||
|
account.last_error_message = f"{emails_failed} emails failed to forward"
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Processed account {account.id}: "
|
||||||
|
f"{emails_forwarded} forwarded, {emails_failed} failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing account {account_id}: {e}")
|
||||||
|
|
||||||
|
# Mark run as failed
|
||||||
|
if 'run' in locals():
|
||||||
|
run.status = "failed"
|
||||||
|
run.error_message = str(e)
|
||||||
|
run.completed_at = datetime.utcnow()
|
||||||
|
run.duration_seconds = (run.completed_at - run.started_at).total_seconds()
|
||||||
|
|
||||||
|
# Update account error status
|
||||||
|
if 'account' in locals():
|
||||||
|
account.status = AccountStatus.ERROR
|
||||||
|
account.last_error_at = datetime.utcnow()
|
||||||
|
account.last_error_message = str(e)
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@celery_app.task(base=AsyncTask, name="app.workers.tasks.process_all_enabled_accounts")
|
||||||
|
async def process_all_enabled_accounts():
|
||||||
|
"""
|
||||||
|
Process all enabled mail accounts.
|
||||||
|
This task is scheduled to run periodically.
|
||||||
|
"""
|
||||||
|
async with async_session_maker() as db:
|
||||||
|
try:
|
||||||
|
# Get all enabled accounts
|
||||||
|
result = await db.execute(
|
||||||
|
select(MailAccount).where(
|
||||||
|
and_(
|
||||||
|
MailAccount.is_enabled == True,
|
||||||
|
MailAccount.status.in_([AccountStatus.ACTIVE, AccountStatus.TESTING])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
accounts = result.scalars().all()
|
||||||
|
|
||||||
|
logger.info(f"Processing {len(accounts)} enabled mail accounts")
|
||||||
|
|
||||||
|
# Process each account
|
||||||
|
for account in accounts:
|
||||||
|
# Check if it's time to check this account
|
||||||
|
if account.last_check_at:
|
||||||
|
time_since_last_check = datetime.utcnow() - account.last_check_at
|
||||||
|
if time_since_last_check.total_seconds() < (account.check_interval_minutes * 60):
|
||||||
|
logger.debug(f"Skipping account {account.id} - not time yet")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Queue processing task
|
||||||
|
process_mail_account.delay(account.id)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing accounts: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@celery_app.task(base=AsyncTask, name="app.workers.tasks.cleanup_old_logs")
|
||||||
|
async def cleanup_old_logs(days_to_keep: int = 30):
|
||||||
|
"""
|
||||||
|
Clean up old processing logs and runs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
days_to_keep: Number of days of logs to retain
|
||||||
|
"""
|
||||||
|
async with async_session_maker() as db:
|
||||||
|
try:
|
||||||
|
cutoff_date = datetime.utcnow() - timedelta(days=days_to_keep)
|
||||||
|
|
||||||
|
# Delete old processing runs
|
||||||
|
result = await db.execute(
|
||||||
|
select(ProcessingRun).where(ProcessingRun.started_at < cutoff_date)
|
||||||
|
)
|
||||||
|
old_runs = result.scalars().all()
|
||||||
|
|
||||||
|
for run in old_runs:
|
||||||
|
await db.delete(run)
|
||||||
|
|
||||||
|
# Delete old processing logs
|
||||||
|
result = await db.execute(
|
||||||
|
select(ProcessingLog).where(ProcessingLog.timestamp < cutoff_date)
|
||||||
|
)
|
||||||
|
old_logs = result.scalars().all()
|
||||||
|
|
||||||
|
for log in old_logs:
|
||||||
|
await db.delete(log)
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Cleaned up {len(old_runs)} old processing runs and "
|
||||||
|
f"{len(old_logs)} old logs"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error cleaning up logs: {e}")
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# PostgreSQL Database
|
||||||
|
postgres:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
container_name: pop3-postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: password
|
||||||
|
POSTGRES_DB: pop3_forwarder
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Redis for caching and Celery
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: pop3-redis
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# FastAPI Backend
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: pop3-backend
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
env_file:
|
||||||
|
- ./backend/.env
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- ./backend:/app
|
||||||
|
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Celery Worker for background email processing
|
||||||
|
celery-worker:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: pop3-celery-worker
|
||||||
|
env_file:
|
||||||
|
- ./backend/.env
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
- backend
|
||||||
|
volumes:
|
||||||
|
- ./backend:/app
|
||||||
|
command: celery -A app.workers.celery_app worker --loglevel=info
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Celery Beat for scheduling
|
||||||
|
celery-beat:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: pop3-celery-beat
|
||||||
|
env_file:
|
||||||
|
- ./backend/.env
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
- backend
|
||||||
|
volumes:
|
||||||
|
- ./backend:/app
|
||||||
|
command: celery -A app.workers.celery_app beat --loglevel=info
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Frontend (React/Next.js) - to be implemented
|
||||||
|
# frontend:
|
||||||
|
# build:
|
||||||
|
# context: ./frontend
|
||||||
|
# dockerfile: Dockerfile
|
||||||
|
# container_name: pop3-frontend
|
||||||
|
# ports:
|
||||||
|
# - "3000:3000"
|
||||||
|
# depends_on:
|
||||||
|
# - backend
|
||||||
|
# volumes:
|
||||||
|
# - ./frontend:/app
|
||||||
|
# - /app/node_modules
|
||||||
|
# restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
redis_data:
|
||||||
Reference in New Issue
Block a user