Initial commit: Customer panel

This commit is contained in:
oguz ozturk 2026-01-11 17:38:39 +03:00
commit 2f98057b43
54 changed files with 7517 additions and 0 deletions

201
ARCHITECTURE.md Normal file
View File

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

257
DEPLOYMENT.md Normal file
View File

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

155
DEPLOYMENT_SUMMARY.md Normal file
View File

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

176
IMPLEMENTATION_PLAN.md Normal file
View File

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

53
README.md Normal file
View File

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

228
WEBHOOK_SETUP.md Normal file
View File

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

13
backend/.env Normal file
View File

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

23
backend/.env.example Normal file
View File

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

0
backend/app/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

View File

32
backend/app/config.py Normal file
View File

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

223
backend/app/main.py Normal file
View File

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

View File

@ -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']

View File

@ -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,
}

123
backend/app/models/user.py Normal file
View File

@ -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}>'

270
backend/app/routes/admin.py Normal file
View File

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

220
backend/app/routes/auth.py Normal file
View File

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

View File

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

293
backend/app/routes/dns.py Normal file
View File

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

View File

View File

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

View File

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

View File

@ -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": []
}

View File

View File

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

32
backend/requirements.txt Normal file
View File

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

151
backend_main.py Normal file
View File

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

98
deploy.sh Executable file
View File

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

2
frontend/.env Normal file
View File

@ -0,0 +1,2 @@
VITE_API_URL=https://api.argeict.net

2
frontend/.env.production Normal file
View File

@ -0,0 +1,2 @@
VITE_API_URL=https://api.argeict.net

14
frontend/index.html Normal file
View File

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

33
frontend/package.json Normal file
View File

@ -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"
}
}

View File

@ -0,0 +1,7 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

79
frontend/src/App.css Normal file
View File

@ -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;
}

47
frontend/src/App.jsx Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>;
};

35
frontend/src/index.css Normal file
View File

@ -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;
}
}

11
frontend/src/main.jsx Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: [],
}

29
frontend/vite.config.js Normal file
View File

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

22
hosting-backend.service Normal file
View File

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