commit 2f98057b43019b31b11041757fc6f35afe4a9143 Author: oguz ozturk Date: Sun Jan 11 17:38:39 2026 +0300 Initial commit: Customer panel diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..68cb837 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,201 @@ +# 🏗️ Hosting Platform - Modular Architecture + +## 📋 Project Overview + +Professional WordPress hosting platform with container infrastructure and automation. + +--- + +## 🎯 Architecture Principles + +1. **Modularity** - Each feature is a separate module +2. **Scalability** - Easy to add new features without breaking existing code +3. **Separation of Concerns** - Clear boundaries between layers +4. **Security First** - Authentication, encryption, and access control +5. **Clean Code** - Readable, maintainable, and well-documented + +--- + +## 🏛️ System Architecture + +### Frontend Architecture + +``` +frontend/ +├── customer-portal/ # argeict.net +│ ├── src/ +│ │ ├── pages/ +│ │ │ ├── Landing.jsx # Register/Login page +│ │ │ ├── Dashboard.jsx # Customer dashboard +│ │ │ ├── DNS/ # DNS module +│ │ │ ├── Container/ # Container module +│ │ │ ├── Network/ # Network module +│ │ │ └── Security/ # Security module +│ │ ├── components/ +│ │ │ ├── auth/ # Auth components +│ │ │ ├── layout/ # Layout components +│ │ │ └── shared/ # Shared components +│ │ ├── services/ +│ │ │ ├── api.js # API client +│ │ │ ├── auth.js # Auth service +│ │ │ └── storage.js # Local storage +│ │ ├── hooks/ # Custom React hooks +│ │ ├── context/ # React context +│ │ └── utils/ # Utility functions +│ └── public/ +│ +├── admin-portal/ # adminpanel.argeict.net +│ ├── src/ +│ │ ├── pages/ +│ │ │ ├── Login.jsx # Admin login +│ │ │ ├── Dashboard.jsx # Admin dashboard +│ │ │ ├── CFAccounts/ # CF management +│ │ │ ├── Customers/ # Customer management +│ │ │ └── Settings/ # System settings +│ │ ├── components/ +│ │ ├── services/ +│ │ └── utils/ +│ └── public/ +│ +└── shared/ # Shared code between portals + ├── components/ + ├── utils/ + └── styles/ +``` + +### Backend Architecture + +``` +backend/ +├── app/ +│ ├── __init__.py +│ ├── main.py # Application entry point +│ ├── config.py # Configuration +│ │ +│ ├── models/ # Database models +│ │ ├── __init__.py +│ │ ├── user.py # User/Customer model +│ │ ├── domain.py # Domain model +│ │ ├── cloudflare.py # CF Account model +│ │ ├── container.py # Container model +│ │ └── base.py # Base model +│ │ +│ ├── routes/ # API routes (blueprints) +│ │ ├── __init__.py +│ │ ├── auth.py # Authentication routes +│ │ ├── customer.py # Customer routes +│ │ ├── dns.py # DNS routes +│ │ ├── container.py # Container routes +│ │ ├── admin.py # Admin routes +│ │ └── webhook.py # Webhook routes +│ │ +│ ├── services/ # Business logic +│ │ ├── __init__.py +│ │ ├── auth_service.py # Authentication logic +│ │ ├── cloudflare_service.py # Cloudflare API +│ │ ├── nameserver_service.py # DNS/NS logic +│ │ ├── container_service.py # Container management +│ │ └── email_service.py # Email notifications +│ │ +│ ├── middleware/ # Middleware +│ │ ├── __init__.py +│ │ ├── auth.py # Auth middleware +│ │ ├── rate_limit.py # Rate limiting +│ │ └── cors.py # CORS configuration +│ │ +│ ├── utils/ # Utilities +│ │ ├── __init__.py +│ │ ├── encryption.py # Encryption helpers +│ │ ├── validators.py # Input validation +│ │ └── helpers.py # General helpers +│ │ +│ └── migrations/ # Database migrations +│ +├── tests/ # Tests +│ ├── unit/ +│ ├── integration/ +│ └── e2e/ +│ +└── requirements.txt +``` + +--- + +## 🗄️ Database Schema + +### Users Table +- id, email, password_hash, full_name +- is_active, is_verified, created_at, updated_at +- role (customer, admin) + +### Customers Table (extends Users) +- user_id (FK), company_name, phone +- billing_info, subscription_plan + +### CloudflareAccounts Table +- id, name, email, api_token_encrypted +- is_active, max_domains, current_domain_count + +### Domains Table +- id, domain_name, customer_id (FK) +- cf_account_id (FK), cf_zone_id +- status, created_at + +### Containers Table (Future) +- id, customer_id (FK), domain_id (FK) +- container_id, status, resources + +--- + +## 🎨 Design System + +### Brand Colors (from logo) +- Primary: #0066CC (Blue) +- Secondary: #00A3E0 (Light Blue) +- Accent: #FF6B35 (Orange) +- Dark: #1A1A1A +- Light: #F5F5F5 + +### Typography +- Headings: Inter, sans-serif +- Body: Inter, sans-serif + +--- + +## 🔐 Security + +1. **Authentication**: JWT tokens +2. **Password**: bcrypt hashing +3. **API Tokens**: Fernet encryption +4. **HTTPS**: All communications +5. **Rate Limiting**: Per endpoint +6. **CORS**: Configured per domain + +--- + +## 📦 Technology Stack + +### Frontend +- React 18 + Vite +- React Router (multi-app routing) +- TailwindCSS +- Axios +- React Query (data fetching) + +### Backend +- Flask 3.0 +- SQLAlchemy 2.0 +- PostgreSQL 16 +- Redis 7.0 +- JWT authentication + +### DevOps +- Docker (containers) +- Nginx (reverse proxy) +- Supervisor (process management) +- Gitea (version control) + +--- + +**Next Steps**: Implementation in phases + diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..090aec3 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,257 @@ +# 🚀 Deployment Guide - Hosting Platform + +## 📊 Server Information + +**Server IP**: `176.96.129.77` +**OS**: Ubuntu 24.04 LTS +**RAM**: 4GB +**CPU**: 4 cores + +--- + +## 🌐 Production URLs + +| Service | URL | Status | +|---------|-----|--------| +| **Frontend** | https://argeict.net | ✅ Running | +| **Backend API** | https://api.argeict.net | ✅ Running | +| **Gitea** | https://gitea.argeict.net | ✅ Running | + +--- + +## 🎯 Deployed Services + +### ✅ Core Services + +| Service | Port | Internal URL | Status | +|---------|------|--------------|--------| +| **Frontend (React + Vite)** | 3001 | http://127.0.0.1:3001 | ✅ Running | +| **Backend API (Flask)** | 5000 | http://127.0.0.1:5000 | ✅ Running | +| **Gitea** | 3000 | http://127.0.0.1:3000 | ✅ Running | +| **PostgreSQL** | 5432 | localhost:5432 | ✅ Running | +| **Redis** | 6379 | localhost:6379 | ✅ Running | +| **Nginx (HTTPS)** | 443 | - | ✅ Running | +| **Nginx (HTTP → HTTPS)** | 80 | - | ✅ Running | + +### 🔐 Credentials + +**Gitea Admin**: +- Username: `hostadmin` +- Password: `HostAdmin2024!` +- Repository: https://gitea.argeict.net/hostadmin/hosting-platform + +**PostgreSQL**: +- User: `hosting_user` +- Password: `HostingDB2024!` +- Database: `hosting_db` + +**Redis**: +- No password (localhost only) + +**SSL Certificates (Let's Encrypt)**: +- Certificate: `/etc/letsencrypt/live/argeict.net/fullchain.pem` +- Private Key: `/etc/letsencrypt/live/argeict.net/privkey.pem` +- Domains: `argeict.net`, `api.argeict.net`, `gitea.argeict.net` +- Expires: `2026-04-10` (Auto-renewal enabled via certbot timer) + +--- + +## 🏗️ Architecture + +``` +Internet + │ + ▼ +┌─────────────────────────────────────┐ +│ Nginx Reverse Proxy (Port 80) │ +│ - Frontend: / │ +│ - Backend API: /api │ +│ - Webhook: /webhook │ +└─────────────────────────────────────┘ + │ + ├──────────────┬──────────────┐ + ▼ ▼ ▼ +┌─────────┐ ┌──────────┐ ┌──────────┐ +│Frontend │ │ Backend │ │ Gitea │ +│ :3001 │ │ :5000 │ │ :3000 │ +└─────────┘ └──────────┘ └──────────┘ + │ + ┌─────────┴─────────┐ + ▼ ▼ + ┌──────────┐ ┌─────────┐ + │PostgreSQL│ │ Redis │ + │ :5432 │ │ :6379 │ + └──────────┘ └─────────┘ +``` + +--- + +## 📁 Directory Structure + +``` +/opt/hosting-platform/ +├── backend/ +│ ├── app/ +│ │ ├── __init__.py +│ │ ├── main.py +│ │ ├── config.py +│ │ ├── models/ +│ │ │ ├── __init__.py +│ │ │ └── domain.py +│ │ ├── services/ +│ │ │ ├── __init__.py +│ │ │ └── cloudflare_service.py +│ │ └── api/ +│ ├── venv/ +│ └── requirements.txt +├── frontend/ +│ ├── src/ +│ │ ├── App.jsx +│ │ ├── main.jsx +│ │ ├── pages/ +│ │ │ ├── DomainSetup.jsx +│ │ │ └── DomainList.jsx +│ │ └── services/ +│ │ └── api.js +│ ├── package.json +│ └── vite.config.js +└── deploy.sh +``` + +--- + +## 🔄 Auto-Deploy Workflow + +1. **Developer pushes code** to `main` branch +2. **Gitea webhook** triggers → `POST http://176.96.129.77:5000/webhook/deploy` +3. **Backend receives webhook** → Executes `/opt/hosting-platform/deploy.sh` +4. **Deploy script**: + - Pulls latest code from Git + - Installs dependencies + - Restarts services via Supervisor + +--- + +## 🛠️ Management Commands + +### Supervisor (Process Management) + +```bash +# Check status +supervisorctl status + +# Restart services +supervisorctl restart hosting-backend +supervisorctl restart hosting-frontend + +# View logs +tail -f /var/log/hosting-backend.log +tail -f /var/log/hosting-frontend.log + +# Stop/Start +supervisorctl stop hosting-backend +supervisorctl start hosting-backend +``` + +### Nginx + +```bash +# Test configuration +nginx -t + +# Reload +systemctl reload nginx + +# Restart +systemctl restart nginx + +# View logs +tail -f /var/log/nginx/access.log +tail -f /var/log/nginx/error.log +``` + +### Database + +```bash +# Connect to PostgreSQL +psql -U hosting_user -d hosting_db + +# Connect to Redis +redis-cli +``` + +--- + +## 🧪 Testing + +### Health Check +```bash +curl https://api.argeict.net/health +``` + +### API Test +```bash +curl https://api.argeict.net/api/domains +``` + +### Frontend +Open browser: https://argeict.net + +### Gitea +Open browser: https://gitea.argeict.net + +### SSL Certificate Check +```bash +openssl s_client -connect argeict.net:443 -servername argeict.net < /dev/null 2>/dev/null | openssl x509 -noout -dates +``` + +--- + +## 📝 Next Steps + +1. ✅ **Add SSL Certificate** (Let's Encrypt) +2. ✅ **Configure Domain Name** +3. ✅ **Set up Monitoring** (Prometheus/Grafana) +4. ✅ **Add Backup System** +5. ✅ **Implement Authentication** + +--- + +## 🆘 Troubleshooting + +### Backend not starting +```bash +# Check logs +tail -f /var/log/hosting-backend.log + +# Check if port is in use +lsof -i :5000 + +# Restart +supervisorctl restart hosting-backend +``` + +### Frontend not loading +```bash +# Check logs +tail -f /var/log/hosting-frontend.log + +# Restart +supervisorctl restart hosting-frontend +``` + +### Database connection issues +```bash +# Check PostgreSQL status +systemctl status postgresql + +# Check connections +psql -U hosting_user -d hosting_db -c "SELECT * FROM pg_stat_activity;" +``` + +--- + +**Deployment Date**: 2026-01-10 +**Version**: 1.0.0 +**Deployed By**: Hosting Platform Team + diff --git a/DEPLOYMENT_SUMMARY.md b/DEPLOYMENT_SUMMARY.md new file mode 100644 index 0000000..5bad7cd --- /dev/null +++ b/DEPLOYMENT_SUMMARY.md @@ -0,0 +1,155 @@ +# 🚀 Deployment Summary - 2026-01-10 + +## ✅ Deployment Status: SUCCESS + +### 📊 Deployment Details +- **Date**: 2026-01-10 12:26 UTC +- **Server**: 176.96.129.77 (argeict.net) +- **Git Commit**: f544674 +- **Deployment Method**: SSH + Supervisor + +--- + +## 🎯 What Was Deployed + +### Backend Changes +- ✅ New Admin Routes (`/api/admin/cf-accounts`) +- ✅ DNS Nameserver Checker (`/api/dns/check-nameservers`) +- ✅ Encrypted CF Token Storage (Fernet encryption) +- ✅ Nameserver Service (dnspython integration) +- ✅ Database models updated (CloudflareAccount) +- ✅ New dependencies: `dnspython==2.4.2` + +### Frontend Changes +- ✅ Admin CF Accounts Page +- ✅ CF Account Modal (Add/Edit) +- ✅ CF Token Guide Component +- ✅ Nameserver Instructions Component +- ✅ Domain Setup (New) Page +- ✅ Enhanced API service layer + +--- + +## 🔧 Configuration Changes + +### Environment Variables +```bash +ENCRYPTION_KEY=tThpEL7KeYwGSg9isM7LUbxv-Lju325c2gtIf56DHV4= +DATABASE_URL=postgresql://hosting:hosting_519c6c66a8e2695ce704ccba@localhost:5432/hosting +FLASK_ENV=production +SECRET_KEY=cfef4ad2f52832def87c20ebddb5067c44379c5ab366ebeb50217b5f484a92df +``` + +### Supervisor Configuration +- Updated to include all environment variables +- Added PYTHONPATH for proper module resolution +- Both services running successfully + +--- + +## 🧪 Test Results + +### API Health Checks +```json +✅ GET /health +{ + "service": "hosting-platform-api", + "status": "ok" +} + +✅ GET /api/admin/cf-accounts +{ + "accounts": [], + "count": 0, + "status": "success" +} + +✅ POST /api/dns/check-nameservers +{ + "current_nameservers": ["ns1.google.com", ...], + "is_cloudflare": false, + "status": "error" +} +``` + +### Service Status +``` +hosting-backend RUNNING pid 18670 +hosting-frontend RUNNING pid 19155 +``` + +--- + +## 🌐 Live URLs + +- **Frontend**: https://argeict.net +- **API**: https://api.argeict.net +- **Gitea**: https://gitea.argeict.net + +--- + +## 📝 Post-Deployment Tasks + +### ✅ Completed +- [x] SSH key authentication configured +- [x] Database password updated +- [x] Environment variables configured +- [x] Supervisor config updated +- [x] Backend dependencies installed +- [x] Frontend built and deployed +- [x] Services restarted +- [x] Health checks passed + +### 📋 Next Steps +1. Test CF Account Management in admin panel +2. Add first Cloudflare account +3. Test domain setup with new wizard +4. Monitor logs for any issues +5. Update documentation if needed + +--- + +## 🔍 Troubleshooting + +### View Logs +```bash +ssh root@176.96.129.77 'tail -f /var/log/hosting-backend.log' +``` + +### Restart Services +```bash +ssh root@176.96.129.77 'supervisorctl restart hosting-backend hosting-frontend' +``` + +### Check Service Status +```bash +ssh root@176.96.129.77 'supervisorctl status' +``` + +--- + +## 📚 Documentation Updates + +- ✅ README.md updated with new features +- ✅ API endpoints documented +- ✅ Deployment script created (`deploy.sh`) +- ✅ Manual deployment instructions added + +--- + +## 🎉 Success Metrics + +- **Deployment Time**: ~15 minutes +- **Downtime**: ~30 seconds (service restart) +- **Issues Encountered**: 3 (all resolved) + 1. Database password mismatch → Fixed + 2. Missing dnspython dependency → Installed + 3. Supervisor environment config → Updated +- **Final Status**: ✅ All systems operational + +--- + +**Deployed by**: Augment Agent +**Deployment Script**: `./deploy.sh` +**Next Deployment**: Use `./deploy.sh` for automated deployment + diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..8fe4448 --- /dev/null +++ b/IMPLEMENTATION_PLAN.md @@ -0,0 +1,176 @@ +# 🚀 Implementation Plan - Hosting Platform + +## Phase 1: Foundation & Authentication (Week 1) + +### Backend Tasks +- [ ] 1.1 Create User model with authentication +- [ ] 1.2 Implement JWT authentication service +- [ ] 1.3 Create auth routes (register, login, logout, verify) +- [ ] 1.4 Add auth middleware for protected routes +- [ ] 1.5 Database migrations for users table +- [ ] 1.6 Password reset functionality + +### Frontend Tasks +- [ ] 1.7 Setup project structure (customer-portal, admin-portal) +- [ ] 1.8 Create Landing page with animations +- [ ] 1.9 Create Register/Login forms +- [ ] 1.10 Implement auth context and hooks +- [ ] 1.11 Protected route wrapper +- [ ] 1.12 Brand colors and design system + +### Testing +- [ ] 1.13 Unit tests for auth service +- [ ] 1.14 Integration tests for auth endpoints +- [ ] 1.15 E2E tests for registration flow + +--- + +## Phase 2: Customer Dashboard & DNS Module (Week 2) + +### Backend Tasks +- [ ] 2.1 Refactor existing DNS routes to use auth +- [ ] 2.2 Link domains to customer_id +- [ ] 2.3 Customer-specific domain listing +- [ ] 2.4 Update CF account selection logic + +### Frontend Tasks +- [ ] 2.5 Create customer dashboard layout +- [ ] 2.6 Sidebar navigation (DNS, Container, Network, Security) +- [ ] 2.7 DNS management page (refactor existing) +- [ ] 2.8 Project creation wizard +- [ ] 2.9 Domain list with filters +- [ ] 2.10 Responsive design + +### Testing +- [ ] 2.11 Test DNS operations with auth +- [ ] 2.12 Test customer isolation + +--- + +## Phase 3: Admin Portal (Week 3) + +### Backend Tasks +- [ ] 3.1 Admin role and permissions +- [ ] 3.2 Admin-only middleware +- [ ] 3.3 Customer management endpoints +- [ ] 3.4 System statistics endpoints + +### Frontend Tasks +- [ ] 3.5 Separate admin portal app +- [ ] 3.6 Admin login page +- [ ] 3.7 Admin dashboard +- [ ] 3.8 CF Accounts management (refactor existing) +- [ ] 3.9 Customer management interface +- [ ] 3.10 System settings page + +### Deployment +- [ ] 3.11 Configure adminpanel.argeict.net subdomain +- [ ] 3.12 Nginx configuration for multi-app +- [ ] 3.13 Build and deploy both portals + +--- + +## Phase 4: Container Module (Week 4-5) + +### Backend Tasks +- [ ] 4.1 Container model and database schema +- [ ] 4.2 Docker integration service +- [ ] 4.3 WordPress container templates +- [ ] 4.4 Container lifecycle management +- [ ] 4.5 Resource monitoring + +### Frontend Tasks +- [ ] 4.6 Container management page +- [ ] 4.7 Container creation wizard +- [ ] 4.8 Container status dashboard +- [ ] 4.9 Resource usage charts + +--- + +## Phase 5: Network & Security Modules (Week 6) + +### Backend Tasks +- [ ] 5.1 Network configuration endpoints +- [ ] 5.2 SSL certificate management +- [ ] 5.3 Firewall rules API +- [ ] 5.4 Security scanning integration + +### Frontend Tasks +- [ ] 5.5 Network management interface +- [ ] 5.6 Security dashboard +- [ ] 5.7 SSL certificate viewer +- [ ] 5.8 Security recommendations + +--- + +## Phase 6: Polish & Production (Week 7-8) + +### Features +- [ ] 6.1 Email notifications +- [ ] 6.2 Billing integration +- [ ] 6.3 Usage analytics +- [ ] 6.4 API documentation +- [ ] 6.5 User documentation + +### Testing & QA +- [ ] 6.6 Full system testing +- [ ] 6.7 Performance optimization +- [ ] 6.8 Security audit +- [ ] 6.9 Load testing + +### Deployment +- [ ] 6.10 Production deployment +- [ ] 6.11 Monitoring setup +- [ ] 6.12 Backup system +- [ ] 6.13 CI/CD pipeline + +--- + +## Current Sprint: Phase 1 - Foundation & Authentication + +### Immediate Tasks (Today) + +1. **Backend: User Model & Auth** + - Create User model + - Implement JWT service + - Create auth routes + +2. **Frontend: Project Structure** + - Setup customer-portal + - Setup admin-portal + - Create Landing page + +3. **Design System** + - Extract brand colors from logo + - Create TailwindCSS config + - Design component library + +--- + +## Success Criteria + +### Phase 1 +- ✅ Users can register and login +- ✅ JWT authentication working +- ✅ Landing page with animations +- ✅ Brand identity applied + +### Phase 2 +- ✅ Customer dashboard functional +- ✅ DNS management integrated +- ✅ Customer isolation working + +### Phase 3 +- ✅ Admin portal deployed +- ✅ CF account management +- ✅ Customer management + +### Phase 4-6 +- ✅ Container management +- ✅ Full feature set +- ✅ Production ready + +--- + +**Let's start with Phase 1!** + diff --git a/README.md b/README.md new file mode 100644 index 0000000..7051297 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# 🚀 Hosting Platform - Professional WordPress Hosting + +Modern, modular hosting platform with container infrastructure and automated DNS management. + +## 🌐 Live URLs + +- **Customer Portal**: https://argeict.net +- **API Backend**: https://api.argeict.net +- **Git Repository**: https://gitea.argeict.net/hostadmin/hosting-platform + +## ✅ Phase 1: Foundation & Authentication (COMPLETED) + +### Backend Features +- ✅ User authentication with JWT tokens +- ✅ User and Customer models with SQLAlchemy +- ✅ Secure password hashing with bcrypt +- ✅ Protected API routes with decorators +- ✅ Customer profile management +- ✅ Subscription plans and limits + +### Frontend Features +- ✅ Beautiful landing page with animations +- ✅ Register/Login functionality +- ✅ Customer dashboard with sidebar navigation +- ✅ Auth context for global state management +- ✅ Protected routes +- ✅ Brand colors and design system from ARGE ICT logo + +## 🎨 Design System + +### Brand Colors (from ARGE ICT logo) +- **Primary Green**: #159052 +- **Dark Green**: #046D3F +- **Light Green**: #53BA6F +- **Orange**: #F69036 +- **Blue**: #0F578B +- **Red**: #B42832 + +## 🏗️ Tech Stack + +**Backend**: Flask 3.0, SQLAlchemy 2.0, PostgreSQL, JWT, Redis +**Frontend**: React 18, Vite, TailwindCSS, React Router +**DevOps**: Nginx, Supervisor, Let's Encrypt, Gitea + +## 📋 Next Steps (Phase 2) + +- Customer Dashboard - DNS Module integration +- Domain management with customer isolation +- Project creation wizard + +## 📝 License + +© 2026 ARGE ICT. All rights reserved. diff --git a/WEBHOOK_SETUP.md b/WEBHOOK_SETUP.md new file mode 100644 index 0000000..29a2d0a --- /dev/null +++ b/WEBHOOK_SETUP.md @@ -0,0 +1,228 @@ +# 🔗 Webhook Setup Guide + +## ✅ Webhook Status + +- **Endpoint**: `https://api.argeict.net/webhook/deploy` +- **Method**: POST +- **Status**: ✅ Working +- **Last Test**: 2026-01-10 12:39:04 UTC + +--- + +## 📝 Gitea Webhook Configuration + +### Step 1: Access Gitea Repository Settings + +1. Go to: https://gitea.argeict.net/hostadmin/hosting-platform +2. Click on **Settings** (top right) +3. Click on **Webhooks** in the left sidebar + +### Step 2: Add New Webhook + +1. Click **Add Webhook** button +2. Select **Gitea** from the dropdown + +### Step 3: Configure Webhook + +Fill in the following details: + +``` +Target URL: https://api.argeict.net/webhook/deploy +HTTP Method: POST +POST Content Type: application/json +Secret: (leave empty for now) +``` + +**Trigger On:** +- ✅ Push events (checked) +- Branch filter: `main` + +**Active:** +- ✅ Active (checked) + +### Step 4: Save and Test + +1. Click **Add Webhook** button +2. The webhook will appear in the list +3. Click on the webhook to view details +4. Click **Test Delivery** button +5. Check the response - should see: + +```json +{ + "status": "success", + "message": "Deployment triggered successfully", + "repository": "hosting-platform", + "pusher": "hostadmin", + "timestamp": "2026-01-10T12:39:04.822854", + "note": "Check /var/log/auto-deploy.log for deployment progress" +} +``` + +--- + +## 🧪 Manual Testing + +Test the webhook manually with curl: + +```bash +curl -X POST https://api.argeict.net/webhook/deploy \ + -H "Content-Type: application/json" \ + -d '{ + "repository": { + "name": "hosting-platform" + }, + "pusher": { + "username": "test-user" + } + }' +``` + +Expected response (HTTP 202): +```json +{ + "status": "success", + "message": "Deployment triggered successfully", + "repository": "hosting-platform", + "pusher": "test-user", + "timestamp": "2026-01-10T12:39:04.822854", + "note": "Check /var/log/auto-deploy.log for deployment progress" +} +``` + +--- + +## 📊 How It Works + +``` +┌─────────────┐ +│ Developer │ +│ git push │ +└──────┬──────┘ + │ + ▼ +┌─────────────────┐ +│ Gitea Server │ +│ (detects push) │ +└──────┬──────────┘ + │ + │ POST /webhook/deploy + ▼ +┌──────────────────────┐ +│ Backend API │ +│ (receives webhook) │ +└──────┬───────────────┘ + │ + │ Triggers async + ▼ +┌────────────────────────────┐ +│ deploy-local.sh │ +│ 1. git pull │ +│ 2. install dependencies │ +│ 3. database migration │ +│ 4. build frontend │ +│ 5. restart services │ +└────────────────────────────┘ + │ + ▼ +┌─────────────────┐ +│ Deployment │ +│ Complete! ✅ │ +└─────────────────┘ +``` + +--- + +## 🔍 Monitoring Deployments + +### View Deployment Logs + +```bash +# Real-time deployment logs +ssh root@176.96.129.77 'tail -f /var/log/auto-deploy.log' + +# Last 50 lines +ssh root@176.96.129.77 'tail -50 /var/log/auto-deploy.log' +``` + +### View Backend Logs + +```bash +ssh root@176.96.129.77 'tail -f /var/log/hosting-backend.log' +``` + +### Check Service Status + +```bash +ssh root@176.96.129.77 'supervisorctl status' +``` + +--- + +## 🛠️ Troubleshooting + +### Webhook Returns Error + +1. Check backend logs: + ```bash + ssh root@176.96.129.77 'tail -100 /var/log/hosting-backend.log' + ``` + +2. Verify deployment script exists: + ```bash + ssh root@176.96.129.77 'ls -la /opt/hosting-platform/deploy-local.sh' + ``` + +3. Test deployment script manually: + ```bash + ssh root@176.96.129.77 '/opt/hosting-platform/deploy-local.sh' + ``` + +### Deployment Fails + +1. Check deployment logs: + ```bash + ssh root@176.96.129.77 'cat /var/log/auto-deploy.log' + ``` + +2. Check for git issues: + ```bash + ssh root@176.96.129.77 'cd /opt/hosting-platform && git status' + ``` + +3. Restart services manually: + ```bash + ssh root@176.96.129.77 'supervisorctl restart hosting-backend hosting-frontend' + ``` + +--- + +## 🔐 Security (Optional) + +To add webhook secret validation: + +1. Generate a secret: + ```bash + openssl rand -hex 32 + ``` + +2. Add to Gitea webhook configuration (Secret field) + +3. Update backend code to validate the secret + +--- + +## ✅ Verification Checklist + +- [ ] Webhook added in Gitea +- [ ] Test Delivery successful (green checkmark) +- [ ] Push to main branch triggers deployment +- [ ] Deployment logs show successful completion +- [ ] Services restart automatically +- [ ] Frontend and backend updated + +--- + +**Last Updated**: 2026-01-10 +**Maintained By**: Hosting Platform Team + diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..a17e5dd --- /dev/null +++ b/backend/.env @@ -0,0 +1,13 @@ +# Database +DATABASE_URL=sqlite:///hosting.db + +# Encryption +ENCRYPTION_KEY=test_key_for_development_only_change_in_production + +# Flask +FLASK_ENV=development +SECRET_KEY=dev_secret_key_change_in_production + +# Cloudflare (optional - for testing) +# CLOUDFLARE_API_TOKEN=your_token_here + diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..02288ac --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,23 @@ +# Flask Configuration +SECRET_KEY=dev-secret-key-change-in-production + +# Database +DATABASE_URL=postgresql://hosting:hosting_pass_2024@localhost:5432/hosting + +# Redis +REDIS_URL=redis://localhost:6379/0 + +# Load Balancer IPs (comma separated) +LB_IPS=176.96.129.77 + +# API Configuration +API_HOST=0.0.0.0 +API_PORT=5000 + +# Encryption Key (REQUIRED - Generate with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())") +ENCRYPTION_KEY=qcaGX4ChgOqDRmfxaikZYYJJ_qYZUDx2nRWVVGHr4sM= + +# Cloudflare Platform Accounts (DEPRECATED - Use database instead) +# PLATFORM_CF_API_TOKEN=your_token_here +# PLATFORM_CF_ACCOUNT_ID=your_account_id_here + diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/__pycache__/__init__.cpython-313.pyc b/backend/app/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..d650cbf Binary files /dev/null and b/backend/app/__pycache__/__init__.cpython-313.pyc differ diff --git a/backend/app/__pycache__/main.cpython-313.pyc b/backend/app/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000..cb365cd Binary files /dev/null and b/backend/app/__pycache__/main.cpython-313.pyc differ diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..de7cc8a --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,32 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + +class Config: + # Flask + SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-key-change-in-production") + + # Database + SQLALCHEMY_DATABASE_URI = os.getenv( + "DATABASE_URL", + "postgresql://hosting:hosting_pass_2024@localhost:5432/hosting" + ) + SQLALCHEMY_TRACK_MODIFICATIONS = False + + # Redis + REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0") + + # Load Balancer IPs + LB_IPS = os.getenv("LB_IPS", "176.96.129.77").split(",") + + # API + API_HOST = os.getenv("API_HOST", "0.0.0.0") + API_PORT = int(os.getenv("API_PORT", 5000)) + + # Encryption (for sensitive data like API tokens) + ENCRYPTION_KEY = os.getenv("ENCRYPTION_KEY") + + # Cloudflare Platform Account (opsiyonel - deprecated, use database instead) + PLATFORM_CF_API_TOKEN = os.getenv("PLATFORM_CF_API_TOKEN") + PLATFORM_CF_ACCOUNT_ID = os.getenv("PLATFORM_CF_ACCOUNT_ID") diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..88f9c44 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,223 @@ +from flask import Flask, request, jsonify +from flask_cors import CORS +from flask_migrate import Migrate +import hashlib +import redis +import os + +from app.config import Config +from app.models import db, Domain, DNSRecord, CloudflareAccount, User, Customer +from app.services.cloudflare_service import CloudflareService + +# Import blueprints +from app.routes.auth import auth_bp +from app.routes.admin import admin_bp +from app.routes.dns import dns_bp +from app.routes.customer import customer_bp + +app = Flask(__name__) +app.config.from_object(Config) + +# Set ENCRYPTION_KEY environment variable +if Config.ENCRYPTION_KEY: + os.environ['ENCRYPTION_KEY'] = Config.ENCRYPTION_KEY + +# Extensions +# CORS - Allow only from argeict.net +CORS(app, resources={ + r"/api/*": { + "origins": ["https://argeict.net"], + "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + "allow_headers": ["Content-Type", "Authorization"] + } +}) +db.init_app(app) +migrate = Migrate(app, db) + +# Register blueprints +app.register_blueprint(auth_bp) +app.register_blueprint(admin_bp) +app.register_blueprint(dns_bp) +app.register_blueprint(customer_bp) + +# Redis +redis_client = redis.from_url(Config.REDIS_URL) + + +# Helper Functions +def select_lb_ip(domain: str) -> str: + """Domain için load balancer IP seç (hash-based)""" + hash_value = int(hashlib.md5(domain.encode()).hexdigest(), 16) + index = hash_value % len(Config.LB_IPS) + return Config.LB_IPS[index] + + +# Routes +@app.route('/health', methods=['GET']) +def health(): + """Health check""" + return jsonify({"status": "ok", "service": "hosting-platform-api"}) + + +@app.route('/api/dns/validate-token', methods=['POST']) +def validate_cf_token(): + """Cloudflare API token doğrula""" + data = request.json + domain = data.get('domain') + cf_token = data.get('cf_token') + + if not domain or not cf_token: + return jsonify({"error": "domain ve cf_token gerekli"}), 400 + + cf_service = CloudflareService(cf_token) + result = cf_service.validate_token_and_get_zone(domain) + + return jsonify(result) + + +@app.route('/api/dns/preview-changes', methods=['POST']) +def preview_changes(): + """DNS değişiklik önizlemesi""" + data = request.json + domain = data.get('domain') + zone_id = data.get('zone_id') + cf_token = data.get('cf_token') + + if not all([domain, zone_id, cf_token]): + return jsonify({"error": "domain, zone_id ve cf_token gerekli"}), 400 + + # Load balancer IP seç + new_ip = select_lb_ip(domain) + + cf_service = CloudflareService(cf_token) + preview = cf_service.generate_dns_preview(domain, zone_id, new_ip) + + return jsonify(preview) + + +@app.route('/api/dns/apply-changes', methods=['POST']) +def apply_changes(): + """DNS değişikliklerini uygula""" + data = request.json + domain = data.get('domain') + zone_id = data.get('zone_id') + cf_token = data.get('cf_token') + preview = data.get('preview') + proxy_enabled = data.get('proxy_enabled', True) + customer_id = data.get('customer_id', 1) # Test için + + if not all([domain, zone_id, cf_token, preview]): + return jsonify({"error": "Eksik parametreler"}), 400 + + cf_service = CloudflareService(cf_token) + + # DNS değişikliklerini uygula + result = cf_service.apply_dns_changes(zone_id, preview, proxy_enabled) + + if result["status"] == "success": + # SSL yapılandır + ssl_config = cf_service.configure_ssl(zone_id) + + # Veritabanına kaydet + domain_obj = Domain.query.filter_by(domain_name=domain).first() + if not domain_obj: + domain_obj = Domain( + domain_name=domain, + customer_id=customer_id, + use_cloudflare=True, + cf_zone_id=zone_id, + cf_proxy_enabled=proxy_enabled, + lb_ip=preview["new_ip"], + status="active", + dns_configured=True, + ssl_configured=len(ssl_config["errors"]) == 0 + ) + db.session.add(domain_obj) + else: + domain_obj.cf_zone_id = zone_id + domain_obj.cf_proxy_enabled = proxy_enabled + domain_obj.lb_ip = preview["new_ip"] + domain_obj.status = "active" + domain_obj.dns_configured = True + domain_obj.ssl_configured = len(ssl_config["errors"]) == 0 + + db.session.commit() + + return jsonify({ + "status": "success", + "dns_result": result, + "ssl_config": ssl_config, + "domain_id": domain_obj.id + }) + + return jsonify(result), 500 + + +@app.route('/api/domains', methods=['GET']) +def list_domains(): + """Domain listesi""" + customer_id = request.args.get('customer_id', 1, type=int) + domains = Domain.query.filter_by(customer_id=customer_id).all() + return jsonify([d.to_dict() for d in domains]) + + +@app.route('/api/domains/', methods=['GET']) +def get_domain(domain_id): + """Domain detayı""" + domain = Domain.query.get_or_404(domain_id) + return jsonify(domain.to_dict()) + + +@app.route('/webhook/deploy', methods=['POST']) +def webhook_deploy(): + """Gitea webhook for auto-deployment""" + import subprocess + import os + from datetime import datetime + + # Get webhook data + data = request.json or {} + + # Log webhook event + repo_name = data.get('repository', {}).get('name', 'unknown') + pusher = data.get('pusher', {}).get('username', 'unknown') + + print(f"📥 Webhook received from {repo_name} by {pusher} at {datetime.now()}") + + # Trigger deployment script in background + try: + # Run deployment script asynchronously + process = subprocess.Popen( + ['/opt/hosting-platform/deploy-local.sh'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + # Don't wait for completion, return immediately + return jsonify({ + "status": "success", + "message": "Deployment triggered successfully", + "repository": repo_name, + "pusher": pusher, + "timestamp": datetime.now().isoformat(), + "note": "Check /var/log/auto-deploy.log for deployment progress" + }), 202 # 202 Accepted + + except FileNotFoundError: + return jsonify({ + "status": "error", + "message": "Deployment script not found at /opt/hosting-platform/deploy-local.sh" + }), 500 + except Exception as e: + return jsonify({ + "status": "error", + "message": f"Failed to trigger deployment: {str(e)}" + }), 500 + + +if __name__ == '__main__': + with app.app_context(): + db.create_all() + app.run(host=Config.API_HOST, port=Config.API_PORT, debug=True) + diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..badbecf --- /dev/null +++ b/backend/app/models/__init__.py @@ -0,0 +1,4 @@ +from app.models.domain import db, CloudflareAccount, Domain, DNSRecord +from app.models.user import User, Customer + +__all__ = ['db', 'CloudflareAccount', 'Domain', 'DNSRecord', 'User', 'Customer'] diff --git a/backend/app/models/domain.py b/backend/app/models/domain.py new file mode 100644 index 0000000..5a56446 --- /dev/null +++ b/backend/app/models/domain.py @@ -0,0 +1,176 @@ +from datetime import datetime +from flask_sqlalchemy import SQLAlchemy +from app.utils.encryption import encrypt_text, decrypt_text + +db = SQLAlchemy() + + +class CloudflareAccount(db.Model): + """Şirket Cloudflare hesapları - Admin tarafından yönetilir""" + __tablename__ = "cloudflare_accounts" + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False, unique=True) # "Account 1", "Production CF", etc. + email = db.Column(db.String(255), nullable=False) + api_token_encrypted = db.Column(db.Text, nullable=False) # Şifreli token + + # Limits & Status + is_active = db.Column(db.Boolean, default=True) + max_domains = db.Column(db.Integer, default=100) # Bu hesapta max kaç domain olabilir + current_domain_count = db.Column(db.Integer, default=0) # Şu an kaç domain var + + # Metadata + notes = db.Column(db.Text, nullable=True) # Admin notları + created_by = db.Column(db.Integer, nullable=True) # Admin user ID + + # Timestamps + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # Relationships + domains = db.relationship("Domain", backref="cf_account", lazy=True) + + def set_api_token(self, plaintext_token: str): + """API token'ı şifrele ve kaydet""" + self.api_token_encrypted = encrypt_text(plaintext_token) + + def get_api_token(self) -> str: + """Şifreli API token'ı çöz ve döndür""" + if not self.api_token_encrypted: + return "" + return decrypt_text(self.api_token_encrypted) + + def to_dict(self, include_token: bool = False): + """ + Model'i dict'e çevir + + Args: + include_token: True ise API token'ı da döndür (sadece admin için) + """ + data = { + "id": self.id, + "name": self.name, + "email": self.email, + "is_active": self.is_active, + "max_domains": self.max_domains, + "current_domain_count": self.current_domain_count, + "notes": self.notes, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + } + + if include_token: + data["api_token"] = self.get_api_token() + + return data + + +class Domain(db.Model): + __tablename__ = "domains" + + id = db.Column(db.Integer, primary_key=True) + domain_name = db.Column(db.String(255), unique=True, nullable=False, index=True) + customer_id = db.Column(db.Integer, db.ForeignKey('customers.id'), nullable=False, index=True) + + # Project Information + project_name = db.Column(db.String(255), nullable=True) # "My WordPress Site" + created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) # User who created this domain + + # Cloudflare Configuration + use_cloudflare = db.Column(db.Boolean, default=True) + cf_account_type = db.Column(db.String(20), nullable=True) # "own" veya "company" + cf_account_id = db.Column(db.Integer, db.ForeignKey("cloudflare_accounts.id"), nullable=True) # Şirket hesabı ise + cf_zone_id = db.Column(db.String(255), nullable=True) + cf_api_token_encrypted = db.Column(db.Text, nullable=True) # Müşterinin kendi token'ı (şifreli) + cf_proxy_enabled = db.Column(db.Boolean, default=True) + + # Nameserver Status + ns_configured = db.Column(db.Boolean, default=False) # NS'ler CF'ye yönlendirildi mi? + ns_checked_at = db.Column(db.DateTime, nullable=True) # Son NS kontrolü + + # DNS + current_ip = db.Column(db.String(45), nullable=True) + lb_ip = db.Column(db.String(45), nullable=True) + + # Status + status = db.Column(db.String(50), default="pending") # pending, active, error + dns_configured = db.Column(db.Boolean, default=False) + ssl_configured = db.Column(db.Boolean, default=False) + + # Timestamps + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # Relationships + dns_records = db.relationship("DNSRecord", backref="domain", lazy=True, cascade="all, delete-orphan") + + def set_cf_api_token(self, plaintext_token: str): + """Müşterinin CF API token'ını şifrele ve kaydet""" + self.cf_api_token_encrypted = encrypt_text(plaintext_token) + + def get_cf_api_token(self) -> str: + """Şifreli CF API token'ı çöz ve döndür""" + if self.cf_account_type == "company" and self.cf_account: + # Şirket hesabı kullanıyorsa, o hesabın token'ını döndür + return self.cf_account.get_api_token() + elif self.cf_api_token_encrypted: + # Kendi token'ı varsa onu döndür + return decrypt_text(self.cf_api_token_encrypted) + return "" + + def to_dict(self): + return { + "id": self.id, + "domain_name": self.domain_name, + "customer_id": self.customer_id, + "project_name": self.project_name, + "created_by": self.created_by, + "use_cloudflare": self.use_cloudflare, + "cf_account_type": self.cf_account_type, + "cf_account_id": self.cf_account_id, + "cf_zone_id": self.cf_zone_id, + "cf_proxy_enabled": self.cf_proxy_enabled, + "ns_configured": self.ns_configured, + "ns_checked_at": self.ns_checked_at.isoformat() if self.ns_checked_at else None, + "current_ip": self.current_ip, + "lb_ip": self.lb_ip, + "status": self.status, + "dns_configured": self.dns_configured, + "ssl_configured": self.ssl_configured, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + } + + +class DNSRecord(db.Model): + __tablename__ = "dns_records" + + id = db.Column(db.Integer, primary_key=True) + domain_id = db.Column(db.Integer, db.ForeignKey("domains.id"), nullable=False) + + record_type = db.Column(db.String(10), nullable=False) # A, CNAME, MX, TXT, etc. + name = db.Column(db.String(255), nullable=False) + content = db.Column(db.Text, nullable=False) + ttl = db.Column(db.Integer, default=300) + proxied = db.Column(db.Boolean, default=False) + + # Cloudflare + cf_record_id = db.Column(db.String(255), nullable=True) + + # Timestamps + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def to_dict(self): + return { + "id": self.id, + "domain_id": self.domain_id, + "record_type": self.record_type, + "name": self.name, + "content": self.content, + "ttl": self.ttl, + "proxied": self.proxied, + "cf_record_id": self.cf_record_id, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + } diff --git a/backend/app/models/user.py b/backend/app/models/user.py new file mode 100644 index 0000000..de47d3d --- /dev/null +++ b/backend/app/models/user.py @@ -0,0 +1,123 @@ +""" +User and Customer models for authentication and customer management +""" +from datetime import datetime +from werkzeug.security import generate_password_hash, check_password_hash +from app.models.domain import db + + +class User(db.Model): + """Base user model for authentication""" + __tablename__ = "users" + + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(255), unique=True, nullable=False, index=True) + password_hash = db.Column(db.String(255), nullable=False) + full_name = db.Column(db.String(255), nullable=False) + + # Account status + is_active = db.Column(db.Boolean, default=True) + is_verified = db.Column(db.Boolean, default=False) + role = db.Column(db.String(20), default='customer') # 'customer' or 'admin' + + # Timestamps + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + last_login = db.Column(db.DateTime, nullable=True) + + # Verification + verification_token = db.Column(db.String(255), nullable=True) + reset_token = db.Column(db.String(255), nullable=True) + reset_token_expires = db.Column(db.DateTime, nullable=True) + + # Relationships + customer = db.relationship('Customer', backref='user', uselist=False, cascade='all, delete-orphan') + + def set_password(self, password): + """Hash and set password""" + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + """Verify password""" + return check_password_hash(self.password_hash, password) + + def to_dict(self, include_sensitive=False): + """Convert to dictionary""" + data = { + 'id': self.id, + 'email': self.email, + 'full_name': self.full_name, + 'role': self.role, + 'is_active': self.is_active, + 'is_verified': self.is_verified, + 'created_at': self.created_at.isoformat() if self.created_at else None, + 'last_login': self.last_login.isoformat() if self.last_login else None + } + + if include_sensitive: + data['verification_token'] = self.verification_token + data['reset_token'] = self.reset_token + + return data + + def __repr__(self): + return f'' + + +class Customer(db.Model): + """Customer profile extending User""" + __tablename__ = "customers" + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, unique=True) + + # Company info + company_name = db.Column(db.String(255), nullable=True) + phone = db.Column(db.String(50), nullable=True) + + # Billing + billing_address = db.Column(db.Text, nullable=True) + billing_city = db.Column(db.String(100), nullable=True) + billing_country = db.Column(db.String(100), nullable=True) + billing_postal_code = db.Column(db.String(20), nullable=True) + + # Subscription + subscription_plan = db.Column(db.String(50), default='free') # free, basic, pro, enterprise + subscription_status = db.Column(db.String(20), default='active') # active, suspended, cancelled + subscription_started = db.Column(db.DateTime, default=datetime.utcnow) + subscription_expires = db.Column(db.DateTime, nullable=True) + + # Limits + max_domains = db.Column(db.Integer, default=5) + max_containers = db.Column(db.Integer, default=3) + + # Timestamps + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # Relationships + domains = db.relationship('Domain', backref='customer', lazy='dynamic', foreign_keys='Domain.customer_id') + + def to_dict(self): + """Convert to dictionary""" + return { + 'id': self.id, + 'user_id': self.user_id, + 'company_name': self.company_name, + 'phone': self.phone, + 'billing_address': self.billing_address, + 'billing_city': self.billing_city, + 'billing_country': self.billing_country, + 'billing_postal_code': self.billing_postal_code, + 'subscription_plan': self.subscription_plan, + 'subscription_status': self.subscription_status, + 'subscription_started': self.subscription_started.isoformat() if self.subscription_started else None, + 'subscription_expires': self.subscription_expires.isoformat() if self.subscription_expires else None, + 'max_domains': self.max_domains, + 'max_containers': self.max_containers, + 'created_at': self.created_at.isoformat() if self.created_at else None + } + + def __repr__(self): + return f'' + diff --git a/backend/app/routes/admin.py b/backend/app/routes/admin.py new file mode 100644 index 0000000..9edd3c9 --- /dev/null +++ b/backend/app/routes/admin.py @@ -0,0 +1,270 @@ +""" +Admin routes - Cloudflare hesap yönetimi +""" +from flask import Blueprint, request, jsonify +from app.models.domain import db, CloudflareAccount +from app.services.cloudflare_service import CloudflareService + +admin_bp = Blueprint('admin', __name__, url_prefix='/api/admin') + + +@admin_bp.route('/cf-accounts', methods=['GET']) +def list_cf_accounts(): + """Tüm Cloudflare hesaplarını listele""" + try: + accounts = CloudflareAccount.query.filter_by(is_active=True).all() + + return jsonify({ + "status": "success", + "accounts": [acc.to_dict(include_token=False) for acc in accounts], + "count": len(accounts) + }) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"Hesaplar listelenirken hata: {str(e)}" + }), 500 + + +@admin_bp.route('/cf-accounts', methods=['POST']) +def create_cf_account(): + """Yeni Cloudflare hesabı ekle""" + try: + data = request.json + + # Validasyon + required_fields = ['name', 'email', 'api_token'] + for field in required_fields: + if not data.get(field): + return jsonify({ + "status": "error", + "message": f"'{field}' alanı gerekli" + }), 400 + + # Token'ı doğrula + cf_service = CloudflareService(data['api_token']) + + # Basit bir API çağrısı yaparak token'ı test et + try: + zones = cf_service.cf.zones.get(params={'per_page': 1}) + # Token geçerli + except Exception as e: + return jsonify({ + "status": "error", + "message": f"Cloudflare API token geçersiz: {str(e)}" + }), 400 + + # Aynı isimde hesap var mı kontrol et + existing = CloudflareAccount.query.filter_by(name=data['name']).first() + if existing: + return jsonify({ + "status": "error", + "message": f"'{data['name']}' isimli hesap zaten mevcut" + }), 400 + + # Yeni hesap oluştur + account = CloudflareAccount( + name=data['name'], + email=data['email'], + max_domains=data.get('max_domains', 100), + notes=data.get('notes', ''), + is_active=True + ) + + # Token'ı şifrele ve kaydet + account.set_api_token(data['api_token']) + + db.session.add(account) + db.session.commit() + + return jsonify({ + "status": "success", + "message": "Cloudflare hesabı başarıyla eklendi", + "account": account.to_dict(include_token=False) + }), 201 + + except Exception as e: + db.session.rollback() + return jsonify({ + "status": "error", + "message": f"Hesap eklenirken hata: {str(e)}" + }), 500 + + +@admin_bp.route('/cf-accounts/', methods=['GET']) +def get_cf_account(account_id): + """Belirli bir Cloudflare hesabını getir""" + try: + account = CloudflareAccount.query.get(account_id) + + if not account: + return jsonify({ + "status": "error", + "message": "Hesap bulunamadı" + }), 404 + + # include_token parametresi ile token'ı da döndürebiliriz (sadece admin için) + include_token = request.args.get('include_token', 'false').lower() == 'true' + + return jsonify({ + "status": "success", + "account": account.to_dict(include_token=include_token) + }) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"Hesap getirilirken hata: {str(e)}" + }), 500 + + +@admin_bp.route('/cf-accounts/', methods=['PUT']) +def update_cf_account(account_id): + """Cloudflare hesabını güncelle""" + try: + account = CloudflareAccount.query.get(account_id) + + if not account: + return jsonify({ + "status": "error", + "message": "Hesap bulunamadı" + }), 404 + + data = request.json + + # Güncellenebilir alanlar + if 'name' in data: + # Aynı isimde başka hesap var mı? + existing = CloudflareAccount.query.filter( + CloudflareAccount.name == data['name'], + CloudflareAccount.id != account_id + ).first() + if existing: + return jsonify({ + "status": "error", + "message": f"'{data['name']}' isimli hesap zaten mevcut" + }), 400 + account.name = data['name'] + + if 'email' in data: + account.email = data['email'] + + if 'api_token' in data: + # Yeni token'ı doğrula + cf_service = CloudflareService(data['api_token']) + try: + zones = cf_service.cf.zones.get(params={'per_page': 1}) + except Exception as e: + return jsonify({ + "status": "error", + "message": f"Cloudflare API token geçersiz: {str(e)}" + }), 400 + + account.set_api_token(data['api_token']) + + if 'max_domains' in data: + account.max_domains = data['max_domains'] + + if 'notes' in data: + account.notes = data['notes'] + + if 'is_active' in data: + account.is_active = data['is_active'] + + db.session.commit() + + return jsonify({ + "status": "success", + "message": "Hesap başarıyla güncellendi", + "account": account.to_dict(include_token=False) + }) + + except Exception as e: + db.session.rollback() + return jsonify({ + "status": "error", + "message": f"Hesap güncellenirken hata: {str(e)}" + }), 500 + + +@admin_bp.route('/cf-accounts/', methods=['DELETE']) +def delete_cf_account(account_id): + """Cloudflare hesabını sil (soft delete)""" + try: + account = CloudflareAccount.query.get(account_id) + + if not account: + return jsonify({ + "status": "error", + "message": "Hesap bulunamadı" + }), 404 + + # Bu hesabı kullanan domain var mı kontrol et + if account.current_domain_count > 0: + return jsonify({ + "status": "error", + "message": f"Bu hesap {account.current_domain_count} domain tarafından kullanılıyor. Önce domain'leri başka hesaba taşıyın." + }), 400 + + # Soft delete (is_active = False) + account.is_active = False + db.session.commit() + + return jsonify({ + "status": "success", + "message": "Hesap başarıyla devre dışı bırakıldı" + }) + + except Exception as e: + db.session.rollback() + return jsonify({ + "status": "error", + "message": f"Hesap silinirken hata: {str(e)}" + }), 500 + + +@admin_bp.route('/cf-accounts//test', methods=['POST']) +def test_cf_account(account_id): + """Cloudflare hesabının API bağlantısını test et""" + try: + account = CloudflareAccount.query.get(account_id) + + if not account: + return jsonify({ + "status": "error", + "message": "Hesap bulunamadı" + }), 404 + + # API token'ı al + api_token = account.get_api_token() + + # Cloudflare API'ye bağlan + cf_service = CloudflareService(api_token) + + try: + # Zone listesini al (test için) + zones = cf_service.cf.zones.get(params={'per_page': 5}) + + return jsonify({ + "status": "success", + "message": "✅ Cloudflare API bağlantısı başarılı", + "zone_count": len(zones), + "sample_zones": [ + {"name": z["name"], "status": z["status"]} + for z in zones[:3] + ] + }) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"❌ Cloudflare API bağlantı hatası: {str(e)}" + }), 400 + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"Test sırasında hata: {str(e)}" + }), 500 + diff --git a/backend/app/routes/auth.py b/backend/app/routes/auth.py new file mode 100644 index 0000000..d867d4d --- /dev/null +++ b/backend/app/routes/auth.py @@ -0,0 +1,220 @@ +""" +Authentication routes - Register, Login, Logout, Profile +""" +from flask import Blueprint, request, jsonify +from app.services.auth_service import AuthService, token_required +from app.models.user import User, Customer +from app.models.domain import db + +auth_bp = Blueprint('auth', __name__, url_prefix='/api/auth') + + +@auth_bp.route('/register', methods=['POST']) +def register(): + """ + Register new customer + + Request body: + { + "email": "user@example.com", + "password": "password123", + "password_confirm": "password123", + "full_name": "John Doe", + "company_name": "Acme Inc" (optional) + } + """ + try: + data = request.json + + # Validate required fields + required_fields = ['email', 'password', 'password_confirm', 'full_name'] + for field in required_fields: + if not data.get(field): + return jsonify({ + 'status': 'error', + 'message': f'{field} is required' + }), 400 + + # Validate email format + email = data['email'].lower().strip() + if '@' not in email or '.' not in email: + return jsonify({ + 'status': 'error', + 'message': 'Invalid email format' + }), 400 + + # Validate password match + if data['password'] != data['password_confirm']: + return jsonify({ + 'status': 'error', + 'message': 'Passwords do not match' + }), 400 + + # Validate password strength + password = data['password'] + if len(password) < 8: + return jsonify({ + 'status': 'error', + 'message': 'Password must be at least 8 characters' + }), 400 + + # Register user + user, customer, error = AuthService.register_user( + email=email, + password=password, + full_name=data['full_name'].strip(), + company_name=data.get('company_name', '').strip() or None + ) + + if error: + return jsonify({ + 'status': 'error', + 'message': error + }), 400 + + # Generate token + token = AuthService.generate_token(user.id, user.role) + + return jsonify({ + 'status': 'success', + 'message': 'Registration successful', + 'token': token, + 'user': user.to_dict(), + 'customer': customer.to_dict() + }), 201 + + except Exception as e: + db.session.rollback() + return jsonify({ + 'status': 'error', + 'message': f'Registration failed: {str(e)}' + }), 500 + + +@auth_bp.route('/login', methods=['POST']) +def login(): + """ + Login user + + Request body: + { + "email": "user@example.com", + "password": "password123" + } + """ + try: + data = request.json + + # Validate required fields + if not data.get('email') or not data.get('password'): + return jsonify({ + 'status': 'error', + 'message': 'Email and password are required' + }), 400 + + # Login user + user, token, error = AuthService.login_user( + email=data['email'].lower().strip(), + password=data['password'] + ) + + if error: + return jsonify({ + 'status': 'error', + 'message': error + }), 401 + + # Get customer profile if customer role + customer_data = None + if user.role == 'customer' and user.customer: + customer_data = user.customer.to_dict() + + return jsonify({ + 'status': 'success', + 'message': 'Login successful', + 'token': token, + 'user': user.to_dict(), + 'customer': customer_data + }), 200 + + except Exception as e: + return jsonify({ + 'status': 'error', + 'message': f'Login failed: {str(e)}' + }), 500 + + +@auth_bp.route('/me', methods=['GET']) +@token_required +def get_profile(current_user): + """ + Get current user profile + + Headers: + Authorization: Bearer + """ + try: + customer_data = None + if current_user.role == 'customer' and current_user.customer: + customer_data = current_user.customer.to_dict() + + return jsonify({ + 'status': 'success', + 'user': current_user.to_dict(), + 'customer': customer_data + }), 200 + + except Exception as e: + return jsonify({ + 'status': 'error', + 'message': f'Failed to get profile: {str(e)}' + }), 500 + + +@auth_bp.route('/verify-token', methods=['POST']) +def verify_token(): + """ + Verify if token is valid + + Request body: + { + "token": "jwt_token_here" + } + """ + try: + data = request.json + token = data.get('token') + + if not token: + return jsonify({ + 'status': 'error', + 'message': 'Token is required', + 'valid': False + }), 400 + + payload = AuthService.verify_token(token) + + if not payload: + return jsonify({ + 'status': 'error', + 'message': 'Invalid or expired token', + 'valid': False + }), 401 + + return jsonify({ + 'status': 'success', + 'message': 'Token is valid', + 'valid': True, + 'payload': { + 'user_id': payload['user_id'], + 'role': payload['role'] + } + }), 200 + + except Exception as e: + return jsonify({ + 'status': 'error', + 'message': str(e), + 'valid': False + }), 500 + diff --git a/backend/app/routes/customer.py b/backend/app/routes/customer.py new file mode 100644 index 0000000..45150af --- /dev/null +++ b/backend/app/routes/customer.py @@ -0,0 +1,328 @@ +""" +Customer Routes - Domain Management +Customer-specific endpoints with isolation +""" +from flask import Blueprint, request, jsonify +from app.models.domain import db, Domain, DNSRecord, CloudflareAccount +from app.models.user import Customer +from app.services.auth_service import token_required +from datetime import datetime + +customer_bp = Blueprint('customer', __name__, url_prefix='/api/customer') + + +@customer_bp.route('/domains', methods=['GET']) +@token_required +def get_domains(current_user): + """Get all domains for the current customer""" + try: + # Get customer + customer = current_user.customer + if not customer: + return jsonify({'error': 'Customer profile not found'}), 404 + + # Get domains with customer isolation + domains = Domain.query.filter_by(customer_id=customer.id).all() + + # Add CF account info + result = [] + for domain in domains: + domain_dict = domain.to_dict() + + # Add CF account name if using company account + if domain.cf_account_type == 'company' and domain.cf_account: + domain_dict['cf_account_name'] = domain.cf_account.name + else: + domain_dict['cf_account_name'] = 'Own Account' + + # Add DNS record count + domain_dict['dns_record_count'] = len(domain.dns_records) + + result.append(domain_dict) + + return jsonify({ + 'domains': result, + 'total': len(result), + 'limit': customer.max_domains + }), 200 + + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@customer_bp.route('/domains/', methods=['GET']) +@token_required +def get_domain(current_user, domain_id): + """Get specific domain details""" + try: + customer = current_user.customer + if not customer: + return jsonify({'error': 'Customer profile not found'}), 404 + + # Get domain with customer isolation + domain = Domain.query.filter_by( + id=domain_id, + customer_id=customer.id + ).first() + + if not domain: + return jsonify({'error': 'Domain not found'}), 404 + + domain_dict = domain.to_dict() + + # Add CF account info + if domain.cf_account_type == 'company' and domain.cf_account: + domain_dict['cf_account_name'] = domain.cf_account.name + + # Add DNS records + domain_dict['dns_records'] = [record.to_dict() for record in domain.dns_records] + + return jsonify(domain_dict), 200 + + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@customer_bp.route('/domains', methods=['POST']) +@token_required +def create_domain(current_user): + """Create a new domain""" + try: + customer = current_user.customer + if not customer: + return jsonify({'error': 'Customer profile not found'}), 404 + + data = request.get_json() + + # Validate required fields + if not data.get('domain_name'): + return jsonify({'error': 'domain_name is required'}), 400 + + domain_name = data['domain_name'].lower().strip() + + # Check domain limit + current_count = Domain.query.filter_by(customer_id=customer.id).count() + if current_count >= customer.max_domains: + return jsonify({ + 'error': f'Domain limit reached. Maximum {customer.max_domains} domains allowed.' + }), 403 + + # Check if domain already exists + existing = Domain.query.filter_by(domain_name=domain_name).first() + if existing: + return jsonify({'error': 'Domain already exists'}), 409 + + # Validate CF account if using company account + cf_account_id = data.get('cf_account_id') + cf_account_type = data.get('cf_account_type', 'company') + + if cf_account_type == 'company': + if not cf_account_id: + return jsonify({'error': 'cf_account_id is required for company account'}), 400 + + cf_account = CloudflareAccount.query.get(cf_account_id) + if not cf_account: + return jsonify({'error': 'Cloudflare account not found'}), 404 + + if not cf_account.is_active: + return jsonify({'error': 'Cloudflare account is not active'}), 400 + + # Check CF account capacity + if cf_account.current_domain_count >= cf_account.max_domains: + return jsonify({ + 'error': f'Cloudflare account is full ({cf_account.max_domains} domains max)' + }), 400 + + # Create domain + domain = Domain( + domain_name=domain_name, + customer_id=customer.id, + created_by=current_user.id, + project_name=data.get('project_name'), + use_cloudflare=data.get('use_cloudflare', True), + cf_account_type=cf_account_type, + cf_account_id=cf_account_id if cf_account_type == 'company' else None, + cf_zone_id=data.get('cf_zone_id'), + cf_proxy_enabled=data.get('cf_proxy_enabled', True), + status='pending' + ) + + # If using own CF account, save encrypted token + if cf_account_type == 'own' and data.get('cf_api_token'): + domain.set_cf_api_token(data['cf_api_token']) + + db.session.add(domain) + + # Update CF account domain count if using company account + if cf_account_type == 'company' and cf_account: + cf_account.current_domain_count += 1 + + db.session.commit() + + return jsonify({ + 'message': 'Domain created successfully', + 'domain': domain.to_dict() + }), 201 + + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + + +@customer_bp.route('/domains/', methods=['PUT']) +@token_required +def update_domain(current_user, domain_id): + """Update domain""" + try: + customer = current_user.customer + if not customer: + return jsonify({'error': 'Customer profile not found'}), 404 + + # Get domain with customer isolation + domain = Domain.query.filter_by( + id=domain_id, + customer_id=customer.id + ).first() + + if not domain: + return jsonify({'error': 'Domain not found'}), 404 + + data = request.get_json() + + # Update allowed fields + if 'project_name' in data: + domain.project_name = data['project_name'] + + if 'cf_proxy_enabled' in data: + domain.cf_proxy_enabled = data['cf_proxy_enabled'] + + if 'status' in data and data['status'] in ['pending', 'active', 'suspended', 'error']: + domain.status = data['status'] + + domain.updated_at = datetime.utcnow() + db.session.commit() + + return jsonify({ + 'message': 'Domain updated successfully', + 'domain': domain.to_dict() + }), 200 + + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + + +@customer_bp.route('/domains/', methods=['DELETE']) +@token_required +def delete_domain(current_user, domain_id): + """Delete domain""" + try: + customer = current_user.customer + if not customer: + return jsonify({'error': 'Customer profile not found'}), 404 + + # Get domain with customer isolation + domain = Domain.query.filter_by( + id=domain_id, + customer_id=customer.id + ).first() + + if not domain: + return jsonify({'error': 'Domain not found'}), 404 + + # Update CF account count if using company account + if domain.cf_account_type == 'company' and domain.cf_account: + domain.cf_account.current_domain_count = max(0, domain.cf_account.current_domain_count - 1) + + db.session.delete(domain) + db.session.commit() + + return jsonify({'message': 'Domain deleted successfully'}), 200 + + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + + +@customer_bp.route('/domains//dns', methods=['GET']) +@token_required +def get_domain_dns(current_user, domain_id): + """Get DNS records for a domain""" + try: + customer = current_user.customer + if not customer: + return jsonify({'error': 'Customer profile not found'}), 404 + + # Get domain with customer isolation + domain = Domain.query.filter_by( + id=domain_id, + customer_id=customer.id + ).first() + + if not domain: + return jsonify({'error': 'Domain not found'}), 404 + + records = [record.to_dict() for record in domain.dns_records] + + return jsonify({ + 'domain': domain.domain_name, + 'records': records, + 'total': len(records) + }), 200 + + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@customer_bp.route('/cloudflare-accounts', methods=['GET']) +@token_required +def get_cloudflare_accounts(current_user): + """Get available Cloudflare accounts (company accounts only)""" + try: + # Get active company CF accounts + accounts = CloudflareAccount.query.filter_by(is_active=True).all() + + result = [] + for account in accounts: + account_dict = account.to_dict(include_token=False) + # Calculate available capacity + account_dict['available_capacity'] = account.max_domains - account.current_domain_count + account_dict['is_full'] = account.current_domain_count >= account.max_domains + result.append(account_dict) + + return jsonify({ + 'accounts': result, + 'total': len(result) + }), 200 + + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@customer_bp.route('/stats', methods=['GET']) +@token_required +def get_customer_stats(current_user): + """Get customer statistics""" + try: + customer = current_user.customer + if not customer: + return jsonify({'error': 'Customer profile not found'}), 404 + + # Count domains by status + total_domains = Domain.query.filter_by(customer_id=customer.id).count() + active_domains = Domain.query.filter_by(customer_id=customer.id, status='active').count() + pending_domains = Domain.query.filter_by(customer_id=customer.id, status='pending').count() + + return jsonify({ + 'total_domains': total_domains, + 'active_domains': active_domains, + 'pending_domains': pending_domains, + 'max_domains': customer.max_domains, + 'available_slots': customer.max_domains - total_domains, + 'subscription_plan': customer.subscription_plan + }), 200 + + except Exception as e: + return jsonify({'error': str(e)}), 500 + + diff --git a/backend/app/routes/dns.py b/backend/app/routes/dns.py new file mode 100644 index 0000000..99bde56 --- /dev/null +++ b/backend/app/routes/dns.py @@ -0,0 +1,293 @@ +""" +DNS routes - Yeni akış ile CF hesap seçimi, NS kontrolü, DNS yönetimi +""" +from flask import Blueprint, request, jsonify +from datetime import datetime +from app.models.domain import db, CloudflareAccount, Domain +from app.services.cloudflare_service import CloudflareService +from app.services.nameserver_service import NameserverService +from app.services.auth_service import token_required +import hashlib + +dns_bp = Blueprint('dns', __name__, url_prefix='/api/dns') + + +def select_lb_ip(domain: str, lb_ips: list) -> str: + """Domain için load balancer IP seç (hash-based)""" + hash_value = int(hashlib.md5(domain.encode()).hexdigest(), 16) + index = hash_value % len(lb_ips) + return lb_ips[index] + + +@dns_bp.route('/check-nameservers', methods=['POST']) +def check_nameservers(): + """Domain'in nameserver'larını kontrol et""" + try: + data = request.json + domain = data.get('domain') + + if not domain: + return jsonify({"error": "domain gerekli"}), 400 + + # NS kontrolü yap + result = NameserverService.check_cloudflare_nameservers(domain) + + return jsonify(result) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"NS kontrolü sırasında hata: {str(e)}" + }), 500 + + +@dns_bp.route('/get-ns-instructions', methods=['POST']) +def get_ns_instructions(): + """NS yönlendirme talimatlarını al""" + try: + data = request.json + domain = data.get('domain') + zone_id = data.get('zone_id') + api_token = data.get('api_token') + + if not all([domain, zone_id, api_token]): + return jsonify({"error": "domain, zone_id ve api_token gerekli"}), 400 + + # Mevcut NS'leri al + current_ns = NameserverService.get_current_nameservers(domain) + + # Cloudflare zone NS'lerini al + cf_ns = NameserverService.get_cloudflare_zone_nameservers(zone_id, api_token) + + if cf_ns["status"] == "error": + return jsonify(cf_ns), 400 + + return jsonify({ + "status": "success", + "domain": domain, + "current_nameservers": current_ns.get("nameservers", []), + "cloudflare_nameservers": cf_ns["nameservers"], + "instructions": [ + "1. Domain sağlayıcınızın (GoDaddy, Namecheap, vb.) kontrol paneline giriş yapın", + "2. Domain yönetimi veya DNS ayarları bölümüne gidin", + "3. 'Nameservers' veya 'Name Servers' seçeneğini bulun", + "4. 'Custom Nameservers' veya 'Use custom nameservers' seçeneğini seçin", + f"5. Aşağıdaki Cloudflare nameserver'larını ekleyin:", + *[f" - {ns}" for ns in cf_ns["nameservers"]], + "6. Değişiklikleri kaydedin", + "7. DNS propagation 24-48 saat sürebilir (genellikle 1-2 saat içinde tamamlanır)" + ] + }) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"NS talimatları alınırken hata: {str(e)}" + }), 500 + + +@dns_bp.route('/validate-token', methods=['POST']) +def validate_cf_token(): + """Cloudflare API token doğrula (müşterinin kendi token'ı)""" + try: + data = request.json + domain = data.get('domain') + cf_token = data.get('cf_token') + + if not domain or not cf_token: + return jsonify({"error": "domain ve cf_token gerekli"}), 400 + + cf_service = CloudflareService(cf_token) + result = cf_service.validate_token_and_get_zone(domain) + + return jsonify(result) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"Token doğrulama hatası: {str(e)}" + }), 500 + + +@dns_bp.route('/select-company-account', methods=['POST']) +def select_company_account(): + """Şirket CF hesabı seç ve zone oluştur/bul""" + try: + data = request.json + domain = data.get('domain') + cf_account_id = data.get('cf_account_id') + + if not domain or not cf_account_id: + return jsonify({"error": "domain ve cf_account_id gerekli"}), 400 + + # CF hesabını al + cf_account = CloudflareAccount.query.get(cf_account_id) + + if not cf_account or not cf_account.is_active: + return jsonify({ + "status": "error", + "message": "Cloudflare hesabı bulunamadı veya aktif değil" + }), 404 + + # Hesap kapasitesi kontrolü + if cf_account.current_domain_count >= cf_account.max_domains: + return jsonify({ + "status": "error", + "message": f"Bu hesap kapasitesi dolmuş ({cf_account.current_domain_count}/{cf_account.max_domains})" + }), 400 + + # API token'ı al + api_token = cf_account.get_api_token() + + # Cloudflare'de zone var mı kontrol et + cf_service = CloudflareService(api_token) + result = cf_service.validate_token_and_get_zone(domain) + + if result["status"] == "success": + # Zone zaten var + return jsonify({ + "status": "success", + "zone_exists": True, + **result + }) + else: + # Zone yok, oluşturulması gerekiyor + # TODO: Zone oluşturma fonksiyonu eklenecek + return jsonify({ + "status": "pending", + "zone_exists": False, + "message": "Zone bulunamadı. Cloudflare'de zone oluşturulması gerekiyor.", + "cf_account": cf_account.to_dict(include_token=False) + }) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"Hesap seçimi sırasında hata: {str(e)}" + }), 500 + + +@dns_bp.route('/preview-changes', methods=['POST']) +@token_required +def preview_changes(current_user): + """DNS değişiklik önizlemesi""" + try: + customer = current_user.customer + if not customer: + return jsonify({'error': 'Customer profile not found'}), 404 + + data = request.json + domain = data.get('domain') + zone_id = data.get('zone_id') + cf_token = data.get('cf_token') + lb_ips = data.get('lb_ips', ['176.96.129.77']) # Default server IP + + if not all([domain, zone_id, cf_token]): + return jsonify({"error": "domain, zone_id ve cf_token gerekli"}), 400 + + # Load balancer IP seç + new_ip = select_lb_ip(domain, lb_ips) + + cf_service = CloudflareService(cf_token) + preview = cf_service.generate_dns_preview(domain, zone_id, new_ip) + + return jsonify(preview) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"Preview oluşturma hatası: {str(e)}" + }), 500 + + +@dns_bp.route('/apply-changes', methods=['POST']) +@token_required +def apply_changes(current_user): + """DNS değişikliklerini uygula ve domain'i kaydet""" + try: + customer = current_user.customer + if not customer: + return jsonify({'error': 'Customer profile not found'}), 404 + + data = request.json + domain_name = data.get('domain') + zone_id = data.get('zone_id') + cf_token = data.get('cf_token') + preview = data.get('preview') + proxy_enabled = data.get('proxy_enabled', True) + cf_account_id = data.get('cf_account_id') + cf_account_type = data.get('cf_account_type', 'company') + project_name = data.get('project_name') + + if not all([domain_name, zone_id, cf_token, preview]): + return jsonify({"error": "Eksik parametreler"}), 400 + + # Check domain limit + current_count = Domain.query.filter_by(customer_id=customer.id).count() + if current_count >= customer.max_domains: + return jsonify({ + 'error': f'Domain limit reached ({customer.max_domains})' + }), 403 + + # Check if domain already exists + existing = Domain.query.filter_by(domain_name=domain_name).first() + if existing: + return jsonify({'error': 'Domain already exists'}), 409 + + cf_service = CloudflareService(cf_token) + + # DNS değişikliklerini uygula + result = cf_service.apply_dns_changes(zone_id, preview, proxy_enabled) + + if result["status"] == "success": + # SSL yapılandır + ssl_config = cf_service.configure_ssl(zone_id) + + # Domain'i veritabanına kaydet + domain_obj = Domain( + domain_name=domain_name, + customer_id=customer.id, + created_by=current_user.id, + project_name=project_name, + use_cloudflare=True, + cf_account_type=cf_account_type, + cf_account_id=cf_account_id if cf_account_type == 'company' else None, + cf_zone_id=zone_id, + cf_proxy_enabled=proxy_enabled, + lb_ip=preview.get("new_ip"), + status="active", + dns_configured=True, + ssl_configured=len(ssl_config.get("errors", [])) == 0 + ) + + # If using own CF account, save encrypted token + if cf_account_type == 'own': + domain_obj.set_cf_api_token(cf_token) + + db.session.add(domain_obj) + + # Update CF account domain count if using company account + if cf_account_type == 'company' and cf_account_id: + cf_account = CloudflareAccount.query.get(cf_account_id) + if cf_account: + cf_account.current_domain_count += 1 + + db.session.commit() + + return jsonify({ + "status": "success", + "dns_result": result, + "ssl_config": ssl_config, + "domain_id": domain_obj.id, + "domain": domain_obj.to_dict() + }), 201 + + return jsonify(result), 500 + + except Exception as e: + db.session.rollback() + return jsonify({ + "status": "error", + "message": f"DNS uygulama hatası: {str(e)}" + }), 500 + diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py new file mode 100644 index 0000000..acb1c15 --- /dev/null +++ b/backend/app/services/auth_service.py @@ -0,0 +1,233 @@ +""" +Authentication service - JWT token generation and validation +""" +import jwt +import secrets +from datetime import datetime, timedelta +from functools import wraps +from flask import request, jsonify, current_app +from app.models.user import User, Customer +from app.models.domain import db + + +class AuthService: + """Authentication service for JWT tokens""" + + @staticmethod + def generate_token(user_id, role='customer', expires_in=24): + """ + Generate JWT token + + Args: + user_id: User ID + role: User role (customer/admin) + expires_in: Token expiration in hours (default 24) + + Returns: + JWT token string + """ + payload = { + 'user_id': user_id, + 'role': role, + 'exp': datetime.utcnow() + timedelta(hours=expires_in), + 'iat': datetime.utcnow() + } + + token = jwt.encode( + payload, + current_app.config['SECRET_KEY'], + algorithm='HS256' + ) + + return token + + @staticmethod + def verify_token(token): + """ + Verify JWT token + + Args: + token: JWT token string + + Returns: + dict: Decoded payload or None if invalid + """ + try: + payload = jwt.decode( + token, + current_app.config['SECRET_KEY'], + algorithms=['HS256'] + ) + return payload + except jwt.ExpiredSignatureError: + return None + except jwt.InvalidTokenError: + return None + + @staticmethod + def register_user(email, password, full_name, company_name=None): + """ + Register new user + + Args: + email: User email + password: User password + full_name: User full name + company_name: Optional company name + + Returns: + tuple: (user, customer, error) + """ + # Check if user exists + existing_user = User.query.filter_by(email=email).first() + if existing_user: + return None, None, "Email already registered" + + # Create user + user = User( + email=email, + full_name=full_name, + role='customer', + is_active=True, + is_verified=False, + verification_token=secrets.token_urlsafe(32) + ) + user.set_password(password) + + db.session.add(user) + db.session.flush() # Get user.id + + # Create customer profile + customer = Customer( + user_id=user.id, + company_name=company_name, + subscription_plan='free', + subscription_status='active', + max_domains=5, + max_containers=3 + ) + + db.session.add(customer) + db.session.commit() + + return user, customer, None + + @staticmethod + def login_user(email, password): + """ + Login user + + Args: + email: User email + password: User password + + Returns: + tuple: (user, token, error) + """ + user = User.query.filter_by(email=email).first() + + if not user: + return None, None, "Invalid email or password" + + if not user.check_password(password): + return None, None, "Invalid email or password" + + if not user.is_active: + return None, None, "Account is deactivated" + + # Update last login + user.last_login = datetime.utcnow() + db.session.commit() + + # Generate token + token = AuthService.generate_token(user.id, user.role) + + return user, token, None + + @staticmethod + def get_current_user(token): + """ + Get current user from token + + Args: + token: JWT token + + Returns: + User object or None + """ + payload = AuthService.verify_token(token) + if not payload: + return None + + user = User.query.get(payload['user_id']) + return user + + +# Decorators for route protection +def token_required(f): + """Decorator to require valid JWT token""" + @wraps(f) + def decorated(*args, **kwargs): + token = None + + # Get token from header + if 'Authorization' in request.headers: + auth_header = request.headers['Authorization'] + try: + token = auth_header.split(' ')[1] # Bearer + except IndexError: + return jsonify({'error': 'Invalid token format'}), 401 + + if not token: + return jsonify({'error': 'Token is missing'}), 401 + + # Verify token + payload = AuthService.verify_token(token) + if not payload: + return jsonify({'error': 'Token is invalid or expired'}), 401 + + # Get user + current_user = User.query.get(payload['user_id']) + if not current_user or not current_user.is_active: + return jsonify({'error': 'User not found or inactive'}), 401 + + return f(current_user, *args, **kwargs) + + return decorated + + +def admin_required(f): + """Decorator to require admin role""" + @wraps(f) + def decorated(*args, **kwargs): + token = None + + # Get token from header + if 'Authorization' in request.headers: + auth_header = request.headers['Authorization'] + try: + token = auth_header.split(' ')[1] + except IndexError: + return jsonify({'error': 'Invalid token format'}), 401 + + if not token: + return jsonify({'error': 'Token is missing'}), 401 + + # Verify token + payload = AuthService.verify_token(token) + if not payload: + return jsonify({'error': 'Token is invalid or expired'}), 401 + + # Check admin role + if payload.get('role') != 'admin': + return jsonify({'error': 'Admin access required'}), 403 + + # Get user + current_user = User.query.get(payload['user_id']) + if not current_user or not current_user.is_active: + return jsonify({'error': 'User not found or inactive'}), 401 + + return f(current_user, *args, **kwargs) + + return decorated + diff --git a/backend/app/services/cloudflare_service.py b/backend/app/services/cloudflare_service.py new file mode 100644 index 0000000..1ef0570 --- /dev/null +++ b/backend/app/services/cloudflare_service.py @@ -0,0 +1,285 @@ +import hashlib +from typing import Dict, List, Optional +import CloudFlare + + +class CloudflareService: + """Cloudflare API işlemleri""" + + def __init__(self, api_token: str): + self.cf = CloudFlare.CloudFlare(token=api_token) + self.api_token = api_token + + def validate_token_and_get_zone(self, domain: str) -> Dict: + """ + API token doğrula ve zone bilgilerini al + """ + try: + # Zone ara + zones = self.cf.zones.get(params={"name": domain}) + + if not zones: + return { + "status": "error", + "message": f"{domain} zone bulunamadı. Domain Cloudflare hesabınızda olduğundan emin olun." + } + + zone = zones[0] + zone_id = zone["id"] + + # Mevcut DNS kayıtlarını al + dns_records = self.cf.zones.dns_records.get(zone_id) + + return { + "status": "success", + "zone_id": zone_id, + "zone_name": zone["name"], + "zone_status": zone["status"], + "nameservers": zone.get("name_servers", []), + "account_email": zone.get("account", {}).get("email", "N/A"), + "current_dns_records": [ + { + "type": r["type"], + "name": r["name"], + "content": r["content"], + "proxied": r.get("proxied", False), + "ttl": r["ttl"], + "id": r["id"] + } + for r in dns_records + ] + } + + except CloudFlare.exceptions.CloudFlareAPIError as e: + return { + "status": "error", + "message": f"Cloudflare API hatası: {str(e)}" + } + except Exception as e: + return { + "status": "error", + "message": f"Beklenmeyen hata: {str(e)}" + } + + def generate_dns_preview(self, domain: str, zone_id: str, new_ip: str) -> Dict: + """ + DNS değişiklik önizlemesi oluştur + """ + try: + # Mevcut A kayıtlarını al + dns_records = self.cf.zones.dns_records.get( + zone_id, + params={"type": "A"} + ) + + current_root = None + current_www = None + + for record in dns_records: + if record["name"] == domain: + current_root = record + elif record["name"] == f"www.{domain}": + current_www = record + + # Önizleme oluştur + preview = { + "domain": domain, + "new_ip": new_ip, + "changes": [] + } + + # Root domain (@) değişikliği + if current_root: + preview["changes"].append({ + "record_type": "A", + "name": "@", + "current": { + "value": current_root["content"], + "proxied": current_root.get("proxied", False), + "ttl": current_root["ttl"] + }, + "new": { + "value": new_ip, + "proxied": current_root.get("proxied", True), + "ttl": "auto" + }, + "action": "update", + "record_id": current_root["id"] + }) + else: + preview["changes"].append({ + "record_type": "A", + "name": "@", + "current": None, + "new": { + "value": new_ip, + "proxied": True, + "ttl": "auto" + }, + "action": "create" + }) + + # www subdomain değişikliği + if current_www: + preview["changes"].append({ + "record_type": "A", + "name": "www", + "current": { + "value": current_www["content"], + "proxied": current_www.get("proxied", False), + "ttl": current_www["ttl"] + }, + "new": { + "value": new_ip, + "proxied": current_www.get("proxied", True), + "ttl": "auto" + }, + "action": "update", + "record_id": current_www["id"] + }) + else: + preview["changes"].append({ + "record_type": "A", + "name": "www", + "current": None, + "new": { + "value": new_ip, + "proxied": True, + "ttl": "auto" + }, + "action": "create" + }) + + # Diğer kayıtlar (değişmeyecek) + all_records = self.cf.zones.dns_records.get(zone_id) + other_records = [ + r for r in all_records + if r["type"] != "A" or (r["name"] != domain and r["name"] != f"www.{domain}") + ] + + preview["preserved_records"] = [ + { + "type": r["type"], + "name": r["name"], + "content": r["content"] + } + for r in other_records[:10] # İlk 10 kayıt + ] + + preview["preserved_count"] = len(other_records) + + return preview + + except Exception as e: + return { + "status": "error", + "message": f"Önizleme oluşturma hatası: {str(e)}" + } + + def apply_dns_changes(self, zone_id: str, preview: Dict, proxy_enabled: bool = True) -> Dict: + """ + DNS değişikliklerini uygula + """ + results = { + "domain": preview["domain"], + "applied_changes": [], + "errors": [] + } + + for change in preview["changes"]: + try: + if change["action"] == "update": + # Mevcut kaydı güncelle + self.cf.zones.dns_records.patch( + zone_id, + change["record_id"], + data={ + "type": "A", + "name": change["name"], + "content": change["new"]["value"], + "proxied": proxy_enabled, + "ttl": 1 if proxy_enabled else 300 + } + ) + results["applied_changes"].append({ + "name": change["name"], + "action": "updated", + "new_value": change["new"]["value"] + }) + + elif change["action"] == "create": + # Yeni kayıt oluştur + self.cf.zones.dns_records.post( + zone_id, + data={ + "type": "A", + "name": change["name"], + "content": change["new"]["value"], + "proxied": proxy_enabled, + "ttl": 1 if proxy_enabled else 300 + } + ) + results["applied_changes"].append({ + "name": change["name"], + "action": "created", + "new_value": change["new"]["value"] + }) + + except Exception as e: + results["errors"].append({ + "name": change["name"], + "error": str(e) + }) + + if results["errors"]: + results["status"] = "partial" + else: + results["status"] = "success" + + return results + + def configure_ssl(self, zone_id: str) -> Dict: + """ + Cloudflare SSL ayarlarını yapılandır + """ + ssl_config = { + "steps": [], + "errors": [] + } + + try: + # 1. SSL/TLS Mode: Full (strict) + self.cf.zones.settings.ssl.patch(zone_id, data={"value": "full"}) + ssl_config["steps"].append({"name": "ssl_mode", "status": "success", "value": "full"}) + except Exception as e: + ssl_config["errors"].append({"step": "ssl_mode", "error": str(e)}) + + try: + # 2. Always Use HTTPS + self.cf.zones.settings.always_use_https.patch(zone_id, data={"value": "on"}) + ssl_config["steps"].append({"name": "always_https", "status": "success"}) + except Exception as e: + ssl_config["errors"].append({"step": "always_https", "error": str(e)}) + + try: + # 3. Automatic HTTPS Rewrites + self.cf.zones.settings.automatic_https_rewrites.patch(zone_id, data={"value": "on"}) + ssl_config["steps"].append({"name": "auto_https_rewrites", "status": "success"}) + except Exception as e: + ssl_config["errors"].append({"step": "auto_https_rewrites", "error": str(e)}) + + try: + # 4. Minimum TLS Version + self.cf.zones.settings.min_tls_version.patch(zone_id, data={"value": "1.2"}) + ssl_config["steps"].append({"name": "min_tls", "status": "success", "value": "1.2"}) + except Exception as e: + ssl_config["errors"].append({"step": "min_tls", "error": str(e)}) + + try: + # 5. TLS 1.3 + self.cf.zones.settings.tls_1_3.patch(zone_id, data={"value": "on"}) + ssl_config["steps"].append({"name": "tls_1_3", "status": "success"}) + except Exception as e: + ssl_config["errors"].append({"step": "tls_1_3", "error": str(e)}) + + return ssl_config diff --git a/backend/app/services/nameserver_service.py b/backend/app/services/nameserver_service.py new file mode 100644 index 0000000..faabbf8 --- /dev/null +++ b/backend/app/services/nameserver_service.py @@ -0,0 +1,173 @@ +""" +Nameserver kontrolü ve doğrulama servisi +""" +import dns.resolver +from typing import Dict, List, Optional +import CloudFlare + + +class NameserverService: + """NS kayıtlarını kontrol eden servis""" + + CLOUDFLARE_NAMESERVERS = [ + "ns1.cloudflare.com", + "ns2.cloudflare.com", + "ns3.cloudflare.com", + "ns4.cloudflare.com", + "ns5.cloudflare.com", + "ns6.cloudflare.com", + "ns7.cloudflare.com", + ] + + @staticmethod + def get_current_nameservers(domain: str) -> Dict: + """ + Domain'in mevcut nameserver'larını al + + Args: + domain: Kontrol edilecek domain + + Returns: + { + "status": "success" | "error", + "nameservers": ["ns1.example.com", ...], + "message": "..." + } + """ + try: + # NS kayıtlarını sorgula + resolver = dns.resolver.Resolver() + resolver.timeout = 5 + resolver.lifetime = 5 + + answers = resolver.resolve(domain, 'NS') + nameservers = [str(rdata.target).rstrip('.') for rdata in answers] + + return { + "status": "success", + "nameservers": nameservers, + "count": len(nameservers) + } + + except dns.resolver.NXDOMAIN: + return { + "status": "error", + "message": f"Domain '{domain}' bulunamadı (NXDOMAIN)", + "nameservers": [] + } + + except dns.resolver.NoAnswer: + return { + "status": "error", + "message": f"Domain '{domain}' için NS kaydı bulunamadı", + "nameservers": [] + } + + except dns.resolver.Timeout: + return { + "status": "error", + "message": "DNS sorgusu zaman aşımına uğradı", + "nameservers": [] + } + + except Exception as e: + return { + "status": "error", + "message": f"NS sorgu hatası: {str(e)}", + "nameservers": [] + } + + @staticmethod + def check_cloudflare_nameservers(domain: str) -> Dict: + """ + Domain'in NS'lerinin Cloudflare'e yönlendirilip yönlendirilmediğini kontrol et + + Returns: + { + "status": "success" | "partial" | "error", + "is_cloudflare": bool, + "current_nameservers": [...], + "cloudflare_nameservers": [...], + "message": "..." + } + """ + result = NameserverService.get_current_nameservers(domain) + + if result["status"] == "error": + return { + "status": "error", + "is_cloudflare": False, + "current_nameservers": [], + "cloudflare_nameservers": [], + "message": result["message"] + } + + current_ns = result["nameservers"] + + # Cloudflare NS'leri ile karşılaştır + cf_ns_found = [] + other_ns_found = [] + + for ns in current_ns: + ns_lower = ns.lower() + if any(cf_ns in ns_lower for cf_ns in NameserverService.CLOUDFLARE_NAMESERVERS): + cf_ns_found.append(ns) + else: + other_ns_found.append(ns) + + # Tüm NS'ler Cloudflare mı? + all_cloudflare = len(cf_ns_found) > 0 and len(other_ns_found) == 0 + + # Kısmi Cloudflare mı? + partial_cloudflare = len(cf_ns_found) > 0 and len(other_ns_found) > 0 + + if all_cloudflare: + status = "success" + message = f"✅ Tüm nameserver'lar Cloudflare'e yönlendirilmiş ({len(cf_ns_found)} NS)" + elif partial_cloudflare: + status = "partial" + message = f"⚠️ Bazı nameserver'lar Cloudflare'e yönlendirilmiş ({len(cf_ns_found)}/{len(current_ns)})" + else: + status = "error" + message = f"❌ Nameserver'lar henüz Cloudflare'e yönlendirilmemiş" + + return { + "status": status, + "is_cloudflare": all_cloudflare, + "current_nameservers": current_ns, + "cloudflare_nameservers": cf_ns_found, + "other_nameservers": other_ns_found, + "message": message + } + + @staticmethod + def get_cloudflare_zone_nameservers(zone_id: str, api_token: str) -> Dict: + """ + Cloudflare zone'un nameserver'larını al + + Returns: + { + "status": "success" | "error", + "nameservers": ["ns1.cloudflare.com", ...], + "message": "..." + } + """ + try: + cf = CloudFlare.CloudFlare(token=api_token) + zone = cf.zones.get(zone_id) + + nameservers = zone.get("name_servers", []) + + return { + "status": "success", + "nameservers": nameservers, + "count": len(nameservers) + } + + except Exception as e: + return { + "status": "error", + "message": f"Cloudflare zone NS sorgu hatası: {str(e)}", + "nameservers": [] + } + diff --git a/backend/app/utils/__init__.py b/backend/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/utils/encryption.py b/backend/app/utils/encryption.py new file mode 100644 index 0000000..f5a0224 --- /dev/null +++ b/backend/app/utils/encryption.py @@ -0,0 +1,117 @@ +""" +Encryption/Decryption utilities for sensitive data +Uses Fernet (symmetric encryption) from cryptography library +""" +from cryptography.fernet import Fernet +import os +import base64 +from typing import Optional + + +class EncryptionService: + """Şifreleme servisi - API token'ları ve hassas verileri şifreler""" + + def __init__(self, encryption_key: Optional[str] = None): + """ + Args: + encryption_key: Base64 encoded Fernet key. + Eğer verilmezse ENCRYPTION_KEY env variable kullanılır. + """ + if encryption_key is None: + encryption_key = os.getenv('ENCRYPTION_KEY') + + if not encryption_key: + raise ValueError( + "ENCRYPTION_KEY environment variable gerekli! " + "Oluşturmak için: python -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())'" + ) + + # Key'i bytes'a çevir + if isinstance(encryption_key, str): + encryption_key = encryption_key.encode() + + self.cipher = Fernet(encryption_key) + + def encrypt(self, plaintext: str) -> str: + """ + Metni şifrele + + Args: + plaintext: Şifrelenecek metin + + Returns: + Base64 encoded şifreli metin + """ + if not plaintext: + return "" + + # String'i bytes'a çevir + plaintext_bytes = plaintext.encode('utf-8') + + # Şifrele + encrypted_bytes = self.cipher.encrypt(plaintext_bytes) + + # Base64 encode et (database'de saklamak için) + return encrypted_bytes.decode('utf-8') + + def decrypt(self, encrypted_text: str) -> str: + """ + Şifreli metni çöz + + Args: + encrypted_text: Base64 encoded şifreli metin + + Returns: + Orijinal metin + """ + if not encrypted_text: + return "" + + try: + # Base64 decode et + encrypted_bytes = encrypted_text.encode('utf-8') + + # Şifreyi çöz + decrypted_bytes = self.cipher.decrypt(encrypted_bytes) + + # Bytes'ı string'e çevir + return decrypted_bytes.decode('utf-8') + + except Exception as e: + raise ValueError(f"Şifre çözme hatası: {str(e)}") + + @staticmethod + def generate_key() -> str: + """ + Yeni bir encryption key oluştur + + Returns: + Base64 encoded Fernet key + """ + return Fernet.generate_key().decode('utf-8') + + +# Global instance (singleton pattern) +_encryption_service = None + + +def get_encryption_service() -> EncryptionService: + """Global encryption service instance'ını al""" + global _encryption_service + + if _encryption_service is None: + _encryption_service = EncryptionService() + + return _encryption_service + + +# Convenience functions +def encrypt_text(plaintext: str) -> str: + """Metni şifrele (convenience function)""" + return get_encryption_service().encrypt(plaintext) + + +def decrypt_text(encrypted_text: str) -> str: + """Şifreli metni çöz (convenience function)""" + return get_encryption_service().decrypt(encrypted_text) + diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..c20f9a0 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,32 @@ +# Web Framework +Flask==3.0.0 +Flask-CORS==4.0.0 +Flask-SQLAlchemy==3.1.1 +Flask-Migrate==4.0.5 + +# Database +# psycopg2-binary==2.9.9 # Commented out for SQLite testing +SQLAlchemy==2.0.23 + +# Redis +redis==5.0.1 + +# Cloudflare +cloudflare==2.19.4 +requests==2.31.0 + +# Utilities +python-dotenv==1.0.0 +pydantic==2.5.2 +python-dateutil==2.8.2 +dnspython==2.4.2 + +# Security +cryptography==41.0.7 +PyJWT==2.8.0 + +# Development +pytest==7.4.3 +pytest-cov==4.1.0 +black==23.12.1 +flake8==6.1.0 diff --git a/backend_main.py b/backend_main.py new file mode 100644 index 0000000..9466fc3 --- /dev/null +++ b/backend_main.py @@ -0,0 +1,151 @@ +from flask import Flask, request, jsonify +from flask_cors import CORS +from flask_migrate import Migrate +import hashlib +import redis + +from app.config import Config +from app.models.domain import db, Domain, DNSRecord +from app.services.cloudflare_service import CloudflareService + +app = Flask(__name__) +app.config.from_object(Config) + +# Extensions +CORS(app) +db.init_app(app) +migrate = Migrate(app, db) + +# Redis +redis_client = redis.from_url(Config.REDIS_URL) + + +# Helper Functions +def select_lb_ip(domain: str) -> str: + """Domain için load balancer IP seç (hash-based)""" + hash_value = int(hashlib.md5(domain.encode()).hexdigest(), 16) + index = hash_value % len(Config.LB_IPS) + return Config.LB_IPS[index] + + +# Routes +@app.route('/health', methods=['GET']) +def health(): + """Health check""" + return jsonify({"status": "ok", "service": "hosting-platform-api"}) + + +@app.route('/api/dns/validate-token', methods=['POST']) +def validate_cf_token(): + """Cloudflare API token doğrula""" + data = request.json + domain = data.get('domain') + cf_token = data.get('cf_token') + + if not domain or not cf_token: + return jsonify({"error": "domain ve cf_token gerekli"}), 400 + + cf_service = CloudflareService(cf_token) + result = cf_service.validate_token_and_get_zone(domain) + + return jsonify(result) + + +@app.route('/api/dns/preview-changes', methods=['POST']) +def preview_changes(): + """DNS değişiklik önizlemesi""" + data = request.json + domain = data.get('domain') + zone_id = data.get('zone_id') + cf_token = data.get('cf_token') + + if not all([domain, zone_id, cf_token]): + return jsonify({"error": "domain, zone_id ve cf_token gerekli"}), 400 + + # Load balancer IP seç + new_ip = select_lb_ip(domain) + + cf_service = CloudflareService(cf_token) + preview = cf_service.generate_dns_preview(domain, zone_id, new_ip) + + return jsonify(preview) + + +@app.route('/api/dns/apply-changes', methods=['POST']) +def apply_changes(): + """DNS değişikliklerini uygula""" + data = request.json + domain = data.get('domain') + zone_id = data.get('zone_id') + cf_token = data.get('cf_token') + preview = data.get('preview') + proxy_enabled = data.get('proxy_enabled', True) + customer_id = data.get('customer_id', 1) # Test için + + if not all([domain, zone_id, cf_token, preview]): + return jsonify({"error": "Eksik parametreler"}), 400 + + cf_service = CloudflareService(cf_token) + + # DNS değişikliklerini uygula + result = cf_service.apply_dns_changes(zone_id, preview, proxy_enabled) + + if result["status"] == "success": + # SSL yapılandır + ssl_config = cf_service.configure_ssl(zone_id) + + # Veritabanına kaydet + domain_obj = Domain.query.filter_by(domain_name=domain).first() + if not domain_obj: + domain_obj = Domain( + domain_name=domain, + customer_id=customer_id, + use_cloudflare=True, + cf_zone_id=zone_id, + cf_proxy_enabled=proxy_enabled, + lb_ip=preview["new_ip"], + status="active", + dns_configured=True, + ssl_configured=len(ssl_config["errors"]) == 0 + ) + db.session.add(domain_obj) + else: + domain_obj.cf_zone_id = zone_id + domain_obj.cf_proxy_enabled = proxy_enabled + domain_obj.lb_ip = preview["new_ip"] + domain_obj.status = "active" + domain_obj.dns_configured = True + domain_obj.ssl_configured = len(ssl_config["errors"]) == 0 + + db.session.commit() + + return jsonify({ + "status": "success", + "dns_result": result, + "ssl_config": ssl_config, + "domain_id": domain_obj.id + }) + + return jsonify(result), 500 + + +@app.route('/api/domains', methods=['GET']) +def list_domains(): + """Domain listesi""" + customer_id = request.args.get('customer_id', 1, type=int) + domains = Domain.query.filter_by(customer_id=customer_id).all() + return jsonify([d.to_dict() for d in domains]) + + +@app.route('/api/domains/', methods=['GET']) +def get_domain(domain_id): + """Domain detayı""" + domain = Domain.query.get_or_404(domain_id) + return jsonify(domain.to_dict()) + + +if __name__ == '__main__': + with app.app_context(): + db.create_all() + app.run(host=Config.API_HOST, port=Config.API_PORT, debug=True) + diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..a46b978 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,98 @@ +#!/bin/bash + +# Hosting Platform Deployment Script +# Usage: ./deploy.sh + +set -e + +HOST="root@176.96.129.77" +SSH_KEY="~/.ssh/id_rsa" + +echo "╔══════════════════════════════════════════════════════════════╗" +echo "║ 🚀 Hosting Platform Deployment Script 🚀 ║" +echo "╚══════════════════════════════════════════════════════════════╝" +echo "" + +# 1. Git Pull +echo "📥 [1/6] Pulling latest code from Gitea..." +ssh -i $SSH_KEY $HOST << 'ENDSSH' +cd /opt/hosting-platform +git pull origin main +ENDSSH +echo "✅ Git pull complete" +echo "" + +# 2. Backend Dependencies +echo "📦 [2/6] Installing backend dependencies..." +ssh -i $SSH_KEY $HOST << 'ENDSSH' +cd /opt/hosting-platform/backend +source venv/bin/activate +pip install -q -r requirements.txt +ENDSSH +echo "✅ Backend dependencies installed" +echo "" + +# 3. Database Migration +echo "🗄️ [3/6] Running database migrations..." +ssh -i $SSH_KEY $HOST << 'ENDSSH' +cd /opt/hosting-platform/backend +source venv/bin/activate +python -c "from app.main import app, db; app.app_context().push(); db.create_all()" +ENDSSH +echo "✅ Database migrations complete" +echo "" + +# 4. Frontend Build +echo "🎨 [4/6] Building frontend..." +ssh -i $SSH_KEY $HOST << 'ENDSSH' +cd /opt/hosting-platform/frontend +npm install --silent +npm run build +ENDSSH +echo "✅ Frontend built" +echo "" + +# 5. Restart Services +echo "🔄 [5/6] Restarting services..." +ssh -i $SSH_KEY $HOST << 'ENDSSH' +supervisorctl restart hosting-backend hosting-frontend +ENDSSH +sleep 3 +echo "✅ Services restarted" +echo "" + +# 6. Health Check +echo "🏥 [6/6] Running health checks..." +sleep 2 + +HEALTH=$(curl -s https://api.argeict.net/health) +if echo "$HEALTH" | grep -q "ok"; then + echo "✅ API Health: OK" +else + echo "❌ API Health: FAILED" + exit 1 +fi + +ADMIN=$(curl -s https://api.argeict.net/api/admin/cf-accounts) +if echo "$ADMIN" | grep -q "success"; then + echo "✅ Admin Endpoints: OK" +else + echo "❌ Admin Endpoints: FAILED" + exit 1 +fi + +echo "" +echo "╔══════════════════════════════════════════════════════════════╗" +echo "║ ✅ DEPLOYMENT SUCCESSFUL! ✅ ║" +echo "╚══════════════════════════════════════════════════════════════╝" +echo "" +echo "🌐 URLs:" +echo " Frontend: https://argeict.net" +echo " API: https://api.argeict.net" +echo " Gitea: https://gitea.argeict.net" +echo "" +echo "📝 Next steps:" +echo " - Test the new features in the admin panel" +echo " - Check logs: ssh $HOST 'tail -f /var/log/hosting-backend.log'" +echo "" + diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 0000000..e79deaa --- /dev/null +++ b/frontend/.env @@ -0,0 +1,2 @@ +VITE_API_URL=https://api.argeict.net + diff --git a/frontend/.env.production b/frontend/.env.production new file mode 100644 index 0000000..e79deaa --- /dev/null +++ b/frontend/.env.production @@ -0,0 +1,2 @@ +VITE_API_URL=https://api.argeict.net + diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..7f0c547 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + + Hosting Platform - DNS & SSL Management + + +
+ + + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..bba8541 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,33 @@ +{ + "name": "hosting-platform-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite --host 0.0.0.0 --port 3001", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "axios": "^1.6.2", + "react-router-dom": "^6.20.1", + "@heroicons/react": "^2.1.1" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "eslint": "^8.55.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "postcss": "^8.4.32", + "tailwindcss": "^3.3.6", + "vite": "^5.0.8" + } +} + diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..b4a6220 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,7 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} + diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..0ad5111 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,79 @@ +.app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; +} + +.card { + background: white; + border-radius: 8px; + padding: 2rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.btn { + padding: 0.75rem 1.5rem; + border-radius: 6px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.btn-primary { + background: #3b82f6; + color: white; + border: none; +} + +.btn-primary:hover { + background: #2563eb; +} + +.btn-secondary { + background: #6b7280; + color: white; + border: none; +} + +.btn-secondary:hover { + background: #4b5563; +} + +.input { + width: 100%; + padding: 0.75rem; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 1rem; +} + +.input:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.alert { + padding: 1rem; + border-radius: 6px; + margin-bottom: 1rem; +} + +.alert-success { + background: #d1fae5; + color: #065f46; + border: 1px solid #6ee7b7; +} + +.alert-error { + background: #fee2e2; + color: #991b1b; + border: 1px solid #fca5a5; +} + +.alert-info { + background: #dbeafe; + color: #1e40af; + border: 1px solid #93c5fd; +} + diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..de1abdb --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,47 @@ +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' +import { AuthProvider, useAuth } from './context/AuthContext' +import Landing from './pages/Landing' +import Dashboard from './pages/Dashboard' +import './App.css' + +// Protected route wrapper +const ProtectedRoute = ({ children }) => { + const { isAuthenticated, loading } = useAuth() + + if (loading) { + return ( +
+
+
+

Loading...

+
+
+ ) + } + + return isAuthenticated ? children : +} + +function App() { + return ( + + + + } /> + + + + } + /> + } /> + + + + ) +} + +export default App + diff --git a/frontend/src/components/AddDomainWizard.jsx b/frontend/src/components/AddDomainWizard.jsx new file mode 100644 index 0000000..b52b6e4 --- /dev/null +++ b/frontend/src/components/AddDomainWizard.jsx @@ -0,0 +1,569 @@ +/** + * Add Domain Wizard - Step-by-step domain addition process + */ +import { useState, useEffect } from 'react'; +import { + XMarkIcon, + CheckCircleIcon, + ArrowRightIcon, + ArrowLeftIcon, + GlobeAltIcon, + CloudIcon, + DocumentTextIcon, + ServerIcon, + ShieldCheckIcon, +} from '@heroicons/react/24/outline'; +import api from '../services/api'; +import CFTokenGuide from './CFTokenGuide'; +import NSInstructions from './NSInstructions'; + +const AddDomainWizard = ({ onClose, onSuccess, customer }) => { + const [currentStep, setCurrentStep] = useState(1); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // Form data + const [domainName, setDomainName] = useState(''); + const [cfAccountType, setCfAccountType] = useState(''); // 'company' or 'own' + const [selectedCompanyAccount, setSelectedCompanyAccount] = useState(null); + const [ownCfToken, setOwnCfToken] = useState(''); + const [ownCfEmail, setOwnCfEmail] = useState(''); + + // Company CF accounts + const [companyAccounts, setCompanyAccounts] = useState([]); + + // Domain setup data + const [domainId, setDomainId] = useState(null); + const [dnsPreview, setDnsPreview] = useState(null); + const [nsInstructions, setNsInstructions] = useState(null); + const [nsStatus, setNsStatus] = useState(null); + + // UI helpers + const [showTokenGuide, setShowTokenGuide] = useState(false); + + const steps = [ + { number: 1, title: 'Domain Name', icon: GlobeAltIcon }, + { number: 2, title: 'Cloudflare Account', icon: CloudIcon }, + { number: 3, title: cfAccountType === 'own' ? 'API Token' : 'DNS Preview', icon: DocumentTextIcon }, + { number: 4, title: 'Nameserver Setup', icon: ServerIcon }, + { number: 5, title: 'Verification', icon: ShieldCheckIcon }, + ]; + + // Fetch company CF accounts + useEffect(() => { + if (currentStep === 2) { + fetchCompanyAccounts(); + } + }, [currentStep]); + + const fetchCompanyAccounts = async () => { + try { + const response = await api.get('/api/customer/cloudflare-accounts'); + setCompanyAccounts(response.data.accounts || []); + } catch (err) { + console.error('Failed to fetch CF accounts:', err); + } + }; + + // Validate domain name + const validateDomain = (domain) => { + const domainRegex = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/i; + return domainRegex.test(domain); + }; + + // Step 1: Submit domain name + const handleStep1Next = async () => { + if (!domainName.trim()) { + setError('Please enter a domain name'); + return; + } + + if (!validateDomain(domainName)) { + setError('Please enter a valid domain name (e.g., example.com)'); + return; + } + + setError(null); + setCurrentStep(2); + }; + + // Step 2: Select CF account type + const handleStep2Next = async () => { + if (!cfAccountType) { + setError('Please select a Cloudflare account option'); + return; + } + + if (cfAccountType === 'company' && !selectedCompanyAccount) { + setError('Please select a company Cloudflare account'); + return; + } + + setError(null); + + // If company account, create domain immediately and skip to step 4 + if (cfAccountType === 'company') { + setLoading(true); + try { + const response = await api.post('/api/customer/domains', { + domain_name: domainName, + cf_account_type: 'company', + cf_account_id: selectedCompanyAccount.id, + }); + + setDomainId(response.data.domain.id); + setDnsPreview(response.data.dns_preview); + setNsInstructions(response.data.ns_instructions); + setCurrentStep(4); // Skip step 3, go directly to NS setup + } catch (err) { + setError(err.response?.data?.error || 'Failed to create domain'); + } finally { + setLoading(false); + } + } else { + setCurrentStep(3); // Go to API token input + } + }; + + // Step 3: Handle based on account type + const handleStep3Next = async () => { + setLoading(true); + setError(null); + + try { + if (cfAccountType === 'own') { + // Validate own CF token + if (!ownCfToken.trim() || !ownCfEmail.trim()) { + setError('Please enter both Cloudflare email and API token'); + setLoading(false); + return; + } + + // Create domain with own CF account + const response = await api.post('/api/customer/domains', { + domain_name: domainName, + cf_account_type: 'own', + cf_email: ownCfEmail, + cf_api_token: ownCfToken, + }); + + setDomainId(response.data.domain.id); + setNsInstructions(response.data.ns_instructions); + setCurrentStep(4); + } else { + // Create domain with company CF account + const response = await api.post('/api/customer/domains', { + domain_name: domainName, + cf_account_type: 'company', + cf_account_id: selectedCompanyAccount.id, + }); + + setDomainId(response.data.domain.id); + setDnsPreview(response.data.dns_preview); + setNsInstructions(response.data.ns_instructions); + setCurrentStep(4); + } + } catch (err) { + setError(err.response?.data?.error || 'Failed to create domain'); + } finally { + setLoading(false); + } + }; + + // Step 4: Check nameserver status + const checkNsStatus = async () => { + setLoading(true); + try { + const response = await api.get(`/api/customer/domains/${domainId}/ns-status`); + setNsStatus(response.data); + + if (response.data.is_cloudflare) { + setCurrentStep(5); + } + } catch (err) { + console.error('Failed to check NS status:', err); + } finally { + setLoading(false); + } + }; + + return ( + <> +
+
+ {/* Header */} +
+

Add New Domain

+ +
+ + {/* Progress Steps */} +
+
+ {steps.map((step, index) => { + const Icon = step.icon; + const isActive = currentStep === step.number; + const isCompleted = currentStep > step.number; + + return ( +
+
+
+ {isCompleted ? ( + + ) : ( + + )} +
+ + {step.title} + +
+ {index < steps.length - 1 && ( +
+ )} +
+ ); + })} +
+
+ + {/* Error Message */} + {error && ( +
+

{error}

+
+ )} + + {/* Step Content */} +
+ {/* Step 1: Domain Name */} + {currentStep === 1 && ( +
+
+

Enter Your Domain Name

+

+ Enter the domain name you want to add to your hosting platform. +

+
+ +
+ + setDomainName(e.target.value.toLowerCase().trim())} + placeholder="example.com" + className="input-field w-full" + autoFocus + /> +

+ Enter without http:// or https:// +

+
+ +
+

📋 Requirements:

+
    +
  • • You must own this domain
  • +
  • • You must have access to domain registrar settings
  • +
  • • Domain limit: {customer?.max_domains} domains
  • +
+
+
+ )} + + {/* Step 2: Cloudflare Account Selection */} + {currentStep === 2 && ( +
+
+

Select Cloudflare Account

+

+ Choose how you want to manage your domain's DNS. +

+
+ +
+ {/* Company Account Option */} +
setCfAccountType('company')} + className={`border-2 rounded-lg p-4 cursor-pointer transition-all ${ + cfAccountType === 'company' + ? 'border-primary-500 bg-primary-50' + : 'border-gray-200 hover:border-gray-300' + }`} + > +
+ setCfAccountType('company')} + className="mt-1 mr-3" + /> +
+

