diff --git a/db.py b/db.py index 7bd8fbd..817e8f5 100644 --- a/db.py +++ b/db.py @@ -222,4 +222,209 @@ ORDER BY invocation_time DESC""", [http_function_id]) 'UPDATE api_keys SET last_used_at=NOW() WHERE id=%s', [key_id], commit=True - ) \ No newline at end of file + ) + + def export_user_data(self, user_id): + """ + Export all user data for backup/migration purposes. + Returns a comprehensive dictionary with all user's data. + """ + export_data = {} + + # User profile + user = self.get_user(user_id) + if user: + export_data['user'] = { + 'id': user['id'], + 'username': user['username'], + 'created_at': user['created_at'].isoformat() if user.get('created_at') else None, + 'theme_preference': user.get('theme_preference') + } + + # HTTP Functions + http_functions = self.execute( + '''SELECT id, user_id, name, path, script_content, invoked_count, + environment_info, is_public, log_request, log_response, + version_number, runtime, description, created_at + FROM http_functions + WHERE user_id=%s + ORDER BY id''', + [user_id] + ) + + if http_functions: + for func in http_functions: + if func.get('created_at'): + func['created_at'] = func['created_at'].isoformat() + export_data['http_functions'] = http_functions or [] + + # Timer Functions + timer_functions = self.execute( + '''SELECT id, name, code, environment, version_number, user_id, + trigger_type, frequency_minutes, run_date, cron_expression, + next_run, last_run, enabled, invocation_count, runtime + FROM timer_functions + WHERE user_id=%s + ORDER BY id''', + [user_id] + ) + + if timer_functions: + for func in timer_functions: + if func.get('run_date'): + func['run_date'] = func['run_date'].isoformat() + if func.get('next_run'): + func['next_run'] = func['next_run'].isoformat() + if func.get('last_run'): + func['last_run'] = func['last_run'].isoformat() + export_data['timer_functions'] = timer_functions or [] + + # Shared Environments + shared_envs = self.execute( + '''SELECT id, user_id, name, environment, description, + created_at, updated_at, version_number + FROM shared_environments + WHERE user_id=%s + ORDER BY name''', + [user_id] + ) + + if shared_envs: + for env in shared_envs: + if env.get('created_at'): + env['created_at'] = env['created_at'].isoformat() + if env.get('updated_at'): + env['updated_at'] = env['updated_at'].isoformat() + export_data['shared_environments'] = shared_envs or [] + + # API Keys (masked for security) + api_keys = self.list_api_keys(user_id) + if api_keys: + for key in api_keys: + # Only include partial key for security + key['key'] = key['key'][:8] + '...' if 'key' in key else None + if key.get('created_at'): + key['created_at'] = key['created_at'].isoformat() + if key.get('last_used_at'): + key['last_used_at'] = key['last_used_at'].isoformat() + export_data['api_keys'] = api_keys or [] + + # HTTP Function Invocations (limited to last 100 per function) + http_invocations = [] + if http_functions: + for func in http_functions: + invocations = self.execute( + '''SELECT id, http_function_id, status, invocation_time, + request_data, response_data, logs, version_number, execution_time + FROM http_function_invocations + WHERE http_function_id=%s + ORDER BY invocation_time DESC + LIMIT 100''', + [func['id']] + ) + if invocations: + for inv in invocations: + if inv.get('invocation_time'): + inv['invocation_time'] = inv['invocation_time'].isoformat() + http_invocations.extend(invocations) + export_data['http_function_invocations'] = http_invocations + + # Timer Function Invocations (limited to last 100 per function) + timer_invocations = [] + if timer_functions: + for func in timer_functions: + invocations = self.execute( + '''SELECT id, timer_function_id, status, invocation_time, + logs, version_number, execution_time + FROM timer_function_invocations + WHERE timer_function_id=%s + ORDER BY invocation_time DESC + LIMIT 100''', + [func['id']] + ) + if invocations: + for inv in invocations: + if inv.get('invocation_time'): + inv['invocation_time'] = inv['invocation_time'].isoformat() + timer_invocations.extend(invocations) + export_data['timer_function_invocations'] = timer_invocations + + # HTTP Function Version History + http_versions = [] + if http_functions: + for func in http_functions: + versions = self.get_http_function_history(func['id']) + if versions: + for ver in versions: + if ver.get('updated_at'): + ver['updated_at'] = ver['updated_at'].isoformat() + http_versions.extend(versions) + export_data['http_function_versions'] = http_versions + + # Timer Function Version History + timer_versions = [] + if timer_functions: + for func in timer_functions: + versions = self.execute( + '''SELECT id as version_id, timer_function_id, script, + version_number, versioned_at + FROM timer_function_versions + WHERE timer_function_id=%s + ORDER BY version_number DESC''', + [func['id']] + ) + if versions: + for ver in versions: + if ver.get('versioned_at'): + ver['versioned_at'] = ver['versioned_at'].isoformat() + timer_versions.extend(versions) + export_data['timer_function_versions'] = timer_versions + + # Shared Environment Version History + shared_env_versions = [] + if shared_envs: + for env in shared_envs: + versions = self.execute( + '''SELECT id as version_id, shared_env_id, environment, + version_number, versioned_at + FROM shared_environment_versions + WHERE shared_env_id=%s + ORDER BY version_number DESC''', + [env['id']] + ) + if versions: + for ver in versions: + if ver.get('versioned_at'): + ver['versioned_at'] = ver['versioned_at'].isoformat() + shared_env_versions.extend(versions) + export_data['shared_environment_versions'] = shared_env_versions + + # HTTP Function to Shared Environment Linkages + http_shared_env_links = self.execute( + '''SELECT hfse.id, hfse.http_function_id, hfse.shared_env_id, hfse.created_at + FROM http_function_shared_envs hfse + JOIN http_functions hf ON hfse.http_function_id = hf.id + WHERE hf.user_id=%s''', + [user_id] + ) + if http_shared_env_links: + for link in http_shared_env_links: + if link.get('created_at'): + link['created_at'] = link['created_at'].isoformat() + export_data['http_function_shared_env_links'] = http_shared_env_links or [] + + # Timer Function to Shared Environment Linkages + timer_shared_env_links = self.execute( + '''SELECT tfse.id, tfse.timer_function_id, tfse.shared_env_id, tfse.created_at + FROM timer_function_shared_envs tfse + JOIN timer_functions tf ON tfse.timer_function_id = tf.id + WHERE tf.user_id=%s''', + [user_id] + ) + if timer_shared_env_links: + for link in timer_shared_env_links: + if link.get('created_at'): + link['created_at'] = link['created_at'].isoformat() + export_data['timer_function_shared_env_links'] = timer_shared_env_links or [] + + return export_data \ No newline at end of file diff --git a/routes/settings.py b/routes/settings.py index 72c0cbd..d29bafb 100644 --- a/routes/settings.py +++ b/routes/settings.py @@ -12,6 +12,48 @@ settings = Blueprint('settings', __name__) def index(): return redirect(url_for('settings.api_keys')) +@settings.route("/export", methods=["GET"]) +@login_required +def export(): + """Display data export page or download data export""" + # Check if this is a download request + if request.args.get('download') == 'true': + from flask import make_response + from datetime import datetime + + user_id = current_user.id + + # Get all user data + export_data = db.export_user_data(user_id) + + # Add export metadata + export_data['_export_metadata'] = { + 'exported_at': datetime.now().isoformat(), + 'export_version': '1.0', + 'application': 'Functions Platform' + } + + # Create JSON response + response = make_response(json.dumps(export_data, indent=2, default=str)) + response.headers['Content-Type'] = 'application/json' + + # Generate filename with username and timestamp + username = current_user.username + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + filename = f'user_data_export_{username}_{timestamp}.json' + response.headers['Content-Disposition'] = f'attachment; filename={filename}' + + return response + + # Otherwise show the export page + if htmx: + return render_block( + environment, + "dashboard/settings/export.html", + "page" + ) + return render_template("dashboard/settings/export.html") + @settings.route("/api-keys", methods=["GET"]) @login_required def api_keys(): diff --git a/templates/dashboard/settings/api_keys.html b/templates/dashboard/settings/api_keys.html index e82699c..08b8030 100644 --- a/templates/dashboard/settings/api_keys.html +++ b/templates/dashboard/settings/api_keys.html @@ -2,6 +2,20 @@ {% block page %}
+ +
+ +
+

API Keys

+{% endblock %} \ No newline at end of file