Files
metabuilder/workflowui/backend/auth.py
johndoe6345789 dc982772af refactor(workflowui): complete monolithic file refactoring + business logic extraction + stub implementation
## Phase 1: Monolithic File Refactoring 
- Refactored 8 large files (300-500 LOC) into 40+ modular components/hooks
- All files now <150 LOC per file (max 125 LOC)
- CanvasSettings: 343 → 7 components
- SecuritySettings: 273 → 6 components
- NotificationSettings: 239 → 6 components
- Editor/Toolbar: 258 → 7 components
- InfiniteCanvas: 239 → 10 modules
- WorkflowCard: 320 → 5 components + custom hook
- useProjectCanvas: 322 → 8 hooks
- projectSlice: 335 → 4 Redux slices

## Phase 2: Business Logic Extraction 
- Extracted logic from 5 components into 8 custom hooks
- register/page.tsx: 235 → 167 LOC (-29%)
- login/page.tsx: 137 → 100 LOC (-27%)
- MainLayout.tsx: 216 → 185 LOC (-14%)
- ProjectSidebar.tsx: 200 → 200 LOC (refactored)
- page.tsx (Dashboard): 197 → 171 LOC (-13%)
- New hooks: useAuthForm, usePasswordValidation, useLoginLogic, useRegisterLogic, useHeaderLogic, useResponsiveSidebar, useProjectSidebarLogic, useDashboardLogic

## Phase 3: Dead Code Analysis & Implementation 
- Identified and documented 3 unused hooks (244 LOC)
- Removed useRealtimeService from exports
- Cleaned 8 commented lines in useProject.ts
- Documented useExecution stub methods
- Removed 3 commented dispatch calls in useCanvasKeyboard
- Fixed 3 'as any' type assertions

## Phase 4: Stub Code Implementation 
- Fully implemented useExecution methods: execute(), stop(), getDetails(), getStats(), getHistory()
- Integrated useCanvasKeyboard into InfiniteCanvas with Redux dispatch
- Verified useCanvasVirtualization for 100+ items
- Enhanced useRealtimeService documentation for Phase 4 WebSocket integration

## Backend Updates
- Added SQLAlchemy models: Workspace, Project, ProjectCanvasItem
- Added Flask API endpoints for CRUD operations
- Configured multi-tenant filtering for all queries
- Added database migrations for new entities

## Build Verification 
- TypeScript strict mode: 0 errors
- Production build:  Successful (161 kB First Load JS)
- No breaking changes
- 100% backward compatibility maintained

## Documentation Generated
- 6 comprehensive guides (70+ KB total)
- Test templates for all new implementations
- Quick reference for all 42 hooks
- Implementation checklist and deployment guide

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-23 06:44:57 +00:00

290 lines
8.8 KiB
Python