Use Company Cloudflare Account

+

+ We'll manage your DNS using our Cloudflare account. Easier setup, no API token needed. +

+ + {cfAccountType === 'company' && companyAccounts.length > 0 && ( +
+ + +
+ )} +
+
+
+ + {/* Own Account Option */} +
setCfAccountType('own')} + className={`border-2 rounded-lg p-4 cursor-pointer transition-all ${ + cfAccountType === 'own' + ? 'border-primary-500 bg-primary-50' + : 'border-gray-200 hover:border-gray-300' + }`} + > +
+ setCfAccountType('own')} + className="mt-1 mr-3" + /> +
+

Use My Own Cloudflare Account

+

+ Use your own Cloudflare account. You'll need to provide an API token. +

+
+
+
+
+
+ )} + + {/* Step 3: Own CF Token OR DNS Preview */} + {currentStep === 3 && cfAccountType === 'own' && ( +
+
+

Enter Cloudflare API Token

+

+ Provide your Cloudflare API token to manage DNS records. +

+
+ +
+

+ Don't have an API token? +

+ +
+ +
+ + setOwnCfEmail(e.target.value)} + placeholder="your-email@example.com" + className="input-field w-full" + /> +
+ +
+ + setOwnCfToken(e.target.value)} + placeholder="Your Cloudflare API Token" + className="input-field w-full font-mono text-sm" + /> +

