279 Zeilen
8.3 KiB
Python
279 Zeilen
8.3 KiB
Python
import os
|
|
import sys
|
|
from flask import Flask, request, jsonify
|
|
from flask_cors import CORS
|
|
import jwt
|
|
from datetime import datetime, timedelta
|
|
import logging
|
|
from functools import wraps
|
|
from prometheus_flask_exporter import PrometheusMetrics
|
|
|
|
# Add parent directory to path for imports
|
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
|
|
from config import get_config
|
|
from repositories.base import BaseRepository
|
|
|
|
# Configure logging
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Initialize Flask app
|
|
app = Flask(__name__)
|
|
config = get_config()
|
|
app.config.from_object(config)
|
|
CORS(app)
|
|
|
|
# Initialize Prometheus metrics
|
|
metrics = PrometheusMetrics(app)
|
|
metrics.info('auth_service_info', 'Auth Service Information', version='1.0.0')
|
|
|
|
# Initialize repository
|
|
db_repo = BaseRepository(config.DATABASE_URL)
|
|
|
|
def create_token(payload: dict, expires_delta: timedelta) -> str:
|
|
"""Create JWT token"""
|
|
to_encode = payload.copy()
|
|
expire = datetime.utcnow() + expires_delta
|
|
to_encode.update({"exp": expire, "iat": datetime.utcnow()})
|
|
|
|
return jwt.encode(
|
|
to_encode,
|
|
config.JWT_SECRET,
|
|
algorithm=config.JWT_ALGORITHM
|
|
)
|
|
|
|
def decode_token(token: str) -> dict:
|
|
"""Decode and validate JWT token"""
|
|
try:
|
|
payload = jwt.decode(
|
|
token,
|
|
config.JWT_SECRET,
|
|
algorithms=[config.JWT_ALGORITHM]
|
|
)
|
|
return payload
|
|
except jwt.ExpiredSignatureError:
|
|
raise ValueError("Token has expired")
|
|
except jwt.InvalidTokenError:
|
|
raise ValueError("Invalid token")
|
|
|
|
def require_api_key(f):
|
|
"""Decorator to require API key"""
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
api_key = request.headers.get('X-API-Key')
|
|
|
|
if not api_key:
|
|
return jsonify({"error": "Missing API key"}), 401
|
|
|
|
# Validate API key
|
|
query = """
|
|
SELECT id, client_name, allowed_endpoints
|
|
FROM api_clients
|
|
WHERE api_key = %s AND is_active = true
|
|
"""
|
|
client = db_repo.execute_one(query, (api_key,))
|
|
|
|
if not client:
|
|
return jsonify({"error": "Invalid API key"}), 401
|
|
|
|
# Check if endpoint is allowed
|
|
endpoint = request.endpoint
|
|
allowed = client.get('allowed_endpoints', [])
|
|
if allowed and endpoint not in allowed:
|
|
return jsonify({"error": "Endpoint not allowed"}), 403
|
|
|
|
# Add client info to request
|
|
request.api_client = client
|
|
|
|
return f(*args, **kwargs)
|
|
|
|
return decorated_function
|
|
|
|
@app.route('/health', methods=['GET'])
|
|
def health_check():
|
|
"""Health check endpoint"""
|
|
return jsonify({
|
|
"status": "healthy",
|
|
"service": "auth",
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
})
|
|
|
|
@app.route('/api/v1/auth/token', methods=['POST'])
|
|
@require_api_key
|
|
def create_access_token():
|
|
"""Create access token for license validation"""
|
|
data = request.get_json()
|
|
|
|
if not data or 'license_id' not in data:
|
|
return jsonify({"error": "Missing license_id"}), 400
|
|
|
|
license_id = data['license_id']
|
|
hardware_id = data.get('hardware_id')
|
|
|
|
# Verify license exists and is active
|
|
query = """
|
|
SELECT id, is_active, max_devices
|
|
FROM licenses
|
|
WHERE id = %s
|
|
"""
|
|
license = db_repo.execute_one(query, (license_id,))
|
|
|
|
if not license:
|
|
return jsonify({"error": "License not found"}), 404
|
|
|
|
if not license['is_active']:
|
|
return jsonify({"error": "License is not active"}), 403
|
|
|
|
# Create token payload
|
|
payload = {
|
|
"sub": license_id,
|
|
"hwid": hardware_id,
|
|
"client_id": request.api_client['id'],
|
|
"type": "access"
|
|
}
|
|
|
|
# Add features and limits based on license
|
|
payload["features"] = data.get('features', [])
|
|
payload["limits"] = {
|
|
"api_calls": config.DEFAULT_RATE_LIMIT_PER_HOUR,
|
|
"concurrent_sessions": config.MAX_CONCURRENT_SESSIONS
|
|
}
|
|
|
|
# Create tokens
|
|
access_token = create_token(payload, config.JWT_ACCESS_TOKEN_EXPIRES)
|
|
|
|
# Create refresh token
|
|
refresh_payload = {
|
|
"sub": license_id,
|
|
"client_id": request.api_client['id'],
|
|
"type": "refresh"
|
|
}
|
|
refresh_token = create_token(refresh_payload, config.JWT_REFRESH_TOKEN_EXPIRES)
|
|
|
|
return jsonify({
|
|
"access_token": access_token,
|
|
"refresh_token": refresh_token,
|
|
"token_type": "Bearer",
|
|
"expires_in": int(config.JWT_ACCESS_TOKEN_EXPIRES.total_seconds())
|
|
})
|
|
|
|
@app.route('/api/v1/auth/refresh', methods=['POST'])
|
|
def refresh_access_token():
|
|
"""Refresh access token"""
|
|
data = request.get_json()
|
|
|
|
if not data or 'refresh_token' not in data:
|
|
return jsonify({"error": "Missing refresh_token"}), 400
|
|
|
|
try:
|
|
# Decode refresh token
|
|
payload = decode_token(data['refresh_token'])
|
|
|
|
if payload.get('type') != 'refresh':
|
|
return jsonify({"error": "Invalid token type"}), 400
|
|
|
|
license_id = payload['sub']
|
|
|
|
# Verify license still active
|
|
query = "SELECT is_active FROM licenses WHERE id = %s"
|
|
license = db_repo.execute_one(query, (license_id,))
|
|
|
|
if not license or not license['is_active']:
|
|
return jsonify({"error": "License is not active"}), 403
|
|
|
|
# Create new access token
|
|
access_payload = {
|
|
"sub": license_id,
|
|
"client_id": payload['client_id'],
|
|
"type": "access"
|
|
}
|
|
|
|
access_token = create_token(access_payload, config.JWT_ACCESS_TOKEN_EXPIRES)
|
|
|
|
return jsonify({
|
|
"access_token": access_token,
|
|
"token_type": "Bearer",
|
|
"expires_in": int(config.JWT_ACCESS_TOKEN_EXPIRES.total_seconds())
|
|
})
|
|
|
|
except ValueError as e:
|
|
return jsonify({"error": str(e)}), 401
|
|
|
|
@app.route('/api/v1/auth/verify', methods=['POST'])
|
|
def verify_token():
|
|
"""Verify token validity"""
|
|
auth_header = request.headers.get('Authorization')
|
|
|
|
if not auth_header or not auth_header.startswith('Bearer '):
|
|
return jsonify({"error": "Missing or invalid authorization header"}), 401
|
|
|
|
token = auth_header.split(' ')[1]
|
|
|
|
try:
|
|
payload = decode_token(token)
|
|
|
|
return jsonify({
|
|
"valid": True,
|
|
"license_id": payload['sub'],
|
|
"expires_at": datetime.fromtimestamp(payload['exp']).isoformat()
|
|
})
|
|
|
|
except ValueError as e:
|
|
return jsonify({
|
|
"valid": False,
|
|
"error": str(e)
|
|
}), 401
|
|
|
|
@app.route('/api/v1/auth/api-key', methods=['POST'])
|
|
def create_api_key():
|
|
"""Create new API key (admin only)"""
|
|
# This endpoint should be protected by admin authentication
|
|
# For now, we'll use a simple secret header
|
|
admin_secret = request.headers.get('X-Admin-Secret')
|
|
|
|
if admin_secret != os.getenv('ADMIN_SECRET', 'change-this-admin-secret'):
|
|
return jsonify({"error": "Unauthorized"}), 401
|
|
|
|
data = request.get_json()
|
|
|
|
if not data or 'client_name' not in data:
|
|
return jsonify({"error": "Missing client_name"}), 400
|
|
|
|
import secrets
|
|
api_key = f"sk_{secrets.token_urlsafe(32)}"
|
|
secret_key = secrets.token_urlsafe(64)
|
|
|
|
query = """
|
|
INSERT INTO api_clients (client_name, api_key, secret_key, allowed_endpoints)
|
|
VALUES (%s, %s, %s, %s)
|
|
RETURNING id
|
|
"""
|
|
|
|
allowed_endpoints = data.get('allowed_endpoints', [])
|
|
client_id = db_repo.execute_insert(
|
|
query,
|
|
(data['client_name'], api_key, secret_key, allowed_endpoints)
|
|
)
|
|
|
|
if not client_id:
|
|
return jsonify({"error": "Failed to create API key"}), 500
|
|
|
|
return jsonify({
|
|
"client_id": client_id,
|
|
"api_key": api_key,
|
|
"secret_key": secret_key,
|
|
"client_name": data['client_name']
|
|
}), 201
|
|
|
|
@app.errorhandler(404)
|
|
def not_found(error):
|
|
return jsonify({"error": "Not found"}), 404
|
|
|
|
@app.errorhandler(500)
|
|
def internal_error(error):
|
|
logger.error(f"Internal error: {error}")
|
|
return jsonify({"error": "Internal server error"}), 500
|
|
|
|
if __name__ == '__main__':
|
|
app.run(host='0.0.0.0', port=5001, debug=True) |