Initial commit: Customer panel
This commit is contained in:
commit
2f98057b43
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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!**
|
||||
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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")
|
||||
|
|
@ -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/<int:domain_id>', 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)
|
||||
|
||||
|
|
@ -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']
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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'<User {self.email}>'
|
||||
|
||||
|
||||
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'<Customer {self.company_name or self.user.email}>'
|
||||
|
||||
|
|
@ -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/<int:account_id>', 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/<int:account_id>', 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/<int:account_id>', 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/<int:account_id>/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
|
||||
|
||||
|
|
@ -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 <token>
|
||||
"""
|
||||
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
|
||||
|
||||
|
|
@ -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/<int:domain_id>', 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/<int:domain_id>', 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/<int:domain_id>', 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/<int:domain_id>/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
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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 <token>
|
||||
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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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": []
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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/<int:domain_id>', 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)
|
||||
|
||||
|
|
@ -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 ""
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
VITE_API_URL=https://api.argeict.net
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
VITE_API_URL=https://api.argeict.net
|
||||
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Hosting Platform - DNS & SSL Management</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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 (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 border-4 border-primary-500 border-t-transparent rounded-full animate-spin mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return isAuthenticated ? children : <Navigate to="/" replace />
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<AuthProvider>
|
||||
<Routes>
|
||||
<Route path="/" element={<Landing />} />
|
||||
<Route
|
||||
path="/dashboard"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Dashboard />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
|
|
@ -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 (
|
||||
<>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
||||
{/* Header */}
|
||||
<div className="sticky top-0 bg-white border-b px-6 py-4 flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold text-gray-900">Add New Domain</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<XMarkIcon className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Progress Steps */}
|
||||
<div className="px-6 py-4 bg-gray-50">
|
||||
<div className="flex items-center justify-between">
|
||||
{steps.map((step, index) => {
|
||||
const Icon = step.icon;
|
||||
const isActive = currentStep === step.number;
|
||||
const isCompleted = currentStep > step.number;
|
||||
|
||||
return (
|
||||
<div key={step.number} className="flex items-center flex-1">
|
||||
<div className="flex flex-col items-center flex-1">
|
||||
<div
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center transition-all ${
|
||||
isCompleted
|
||||
? 'bg-green-500 text-white'
|
||||
: isActive
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-gray-200 text-gray-500'
|
||||
}`}
|
||||
>
|
||||
{isCompleted ? (
|
||||
<CheckCircleIcon className="w-6 h-6" />
|
||||
) : (
|
||||
<Icon className="w-5 h-5" />
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className={`text-xs mt-2 font-medium ${
|
||||
isActive ? 'text-primary-600' : 'text-gray-500'
|
||||
}`}
|
||||
>
|
||||
{step.title}
|
||||
</span>
|
||||
</div>
|
||||
{index < steps.length - 1 && (
|
||||
<div
|
||||
className={`h-0.5 flex-1 mx-2 ${
|
||||
isCompleted ? 'bg-green-500' : 'bg-gray-200'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="mx-6 mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<p className="text-red-800 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step Content */}
|
||||
<div className="p-6">
|
||||
{/* Step 1: Domain Name */}
|
||||
{currentStep === 1 && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">Enter Your Domain Name</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
Enter the domain name you want to add to your hosting platform.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Domain Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={domainName}
|
||||
onChange={(e) => setDomainName(e.target.value.toLowerCase().trim())}
|
||||
placeholder="example.com"
|
||||
className="input-field w-full"
|
||||
autoFocus
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Enter without http:// or https://
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<h4 className="font-medium text-blue-900 mb-2">📋 Requirements:</h4>
|
||||
<ul className="text-sm text-blue-800 space-y-1">
|
||||
<li>• You must own this domain</li>
|
||||
<li>• You must have access to domain registrar settings</li>
|
||||
<li>• Domain limit: {customer?.max_domains} domains</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: Cloudflare Account Selection */}
|
||||
{currentStep === 2 && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">Select Cloudflare Account</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
Choose how you want to manage your domain's DNS.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* Company Account Option */}
|
||||
<div
|
||||
onClick={() => 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'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start">
|
||||
<input
|
||||
type="radio"
|
||||
checked={cfAccountType === 'company'}
|
||||
onChange={() => setCfAccountType('company')}
|
||||
className="mt-1 mr-3"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-gray-900">Use Company Cloudflare Account</h4>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
We'll manage your DNS using our Cloudflare account. Easier setup, no API token needed.
|
||||
</p>
|
||||
|
||||
{cfAccountType === 'company' && companyAccounts.length > 0 && (
|
||||
<div className="mt-3">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Select Account:
|
||||
</label>
|
||||
<select
|
||||
value={selectedCompanyAccount?.id || ''}
|
||||
onChange={(e) => {
|
||||
const account = companyAccounts.find(a => a.id === parseInt(e.target.value));
|
||||
setSelectedCompanyAccount(account);
|
||||
}}
|
||||
className="input-field w-full"
|
||||
>
|
||||
<option value="">Choose an account...</option>
|
||||
{companyAccounts.map((account) => (
|
||||
<option key={account.id} value={account.id}>
|
||||
{account.name} ({account.email})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Own Account Option */}
|
||||
<div
|
||||
onClick={() => 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'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start">
|
||||
<input
|
||||
type="radio"
|
||||
checked={cfAccountType === 'own'}
|
||||
onChange={() => setCfAccountType('own')}
|
||||
className="mt-1 mr-3"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-gray-900">Use My Own Cloudflare Account</h4>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
Use your own Cloudflare account. You'll need to provide an API token.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Own CF Token OR DNS Preview */}
|
||||
{currentStep === 3 && cfAccountType === 'own' && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">Enter Cloudflare API Token</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
Provide your Cloudflare API token to manage DNS records.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<p className="text-sm text-blue-800 mb-2">
|
||||
<strong>Don't have an API token?</strong>
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setShowTokenGuide(true)}
|
||||
className="text-blue-600 hover:text-blue-800 font-medium text-sm underline"
|
||||
>
|
||||
📖 View API Token Creation Guide
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Cloudflare Email *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={ownCfEmail}
|
||||
onChange={(e) => setOwnCfEmail(e.target.value)}
|
||||
placeholder="your-email@example.com"
|
||||
className="input-field w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
API Token *
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={ownCfToken}
|
||||
onChange={(e) => setOwnCfToken(e.target.value)}
|
||||
placeholder="Your Cloudflare API Token"
|
||||
className="input-field w-full font-mono text-sm"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Your token will be encrypted and stored securely
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 3 && cfAccountType === 'company' && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">DNS Records Preview</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
These DNS records will be created for your domain.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{dnsPreview && (
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-300">
|
||||
<th className="text-left py-2 px-2">Type</th>
|
||||
<th className="text-left py-2 px-2">Name</th>
|
||||
<th className="text-left py-2 px-2">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dnsPreview.records?.map((record, idx) => (
|
||||
<tr key={idx} className="border-b border-gray-200">
|
||||
<td className="py-2 px-2 font-mono">{record.type}</td>
|
||||
<td className="py-2 px-2 font-mono">{record.name}</td>
|
||||
<td className="py-2 px-2 font-mono text-xs">{record.value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<p className="text-sm text-green-800">
|
||||
✓ DNS records will be automatically configured when you complete the setup.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 4: Nameserver Setup */}
|
||||
{currentStep === 4 && (
|
||||
<div className="space-y-4">
|
||||
<NSInstructions
|
||||
domain={domainName}
|
||||
nsInstructions={nsInstructions}
|
||||
nsStatus={nsStatus}
|
||||
onCheck={checkNsStatus}
|
||||
onContinue={() => setCurrentStep(5)}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 5: Verification & Completion */}
|
||||
{currentStep === 5 && (
|
||||
<div className="space-y-4 text-center py-8">
|
||||
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto">
|
||||
<CheckCircleIcon className="w-12 h-12 text-green-600" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
Domain Successfully Added!
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
Your domain <strong>{domainName}</strong> has been configured and is ready to use.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4 text-left max-w-md mx-auto">
|
||||
<h4 className="font-semibold text-green-900 mb-2">✓ What's Next?</h4>
|
||||
<ul className="text-sm text-green-800 space-y-1">
|
||||
<li>• DNS records have been configured</li>
|
||||
<li>• SSL certificate will be issued automatically</li>
|
||||
<li>• Your domain will be active within a few minutes</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
onSuccess();
|
||||
onClose();
|
||||
}}
|
||||
className="btn-primary mx-auto"
|
||||
>
|
||||
Go to Domains
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer Actions */}
|
||||
<div className="sticky bottom-0 bg-gray-50 border-t px-6 py-4 flex items-center justify-between">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (currentStep > 1) setCurrentStep(currentStep - 1);
|
||||
else onClose();
|
||||
}}
|
||||
className="btn-secondary inline-flex items-center"
|
||||
>
|
||||
<ArrowLeftIcon className="w-4 h-4 mr-2" />
|
||||
{currentStep === 1 ? 'Cancel' : 'Back'}
|
||||
</button>
|
||||
|
||||
{currentStep < 5 && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (currentStep === 1) handleStep1Next();
|
||||
else if (currentStep === 2) handleStep2Next();
|
||||
else if (currentStep === 3) handleStep3Next();
|
||||
}}
|
||||
disabled={loading}
|
||||
className="btn-primary inline-flex items-center disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Processing...' : 'Next'}
|
||||
<ArrowRightIcon className="w-4 h-4 ml-2" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Token Guide Modal */}
|
||||
{showTokenGuide && <CFTokenGuide onClose={() => setShowTokenGuide(false)} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddDomainWizard;
|
||||
|
||||
|
|
@ -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 (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-bold">
|
||||
{account ? 'Hesap Düzenle' : 'Yeni Cloudflare Hesabı Ekle'}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-500 hover:text-gray-700 text-2xl"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-4 bg-red-100 border border-red-400 text-red-700 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Name */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Hesap Adı *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Cloudflare Email *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* API Token */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
API Token {account ? '(Değiştirmek için girin)' : '*'}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={formData.api_token}
|
||||
onChange={(e) => 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}
|
||||
/>
|
||||
<p className="mt-1 text-sm text-gray-600">
|
||||
Token şifreli olarak saklanacaktır
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Max Domains */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Maksimum Domain Sayısı *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.max_domains}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
<p className="mt-1 text-sm text-gray-600">
|
||||
Bu hesapta maksimum kaç domain barındırılabilir
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Notes */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Notlar
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.notes}
|
||||
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
|
||||
placeholder="Hesap hakkında notlar..."
|
||||
rows="3"
|
||||
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Active Status */}
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="is_active"
|
||||
checked={formData.is_active}
|
||||
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
|
||||
className="mr-2"
|
||||
/>
|
||||
<label htmlFor="is_active" className="text-sm font-medium">
|
||||
Hesap aktif
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="flex-1 px-6 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
İptal
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Kaydediliyor...' : account ? 'Güncelle' : 'Ekle'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CFAccountModal
|
||||
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
function CFTokenGuide({ onClose }) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-lg max-w-3xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-bold">Cloudflare API Token Oluşturma Rehberi</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-500 hover:text-gray-700 text-2xl"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Step 1 */}
|
||||
<div className="border-l-4 border-blue-500 pl-4">
|
||||
<h3 className="font-bold text-lg mb-2">1. Cloudflare Dashboard'a Giriş Yapın</h3>
|
||||
<p className="text-gray-700 mb-2">
|
||||
<a
|
||||
href="https://dash.cloudflare.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline"
|
||||
>
|
||||
https://dash.cloudflare.com
|
||||
</a> adresine gidin ve giriş yapın.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Step 2 */}
|
||||
<div className="border-l-4 border-blue-500 pl-4">
|
||||
<h3 className="font-bold text-lg mb-2">2. API Tokens Sayfasına Gidin</h3>
|
||||
<ul className="list-disc list-inside text-gray-700 space-y-1">
|
||||
<li>Sağ üst köşedeki profil ikonuna tıklayın</li>
|
||||
<li>"My Profile" seçeneğini seçin</li>
|
||||
<li>Sol menüden "API Tokens" sekmesine tıklayın</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Step 3 */}
|
||||
<div className="border-l-4 border-blue-500 pl-4">
|
||||
<h3 className="font-bold text-lg mb-2">3. Yeni Token Oluşturun</h3>
|
||||
<ul className="list-disc list-inside text-gray-700 space-y-1">
|
||||
<li>"Create Token" butonuna tıklayın</li>
|
||||
<li>"Edit zone DNS" template'ini seçin</li>
|
||||
<li>Veya "Create Custom Token" ile özel token oluşturun</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Step 4 */}
|
||||
<div className="border-l-4 border-blue-500 pl-4">
|
||||
<h3 className="font-bold text-lg mb-2">4. Token İzinlerini Ayarlayın</h3>
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<p className="font-medium mb-2">Gerekli İzinler:</p>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-600 mr-2">✓</span>
|
||||
<span><strong>Zone - DNS - Edit:</strong> DNS kayıtlarını düzenlemek için</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-600 mr-2">✓</span>
|
||||
<span><strong>Zone - Zone - Read:</strong> Zone bilgilerini okumak için</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-600 mr-2">✓</span>
|
||||
<span><strong>Zone - Zone Settings - Edit:</strong> SSL ayarları için</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step 5 */}
|
||||
<div className="border-l-4 border-blue-500 pl-4">
|
||||
<h3 className="font-bold text-lg mb-2">5. Zone Seçimi</h3>
|
||||
<p className="text-gray-700 mb-2">
|
||||
"Zone Resources" bölümünde:
|
||||
</p>
|
||||
<ul className="list-disc list-inside text-gray-700 space-y-1">
|
||||
<li><strong>Specific zone:</strong> Sadece belirli bir domain için</li>
|
||||
<li><strong>All zones:</strong> Tüm domain'leriniz için (önerilen)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Step 6 */}
|
||||
<div className="border-l-4 border-blue-500 pl-4">
|
||||
<h3 className="font-bold text-lg mb-2">6. Token'ı Oluşturun ve Kopyalayın</h3>
|
||||
<ul className="list-disc list-inside text-gray-700 space-y-1">
|
||||
<li>"Continue to summary" butonuna tıklayın</li>
|
||||
<li>"Create Token" butonuna tıklayın</li>
|
||||
<li>Oluşturulan token'ı kopyalayın</li>
|
||||
</ul>
|
||||
<div className="mt-3 p-3 bg-yellow-50 border border-yellow-200 rounded">
|
||||
<p className="text-sm text-yellow-800">
|
||||
<strong>⚠️ Önemli:</strong> Token sadece bir kez gösterilir!
|
||||
Mutlaka güvenli bir yere kaydedin.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Example Token */}
|
||||
<div className="border-l-4 border-green-500 pl-4">
|
||||
<h3 className="font-bold text-lg mb-2">Token Örneği</h3>
|
||||
<div className="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm overflow-x-auto">
|
||||
y_12345abcdefghijklmnopqrstuvwxyz1234567890ABCD
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mt-2">
|
||||
Token bu formatta olacaktır (genellikle 40 karakter)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Security Tips */}
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<h3 className="font-bold mb-2 flex items-center">
|
||||
<svg className="w-5 h-5 mr-2 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
Güvenlik İpuçları
|
||||
</h3>
|
||||
<ul className="text-sm text-blue-800 space-y-1">
|
||||
<li>• Token'ı kimseyle paylaşmayın</li>
|
||||
<li>• Token'ı güvenli bir şifre yöneticisinde saklayın</li>
|
||||
<li>• Kullanılmayan token'ları silin</li>
|
||||
<li>• Token'ı düzenli olarak yenileyin</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
Anladım
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CFTokenGuide
|
||||
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
|
||||
function NSInstructions({
|
||||
domain,
|
||||
nsInstructions,
|
||||
nsStatus,
|
||||
onCheck,
|
||||
onContinue,
|
||||
loading
|
||||
}) {
|
||||
const [autoCheck, setAutoCheck] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (autoCheck && nsStatus && !nsStatus.is_cloudflare) {
|
||||
const interval = setInterval(() => {
|
||||
onCheck()
|
||||
}, 30000) // Check every 30 seconds
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}
|
||||
}, [autoCheck, nsStatus, onCheck])
|
||||
|
||||
const isConfigured = nsStatus?.is_cloudflare
|
||||
|
||||
return (
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-xl font-bold mb-4">Nameserver Yönlendirmesi</h2>
|
||||
|
||||
<div className="mb-6">
|
||||
<p className="text-gray-600 mb-2">
|
||||
Domain: <strong>{domain}</strong>
|
||||
</p>
|
||||
|
||||
{/* NS Status */}
|
||||
{nsStatus && (
|
||||
<div className={`p-4 rounded-lg mb-4 ${
|
||||
isConfigured
|
||||
? 'bg-green-50 border border-green-200'
|
||||
: 'bg-yellow-50 border border-yellow-200'
|
||||
}`}>
|
||||
<div className="flex items-center">
|
||||
{isConfigured ? (
|
||||
<>
|
||||
<svg className="w-6 h-6 text-green-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="text-green-800 font-medium">
|
||||
✅ Nameserver'lar Cloudflare'e yönlendirilmiş!
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-6 h-6 text-yellow-600 mr-2 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
<span className="text-yellow-800 font-medium">
|
||||
⏳ Nameserver yönlendirmesi bekleniyor...
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{nsStatus.current_nameservers && nsStatus.current_nameservers.length > 0 && (
|
||||
<div className="mt-3 text-sm">
|
||||
<p className="font-medium text-gray-700">Mevcut Nameserver'lar:</p>
|
||||
<ul className="mt-1 space-y-1">
|
||||
{nsStatus.current_nameservers.map((ns, idx) => (
|
||||
<li key={idx} className="text-gray-600 font-mono text-xs">
|
||||
{ns}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
{!isConfigured && nsInstructions && (
|
||||
<div className="mb-6">
|
||||
<h3 className="font-bold mb-3">Nameserver Değiştirme Talimatları:</h3>
|
||||
|
||||
<div className="bg-gray-50 p-4 rounded-lg mb-4">
|
||||
<p className="font-medium text-gray-700 mb-2">Cloudflare Nameserver'ları:</p>
|
||||
<div className="space-y-1">
|
||||
{nsInstructions.cloudflare_nameservers.map((ns, idx) => (
|
||||
<div key={idx} className="flex items-center justify-between bg-white p-2 rounded border">
|
||||
<code className="text-sm font-mono">{ns}</code>
|
||||
<button
|
||||
onClick={() => navigator.clipboard.writeText(ns)}
|
||||
className="text-blue-600 hover:text-blue-800 text-sm"
|
||||
>
|
||||
📋 Kopyala
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-sm text-gray-700">
|
||||
{nsInstructions.instructions.map((instruction, idx) => (
|
||||
<div key={idx} className="flex items-start">
|
||||
<span className="mr-2">{instruction.startsWith(' -') ? ' •' : ''}</span>
|
||||
<span>{instruction.replace(' - ', '')}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>💡 İpucu:</strong> DNS propagation genellikle 1-2 saat içinde tamamlanır,
|
||||
ancak bazı durumlarda 24-48 saat sürebilir.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Auto-check toggle */}
|
||||
{!isConfigured && (
|
||||
<div className="mb-4 flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="autoCheck"
|
||||
checked={autoCheck}
|
||||
onChange={(e) => setAutoCheck(e.target.checked)}
|
||||
className="mr-2"
|
||||
/>
|
||||
<label htmlFor="autoCheck" className="text-sm text-gray-700">
|
||||
Otomatik kontrol (30 saniyede bir)
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3">
|
||||
{!isConfigured && (
|
||||
<button
|
||||
onClick={onCheck}
|
||||
disabled={loading}
|
||||
className="px-6 py-2 border border-blue-600 text-blue-600 rounded-lg hover:bg-blue-50 disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Kontrol ediliyor...' : '🔄 Manuel Kontrol Et'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isConfigured && (
|
||||
<button
|
||||
onClick={onContinue}
|
||||
className="flex-1 bg-green-600 text-white py-2 px-4 rounded-lg hover:bg-green-700"
|
||||
>
|
||||
Devam Et →
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NSInstructions
|
||||
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* Auth Context - Global authentication state management
|
||||
*/
|
||||
import { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { authAPI } from '../services/api';
|
||||
|
||||
const AuthContext = createContext(null);
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [customer, setCustomer] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
|
||||
// Check if user is logged in on mount
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (token) {
|
||||
try {
|
||||
const response = await authAPI.getProfile();
|
||||
setUser(response.data.user);
|
||||
setCustomer(response.data.customer);
|
||||
setIsAuthenticated(true);
|
||||
} catch (error) {
|
||||
console.error('Auth check failed:', error);
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('user');
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
}, []);
|
||||
|
||||
const login = async (email, password) => {
|
||||
try {
|
||||
const response = await authAPI.login({ email, password });
|
||||
const { token, user: userData, customer: customerData } = response.data;
|
||||
|
||||
localStorage.setItem('auth_token', token);
|
||||
localStorage.setItem('user', JSON.stringify(userData));
|
||||
|
||||
setUser(userData);
|
||||
setCustomer(customerData);
|
||||
setIsAuthenticated(true);
|
||||
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.message || 'Login failed',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const register = async (data) => {
|
||||
try {
|
||||
console.log('🔵 Registration attempt:', { email: data.email });
|
||||
const response = await authAPI.register(data);
|
||||
console.log('✅ Registration response:', response.data);
|
||||
const { token, user: userData, customer: customerData } = response.data;
|
||||
|
||||
localStorage.setItem('auth_token', token);
|
||||
localStorage.setItem('user', JSON.stringify(userData));
|
||||
|
||||
setUser(userData);
|
||||
setCustomer(customerData);
|
||||
setIsAuthenticated(true);
|
||||
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
console.error('❌ Registration failed:', error);
|
||||
console.error('Error details:', error.response?.data);
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.message || 'Registration failed',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('user');
|
||||
setUser(null);
|
||||
setCustomer(null);
|
||||
setIsAuthenticated(false);
|
||||
};
|
||||
|
||||
const value = {
|
||||
user,
|
||||
customer,
|
||||
loading,
|
||||
isAuthenticated,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
};
|
||||
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-gray-200;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-gray-50 text-gray-900 font-sans;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Custom component styles */
|
||||
.btn-primary {
|
||||
@apply bg-primary-500 hover:bg-primary-600 text-white font-medium px-4 py-2 rounded-lg transition-colors;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply bg-secondary-500 hover:bg-secondary-600 text-white font-medium px-4 py-2 rounded-lg transition-colors;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
@apply w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none transition-all;
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply bg-white rounded-xl shadow-sm border border-gray-200 p-6;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { adminAPI } from '../services/api'
|
||||
import CFAccountModal from '../components/CFAccountModal'
|
||||
|
||||
function AdminCFAccounts() {
|
||||
const [accounts, setAccounts] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const [success, setSuccess] = useState(null)
|
||||
const [showAddModal, setShowAddModal] = useState(false)
|
||||
const [editingAccount, setEditingAccount] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
loadAccounts()
|
||||
}, [])
|
||||
|
||||
const loadAccounts = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await adminAPI.listCFAccounts()
|
||||
if (response.data.status === 'success') {
|
||||
setAccounts(response.data.accounts)
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Hesaplar yüklenemedi: ' + err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTest = async (accountId) => {
|
||||
try {
|
||||
const response = await adminAPI.testCFAccount(accountId)
|
||||
if (response.data.status === 'success') {
|
||||
setSuccess(`✅ ${response.data.message}`)
|
||||
setTimeout(() => setSuccess(null), 3000)
|
||||
} else {
|
||||
setError(`❌ ${response.data.message}`)
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Test başarısız: ' + err.message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (accountId) => {
|
||||
if (!confirm('Bu hesabı devre dışı bırakmak istediğinizden emin misiniz?')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await adminAPI.deleteCFAccount(accountId)
|
||||
if (response.data.status === 'success') {
|
||||
setSuccess('Hesap devre dışı bırakıldı')
|
||||
loadAccounts()
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.message || 'Silme başarısız')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-3xl font-bold">Cloudflare Hesap Yönetimi</h1>
|
||||
<button
|
||||
onClick={() => setShowAddModal(true)}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
+ Yeni Hesap Ekle
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
{error && (
|
||||
<div className="mb-4 p-4 bg-red-100 border border-red-400 text-red-700 rounded">
|
||||
{error}
|
||||
<button onClick={() => setError(null)} className="float-right">×</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="mb-4 p-4 bg-green-100 border border-green-400 text-green-700 rounded">
|
||||
{success}
|
||||
<button onClick={() => setSuccess(null)} className="float-right">×</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Accounts List */}
|
||||
{loading ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Yükleniyor...</p>
|
||||
</div>
|
||||
) : accounts.length === 0 ? (
|
||||
<div className="bg-white p-12 rounded-lg shadow text-center">
|
||||
<p className="text-gray-600 mb-4">Henüz Cloudflare hesabı eklenmemiş</p>
|
||||
<button
|
||||
onClick={() => setShowAddModal(true)}
|
||||
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
İlk Hesabı Ekle
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{accounts.map((account) => (
|
||||
<div
|
||||
key={account.id}
|
||||
className={`bg-white p-6 rounded-lg shadow border-2 ${
|
||||
account.is_active ? 'border-green-200' : 'border-gray-200'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold">{account.name}</h3>
|
||||
<p className="text-sm text-gray-600">{account.email}</p>
|
||||
</div>
|
||||
<span
|
||||
className={`px-3 py-1 rounded text-sm font-medium ${
|
||||
account.is_active
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{account.is_active ? 'Aktif' : 'Pasif'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Domain Sayısı:</span>
|
||||
<span className="font-medium">
|
||||
{account.current_domain_count} / {account.max_domains}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full"
|
||||
style={{
|
||||
width: `${(account.current_domain_count / account.max_domains) * 100}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{account.notes && (
|
||||
<div className="mb-4 p-3 bg-gray-50 rounded text-sm text-gray-700">
|
||||
<strong>Not:</strong> {account.notes}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleTest(account.id)}
|
||||
className="flex-1 px-3 py-2 border border-blue-600 text-blue-600 rounded hover:bg-blue-50 text-sm"
|
||||
>
|
||||
🧪 Test
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditingAccount(account)}
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded hover:bg-gray-50 text-sm"
|
||||
>
|
||||
✏️ Düzenle
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(account.id)}
|
||||
className="px-3 py-2 border border-red-600 text-red-600 rounded hover:bg-red-50 text-sm"
|
||||
disabled={account.current_domain_count > 0}
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add/Edit Modal */}
|
||||
{(showAddModal || editingAccount) && (
|
||||
<CFAccountModal
|
||||
account={editingAccount}
|
||||
onClose={() => {
|
||||
setShowAddModal(false)
|
||||
setEditingAccount(null)
|
||||
}}
|
||||
onSuccess={() => {
|
||||
setShowAddModal(false)
|
||||
setEditingAccount(null)
|
||||
loadAccounts()
|
||||
setSuccess(editingAccount ? 'Hesap güncellendi' : 'Hesap eklendi')
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AdminCFAccounts
|
||||
|
||||
|
|
@ -0,0 +1,359 @@
|
|||
/**
|
||||
* Customer Dashboard - Main dashboard with sidebar navigation
|
||||
*/
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import {
|
||||
HomeIcon,
|
||||
GlobeAltIcon,
|
||||
ServerIcon,
|
||||
WifiIcon,
|
||||
ShieldCheckIcon,
|
||||
Cog6ToothIcon,
|
||||
ArrowRightOnRectangleIcon,
|
||||
PlusIcon,
|
||||
CheckCircleIcon,
|
||||
XCircleIcon,
|
||||
ClockIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import api from '../services/api';
|
||||
import AddDomainWizard from '../components/AddDomainWizard';
|
||||
|
||||
// Domains Content Component
|
||||
const DomainsContent = ({ customer }) => {
|
||||
const [domains, setDomains] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDomains();
|
||||
}, []);
|
||||
|
||||
const fetchDomains = async () => {
|
||||
try {
|
||||
const response = await api.get('/api/customer/domains');
|
||||
setDomains(response.data.domains || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch domains:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status) => {
|
||||
const badges = {
|
||||
active: { color: 'bg-green-100 text-green-800', icon: CheckCircleIcon, text: 'Active' },
|
||||
pending: { color: 'bg-yellow-100 text-yellow-800', icon: ClockIcon, text: 'Pending' },
|
||||
failed: { color: 'bg-red-100 text-red-800', icon: XCircleIcon, text: 'Failed' },
|
||||
};
|
||||
const badge = badges[status] || badges.pending;
|
||||
const Icon = badge.icon;
|
||||
|
||||
return (
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${badge.color}`}>
|
||||
<Icon className="w-4 h-4 mr-1" />
|
||||
{badge.text}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 border-4 border-primary-500 border-t-transparent rounded-full animate-spin mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Loading domains...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header with Add Button */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">Your Domains</h2>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
{domains.length} of {customer?.max_domains} domains used
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowAddModal(true)}
|
||||
disabled={domains.length >= customer?.max_domains}
|
||||
className="btn-primary inline-flex items-center disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<PlusIcon className="w-5 h-5 mr-2" />
|
||||
Add Domain
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Domain List */}
|
||||
{domains.length === 0 ? (
|
||||
<div className="card text-center py-12">
|
||||
<GlobeAltIcon className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">No domains yet</h3>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Get started by adding your first domain
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setShowAddModal(true)}
|
||||
className="btn-primary inline-flex items-center"
|
||||
>
|
||||
<PlusIcon className="w-5 h-5 mr-2" />
|
||||
Add Your First Domain
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4">
|
||||
{domains.map((domain) => (
|
||||
<div key={domain.id} className="card hover:shadow-lg transition-shadow">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{domain.domain_name}
|
||||
</h3>
|
||||
{getStatusBadge(domain.status)}
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-4 text-sm text-gray-600">
|
||||
<span>DNS: {domain.dns_configured ? '✓ Configured' : '✗ Not configured'}</span>
|
||||
<span>SSL: {domain.ssl_configured ? '✓ Active' : '✗ Pending'}</span>
|
||||
{domain.lb_ip && <span>IP: {domain.lb_ip}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn-secondary">
|
||||
Manage
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add Domain Wizard */}
|
||||
{showAddModal && (
|
||||
<AddDomainWizard
|
||||
onClose={() => setShowAddModal(false)}
|
||||
onSuccess={fetchDomains}
|
||||
customer={customer}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Dashboard = () => {
|
||||
const { user, customer, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [activeTab, setActiveTab] = useState('overview');
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{ id: 'overview', name: 'Overview', icon: HomeIcon },
|
||||
{ id: 'domains', name: 'Domains', icon: GlobeAltIcon },
|
||||
{ id: 'containers', name: 'Containers', icon: ServerIcon },
|
||||
{ id: 'network', name: 'Network', icon: WifiIcon },
|
||||
{ id: 'security', name: 'Security', icon: ShieldCheckIcon },
|
||||
{ id: 'settings', name: 'Settings', icon: Cog6ToothIcon },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-64 bg-white border-r border-gray-200 flex flex-col">
|
||||
{/* Logo */}
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
<img
|
||||
src="https://www.argeict.com/wp-content/uploads/2016/09/arge_logo-4.svg"
|
||||
alt="ARGE ICT"
|
||||
className="h-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* User info */}
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 bg-primary-500 rounded-full flex items-center justify-center text-white font-semibold">
|
||||
{user?.full_name?.charAt(0) || 'U'}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 truncate">
|
||||
{user?.full_name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 truncate">{customer?.company_name || user?.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex items-center justify-between text-xs">
|
||||
<span className="text-gray-500">Plan:</span>
|
||||
<span className="px-2 py-1 bg-primary-100 text-primary-700 rounded-full font-medium">
|
||||
{customer?.subscription_plan || 'Free'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 p-4 space-y-1">
|
||||
{menuItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = activeTab === item.id;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => setActiveTab(item.id)}
|
||||
className={`w-full flex items-center space-x-3 px-4 py-3 rounded-lg transition-colors ${
|
||||
isActive
|
||||
? 'bg-primary-50 text-primary-700'
|
||||
: 'text-gray-700 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-5 h-5" />
|
||||
<span className="font-medium">{item.name}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Logout */}
|
||||
<div className="p-4 border-t border-gray-200">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center space-x-3 px-4 py-3 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||
>
|
||||
<ArrowRightOnRectangleIcon className="w-5 h-5" />
|
||||
<span className="font-medium">Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 overflow-auto">
|
||||
<div className="p-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
{menuItems.find((item) => item.id === activeTab)?.name}
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-1">
|
||||
Welcome back, {user?.full_name}!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Content based on active tab */}
|
||||
{activeTab === 'overview' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{/* Stats cards */}
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Domains</p>
|
||||
<p className="text-3xl font-bold text-gray-900 mt-1">0</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
of {customer?.max_domains} limit
|
||||
</p>
|
||||
</div>
|
||||
<GlobeAltIcon className="w-12 h-12 text-primary-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Containers</p>
|
||||
<p className="text-3xl font-bold text-gray-900 mt-1">0</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
of {customer?.max_containers} limit
|
||||
</p>
|
||||
</div>
|
||||
<ServerIcon className="w-12 h-12 text-secondary-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Status</p>
|
||||
<p className="text-3xl font-bold text-green-600 mt-1">Active</p>
|
||||
<p className="text-xs text-gray-500 mt-1">All systems operational</p>
|
||||
</div>
|
||||
<ShieldCheckIcon className="w-12 h-12 text-green-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'domains' && (
|
||||
<DomainsContent customer={customer} />
|
||||
)}
|
||||
|
||||
{activeTab === 'containers' && (
|
||||
<div className="card">
|
||||
<p className="text-gray-600">Container management module coming soon...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'network' && (
|
||||
<div className="card">
|
||||
<p className="text-gray-600">Network configuration module coming soon...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'security' && (
|
||||
<div className="card">
|
||||
<p className="text-gray-600">Security settings module coming soon...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'settings' && (
|
||||
<div className="card">
|
||||
<h2 className="text-xl font-semibold mb-4">Account Settings</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={user?.email || ''}
|
||||
disabled
|
||||
className="input-field bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Full Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={user?.full_name || ''}
|
||||
disabled
|
||||
className="input-field bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Company Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={customer?.company_name || ''}
|
||||
disabled
|
||||
className="input-field bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { dnsAPI } from '../services/api'
|
||||
|
||||
function DomainList() {
|
||||
const [domains, setDomains] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
fetchDomains()
|
||||
}, [])
|
||||
|
||||
const fetchDomains = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await dnsAPI.getDomains()
|
||||
setDomains(response.data)
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.error || 'Failed to fetch domains')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusBadge = (status) => {
|
||||
const colors = {
|
||||
active: 'bg-green-100 text-green-800',
|
||||
pending: 'bg-yellow-100 text-yellow-800',
|
||||
error: 'bg-red-100 text-red-800',
|
||||
}
|
||||
return colors[status] || 'bg-gray-100 text-gray-800'
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Loading domains...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="alert alert-error">
|
||||
<strong>Error:</strong> {error}
|
||||
</div>
|
||||
<button onClick={fetchDomains} className="btn btn-primary mt-4">
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (domains.length === 0) {
|
||||
return (
|
||||
<div className="card text-center">
|
||||
<div className="w-20 h-20 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-10 h-10 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-800 mb-2">No Domains Yet</h2>
|
||||
<p className="text-gray-600 mb-6">
|
||||
You haven't added any domains yet. Click "Add Domain" to get started.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold text-gray-800">My Domains</h2>
|
||||
<button onClick={fetchDomains} className="btn btn-secondary">
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{domains.map((domain) => (
|
||||
<div key={domain.id} className="card">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-3 mb-2">
|
||||
<h3 className="text-xl font-bold text-gray-800">
|
||||
{domain.domain_name}
|
||||
</h3>
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getStatusBadge(domain.status)}`}>
|
||||
{domain.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-500">Load Balancer IP:</span>
|
||||
<p className="font-medium text-gray-800">{domain.lb_ip || 'N/A'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="text-gray-500">DNS Configured:</span>
|
||||
<p className="font-medium text-gray-800">
|
||||
{domain.dns_configured ? '✓ Yes' : '✗ No'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="text-gray-500">SSL Configured:</span>
|
||||
<p className="font-medium text-gray-800">
|
||||
{domain.ssl_configured ? '✓ Yes' : '✗ No'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="text-gray-500">Cloudflare Proxy:</span>
|
||||
<p className="font-medium text-gray-800">
|
||||
{domain.cf_proxy_enabled ? '✓ Enabled' : '✗ Disabled'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-xs text-gray-500">
|
||||
Added: {new Date(domain.created_at).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ml-4">
|
||||
<a
|
||||
href={`https://${domain.domain_name}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn btn-primary"
|
||||
>
|
||||
Visit Site →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DomainList
|
||||
|
||||
|
|
@ -0,0 +1,346 @@
|
|||
import { useState } from 'react'
|
||||
import { dnsAPI } from '../services/api'
|
||||
|
||||
function DomainSetup() {
|
||||
const [step, setStep] = useState(1)
|
||||
const [domain, setDomain] = useState('')
|
||||
const [cfToken, setCfToken] = useState('')
|
||||
const [zoneInfo, setZoneInfo] = useState(null)
|
||||
const [preview, setPreview] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const [success, setSuccess] = useState(null)
|
||||
|
||||
const handleValidateToken = async (e) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await dnsAPI.validateToken(domain, cfToken)
|
||||
|
||||
if (response.data.status === 'success') {
|
||||
setZoneInfo(response.data)
|
||||
setStep(2)
|
||||
} else {
|
||||
setError(response.data.message)
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.error || 'Failed to validate token')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePreviewChanges = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await dnsAPI.previewChanges(
|
||||
domain,
|
||||
zoneInfo.zone_id,
|
||||
cfToken
|
||||
)
|
||||
|
||||
if (response.data.status !== 'error') {
|
||||
setPreview(response.data)
|
||||
setStep(3)
|
||||
} else {
|
||||
setError(response.data.message)
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.error || 'Failed to preview changes')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleApplyChanges = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await dnsAPI.applyChanges(
|
||||
domain,
|
||||
zoneInfo.zone_id,
|
||||
cfToken,
|
||||
preview,
|
||||
true
|
||||
)
|
||||
|
||||
if (response.data.status === 'success') {
|
||||
setSuccess('Domain successfully configured!')
|
||||
setStep(4)
|
||||
} else {
|
||||
setError('Failed to apply changes')
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.error || 'Failed to apply changes')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setStep(1)
|
||||
setDomain('')
|
||||
setCfToken('')
|
||||
setZoneInfo(null)
|
||||
setPreview(null)
|
||||
setError(null)
|
||||
setSuccess(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Progress Steps */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between">
|
||||
{[1, 2, 3, 4].map((s) => (
|
||||
<div key={s} className="flex items-center">
|
||||
<div
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center font-bold ${
|
||||
step >= s
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-200 text-gray-500'
|
||||
}`}
|
||||
>
|
||||
{s}
|
||||
</div>
|
||||
{s < 4 && (
|
||||
<div
|
||||
className={`w-24 h-1 mx-2 ${
|
||||
step > s ? 'bg-blue-500' : 'bg-gray-200'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-between mt-2 text-sm text-gray-600">
|
||||
<span>Domain Info</span>
|
||||
<span>Verify</span>
|
||||
<span>Preview</span>
|
||||
<span>Complete</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Alert */}
|
||||
{error && (
|
||||
<div className="alert alert-error mb-4">
|
||||
<strong>Error:</strong> {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Success Alert */}
|
||||
{success && (
|
||||
<div className="alert alert-success mb-4">
|
||||
<strong>Success!</strong> {success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 1: Domain & Token */}
|
||||
{step === 1 && (
|
||||
<div className="card">
|
||||
<h2 className="text-2xl font-bold mb-6 text-gray-800">
|
||||
Step 1: Enter Domain & Cloudflare Token
|
||||
</h2>
|
||||
<form onSubmit={handleValidateToken} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Domain Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
placeholder="example.com"
|
||||
value={domain}
|
||||
onChange={(e) => setDomain(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Cloudflare API Token
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className="input"
|
||||
placeholder="Your Cloudflare API Token"
|
||||
value={cfToken}
|
||||
onChange={(e) => setCfToken(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<p className="mt-2 text-sm text-gray-500">
|
||||
Get your API token from Cloudflare Dashboard → My Profile → API Tokens
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary w-full"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Validating...' : 'Validate & Continue'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: Zone Info */}
|
||||
{step === 2 && zoneInfo && (
|
||||
<div className="card">
|
||||
<h2 className="text-2xl font-bold mb-6 text-gray-800">
|
||||
Step 2: Verify Zone Information
|
||||
</h2>
|
||||
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-6">
|
||||
<p className="text-green-800 font-medium">✓ Token validated successfully!</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 text-left">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-500">Zone Name</label>
|
||||
<p className="text-lg font-semibold text-gray-800">{zoneInfo.zone_name}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-500">Zone ID</label>
|
||||
<p className="text-gray-800 font-mono text-sm">{zoneInfo.zone_id}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-500">Status</label>
|
||||
<p className="text-gray-800">{zoneInfo.zone_status}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-500">Nameservers</label>
|
||||
<ul className="list-disc list-inside text-gray-800">
|
||||
{zoneInfo.nameservers?.map((ns, i) => (
|
||||
<li key={i}>{ns}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-4 mt-6">
|
||||
<button
|
||||
onClick={() => setStep(1)}
|
||||
className="btn btn-secondary flex-1"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePreviewChanges}
|
||||
className="btn btn-primary flex-1"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Loading...' : 'Preview Changes'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Preview Changes */}
|
||||
{step === 3 && preview && (
|
||||
<div className="card">
|
||||
<h2 className="text-2xl font-bold mb-6 text-gray-800">
|
||||
Step 3: Preview DNS Changes
|
||||
</h2>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
||||
<p className="text-blue-800">
|
||||
<strong>New IP:</strong> {preview.new_ip}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 text-left">
|
||||
<h3 className="font-semibold text-gray-800">Changes to be applied:</h3>
|
||||
|
||||
{preview.changes?.map((change, i) => (
|
||||
<div key={i} className="border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-medium text-gray-800">
|
||||
{change.record_type} Record: {change.name}
|
||||
</span>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
change.action === 'create' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
|
||||
}`}>
|
||||
{change.action.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{change.current && (
|
||||
<div className="text-sm text-gray-600 mb-1">
|
||||
Current: {change.current.value} (Proxied: {change.current.proxied ? 'Yes' : 'No'})
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-sm text-gray-800 font-medium">
|
||||
New: {change.new.value} (Proxied: {change.new.proxied ? 'Yes' : 'No'})
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-4 mt-6">
|
||||
<button
|
||||
onClick={() => setStep(2)}
|
||||
className="btn btn-secondary flex-1"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
onClick={handleApplyChanges}
|
||||
className="btn btn-primary flex-1"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Applying...' : 'Apply Changes'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 4: Success */}
|
||||
{step === 4 && (
|
||||
<div className="card text-center">
|
||||
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-10 h-10 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h2 className="text-3xl font-bold text-gray-800 mb-4">
|
||||
Domain Configured Successfully!
|
||||
</h2>
|
||||
|
||||
<p className="text-gray-600 mb-6">
|
||||
Your domain <strong>{domain}</strong> has been configured with DNS and SSL settings.
|
||||
</p>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6 text-left">
|
||||
<h3 className="font-semibold text-blue-900 mb-2">What's Next?</h3>
|
||||
<ul className="list-disc list-inside text-blue-800 space-y-1">
|
||||
<li>DNS changes may take a few minutes to propagate</li>
|
||||
<li>SSL certificate will be automatically provisioned by Cloudflare</li>
|
||||
<li>Your site will be accessible via HTTPS within 15 minutes</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={resetForm}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
Add Another Domain
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DomainSetup
|
||||
|
||||
|
|
@ -0,0 +1,710 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { dnsAPI, adminAPI } from '../services/api'
|
||||
import NSInstructions from '../components/NSInstructions'
|
||||
import CFTokenGuide from '../components/CFTokenGuide'
|
||||
|
||||
function DomainSetupNew() {
|
||||
// State management
|
||||
const [step, setStep] = useState(1)
|
||||
const [domain, setDomain] = useState('')
|
||||
const [cfAccountType, setCfAccountType] = useState(null) // 'own' or 'company'
|
||||
|
||||
// Company CF account selection
|
||||
const [companyCFAccounts, setCompanyCFAccounts] = useState([])
|
||||
const [selectedCFAccount, setSelectedCFAccount] = useState(null)
|
||||
|
||||
// Own CF token
|
||||
const [cfToken, setCfToken] = useState('')
|
||||
const [showTokenGuide, setShowTokenGuide] = useState(false)
|
||||
|
||||
// Zone info
|
||||
const [zoneInfo, setZoneInfo] = useState(null)
|
||||
|
||||
// NS status
|
||||
const [nsStatus, setNsStatus] = useState(null)
|
||||
const [nsInstructions, setNsInstructions] = useState(null)
|
||||
const [nsPolling, setNsPolling] = useState(false)
|
||||
|
||||
// DNS preview
|
||||
const [preview, setPreview] = useState(null)
|
||||
|
||||
// Proxy setting
|
||||
const [proxyEnabled, setProxyEnabled] = useState(true)
|
||||
|
||||
// UI state
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const [success, setSuccess] = useState(null)
|
||||
|
||||
// Load company CF accounts on mount
|
||||
useEffect(() => {
|
||||
loadCompanyCFAccounts()
|
||||
}, [])
|
||||
|
||||
// NS polling effect
|
||||
useEffect(() => {
|
||||
let interval
|
||||
if (nsPolling && zoneInfo) {
|
||||
interval = setInterval(() => {
|
||||
checkNameservers()
|
||||
}, 30000) // Check every 30 seconds
|
||||
}
|
||||
return () => clearInterval(interval)
|
||||
}, [nsPolling, zoneInfo])
|
||||
|
||||
const loadCompanyCFAccounts = async () => {
|
||||
try {
|
||||
const response = await adminAPI.listCFAccounts()
|
||||
if (response.data.status === 'success') {
|
||||
setCompanyCFAccounts(response.data.accounts)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load CF accounts:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDomainSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
if (!domain) {
|
||||
setError('Lütfen domain adı girin')
|
||||
return
|
||||
}
|
||||
setError(null)
|
||||
setStep(2)
|
||||
}
|
||||
|
||||
const handleCFAccountTypeSelect = (type) => {
|
||||
setCfAccountType(type)
|
||||
setError(null)
|
||||
|
||||
if (type === 'own') {
|
||||
setStep(3) // Go to token input
|
||||
} else {
|
||||
setStep(4) // Go to company account selection
|
||||
}
|
||||
}
|
||||
|
||||
const handleOwnTokenValidate = async (e) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await dnsAPI.validateToken(domain, cfToken)
|
||||
|
||||
if (response.data.status === 'success') {
|
||||
setZoneInfo(response.data)
|
||||
// Check if NS is already configured
|
||||
await checkNameservers()
|
||||
setStep(5) // Go to NS check/instructions
|
||||
} else {
|
||||
setError(response.data.message)
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.error || 'Token doğrulama başarısız')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCompanyAccountSelect = async (accountId) => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await dnsAPI.selectCompanyAccount(domain, accountId)
|
||||
|
||||
if (response.data.status === 'success') {
|
||||
setSelectedCFAccount(accountId)
|
||||
setZoneInfo(response.data)
|
||||
// Get NS instructions
|
||||
await getNSInstructions(response.data.zone_id, accountId)
|
||||
setStep(5) // Go to NS instructions
|
||||
} else if (response.data.status === 'pending') {
|
||||
setError('Zone bulunamadı. Cloudflare\'de zone oluşturulması gerekiyor.')
|
||||
} else {
|
||||
setError(response.data.message)
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.error || 'Hesap seçimi başarısız')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const checkNameservers = async () => {
|
||||
try {
|
||||
const response = await dnsAPI.checkNameservers(domain)
|
||||
setNsStatus(response.data)
|
||||
|
||||
if (response.data.is_cloudflare) {
|
||||
// NS configured, move to next step
|
||||
setNsPolling(false)
|
||||
setStep(6) // Go to DNS preview
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('NS check failed:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const getNSInstructions = async (zoneId, accountId) => {
|
||||
try {
|
||||
// Get API token for the account
|
||||
const accountResponse = await adminAPI.getCFAccount(accountId, true)
|
||||
const apiToken = accountResponse.data.account.api_token
|
||||
|
||||
const response = await dnsAPI.getNSInstructions(domain, zoneId, apiToken)
|
||||
|
||||
if (response.data.status === 'success') {
|
||||
setNsInstructions(response.data)
|
||||
setNsPolling(true) // Start polling
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to get NS instructions:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const startNSPolling = () => {
|
||||
setNsPolling(true)
|
||||
checkNameservers()
|
||||
}
|
||||
|
||||
const handlePreviewChanges = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const apiToken = cfAccountType === 'own'
|
||||
? cfToken
|
||||
: (await adminAPI.getCFAccount(selectedCFAccount, true)).data.account.api_token
|
||||
|
||||
const response = await dnsAPI.previewChanges(
|
||||
domain,
|
||||
zoneInfo.zone_id,
|
||||
apiToken
|
||||
)
|
||||
|
||||
if (response.data.status !== 'error') {
|
||||
setPreview(response.data)
|
||||
setStep(7) // Go to preview + proxy selection
|
||||
} else {
|
||||
setError(response.data.message)
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.error || 'Önizleme oluşturulamadı')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleApplyChanges = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const apiToken = cfAccountType === 'own'
|
||||
? cfToken
|
||||
: (await adminAPI.getCFAccount(selectedCFAccount, true)).data.account.api_token
|
||||
|
||||
const response = await dnsAPI.applyChanges(
|
||||
domain,
|
||||
zoneInfo.zone_id,
|
||||
apiToken,
|
||||
preview,
|
||||
proxyEnabled
|
||||
)
|
||||
|
||||
if (response.data.status === 'success') {
|
||||
setSuccess('Domain başarıyla yapılandırıldı!')
|
||||
setStep(8) // Success screen
|
||||
} else {
|
||||
setError('Değişiklikler uygulanamadı')
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.error || 'Değişiklikler uygulanamadı')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setStep(1)
|
||||
setDomain('')
|
||||
setCfAccountType(null)
|
||||
setSelectedCFAccount(null)
|
||||
setCfToken('')
|
||||
setZoneInfo(null)
|
||||
setNsStatus(null)
|
||||
setNsInstructions(null)
|
||||
setNsPolling(false)
|
||||
setPreview(null)
|
||||
setProxyEnabled(true)
|
||||
setError(null)
|
||||
setSuccess(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<h1 className="text-3xl font-bold mb-8">Domain Kurulumu</h1>
|
||||
|
||||
{/* Progress Steps */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between">
|
||||
{[
|
||||
{ num: 1, label: 'Domain' },
|
||||
{ num: 2, label: 'CF Hesabı' },
|
||||
{ num: 3, label: 'NS Yönlendirme' },
|
||||
{ num: 4, label: 'DNS Önizleme' },
|
||||
{ num: 5, label: 'Tamamla' },
|
||||
].map((s, idx) => (
|
||||
<div key={s.num} className="flex items-center">
|
||||
<div
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center font-bold ${
|
||||
step >= s.num
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-300 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{s.num}
|
||||
</div>
|
||||
<span className="ml-2 text-sm font-medium">{s.label}</span>
|
||||
{idx < 4 && (
|
||||
<div
|
||||
className={`w-16 h-1 mx-2 ${
|
||||
step > s.num ? 'bg-blue-600' : 'bg-gray-300'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error/Success Messages */}
|
||||
{error && (
|
||||
<div className="mb-4 p-4 bg-red-100 border border-red-400 text-red-700 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="mb-4 p-4 bg-green-100 border border-green-400 text-green-700 rounded">
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 1: Domain Input */}
|
||||
{step === 1 && (
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-xl font-bold mb-4">Domain Adınızı Girin</h2>
|
||||
<form onSubmit={handleDomainSubmit}>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Domain Adı
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={domain}
|
||||
onChange={(e) => setDomain(e.target.value)}
|
||||
placeholder="example.com"
|
||||
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
<p className="mt-2 text-sm text-gray-600">
|
||||
Domain'iniz herhangi bir sağlayıcıdan (GoDaddy, Namecheap, vb.) alınmış olabilir.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
Devam Et
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: CF Account Type Selection */}
|
||||
{step === 2 && (
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-xl font-bold mb-4">Cloudflare Hesabı Seçimi</h2>
|
||||
<p className="mb-6 text-gray-600">
|
||||
Domain: <strong>{domain}</strong>
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Own CF Account */}
|
||||
<button
|
||||
onClick={() => handleCFAccountTypeSelect('own')}
|
||||
className="p-6 border-2 border-gray-300 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition text-left"
|
||||
>
|
||||
<div className="flex items-center mb-3">
|
||||
<svg className="w-8 h-8 text-blue-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
<h3 className="text-lg font-bold">Kendi Cloudflare Hesabım</h3>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">
|
||||
Kendi Cloudflare hesabınızı kullanın. API token oluşturmanız gerekecek.
|
||||
</p>
|
||||
<ul className="mt-3 text-sm text-gray-600 space-y-1">
|
||||
<li>✓ Tam kontrol</li>
|
||||
<li>✓ Kendi hesap ayarlarınız</li>
|
||||
<li>✓ API token gerekli</li>
|
||||
</ul>
|
||||
</button>
|
||||
|
||||
{/* Company CF Account */}
|
||||
<button
|
||||
onClick={() => handleCFAccountTypeSelect('company')}
|
||||
className="p-6 border-2 border-gray-300 rounded-lg hover:border-green-500 hover:bg-green-50 transition text-left"
|
||||
>
|
||||
<div className="flex items-center mb-3">
|
||||
<svg className="w-8 h-8 text-green-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
<h3 className="text-lg font-bold">Sizin Cloudflare Hesabınız</h3>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">
|
||||
Bizim yönettiğimiz Cloudflare hesabını kullanın. API token gerekmez.
|
||||
</p>
|
||||
<ul className="mt-3 text-sm text-gray-600 space-y-1">
|
||||
<li>✓ Kolay kurulum</li>
|
||||
<li>✓ API token gerekmez</li>
|
||||
<li>✓ Yönetilen servis</li>
|
||||
</ul>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setStep(1)}
|
||||
className="mt-6 text-gray-600 hover:text-gray-800"
|
||||
>
|
||||
← Geri
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Own CF Token Input */}
|
||||
{step === 3 && cfAccountType === 'own' && (
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-xl font-bold mb-4">Cloudflare API Token</h2>
|
||||
|
||||
<div className="mb-4 p-4 bg-blue-50 border border-blue-200 rounded">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>Not:</strong> Cloudflare API token'ınız yoksa, aşağıdaki butona tıklayarak nasıl oluşturacağınızı öğrenebilirsiniz.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setShowTokenGuide(true)}
|
||||
className="mt-2 text-blue-600 hover:text-blue-800 font-medium text-sm"
|
||||
>
|
||||
📖 API Token Oluşturma Rehberi
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleOwnTokenValidate}>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
API Token
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={cfToken}
|
||||
onChange={(e) => setCfToken(e.target.value)}
|
||||
placeholder="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
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep(2)}
|
||||
className="px-6 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
← Geri
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Doğrulanıyor...' : 'Token\'ı Doğrula'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 4: Company CF Account Selection */}
|
||||
{step === 4 && cfAccountType === 'company' && (
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-xl font-bold mb-4">Cloudflare Hesabı Seçin</h2>
|
||||
|
||||
{companyCFAccounts.length === 0 ? (
|
||||
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded">
|
||||
<p className="text-yellow-800">
|
||||
Henüz tanımlı Cloudflare hesabı yok. Lütfen admin panelinden hesap ekleyin.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{companyCFAccounts.map((account) => (
|
||||
<button
|
||||
key={account.id}
|
||||
onClick={() => handleCompanyAccountSelect(account.id)}
|
||||
disabled={loading || !account.is_active}
|
||||
className={`w-full p-4 border-2 rounded-lg text-left transition ${
|
||||
account.is_active
|
||||
? 'border-gray-300 hover:border-blue-500 hover:bg-blue-50'
|
||||
: 'border-gray-200 bg-gray-100 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-bold">{account.name}</h3>
|
||||
<p className="text-sm text-gray-600">{account.email}</p>
|
||||
</div>
|
||||
<div className="text-right text-sm">
|
||||
<span className={`px-2 py-1 rounded ${
|
||||
account.is_active ? 'bg-green-100 text-green-800' : 'bg-gray-200 text-gray-600'
|
||||
}`}>
|
||||
{account.is_active ? 'Aktif' : 'Pasif'}
|
||||
</span>
|
||||
<p className="mt-1 text-gray-600">
|
||||
{account.current_domain_count}/{account.max_domains} domain
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setStep(2)}
|
||||
className="mt-6 text-gray-600 hover:text-gray-800"
|
||||
>
|
||||
← Geri
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 5: NS Instructions/Check */}
|
||||
{step === 5 && (
|
||||
<NSInstructions
|
||||
domain={domain}
|
||||
nsInstructions={nsInstructions}
|
||||
nsStatus={nsStatus}
|
||||
onCheck={checkNameservers}
|
||||
onContinue={handlePreviewChanges}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Step 6-7: DNS Preview + Proxy Selection */}
|
||||
{(step === 6 || step === 7) && preview && (
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-xl font-bold mb-4">DNS Değişiklik Önizlemesi</h2>
|
||||
|
||||
<div className="mb-6">
|
||||
<p className="text-gray-600 mb-4">
|
||||
Domain: <strong>{domain}</strong>
|
||||
</p>
|
||||
|
||||
{/* DNS Changes */}
|
||||
<div className="space-y-4">
|
||||
{preview.changes && preview.changes.map((change, idx) => (
|
||||
<div key={idx} className="border rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-bold">
|
||||
{change.record_type} Kaydı: {change.name === '@' ? domain : `${change.name}.${domain}`}
|
||||
</h3>
|
||||
<span className={`px-3 py-1 rounded text-sm font-medium ${
|
||||
change.action === 'create'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-blue-100 text-blue-800'
|
||||
}`}>
|
||||
{change.action === 'create' ? 'Yeni Kayıt' : 'Güncelleme'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Current */}
|
||||
{change.current && (
|
||||
<div className="bg-red-50 p-3 rounded">
|
||||
<p className="text-sm font-medium text-red-800 mb-2">Mevcut Durum</p>
|
||||
<p className="text-sm text-gray-700">IP: <code>{change.current.value}</code></p>
|
||||
<p className="text-sm text-gray-700">
|
||||
Proxy: {change.current.proxied ? '✅ Açık' : '❌ Kapalı'}
|
||||
</p>
|
||||
<p className="text-sm text-gray-700">TTL: {change.current.ttl}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* New */}
|
||||
<div className={`p-3 rounded ${change.current ? 'bg-green-50' : 'bg-blue-50'}`}>
|
||||
<p className={`text-sm font-medium mb-2 ${
|
||||
change.current ? 'text-green-800' : 'text-blue-800'
|
||||
}`}>
|
||||
Yeni Durum
|
||||
</p>
|
||||
<p className="text-sm text-gray-700">IP: <code>{change.new.value}</code></p>
|
||||
<p className="text-sm text-gray-700">
|
||||
Proxy: {change.new.proxied ? '✅ Açık' : '❌ Kapalı'}
|
||||
</p>
|
||||
<p className="text-sm text-gray-700">TTL: {change.new.ttl}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Preserved Records */}
|
||||
{preview.preserved_count > 0 && (
|
||||
<div className="mt-4 p-4 bg-gray-50 rounded-lg">
|
||||
<p className="text-sm text-gray-700">
|
||||
<strong>{preview.preserved_count}</strong> adet diğer DNS kaydı korunacak
|
||||
(MX, TXT, CNAME, vb.)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Proxy Selection */}
|
||||
<div className="mb-6 p-4 border-2 border-blue-200 rounded-lg bg-blue-50">
|
||||
<h3 className="font-bold mb-3 flex items-center">
|
||||
<svg className="w-5 h-5 mr-2 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
Cloudflare Proxy Ayarı
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-start p-3 bg-white rounded border-2 border-green-500 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="proxy"
|
||||
checked={proxyEnabled}
|
||||
onChange={() => setProxyEnabled(true)}
|
||||
className="mt-1 mr-3"
|
||||
/>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">✅ Proxy Açık (Önerilen)</p>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
• DDoS koruması<br/>
|
||||
• CDN hızlandırma<br/>
|
||||
• SSL/TLS şifreleme<br/>
|
||||
• IP gizleme
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="flex items-start p-3 bg-white rounded border-2 border-gray-300 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="proxy"
|
||||
checked={!proxyEnabled}
|
||||
onChange={() => setProxyEnabled(false)}
|
||||
className="mt-1 mr-3"
|
||||
/>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">❌ Proxy Kapalı (Sadece DNS)</p>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
• Sadece DNS yönlendirmesi<br/>
|
||||
• Cloudflare koruması yok<br/>
|
||||
• Sunucu IP'si görünür
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Confirmation */}
|
||||
<div className="mb-6 p-4 bg-yellow-50 border border-yellow-200 rounded">
|
||||
<p className="text-sm text-yellow-800">
|
||||
<strong>⚠️ Dikkat:</strong> Bu değişiklikleri uyguladığınızda, domain'inizin DNS kayıtları
|
||||
güncellenecektir. DNS propagation 1-2 saat sürebilir.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => setStep(5)}
|
||||
className="px-6 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
← Geri
|
||||
</button>
|
||||
<button
|
||||
onClick={handleApplyChanges}
|
||||
disabled={loading}
|
||||
className="flex-1 bg-green-600 text-white py-2 px-4 rounded-lg hover:bg-green-700 disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Uygulanıyor...' : '✅ Değişiklikleri Uygula'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 8: Success */}
|
||||
{step === 8 && (
|
||||
<div className="bg-white p-6 rounded-lg shadow text-center">
|
||||
<div className="mb-6">
|
||||
<svg className="w-20 h-20 text-green-600 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-bold text-green-600 mb-4">
|
||||
🎉 Domain Başarıyla Yapılandırıldı!
|
||||
</h2>
|
||||
|
||||
<div className="mb-6 text-left bg-gray-50 p-4 rounded-lg">
|
||||
<h3 className="font-bold mb-2">Yapılandırma Özeti:</h3>
|
||||
<ul className="space-y-2 text-sm text-gray-700">
|
||||
<li>✅ Domain: <strong>{domain}</strong></li>
|
||||
<li>✅ Cloudflare: {cfAccountType === 'own' ? 'Kendi Hesabınız' : 'Şirket Hesabı'}</li>
|
||||
<li>✅ DNS Kayıtları: Güncellendi</li>
|
||||
<li>✅ SSL/TLS: Yapılandırıldı</li>
|
||||
<li>✅ Proxy: {proxyEnabled ? 'Açık' : 'Kapalı'}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>📌 Sonraki Adımlar:</strong><br/>
|
||||
• DNS propagation 1-2 saat içinde tamamlanacak<br/>
|
||||
• SSL sertifikası otomatik olarak oluşturulacak<br/>
|
||||
• Domain'iniz 24 saat içinde aktif olacak
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 justify-center">
|
||||
<button
|
||||
onClick={resetForm}
|
||||
className="px-6 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
Yeni Domain Ekle
|
||||
</button>
|
||||
<button
|
||||
onClick={() => window.location.href = '/domains'}
|
||||
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
Domain Listesine Git
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CF Token Guide Modal */}
|
||||
{showTokenGuide && (
|
||||
<CFTokenGuide onClose={() => setShowTokenGuide(false)} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DomainSetupNew
|
||||
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
/**
|
||||
* Landing Page - Register/Login with animations
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
const Landing = () => {
|
||||
const [isLogin, setIsLogin] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const navigate = useNavigate();
|
||||
const { login, register } = useAuth();
|
||||
|
||||
// Form states
|
||||
const [formData, setFormData] = useState({
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirm: '',
|
||||
full_name: '',
|
||||
company_name: '',
|
||||
});
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||
setError('');
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
let result;
|
||||
if (isLogin) {
|
||||
result = await login(formData.email, formData.password);
|
||||
} else {
|
||||
result = await register(formData);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
setError(result.error);
|
||||
}
|
||||
} catch (err) {
|
||||
setError('An unexpected error occurred');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-primary-50 via-white to-secondary-50 flex items-center justify-center p-4">
|
||||
{/* Background decoration */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute -top-40 -right-40 w-80 h-80 bg-primary-200 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob"></div>
|
||||
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-secondary-200 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-2000"></div>
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-80 h-80 bg-brand-green-light/20 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-4000"></div>
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-md relative z-10">
|
||||
{/* Logo */}
|
||||
<div className="text-center mb-8">
|
||||
<img
|
||||
src="https://www.argeict.com/wp-content/uploads/2016/09/arge_logo-4.svg"
|
||||
alt="ARGE ICT"
|
||||
className="h-16 mx-auto mb-4"
|
||||
/>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Hosting Platform</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Professional WordPress hosting with container infrastructure
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form Card */}
|
||||
<div className="card">
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b border-gray-200 mb-6">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsLogin(true);
|
||||
setError('');
|
||||
}}
|
||||
className={`flex-1 py-3 text-center font-medium transition-colors ${
|
||||
isLogin
|
||||
? 'text-primary-600 border-b-2 border-primary-600'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsLogin(false);
|
||||
setError('');
|
||||
}}
|
||||
className={`flex-1 py-3 text-center font-medium transition-colors ${
|
||||
!isLogin
|
||||
? 'text-primary-600 border-b-2 border-primary-600'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Register
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 text-red-700 rounded-lg text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{!isLogin && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Full Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="full_name"
|
||||
value={formData.full_name}
|
||||
onChange={handleChange}
|
||||
className="input-field"
|
||||
required={!isLogin}
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Company Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="company_name"
|
||||
value={formData.company_name}
|
||||
onChange={handleChange}
|
||||
className="input-field"
|
||||
placeholder="Acme Inc (optional)"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
className="input-field"
|
||||
required
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Password *
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
className="input-field"
|
||||
required
|
||||
placeholder="••••••••"
|
||||
minLength={8}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isLogin && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Confirm Password *
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password_confirm"
|
||||
value={formData.password_confirm}
|
||||
onChange={handleChange}
|
||||
className="input-field"
|
||||
required={!isLogin}
|
||||
placeholder="••••••••"
|
||||
minLength={8}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full btn-primary disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? 'Please wait...' : isLogin ? 'Login' : 'Create Account'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<p className="text-center text-sm text-gray-600 mt-6">
|
||||
© 2026 ARGE ICT. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@keyframes blob {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
33% { transform: translate(30px, -50px) scale(1.1); }
|
||||
66% { transform: translate(-20px, 20px) scale(0.9); }
|
||||
}
|
||||
.animate-blob {
|
||||
animation: blob 7s infinite;
|
||||
}
|
||||
.animation-delay-2000 {
|
||||
animation-delay: 2s;
|
||||
}
|
||||
.animation-delay-4000 {
|
||||
animation-delay: 4s;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Landing;
|
||||
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
import axios from 'axios'
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL || 'https://api.argeict.net'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
// Request interceptor - Add auth token
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('auth_token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// Response interceptor - Handle errors
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// Token expired or invalid
|
||||
localStorage.removeItem('auth_token')
|
||||
localStorage.removeItem('user')
|
||||
window.location.href = '/'
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// Auth API
|
||||
export const authAPI = {
|
||||
register: (data) => api.post('/api/auth/register', data),
|
||||
login: (data) => api.post('/api/auth/login', data),
|
||||
getProfile: () => api.get('/api/auth/me'),
|
||||
verifyToken: (token) => api.post('/api/auth/verify-token', { token }),
|
||||
}
|
||||
|
||||
export const dnsAPI = {
|
||||
// Health check
|
||||
health: () => api.get('/health'),
|
||||
|
||||
// Nameserver operations
|
||||
checkNameservers: (domain) =>
|
||||
api.post('/api/dns/check-nameservers', { domain }),
|
||||
|
||||
getNSInstructions: (domain, zoneId, apiToken) =>
|
||||
api.post('/api/dns/get-ns-instructions', {
|
||||
domain,
|
||||
zone_id: zoneId,
|
||||
api_token: apiToken,
|
||||
}),
|
||||
|
||||
// Validate Cloudflare token (customer's own token)
|
||||
validateToken: (domain, cfToken) =>
|
||||
api.post('/api/dns/validate-token', {
|
||||
domain,
|
||||
cf_token: cfToken,
|
||||
}),
|
||||
|
||||
// Select company CF account
|
||||
selectCompanyAccount: (domain, cfAccountId) =>
|
||||
api.post('/api/dns/select-company-account', {
|
||||
domain,
|
||||
cf_account_id: cfAccountId,
|
||||
}),
|
||||
|
||||
// Preview DNS changes
|
||||
previewChanges: (domain, zoneId, cfToken) =>
|
||||
api.post('/api/dns/preview-changes', {
|
||||
domain,
|
||||
zone_id: zoneId,
|
||||
cf_token: cfToken,
|
||||
}),
|
||||
|
||||
// Apply DNS changes
|
||||
applyChanges: (domain, zoneId, cfToken, preview, proxyEnabled = true) =>
|
||||
api.post('/api/dns/apply-changes', {
|
||||
domain,
|
||||
zone_id: zoneId,
|
||||
cf_token: cfToken,
|
||||
preview,
|
||||
proxy_enabled: proxyEnabled,
|
||||
customer_id: 1, // TODO: Get from auth
|
||||
}),
|
||||
|
||||
// Get domains
|
||||
getDomains: (customerId = 1) =>
|
||||
api.get('/api/domains', {
|
||||
params: { customer_id: customerId },
|
||||
}),
|
||||
|
||||
// Get domain by ID
|
||||
getDomain: (domainId) => api.get(`/api/domains/${domainId}`),
|
||||
}
|
||||
|
||||
// Admin API
|
||||
export const adminAPI = {
|
||||
// CF Account management
|
||||
listCFAccounts: () => api.get('/api/admin/cf-accounts'),
|
||||
|
||||
getCFAccount: (accountId, includeToken = false) =>
|
||||
api.get(`/api/admin/cf-accounts/${accountId}`, {
|
||||
params: { include_token: includeToken },
|
||||
}),
|
||||
|
||||
createCFAccount: (data) => api.post('/api/admin/cf-accounts', data),
|
||||
|
||||
updateCFAccount: (accountId, data) =>
|
||||
api.put(`/api/admin/cf-accounts/${accountId}`, data),
|
||||
|
||||
deleteCFAccount: (accountId) =>
|
||||
api.delete(`/api/admin/cf-accounts/${accountId}`),
|
||||
|
||||
testCFAccount: (accountId) =>
|
||||
api.post(`/api/admin/cf-accounts/${accountId}/test`),
|
||||
}
|
||||
|
||||
export default api
|
||||
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// Brand colors from ARGE ICT logo
|
||||
brand: {
|
||||
green: {
|
||||
DEFAULT: '#159052',
|
||||
dark: '#046D3F',
|
||||
light: '#53BA6F',
|
||||
},
|
||||
orange: '#F69036',
|
||||
blue: '#0F578B',
|
||||
red: '#B42832',
|
||||
},
|
||||
// Semantic colors
|
||||
primary: {
|
||||
50: '#e6f7ef',
|
||||
100: '#b3e6d0',
|
||||
200: '#80d5b1',
|
||||
300: '#4dc492',
|
||||
400: '#1ab373',
|
||||
500: '#159052', // Main brand green
|
||||
600: '#117342',
|
||||
700: '#0d5631',
|
||||
800: '#093921',
|
||||
900: '#051c10',
|
||||
},
|
||||
secondary: {
|
||||
50: '#fff3e6',
|
||||
100: '#ffdbb3',
|
||||
200: '#ffc380',
|
||||
300: '#ffab4d',
|
||||
400: '#ff931a',
|
||||
500: '#F69036', // Brand orange
|
||||
600: '#c5722b',
|
||||
700: '#945520',
|
||||
800: '#633816',
|
||||
900: '#321c0b',
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 3001,
|
||||
cors: true,
|
||||
allowedHosts: [
|
||||
'argeict.net',
|
||||
'www.argeict.net',
|
||||
'176.96.129.77',
|
||||
'localhost'
|
||||
],
|
||||
hmr: {
|
||||
clientPort: 443,
|
||||
protocol: 'wss'
|
||||
},
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'https://api.argeict.net',
|
||||
changeOrigin: true,
|
||||
secure: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
[Unit]
|
||||
Description=Hosting Platform Backend API
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www-data
|
||||
Group=www-data
|
||||
WorkingDirectory=/var/www/hosting-backend
|
||||
Environment="PATH=/var/www/hosting-backend/venv/bin"
|
||||
ExecStart=/var/www/hosting-backend/venv/bin/python app/main.py
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=hosting-backend
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
Loading…
Reference in New Issue