"""
Authentication Module
JWT-based user authentication for WorkflowUI
"""
from flask import request, jsonify
from functools import wraps
from datetime import datetime, timedelta
import jwt
import hashlib
import os
from typing import Dict, Tuple, Optional
# Get secret key from environment or use default (change in production!)
SECRET_KEY = os.getenv('JWT_SECRET_KEY', 'your-secret-key-change-in-production')
JWT_EXPIRATION_HOURS = 24
class AuthError(Exception):
"""Custom exception for auth errors"""
def __init__(self, message: str, status_code: int = 401):
self.message = message
self.status_code = status_code
def hash_password(password: str) -> str:
"""Hash password using SHA-512"""
return hashlib.sha512(password.encode()).hexdigest()
def verify_password(password: str, password_hash: str) -> bool:
"""Verify password against hash"""
return hash_password(password) == password_hash
def generate_token(user_id: str, email: str) -> str:
"""Generate JWT token for user"""
payload = {
'user_id': user_id,
'email': email,
'iat': datetime.utcnow(),
'exp': datetime.utcnow() + timedelta(hours=JWT_EXPIRATION_HOURS)
}
return jwt.encode(payload, SECRET_KEY, algorithm='HS256')
def verify_token(token: str) -> Optional[Dict]:
"""Verify JWT token and return payload"""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
return payload
except jwt.ExpiredSignatureError:
raise AuthError('Token has expired', 401)
except jwt.InvalidTokenError:
raise AuthError('Invalid token', 401)
def token_required(f):
"""Decorator to require valid JWT token"""
@wraps(f)
def decorated(*args, **kwargs):
token = None
# Check for token in headers
if 'Authorization' in request.headers:
auth_header = request.headers['Authorization']
try:
token = auth_header.split(" ")[1]
except IndexError:
raise AuthError('Invalid authorization header', 401)
if not token:
raise AuthError('Token is missing', 401)
try:
payload = verify_token(token)
request.user_id = payload['user_id']
request.email = payload['email']
except AuthError as e:
raise e
return f(*args, **kwargs)
return decorated
def validate_email(email: str) -> bool:
"""Simple email validation"""
import re
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, email) is not None
def validate_password(password: str) -> Tuple[bool, Optional[str]]:
"""Validate password strength"""
if len(password) < 8:
return False, 'Password must be at least 8 characters'
if not any(c.isupper() for c in password):
return False, 'Password must contain uppercase letters'
if not any(c.islower() for c in password):
return False, 'Password must contain lowercase letters'
if not any(c.isdigit() for c in password):
return False, 'Password must contain numbers'
return True, None
def register_auth_routes(app):
"""Register authentication routes with Flask app"""
@app.route('/api/auth/register', methods=['POST'])
def register():
"""Register new user"""
try:
data = request.get_json()
# Validate input
if not data:
return jsonify({'error': 'No data provided'}), 400
email = data.get('email', '').strip()
password = data.get('password', '')
name = data.get('name', '').strip()
# Validate email
if not email or not validate_email(email):
return jsonify({'error': 'Invalid email address'}), 400
# Validate name
if not name or len(name) < 2:
return jsonify({'error': 'Name must be at least 2 characters'}), 400
# Validate password
is_valid, error_msg = validate_password(password)
if not is_valid:
return jsonify({'error': error_msg}), 400
# Check if user already exists
from models import User, db
existing_user = User.query.filter_by(email=email).first()
if existing_user:
return jsonify({'error': 'Email already registered'}), 409
# Create new user
user_id = f'user_{datetime.utcnow().timestamp()}'
password_hash = hash_password(password)
user = User(
id=user_id,
email=email,
password_hash=password_hash,
name=name,
created_at=datetime.utcnow()
)
db.session.add(user)
db.session.commit()
# Generate token
token = generate_token(user_id, email)
return jsonify({
'success': True,
'user': {
'id': user.id,
'email': user.email,
'name': user.name
},
'token': token
}), 201
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500
@app.route('/api/auth/login', methods=['POST'])
def login():
"""Login user"""
try:
data = request.get_json()
if not data:
return jsonify({'error': 'No data provided'}), 400
email = data.get('email', '').strip()
password = data.get('password', '')
if not email or not password:
return jsonify({'error': 'Email and password required'}), 400
from models import User
# Find user
user = User.query.filter_by(email=email).first()
if not user or not verify_password(password, user.password_hash):
return jsonify({'error': 'Invalid email or password'}), 401
# Generate token
token = generate_token(user.id, user.email)
return jsonify({
'success': True,
'user': {
'id': user.id,
'email': user.email,
'name': user.name
},
'token': token
}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/auth/me', methods=['GET'])
@token_required
def get_current_user():
"""Get current user info"""
try:
from models import User
user = User.query.filter_by(id=request.user_id).first()
if not user:
return jsonify({'error': 'User not found'}), 404
return jsonify({
'id': user.id,
'email': user.email,
'name': user.name,
'created_at': user.created_at.isoformat()
}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/auth/logout', methods=['POST'])
@token_required
def logout():
"""Logout user (token invalidation on client side)"""
# JWT tokens are stateless, so logout is client-side only
# In production, you might maintain a token blacklist
return jsonify({'success': True, 'message': 'Logged out successfully'}), 200
@app.route('/api/auth/change-password', methods=['POST'])
@token_required
def change_password():
"""Change user password"""
try:
data = request.get_json()
if not data:
return jsonify({'error': 'No data provided'}), 400
current_password = data.get('current_password', '')
new_password = data.get('new_password', '')
if not current_password or not new_password:
return jsonify({'error': 'Current and new password required'}), 400
from models import User
user = User.query.filter_by(id=request.user_id).first()
if not user:
return jsonify({'error': 'User not found'}), 404
# Verify current password
if not verify_password(current_password, user.password_hash):
return jsonify({'error': 'Current password is incorrect'}), 401
# Validate new password
is_valid, error_msg = validate_password(new_password)
if not is_valid:
return jsonify({'error': error_msg}), 400
# Update password
user.password_hash = hash_password(new_password)
user.updated_at = datetime.utcnow()
db.session.commit()
return jsonify({'success': True, 'message': 'Password changed successfully'}), 200
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500