+ Your token will be encrypted and stored securely +

+
+
+ )} + + {currentStep === 3 && cfAccountType === 'company' && ( +
+
+

DNS Records Preview

+

+ These DNS records will be created for your domain. +

+
+ + {dnsPreview && ( +
+ + + + + + + + + + {dnsPreview.records?.map((record, idx) => ( + + + + + + ))} + +
TypeNameValue
{record.type}{record.name}{record.value}
+
+ )} + +
+

+ ✓ DNS records will be automatically configured when you complete the setup. +

+
+
+ )} + + {/* Step 4: Nameserver Setup */} + {currentStep === 4 && ( +
+ setCurrentStep(5)} + loading={loading} + /> +
+ )} + + {/* Step 5: Verification & Completion */} + {currentStep === 5 && ( +
+
+ +
+ +
+

+ Domain Successfully Added! +

+

+ Your domain {domainName} has been configured and is ready to use. +

+
+ +
+

✓ What's Next?

+
    +
  • • DNS records have been configured
  • +
  • • SSL certificate will be issued automatically
  • +
  • • Your domain will be active within a few minutes
  • +
+
+ + +
+ )} +
+ + {/* Footer Actions */} +
+ + + {currentStep < 5 && ( + + )} +
+
+
+ + {/* Token Guide Modal */} + {showTokenGuide && setShowTokenGuide(false)} />} + + ); +}; + +export default AddDomainWizard; + diff --git a/frontend/src/components/CFAccountModal.jsx b/frontend/src/components/CFAccountModal.jsx new file mode 100644 index 0000000..b56d7ac --- /dev/null +++ b/frontend/src/components/CFAccountModal.jsx @@ -0,0 +1,200 @@ +import { useState, useEffect } from 'react' +import { adminAPI } from '../services/api' + +function CFAccountModal({ account, onClose, onSuccess }) { + const [formData, setFormData] = useState({ + name: '', + email: '', + api_token: '', + max_domains: 100, + notes: '', + is_active: true, + }) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + if (account) { + setFormData({ + name: account.name, + email: account.email, + api_token: '', // Don't show existing token + max_domains: account.max_domains, + notes: account.notes || '', + is_active: account.is_active, + }) + } + }, [account]) + + const handleSubmit = async (e) => { + e.preventDefault() + setLoading(true) + setError(null) + + try { + // Prepare data - don't send empty api_token on update + const data = { ...formData } + if (account && !data.api_token) { + delete data.api_token + } + + const response = account + ? await adminAPI.updateCFAccount(account.id, data) + : await adminAPI.createCFAccount(data) + + if (response.data.status === 'success') { + onSuccess() + } else { + setError(response.data.message) + } + } catch (err) { + setError(err.response?.data?.error || 'İşlem başarısız') + } finally { + setLoading(false) + } + } + + return ( +
+
+
+
+

+ {account ? 'Hesap Düzenle' : 'Yeni Cloudflare Hesabı Ekle'} +

+ +
+ + {error && ( +
+ {error} +
+ )} + +
+ {/* Name */} +
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="Örn: Ana CF Hesabı" + className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" + required + /> +
+ + {/* Email */} +
+ + setFormData({ ...formData, email: e.target.value })} + placeholder="email@example.com" + className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" + required + /> +
+ + {/* API Token */} +
+ + setFormData({ ...formData, api_token: e.target.value })} + placeholder={account ? 'Mevcut token korunacak' : 'Cloudflare API Token'} + className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 font-mono text-sm" + required={!account} + /> +

+ Token şifreli olarak saklanacaktır +

+
+ + {/* Max Domains */} +
+ + setFormData({ ...formData, max_domains: parseInt(e.target.value) })} + min="1" + max="1000" + className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" + required + /> +

+ Bu hesapta maksimum kaç domain barındırılabilir +

+
+ + {/* Notes */} +
+ +