Files
ServerKit/backend/app/api/templates.py
T
Juan Denis bf237c698d Add Docker database and app management endpoints
Implements endpoints for managing and querying MySQL/PostgreSQL databases running in Docker containers, including listing containers, databases, tables, and executing queries. Adds endpoints for retrieving app logs and real-time status for Docker apps. Updates template variable processing to flag auto-generated and hidden variables. Extends DatabaseService with Docker MySQL/PostgreSQL discovery and query utilities.
2026-01-18 23:55:12 -05:00

357 lines
12 KiB
Python

"""
Template API Endpoints
Provides REST endpoints for:
- Listing and browsing templates
- Template installation
- App updates from templates
- Template repository management
"""
from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from app.models import User, Application
from app.services.template_service import TemplateService
templates_bp = Blueprint('templates', __name__)
def admin_required(fn):
"""Decorator to require admin role."""
from functools import wraps
@wraps(fn)
def wrapper(*args, **kwargs):
current_user_id = get_jwt_identity()
user = User.query.get(current_user_id)
if not user or user.role != 'admin':
return jsonify({'error': 'Admin access required'}), 403
return fn(*args, **kwargs)
return wrapper
# ==================== TEMPLATE BROWSING ====================
@templates_bp.route('/', methods=['GET'])
@jwt_required()
def list_templates():
"""List all available templates."""
category = request.args.get('category')
search = request.args.get('search')
templates = TemplateService.list_all_templates(category=category, search=search)
return jsonify({
'templates': templates,
'count': len(templates)
}), 200
@templates_bp.route('/categories', methods=['GET'])
@jwt_required()
def get_categories():
"""Get all template categories."""
categories = TemplateService.get_categories()
return jsonify({'categories': categories}), 200
@templates_bp.route('/<template_id>', methods=['GET'])
@jwt_required()
def get_template(template_id):
"""Get detailed template information."""
result = TemplateService.get_template(template_id)
if not result.get('success'):
return jsonify(result), 404
template = result['template']
# Add processed variable info - handle both list and dict formats
# Auto-generated types that users don't need to fill in
auto_generated_types = ['port', 'password', 'random', 'uuid']
variables = []
raw_vars = template.get('variables', [])
if isinstance(raw_vars, list):
# New list format: [{name: 'PORT', type: 'port', ...}, ...]
for var in raw_vars:
if isinstance(var, dict):
var_type = var.get('type', 'string')
variables.append({
'name': var.get('name', ''),
'description': var.get('description', ''),
'type': var_type,
'default': var.get('default', ''),
'required': var.get('required', False),
'options': var.get('options', None),
'auto_generated': var_type in auto_generated_types,
'hidden': var_type == 'port' # Ports are hidden from users
})
elif isinstance(raw_vars, dict):
# Old dict format: {PORT: {type: 'port', ...}, ...}
for var_name, var_config in raw_vars.items():
if isinstance(var_config, dict):
var_type = var_config.get('type', 'string')
variables.append({
'name': var_name,
'description': var_config.get('description', ''),
'type': var_type,
'default': var_config.get('default', ''),
'required': var_config.get('required', False),
'options': var_config.get('options', None),
'auto_generated': var_type in auto_generated_types,
'hidden': var_type == 'port'
})
return jsonify({
'template': {
'id': template_id,
'name': template.get('name'),
'version': template.get('version'),
'description': template.get('description'),
'icon': template.get('icon'),
'categories': template.get('categories', []),
'source': template.get('source'),
'documentation': template.get('documentation'),
'website': template.get('website'),
'variables': variables,
'has_compose': 'compose' in template,
'has_dockerfile': 'dockerfile' in template,
'scripts': list(template.get('scripts', {}).keys()),
'requirements': template.get('requirements', {}),
'ports': template.get('ports', [])
}
}), 200
# ==================== TEMPLATE INSTALLATION ====================
@templates_bp.route('/<template_id>/install', methods=['POST'])
@jwt_required()
@admin_required
def install_template(template_id):
"""Install a template as a new application."""
try:
current_user_id = get_jwt_identity()
data = request.get_json()
if not data:
return jsonify({'error': 'No data provided'}), 400
app_name = data.get('app_name')
if not app_name:
return jsonify({'error': 'app_name is required'}), 400
# Validate app name
import re
if not re.match(r'^[a-z0-9][a-z0-9-]*[a-z0-9]$', app_name) or len(app_name) < 3:
return jsonify({
'error': 'App name must be lowercase alphanumeric with hyphens, at least 3 characters'
}), 400
user_variables = data.get('variables', {})
result = TemplateService.install_template(
template_id=template_id,
app_name=app_name,
user_variables=user_variables,
user_id=current_user_id
)
return jsonify(result), 201 if result.get('success') else 400
except Exception as e:
import traceback
error_trace = traceback.format_exc()
print(f"Template install error: {error_trace}")
return jsonify({'error': str(e), 'trace': error_trace}), 500
@templates_bp.route('/validate-install', methods=['POST'])
@jwt_required()
def validate_installation():
"""Validate template installation parameters before installing."""
data = request.get_json()
if not data:
return jsonify({'error': 'No data provided'}), 400
template_id = data.get('template_id')
app_name = data.get('app_name')
user_variables = data.get('variables', {})
errors = []
# Validate app name
import re
if not app_name:
errors.append('App name is required')
elif not re.match(r'^[a-z0-9][a-z0-9-]*[a-z0-9]$', app_name) or len(app_name) < 3:
errors.append('App name must be lowercase alphanumeric with hyphens, at least 3 characters')
# Check if app name is taken
existing = Application.query.filter_by(name=app_name).first()
if existing:
errors.append(f'An application named "{app_name}" already exists')
# Validate template exists and check required variables
result = TemplateService.get_template(template_id)
if not result.get('success'):
errors.append('Template not found')
else:
template = result['template']
raw_vars = template.get('variables', [])
# Handle both list and dict formats
if isinstance(raw_vars, list):
for var in raw_vars:
if isinstance(var, dict) and var.get('required', False):
var_name = var.get('name', '')
if var_name and var_name not in user_variables:
errors.append(f'Required variable "{var_name}" is not provided')
elif isinstance(raw_vars, dict):
for var_name, var_config in raw_vars.items():
if var_config.get('required', False) and var_name not in user_variables:
errors.append(f'Required variable "{var_name}" is not provided')
if errors:
return jsonify({'valid': False, 'errors': errors}), 400
return jsonify({'valid': True}), 200
# ==================== APP UPDATES ====================
@templates_bp.route('/apps/<int:app_id>/check-update', methods=['GET'])
@jwt_required()
def check_app_update(app_id):
"""Check if an installed app has updates available."""
current_user_id = get_jwt_identity()
user = User.query.get(current_user_id)
app = Application.query.get(app_id)
if not app:
return jsonify({'error': 'Application not found'}), 404
if user.role != 'admin' and app.user_id != current_user_id:
return jsonify({'error': 'Access denied'}), 403
result = TemplateService.check_updates(app_id)
return jsonify(result), 200 if result.get('success') else 400
@templates_bp.route('/apps/<int:app_id>/update', methods=['POST'])
@jwt_required()
@admin_required
def update_app(app_id):
"""Update an app to the latest template version."""
current_user_id = get_jwt_identity()
app = Application.query.get(app_id)
if not app:
return jsonify({'error': 'Application not found'}), 404
result = TemplateService.update_app(app_id, user_id=current_user_id)
return jsonify(result), 200 if result.get('success') else 400
@templates_bp.route('/apps/<int:app_id>/template-info', methods=['GET'])
@jwt_required()
def get_app_template_info(app_id):
"""Get template installation info for an app."""
current_user_id = get_jwt_identity()
user = User.query.get(current_user_id)
app = Application.query.get(app_id)
if not app:
return jsonify({'error': 'Application not found'}), 404
if user.role != 'admin' and app.user_id != current_user_id:
return jsonify({'error': 'Access denied'}), 403
info = TemplateService.get_installed_info(app_id)
if info:
return jsonify({'installed_from_template': True, 'info': info}), 200
return jsonify({'installed_from_template': False}), 200
# ==================== REPOSITORY MANAGEMENT ====================
@templates_bp.route('/repos', methods=['GET'])
@jwt_required()
def list_repositories():
"""List configured template repositories."""
repos = TemplateService.list_repositories()
return jsonify({'repositories': repos}), 200
@templates_bp.route('/repos', methods=['POST'])
@jwt_required()
@admin_required
def add_repository():
"""Add a template repository."""
data = request.get_json()
if not data:
return jsonify({'error': 'No data provided'}), 400
name = data.get('name')
url = data.get('url')
if not name or not url:
return jsonify({'error': 'name and url are required'}), 400
result = TemplateService.add_repository(name, url)
return jsonify(result), 201 if result.get('success') else 400
@templates_bp.route('/repos', methods=['DELETE'])
@jwt_required()
@admin_required
def remove_repository():
"""Remove a template repository."""
data = request.get_json()
url = data.get('url') if data else None
if not url:
return jsonify({'error': 'url is required'}), 400
result = TemplateService.remove_repository(url)
return jsonify(result), 200 if result.get('success') else 400
@templates_bp.route('/sync', methods=['POST'])
@jwt_required()
@admin_required
def sync_templates():
"""Sync templates from all repositories."""
result = TemplateService.sync_templates()
return jsonify(result), 200
# ==================== LOCAL TEMPLATE MANAGEMENT ====================
@templates_bp.route('/local', methods=['POST'])
@jwt_required()
@admin_required
def create_local_template():
"""Create a local template."""
data = request.get_json()
if not data:
return jsonify({'error': 'No data provided'}), 400
result = TemplateService.create_local_template(data)
return jsonify(result), 201 if result.get('success') else 400
@templates_bp.route('/local/<template_id>', methods=['DELETE'])
@jwt_required()
@admin_required
def delete_local_template(template_id):
"""Delete a local template."""
result = TemplateService.delete_local_template(template_id)
return jsonify(result), 200 if result.get('success') else 404