Add authentication system - User model, JWT service, auth routes
This commit is contained in:
parent
6fa9e5c43b
commit
87611a284b
|
|
@ -0,0 +1,201 @@
|
||||||
|
# 🏗️ Hosting Platform - Modular Architecture
|
||||||
|
|
||||||
|
## 📋 Project Overview
|
||||||
|
|
||||||
|
Professional WordPress hosting platform with container infrastructure and automation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Architecture Principles
|
||||||
|
|
||||||
|
1. **Modularity** - Each feature is a separate module
|
||||||
|
2. **Scalability** - Easy to add new features without breaking existing code
|
||||||
|
3. **Separation of Concerns** - Clear boundaries between layers
|
||||||
|
4. **Security First** - Authentication, encryption, and access control
|
||||||
|
5. **Clean Code** - Readable, maintainable, and well-documented
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏛️ System Architecture
|
||||||
|
|
||||||
|
### Frontend Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/
|
||||||
|
├── customer-portal/ # argeict.net
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── pages/
|
||||||
|
│ │ │ ├── Landing.jsx # Register/Login page
|
||||||
|
│ │ │ ├── Dashboard.jsx # Customer dashboard
|
||||||
|
│ │ │ ├── DNS/ # DNS module
|
||||||
|
│ │ │ ├── Container/ # Container module
|
||||||
|
│ │ │ ├── Network/ # Network module
|
||||||
|
│ │ │ └── Security/ # Security module
|
||||||
|
│ │ ├── components/
|
||||||
|
│ │ │ ├── auth/ # Auth components
|
||||||
|
│ │ │ ├── layout/ # Layout components
|
||||||
|
│ │ │ └── shared/ # Shared components
|
||||||
|
│ │ ├── services/
|
||||||
|
│ │ │ ├── api.js # API client
|
||||||
|
│ │ │ ├── auth.js # Auth service
|
||||||
|
│ │ │ └── storage.js # Local storage
|
||||||
|
│ │ ├── hooks/ # Custom React hooks
|
||||||
|
│ │ ├── context/ # React context
|
||||||
|
│ │ └── utils/ # Utility functions
|
||||||
|
│ └── public/
|
||||||
|
│
|
||||||
|
├── admin-portal/ # adminpanel.argeict.net
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── pages/
|
||||||
|
│ │ │ ├── Login.jsx # Admin login
|
||||||
|
│ │ │ ├── Dashboard.jsx # Admin dashboard
|
||||||
|
│ │ │ ├── CFAccounts/ # CF management
|
||||||
|
│ │ │ ├── Customers/ # Customer management
|
||||||
|
│ │ │ └── Settings/ # System settings
|
||||||
|
│ │ ├── components/
|
||||||
|
│ │ ├── services/
|
||||||
|
│ │ └── utils/
|
||||||
|
│ └── public/
|
||||||
|
│
|
||||||
|
└── shared/ # Shared code between portals
|
||||||
|
├── components/
|
||||||
|
├── utils/
|
||||||
|
└── styles/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/
|
||||||
|
├── app/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── main.py # Application entry point
|
||||||
|
│ ├── config.py # Configuration
|
||||||
|
│ │
|
||||||
|
│ ├── models/ # Database models
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── user.py # User/Customer model
|
||||||
|
│ │ ├── domain.py # Domain model
|
||||||
|
│ │ ├── cloudflare.py # CF Account model
|
||||||
|
│ │ ├── container.py # Container model
|
||||||
|
│ │ └── base.py # Base model
|
||||||
|
│ │
|
||||||
|
│ ├── routes/ # API routes (blueprints)
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── auth.py # Authentication routes
|
||||||
|
│ │ ├── customer.py # Customer routes
|
||||||
|
│ │ ├── dns.py # DNS routes
|
||||||
|
│ │ ├── container.py # Container routes
|
||||||
|
│ │ ├── admin.py # Admin routes
|
||||||
|
│ │ └── webhook.py # Webhook routes
|
||||||
|
│ │
|
||||||
|
│ ├── services/ # Business logic
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── auth_service.py # Authentication logic
|
||||||
|
│ │ ├── cloudflare_service.py # Cloudflare API
|
||||||
|
│ │ ├── nameserver_service.py # DNS/NS logic
|
||||||
|
│ │ ├── container_service.py # Container management
|
||||||
|
│ │ └── email_service.py # Email notifications
|
||||||
|
│ │
|
||||||
|
│ ├── middleware/ # Middleware
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── auth.py # Auth middleware
|
||||||
|
│ │ ├── rate_limit.py # Rate limiting
|
||||||
|
│ │ └── cors.py # CORS configuration
|
||||||
|
│ │
|
||||||
|
│ ├── utils/ # Utilities
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── encryption.py # Encryption helpers
|
||||||
|
│ │ ├── validators.py # Input validation
|
||||||
|
│ │ └── helpers.py # General helpers
|
||||||
|
│ │
|
||||||
|
│ └── migrations/ # Database migrations
|
||||||
|
│
|
||||||
|
├── tests/ # Tests
|
||||||
|
│ ├── unit/
|
||||||
|
│ ├── integration/
|
||||||
|
│ └── e2e/
|
||||||
|
│
|
||||||
|
└── requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗄️ Database Schema
|
||||||
|
|
||||||
|
### Users Table
|
||||||
|
- id, email, password_hash, full_name
|
||||||
|
- is_active, is_verified, created_at, updated_at
|
||||||
|
- role (customer, admin)
|
||||||
|
|
||||||
|
### Customers Table (extends Users)
|
||||||
|
- user_id (FK), company_name, phone
|
||||||
|
- billing_info, subscription_plan
|
||||||
|
|
||||||
|
### CloudflareAccounts Table
|
||||||
|
- id, name, email, api_token_encrypted
|
||||||
|
- is_active, max_domains, current_domain_count
|
||||||
|
|
||||||
|
### Domains Table
|
||||||
|
- id, domain_name, customer_id (FK)
|
||||||
|
- cf_account_id (FK), cf_zone_id
|
||||||
|
- status, created_at
|
||||||
|
|
||||||
|
### Containers Table (Future)
|
||||||
|
- id, customer_id (FK), domain_id (FK)
|
||||||
|
- container_id, status, resources
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Design System
|
||||||
|
|
||||||
|
### Brand Colors (from logo)
|
||||||
|
- Primary: #0066CC (Blue)
|
||||||
|
- Secondary: #00A3E0 (Light Blue)
|
||||||
|
- Accent: #FF6B35 (Orange)
|
||||||
|
- Dark: #1A1A1A
|
||||||
|
- Light: #F5F5F5
|
||||||
|
|
||||||
|
### Typography
|
||||||
|
- Headings: Inter, sans-serif
|
||||||
|
- Body: Inter, sans-serif
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Security
|
||||||
|
|
||||||
|
1. **Authentication**: JWT tokens
|
||||||
|
2. **Password**: bcrypt hashing
|
||||||
|
3. **API Tokens**: Fernet encryption
|
||||||
|
4. **HTTPS**: All communications
|
||||||
|
5. **Rate Limiting**: Per endpoint
|
||||||
|
6. **CORS**: Configured per domain
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Technology Stack
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- React 18 + Vite
|
||||||
|
- React Router (multi-app routing)
|
||||||
|
- TailwindCSS
|
||||||
|
- Axios
|
||||||
|
- React Query (data fetching)
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- Flask 3.0
|
||||||
|
- SQLAlchemy 2.0
|
||||||
|
- PostgreSQL 16
|
||||||
|
- Redis 7.0
|
||||||
|
- JWT authentication
|
||||||
|
|
||||||
|
### DevOps
|
||||||
|
- Docker (containers)
|
||||||
|
- Nginx (reverse proxy)
|
||||||
|
- Supervisor (process management)
|
||||||
|
- Gitea (version control)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Next Steps**: Implementation in phases
|
||||||
|
|
||||||
|
|
@ -0,0 +1,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!**
|
||||||
|
|
||||||
|
|
@ -6,10 +6,11 @@ import redis
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from app.config import Config
|
from app.config import Config
|
||||||
from app.models.domain import db, Domain, DNSRecord, CloudflareAccount
|
from app.models import db, Domain, DNSRecord, CloudflareAccount, User, Customer
|
||||||
from app.services.cloudflare_service import CloudflareService
|
from app.services.cloudflare_service import CloudflareService
|
||||||
|
|
||||||
# Import blueprints
|
# Import blueprints
|
||||||
|
from app.routes.auth import auth_bp
|
||||||
from app.routes.admin import admin_bp
|
from app.routes.admin import admin_bp
|
||||||
from app.routes.dns import dns_bp
|
from app.routes.dns import dns_bp
|
||||||
|
|
||||||
|
|
@ -26,6 +27,7 @@ db.init_app(app)
|
||||||
migrate = Migrate(app, db)
|
migrate = Migrate(app, db)
|
||||||
|
|
||||||
# Register blueprints
|
# Register blueprints
|
||||||
|
app.register_blueprint(auth_bp)
|
||||||
app.register_blueprint(admin_bp)
|
app.register_blueprint(admin_bp)
|
||||||
app.register_blueprint(dns_bp)
|
app.register_blueprint(dns_bp)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
from app.models.domain import db, CloudflareAccount, Domain, DNSRecord
|
from app.models.domain import db, CloudflareAccount, Domain, DNSRecord
|
||||||
|
from app.models.user import User, Customer
|
||||||
|
|
||||||
__all__ = ['db', 'CloudflareAccount', 'Domain', 'DNSRecord']
|
__all__ = ['db', 'CloudflareAccount', 'Domain', 'DNSRecord', 'User', 'Customer']
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
"""
|
||||||
|
User and Customer models for authentication and customer management
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
from app.models.domain import db
|
||||||
|
|
||||||
|
|
||||||
|
class User(db.Model):
|
||||||
|
"""Base user model for authentication"""
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
email = db.Column(db.String(255), unique=True, nullable=False, index=True)
|
||||||
|
password_hash = db.Column(db.String(255), nullable=False)
|
||||||
|
full_name = db.Column(db.String(255), nullable=False)
|
||||||
|
|
||||||
|
# Account status
|
||||||
|
is_active = db.Column(db.Boolean, default=True)
|
||||||
|
is_verified = db.Column(db.Boolean, default=False)
|
||||||
|
role = db.Column(db.String(20), default='customer') # 'customer' or 'admin'
|
||||||
|
|
||||||
|
# Timestamps
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
last_login = db.Column(db.DateTime, nullable=True)
|
||||||
|
|
||||||
|
# Verification
|
||||||
|
verification_token = db.Column(db.String(255), nullable=True)
|
||||||
|
reset_token = db.Column(db.String(255), nullable=True)
|
||||||
|
reset_token_expires = db.Column(db.DateTime, nullable=True)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
customer = db.relationship('Customer', backref='user', uselist=False, cascade='all, delete-orphan')
|
||||||
|
|
||||||
|
def set_password(self, password):
|
||||||
|
"""Hash and set password"""
|
||||||
|
self.password_hash = generate_password_hash(password)
|
||||||
|
|
||||||
|
def check_password(self, password):
|
||||||
|
"""Verify password"""
|
||||||
|
return check_password_hash(self.password_hash, password)
|
||||||
|
|
||||||
|
def to_dict(self, include_sensitive=False):
|
||||||
|
"""Convert to dictionary"""
|
||||||
|
data = {
|
||||||
|
'id': self.id,
|
||||||
|
'email': self.email,
|
||||||
|
'full_name': self.full_name,
|
||||||
|
'role': self.role,
|
||||||
|
'is_active': self.is_active,
|
||||||
|
'is_verified': self.is_verified,
|
||||||
|
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||||
|
'last_login': self.last_login.isoformat() if self.last_login else None
|
||||||
|
}
|
||||||
|
|
||||||
|
if include_sensitive:
|
||||||
|
data['verification_token'] = self.verification_token
|
||||||
|
data['reset_token'] = self.reset_token
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<User {self.email}>'
|
||||||
|
|
||||||
|
|
||||||
|
class Customer(db.Model):
|
||||||
|
"""Customer profile extending User"""
|
||||||
|
__tablename__ = "customers"
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, unique=True)
|
||||||
|
|
||||||
|
# Company info
|
||||||
|
company_name = db.Column(db.String(255), nullable=True)
|
||||||
|
phone = db.Column(db.String(50), nullable=True)
|
||||||
|
|
||||||
|
# Billing
|
||||||
|
billing_address = db.Column(db.Text, nullable=True)
|
||||||
|
billing_city = db.Column(db.String(100), nullable=True)
|
||||||
|
billing_country = db.Column(db.String(100), nullable=True)
|
||||||
|
billing_postal_code = db.Column(db.String(20), nullable=True)
|
||||||
|
|
||||||
|
# Subscription
|
||||||
|
subscription_plan = db.Column(db.String(50), default='free') # free, basic, pro, enterprise
|
||||||
|
subscription_status = db.Column(db.String(20), default='active') # active, suspended, cancelled
|
||||||
|
subscription_started = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
subscription_expires = db.Column(db.DateTime, nullable=True)
|
||||||
|
|
||||||
|
# Limits
|
||||||
|
max_domains = db.Column(db.Integer, default=5)
|
||||||
|
max_containers = db.Column(db.Integer, default=3)
|
||||||
|
|
||||||
|
# Timestamps
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
domains = db.relationship('Domain', backref='customer', lazy='dynamic', foreign_keys='Domain.customer_id')
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""Convert to dictionary"""
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'user_id': self.user_id,
|
||||||
|
'company_name': self.company_name,
|
||||||
|
'phone': self.phone,
|
||||||
|
'billing_address': self.billing_address,
|
||||||
|
'billing_city': self.billing_city,
|
||||||
|
'billing_country': self.billing_country,
|
||||||
|
'billing_postal_code': self.billing_postal_code,
|
||||||
|
'subscription_plan': self.subscription_plan,
|
||||||
|
'subscription_status': self.subscription_status,
|
||||||
|
'subscription_started': self.subscription_started.isoformat() if self.subscription_started else None,
|
||||||
|
'subscription_expires': self.subscription_expires.isoformat() if self.subscription_expires else None,
|
||||||
|
'max_domains': self.max_domains,
|
||||||
|
'max_containers': self.max_containers,
|
||||||
|
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Customer {self.company_name or self.user.email}>'
|
||||||
|
|
||||||
|
|
@ -0,0 +1,220 @@
|
||||||
|
"""
|
||||||
|
Authentication routes - Register, Login, Logout, Profile
|
||||||
|
"""
|
||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
from app.services.auth_service import AuthService, token_required
|
||||||
|
from app.models.user import User, Customer
|
||||||
|
from app.models.domain import db
|
||||||
|
|
||||||
|
auth_bp = Blueprint('auth', __name__, url_prefix='/api/auth')
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/register', methods=['POST'])
|
||||||
|
def register():
|
||||||
|
"""
|
||||||
|
Register new customer
|
||||||
|
|
||||||
|
Request body:
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
"password_confirm": "password123",
|
||||||
|
"full_name": "John Doe",
|
||||||
|
"company_name": "Acme Inc" (optional)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
|
||||||
|
# Validate required fields
|
||||||
|
required_fields = ['email', 'password', 'password_confirm', 'full_name']
|
||||||
|
for field in required_fields:
|
||||||
|
if not data.get(field):
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': f'{field} is required'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# Validate email format
|
||||||
|
email = data['email'].lower().strip()
|
||||||
|
if '@' not in email or '.' not in email:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Invalid email format'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# Validate password match
|
||||||
|
if data['password'] != data['password_confirm']:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Passwords do not match'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# Validate password strength
|
||||||
|
password = data['password']
|
||||||
|
if len(password) < 8:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Password must be at least 8 characters'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# Register user
|
||||||
|
user, customer, error = AuthService.register_user(
|
||||||
|
email=email,
|
||||||
|
password=password,
|
||||||
|
full_name=data['full_name'].strip(),
|
||||||
|
company_name=data.get('company_name', '').strip() or None
|
||||||
|
)
|
||||||
|
|
||||||
|
if error:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': error
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# Generate token
|
||||||
|
token = AuthService.generate_token(user.id, user.role)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'message': 'Registration successful',
|
||||||
|
'token': token,
|
||||||
|
'user': user.to_dict(),
|
||||||
|
'customer': customer.to_dict()
|
||||||
|
}), 201
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': f'Registration failed: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/login', methods=['POST'])
|
||||||
|
def login():
|
||||||
|
"""
|
||||||
|
Login user
|
||||||
|
|
||||||
|
Request body:
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "password123"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
|
||||||
|
# Validate required fields
|
||||||
|
if not data.get('email') or not data.get('password'):
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Email and password are required'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# Login user
|
||||||
|
user, token, error = AuthService.login_user(
|
||||||
|
email=data['email'].lower().strip(),
|
||||||
|
password=data['password']
|
||||||
|
)
|
||||||
|
|
||||||
|
if error:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': error
|
||||||
|
}), 401
|
||||||
|
|
||||||
|
# Get customer profile if customer role
|
||||||
|
customer_data = None
|
||||||
|
if user.role == 'customer' and user.customer:
|
||||||
|
customer_data = user.customer.to_dict()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'message': 'Login successful',
|
||||||
|
'token': token,
|
||||||
|
'user': user.to_dict(),
|
||||||
|
'customer': customer_data
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': f'Login failed: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/me', methods=['GET'])
|
||||||
|
@token_required
|
||||||
|
def get_profile(current_user):
|
||||||
|
"""
|
||||||
|
Get current user profile
|
||||||
|
|
||||||
|
Headers:
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
customer_data = None
|
||||||
|
if current_user.role == 'customer' and current_user.customer:
|
||||||
|
customer_data = current_user.customer.to_dict()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'user': current_user.to_dict(),
|
||||||
|
'customer': customer_data
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': f'Failed to get profile: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/verify-token', methods=['POST'])
|
||||||
|
def verify_token():
|
||||||
|
"""
|
||||||
|
Verify if token is valid
|
||||||
|
|
||||||
|
Request body:
|
||||||
|
{
|
||||||
|
"token": "jwt_token_here"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
token = data.get('token')
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Token is required',
|
||||||
|
'valid': False
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
payload = AuthService.verify_token(token)
|
||||||
|
|
||||||
|
if not payload:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Invalid or expired token',
|
||||||
|
'valid': False
|
||||||
|
}), 401
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'message': 'Token is valid',
|
||||||
|
'valid': True,
|
||||||
|
'payload': {
|
||||||
|
'user_id': payload['user_id'],
|
||||||
|
'role': payload['role']
|
||||||
|
}
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': str(e),
|
||||||
|
'valid': False
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
@ -0,0 +1,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
|
||||||
|
|
||||||
|
|
@ -23,6 +23,7 @@ dnspython==2.4.2
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
cryptography==41.0.7
|
cryptography==41.0.7
|
||||||
|
PyJWT==2.8.0
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
pytest==7.4.3
|
pytest==7.4.3
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue