From 87611a284b4213516790708bca01c7f89c87f69d Mon Sep 17 00:00:00 2001 From: oguz ozturk Date: Sat, 10 Jan 2026 16:07:57 +0300 Subject: [PATCH] Add authentication system - User model, JWT service, auth routes --- ARCHITECTURE.md | 201 +++++++++++++++++++++++ IMPLEMENTATION_PLAN.md | 176 ++++++++++++++++++++ backend/app/main.py | 4 +- backend/app/models/__init__.py | 3 +- backend/app/models/user.py | 123 ++++++++++++++ backend/app/routes/auth.py | 220 +++++++++++++++++++++++++ backend/app/services/auth_service.py | 233 +++++++++++++++++++++++++++ backend/requirements.txt | 1 + 8 files changed, 959 insertions(+), 2 deletions(-) create mode 100644 ARCHITECTURE.md create mode 100644 IMPLEMENTATION_PLAN.md create mode 100644 backend/app/models/user.py create mode 100644 backend/app/routes/auth.py create mode 100644 backend/app/services/auth_service.py diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..68cb837 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,201 @@ +# 🏗️ Hosting Platform - Modular Architecture + +## 📋 Project Overview + +Professional WordPress hosting platform with container infrastructure and automation. + +--- + +## 🎯 Architecture Principles + +1. **Modularity** - Each feature is a separate module +2. **Scalability** - Easy to add new features without breaking existing code +3. **Separation of Concerns** - Clear boundaries between layers +4. **Security First** - Authentication, encryption, and access control +5. **Clean Code** - Readable, maintainable, and well-documented + +--- + +## 🏛️ System Architecture + +### Frontend Architecture + +``` +frontend/ +├── customer-portal/ # argeict.net +│ ├── src/ +│ │ ├── pages/ +│ │ │ ├── Landing.jsx # Register/Login page +│ │ │ ├── Dashboard.jsx # Customer dashboard +│ │ │ ├── DNS/ # DNS module +│ │ │ ├── Container/ # Container module +│ │ │ ├── Network/ # Network module +│ │ │ └── Security/ # Security module +│ │ ├── components/ +│ │ │ ├── auth/ # Auth components +│ │ │ ├── layout/ # Layout components +│ │ │ └── shared/ # Shared components +│ │ ├── services/ +│ │ │ ├── api.js # API client +│ │ │ ├── auth.js # Auth service +│ │ │ └── storage.js # Local storage +│ │ ├── hooks/ # Custom React hooks +│ │ ├── context/ # React context +│ │ └── utils/ # Utility functions +│ └── public/ +│ +├── admin-portal/ # adminpanel.argeict.net +│ ├── src/ +│ │ ├── pages/ +│ │ │ ├── Login.jsx # Admin login +│ │ │ ├── Dashboard.jsx # Admin dashboard +│ │ │ ├── CFAccounts/ # CF management +│ │ │ ├── Customers/ # Customer management +│ │ │ └── Settings/ # System settings +│ │ ├── components/ +│ │ ├── services/ +│ │ └── utils/ +│ └── public/ +│ +└── shared/ # Shared code between portals + ├── components/ + ├── utils/ + └── styles/ +``` + +### Backend Architecture + +``` +backend/ +├── app/ +│ ├── __init__.py +│ ├── main.py # Application entry point +│ ├── config.py # Configuration +│ │ +│ ├── models/ # Database models +│ │ ├── __init__.py +│ │ ├── user.py # User/Customer model +│ │ ├── domain.py # Domain model +│ │ ├── cloudflare.py # CF Account model +│ │ ├── container.py # Container model +│ │ └── base.py # Base model +│ │ +│ ├── routes/ # API routes (blueprints) +│ │ ├── __init__.py +│ │ ├── auth.py # Authentication routes +│ │ ├── customer.py # Customer routes +│ │ ├── dns.py # DNS routes +│ │ ├── container.py # Container routes +│ │ ├── admin.py # Admin routes +│ │ └── webhook.py # Webhook routes +│ │ +│ ├── services/ # Business logic +│ │ ├── __init__.py +│ │ ├── auth_service.py # Authentication logic +│ │ ├── cloudflare_service.py # Cloudflare API +│ │ ├── nameserver_service.py # DNS/NS logic +│ │ ├── container_service.py # Container management +│ │ └── email_service.py # Email notifications +│ │ +│ ├── middleware/ # Middleware +│ │ ├── __init__.py +│ │ ├── auth.py # Auth middleware +│ │ ├── rate_limit.py # Rate limiting +│ │ └── cors.py # CORS configuration +│ │ +│ ├── utils/ # Utilities +│ │ ├── __init__.py +│ │ ├── encryption.py # Encryption helpers +│ │ ├── validators.py # Input validation +│ │ └── helpers.py # General helpers +│ │ +│ └── migrations/ # Database migrations +│ +├── tests/ # Tests +│ ├── unit/ +│ ├── integration/ +│ └── e2e/ +│ +└── requirements.txt +``` + +--- + +## 🗄️ Database Schema + +### Users Table +- id, email, password_hash, full_name +- is_active, is_verified, created_at, updated_at +- role (customer, admin) + +### Customers Table (extends Users) +- user_id (FK), company_name, phone +- billing_info, subscription_plan + +### CloudflareAccounts Table +- id, name, email, api_token_encrypted +- is_active, max_domains, current_domain_count + +### Domains Table +- id, domain_name, customer_id (FK) +- cf_account_id (FK), cf_zone_id +- status, created_at + +### Containers Table (Future) +- id, customer_id (FK), domain_id (FK) +- container_id, status, resources + +--- + +## 🎨 Design System + +### Brand Colors (from logo) +- Primary: #0066CC (Blue) +- Secondary: #00A3E0 (Light Blue) +- Accent: #FF6B35 (Orange) +- Dark: #1A1A1A +- Light: #F5F5F5 + +### Typography +- Headings: Inter, sans-serif +- Body: Inter, sans-serif + +--- + +## 🔐 Security + +1. **Authentication**: JWT tokens +2. **Password**: bcrypt hashing +3. **API Tokens**: Fernet encryption +4. **HTTPS**: All communications +5. **Rate Limiting**: Per endpoint +6. **CORS**: Configured per domain + +--- + +## 📦 Technology Stack + +### Frontend +- React 18 + Vite +- React Router (multi-app routing) +- TailwindCSS +- Axios +- React Query (data fetching) + +### Backend +- Flask 3.0 +- SQLAlchemy 2.0 +- PostgreSQL 16 +- Redis 7.0 +- JWT authentication + +### DevOps +- Docker (containers) +- Nginx (reverse proxy) +- Supervisor (process management) +- Gitea (version control) + +--- + +**Next Steps**: Implementation in phases + diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..8fe4448 --- /dev/null +++ b/IMPLEMENTATION_PLAN.md @@ -0,0 +1,176 @@ +# 🚀 Implementation Plan - Hosting Platform + +## Phase 1: Foundation & Authentication (Week 1) + +### Backend Tasks +- [ ] 1.1 Create User model with authentication +- [ ] 1.2 Implement JWT authentication service +- [ ] 1.3 Create auth routes (register, login, logout, verify) +- [ ] 1.4 Add auth middleware for protected routes +- [ ] 1.5 Database migrations for users table +- [ ] 1.6 Password reset functionality + +### Frontend Tasks +- [ ] 1.7 Setup project structure (customer-portal, admin-portal) +- [ ] 1.8 Create Landing page with animations +- [ ] 1.9 Create Register/Login forms +- [ ] 1.10 Implement auth context and hooks +- [ ] 1.11 Protected route wrapper +- [ ] 1.12 Brand colors and design system + +### Testing +- [ ] 1.13 Unit tests for auth service +- [ ] 1.14 Integration tests for auth endpoints +- [ ] 1.15 E2E tests for registration flow + +--- + +## Phase 2: Customer Dashboard & DNS Module (Week 2) + +### Backend Tasks +- [ ] 2.1 Refactor existing DNS routes to use auth +- [ ] 2.2 Link domains to customer_id +- [ ] 2.3 Customer-specific domain listing +- [ ] 2.4 Update CF account selection logic + +### Frontend Tasks +- [ ] 2.5 Create customer dashboard layout +- [ ] 2.6 Sidebar navigation (DNS, Container, Network, Security) +- [ ] 2.7 DNS management page (refactor existing) +- [ ] 2.8 Project creation wizard +- [ ] 2.9 Domain list with filters +- [ ] 2.10 Responsive design + +### Testing +- [ ] 2.11 Test DNS operations with auth +- [ ] 2.12 Test customer isolation + +--- + +## Phase 3: Admin Portal (Week 3) + +### Backend Tasks +- [ ] 3.1 Admin role and permissions +- [ ] 3.2 Admin-only middleware +- [ ] 3.3 Customer management endpoints +- [ ] 3.4 System statistics endpoints + +### Frontend Tasks +- [ ] 3.5 Separate admin portal app +- [ ] 3.6 Admin login page +- [ ] 3.7 Admin dashboard +- [ ] 3.8 CF Accounts management (refactor existing) +- [ ] 3.9 Customer management interface +- [ ] 3.10 System settings page + +### Deployment +- [ ] 3.11 Configure adminpanel.argeict.net subdomain +- [ ] 3.12 Nginx configuration for multi-app +- [ ] 3.13 Build and deploy both portals + +--- + +## Phase 4: Container Module (Week 4-5) + +### Backend Tasks +- [ ] 4.1 Container model and database schema +- [ ] 4.2 Docker integration service +- [ ] 4.3 WordPress container templates +- [ ] 4.4 Container lifecycle management +- [ ] 4.5 Resource monitoring + +### Frontend Tasks +- [ ] 4.6 Container management page +- [ ] 4.7 Container creation wizard +- [ ] 4.8 Container status dashboard +- [ ] 4.9 Resource usage charts + +--- + +## Phase 5: Network & Security Modules (Week 6) + +### Backend Tasks +- [ ] 5.1 Network configuration endpoints +- [ ] 5.2 SSL certificate management +- [ ] 5.3 Firewall rules API +- [ ] 5.4 Security scanning integration + +### Frontend Tasks +- [ ] 5.5 Network management interface +- [ ] 5.6 Security dashboard +- [ ] 5.7 SSL certificate viewer +- [ ] 5.8 Security recommendations + +--- + +## Phase 6: Polish & Production (Week 7-8) + +### Features +- [ ] 6.1 Email notifications +- [ ] 6.2 Billing integration +- [ ] 6.3 Usage analytics +- [ ] 6.4 API documentation +- [ ] 6.5 User documentation + +### Testing & QA +- [ ] 6.6 Full system testing +- [ ] 6.7 Performance optimization +- [ ] 6.8 Security audit +- [ ] 6.9 Load testing + +### Deployment +- [ ] 6.10 Production deployment +- [ ] 6.11 Monitoring setup +- [ ] 6.12 Backup system +- [ ] 6.13 CI/CD pipeline + +--- + +## Current Sprint: Phase 1 - Foundation & Authentication + +### Immediate Tasks (Today) + +1. **Backend: User Model & Auth** + - Create User model + - Implement JWT service + - Create auth routes + +2. **Frontend: Project Structure** + - Setup customer-portal + - Setup admin-portal + - Create Landing page + +3. **Design System** + - Extract brand colors from logo + - Create TailwindCSS config + - Design component library + +--- + +## Success Criteria + +### Phase 1 +- ✅ Users can register and login +- ✅ JWT authentication working +- ✅ Landing page with animations +- ✅ Brand identity applied + +### Phase 2 +- ✅ Customer dashboard functional +- ✅ DNS management integrated +- ✅ Customer isolation working + +### Phase 3 +- ✅ Admin portal deployed +- ✅ CF account management +- ✅ Customer management + +### Phase 4-6 +- ✅ Container management +- ✅ Full feature set +- ✅ Production ready + +--- + +**Let's start with Phase 1!** + diff --git a/backend/app/main.py b/backend/app/main.py index dce13b2..3094841 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -6,10 +6,11 @@ import redis import os 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 # Import blueprints +from app.routes.auth import auth_bp from app.routes.admin import admin_bp from app.routes.dns import dns_bp @@ -26,6 +27,7 @@ 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) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index f484932..badbecf 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,3 +1,4 @@ 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'] diff --git a/backend/app/models/user.py b/backend/app/models/user.py new file mode 100644 index 0000000..de47d3d --- /dev/null +++ b/backend/app/models/user.py @@ -0,0 +1,123 @@ +""" +User and Customer models for authentication and customer management +""" +from datetime import datetime +from werkzeug.security import generate_password_hash, check_password_hash +from app.models.domain import db + + +class User(db.Model): + """Base user model for authentication""" + __tablename__ = "users" + + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(255), unique=True, nullable=False, index=True) + password_hash = db.Column(db.String(255), nullable=False) + full_name = db.Column(db.String(255), nullable=False) + + # Account status + is_active = db.Column(db.Boolean, default=True) + is_verified = db.Column(db.Boolean, default=False) + role = db.Column(db.String(20), default='customer') # 'customer' or 'admin' + + # Timestamps + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + last_login = db.Column(db.DateTime, nullable=True) + + # Verification + verification_token = db.Column(db.String(255), nullable=True) + reset_token = db.Column(db.String(255), nullable=True) + reset_token_expires = db.Column(db.DateTime, nullable=True) + + # Relationships + customer = db.relationship('Customer', backref='user', uselist=False, cascade='all, delete-orphan') + + def set_password(self, password): + """Hash and set password""" + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + """Verify password""" + return check_password_hash(self.password_hash, password) + + def to_dict(self, include_sensitive=False): + """Convert to dictionary""" + data = { + 'id': self.id, + 'email': self.email, + 'full_name': self.full_name, + 'role': self.role, + 'is_active': self.is_active, + 'is_verified': self.is_verified, + 'created_at': self.created_at.isoformat() if self.created_at else None, + 'last_login': self.last_login.isoformat() if self.last_login else None + } + + if include_sensitive: + data['verification_token'] = self.verification_token + data['reset_token'] = self.reset_token + + return data + + def __repr__(self): + return f'' + + +class Customer(db.Model): + """Customer profile extending User""" + __tablename__ = "customers" + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, unique=True) + + # Company info + company_name = db.Column(db.String(255), nullable=True) + phone = db.Column(db.String(50), nullable=True) + + # Billing + billing_address = db.Column(db.Text, nullable=True) + billing_city = db.Column(db.String(100), nullable=True) + billing_country = db.Column(db.String(100), nullable=True) + billing_postal_code = db.Column(db.String(20), nullable=True) + + # Subscription + subscription_plan = db.Column(db.String(50), default='free') # free, basic, pro, enterprise + subscription_status = db.Column(db.String(20), default='active') # active, suspended, cancelled + subscription_started = db.Column(db.DateTime, default=datetime.utcnow) + subscription_expires = db.Column(db.DateTime, nullable=True) + + # Limits + max_domains = db.Column(db.Integer, default=5) + max_containers = db.Column(db.Integer, default=3) + + # Timestamps + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # Relationships + domains = db.relationship('Domain', backref='customer', lazy='dynamic', foreign_keys='Domain.customer_id') + + def to_dict(self): + """Convert to dictionary""" + return { + 'id': self.id, + 'user_id': self.user_id, + 'company_name': self.company_name, + 'phone': self.phone, + 'billing_address': self.billing_address, + 'billing_city': self.billing_city, + 'billing_country': self.billing_country, + 'billing_postal_code': self.billing_postal_code, + 'subscription_plan': self.subscription_plan, + 'subscription_status': self.subscription_status, + 'subscription_started': self.subscription_started.isoformat() if self.subscription_started else None, + 'subscription_expires': self.subscription_expires.isoformat() if self.subscription_expires else None, + 'max_domains': self.max_domains, + 'max_containers': self.max_containers, + 'created_at': self.created_at.isoformat() if self.created_at else None + } + + def __repr__(self): + return f'' + diff --git a/backend/app/routes/auth.py b/backend/app/routes/auth.py new file mode 100644 index 0000000..d867d4d --- /dev/null +++ b/backend/app/routes/auth.py @@ -0,0 +1,220 @@ +""" +Authentication routes - Register, Login, Logout, Profile +""" +from flask import Blueprint, request, jsonify +from app.services.auth_service import AuthService, token_required +from app.models.user import User, Customer +from app.models.domain import db + +auth_bp = Blueprint('auth', __name__, url_prefix='/api/auth') + + +@auth_bp.route('/register', methods=['POST']) +def register(): + """ + Register new customer + + Request body: + { + "email": "user@example.com", + "password": "password123", + "password_confirm": "password123", + "full_name": "John Doe", + "company_name": "Acme Inc" (optional) + } + """ + try: + data = request.json + + # Validate required fields + required_fields = ['email', 'password', 'password_confirm', 'full_name'] + for field in required_fields: + if not data.get(field): + return jsonify({ + 'status': 'error', + 'message': f'{field} is required' + }), 400 + + # Validate email format + email = data['email'].lower().strip() + if '@' not in email or '.' not in email: + return jsonify({ + 'status': 'error', + 'message': 'Invalid email format' + }), 400 + + # Validate password match + if data['password'] != data['password_confirm']: + return jsonify({ + 'status': 'error', + 'message': 'Passwords do not match' + }), 400 + + # Validate password strength + password = data['password'] + if len(password) < 8: + return jsonify({ + 'status': 'error', + 'message': 'Password must be at least 8 characters' + }), 400 + + # Register user + user, customer, error = AuthService.register_user( + email=email, + password=password, + full_name=data['full_name'].strip(), + company_name=data.get('company_name', '').strip() or None + ) + + if error: + return jsonify({ + 'status': 'error', + 'message': error + }), 400 + + # Generate token + token = AuthService.generate_token(user.id, user.role) + + return jsonify({ + 'status': 'success', + 'message': 'Registration successful', + 'token': token, + 'user': user.to_dict(), + 'customer': customer.to_dict() + }), 201 + + except Exception as e: + db.session.rollback() + return jsonify({ + 'status': 'error', + 'message': f'Registration failed: {str(e)}' + }), 500 + + +@auth_bp.route('/login', methods=['POST']) +def login(): + """ + Login user + + Request body: + { + "email": "user@example.com", + "password": "password123" + } + """ + try: + data = request.json + + # Validate required fields + if not data.get('email') or not data.get('password'): + return jsonify({ + 'status': 'error', + 'message': 'Email and password are required' + }), 400 + + # Login user + user, token, error = AuthService.login_user( + email=data['email'].lower().strip(), + password=data['password'] + ) + + if error: + return jsonify({ + 'status': 'error', + 'message': error + }), 401 + + # Get customer profile if customer role + customer_data = None + if user.role == 'customer' and user.customer: + customer_data = user.customer.to_dict() + + return jsonify({ + 'status': 'success', + 'message': 'Login successful', + 'token': token, + 'user': user.to_dict(), + 'customer': customer_data + }), 200 + + except Exception as e: + return jsonify({ + 'status': 'error', + 'message': f'Login failed: {str(e)}' + }), 500 + + +@auth_bp.route('/me', methods=['GET']) +@token_required +def get_profile(current_user): + """ + Get current user profile + + Headers: + Authorization: Bearer + """ + try: + customer_data = None + if current_user.role == 'customer' and current_user.customer: + customer_data = current_user.customer.to_dict() + + return jsonify({ + 'status': 'success', + 'user': current_user.to_dict(), + 'customer': customer_data + }), 200 + + except Exception as e: + return jsonify({ + 'status': 'error', + 'message': f'Failed to get profile: {str(e)}' + }), 500 + + +@auth_bp.route('/verify-token', methods=['POST']) +def verify_token(): + """ + Verify if token is valid + + Request body: + { + "token": "jwt_token_here" + } + """ + try: + data = request.json + token = data.get('token') + + if not token: + return jsonify({ + 'status': 'error', + 'message': 'Token is required', + 'valid': False + }), 400 + + payload = AuthService.verify_token(token) + + if not payload: + return jsonify({ + 'status': 'error', + 'message': 'Invalid or expired token', + 'valid': False + }), 401 + + return jsonify({ + 'status': 'success', + 'message': 'Token is valid', + 'valid': True, + 'payload': { + 'user_id': payload['user_id'], + 'role': payload['role'] + } + }), 200 + + except Exception as e: + return jsonify({ + 'status': 'error', + 'message': str(e), + 'valid': False + }), 500 + diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py new file mode 100644 index 0000000..acb1c15 --- /dev/null +++ b/backend/app/services/auth_service.py @@ -0,0 +1,233 @@ +""" +Authentication service - JWT token generation and validation +""" +import jwt +import secrets +from datetime import datetime, timedelta +from functools import wraps +from flask import request, jsonify, current_app +from app.models.user import User, Customer +from app.models.domain import db + + +class AuthService: + """Authentication service for JWT tokens""" + + @staticmethod + def generate_token(user_id, role='customer', expires_in=24): + """ + Generate JWT token + + Args: + user_id: User ID + role: User role (customer/admin) + expires_in: Token expiration in hours (default 24) + + Returns: + JWT token string + """ + payload = { + 'user_id': user_id, + 'role': role, + 'exp': datetime.utcnow() + timedelta(hours=expires_in), + 'iat': datetime.utcnow() + } + + token = jwt.encode( + payload, + current_app.config['SECRET_KEY'], + algorithm='HS256' + ) + + return token + + @staticmethod + def verify_token(token): + """ + Verify JWT token + + Args: + token: JWT token string + + Returns: + dict: Decoded payload or None if invalid + """ + try: + payload = jwt.decode( + token, + current_app.config['SECRET_KEY'], + algorithms=['HS256'] + ) + return payload + except jwt.ExpiredSignatureError: + return None + except jwt.InvalidTokenError: + return None + + @staticmethod + def register_user(email, password, full_name, company_name=None): + """ + Register new user + + Args: + email: User email + password: User password + full_name: User full name + company_name: Optional company name + + Returns: + tuple: (user, customer, error) + """ + # Check if user exists + existing_user = User.query.filter_by(email=email).first() + if existing_user: + return None, None, "Email already registered" + + # Create user + user = User( + email=email, + full_name=full_name, + role='customer', + is_active=True, + is_verified=False, + verification_token=secrets.token_urlsafe(32) + ) + user.set_password(password) + + db.session.add(user) + db.session.flush() # Get user.id + + # Create customer profile + customer = Customer( + user_id=user.id, + company_name=company_name, + subscription_plan='free', + subscription_status='active', + max_domains=5, + max_containers=3 + ) + + db.session.add(customer) + db.session.commit() + + return user, customer, None + + @staticmethod + def login_user(email, password): + """ + Login user + + Args: + email: User email + password: User password + + Returns: + tuple: (user, token, error) + """ + user = User.query.filter_by(email=email).first() + + if not user: + return None, None, "Invalid email or password" + + if not user.check_password(password): + return None, None, "Invalid email or password" + + if not user.is_active: + return None, None, "Account is deactivated" + + # Update last login + user.last_login = datetime.utcnow() + db.session.commit() + + # Generate token + token = AuthService.generate_token(user.id, user.role) + + return user, token, None + + @staticmethod + def get_current_user(token): + """ + Get current user from token + + Args: + token: JWT token + + Returns: + User object or None + """ + payload = AuthService.verify_token(token) + if not payload: + return None + + user = User.query.get(payload['user_id']) + return user + + +# Decorators for route protection +def token_required(f): + """Decorator to require valid JWT token""" + @wraps(f) + def decorated(*args, **kwargs): + token = None + + # Get token from header + if 'Authorization' in request.headers: + auth_header = request.headers['Authorization'] + try: + token = auth_header.split(' ')[1] # Bearer + except IndexError: + return jsonify({'error': 'Invalid token format'}), 401 + + if not token: + return jsonify({'error': 'Token is missing'}), 401 + + # Verify token + payload = AuthService.verify_token(token) + if not payload: + return jsonify({'error': 'Token is invalid or expired'}), 401 + + # Get user + current_user = User.query.get(payload['user_id']) + if not current_user or not current_user.is_active: + return jsonify({'error': 'User not found or inactive'}), 401 + + return f(current_user, *args, **kwargs) + + return decorated + + +def admin_required(f): + """Decorator to require admin role""" + @wraps(f) + def decorated(*args, **kwargs): + token = None + + # Get token from header + if 'Authorization' in request.headers: + auth_header = request.headers['Authorization'] + try: + token = auth_header.split(' ')[1] + except IndexError: + return jsonify({'error': 'Invalid token format'}), 401 + + if not token: + return jsonify({'error': 'Token is missing'}), 401 + + # Verify token + payload = AuthService.verify_token(token) + if not payload: + return jsonify({'error': 'Token is invalid or expired'}), 401 + + # Check admin role + if payload.get('role') != 'admin': + return jsonify({'error': 'Admin access required'}), 403 + + # Get user + current_user = User.query.get(payload['user_id']) + if not current_user or not current_user.is_active: + return jsonify({'error': 'User not found or inactive'}), 401 + + return f(current_user, *args, **kwargs) + + return decorated + diff --git a/backend/requirements.txt b/backend/requirements.txt index 01a727b..c20f9a0 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -23,6 +23,7 @@ dnspython==2.4.2 # Security cryptography==41.0.7 +PyJWT==2.8.0 # Development pytest==7.4.3