Add export data functionality on settings page
This commit is contained in:
207
db.py
207
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
|
||||
)
|
||||
)
|
||||
|
||||
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
|
||||
@@ -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():
|
||||
|
||||
@@ -2,6 +2,20 @@
|
||||
|
||||
{% block page %}
|
||||
<div class="p-6 max-w-4xl mx-auto">
|
||||
<!-- Settings Navigation -->
|
||||
<div class="mb-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<nav class="-mb-px flex space-x-8">
|
||||
<a href="{{ url_for('settings.api_keys') }}"
|
||||
class="border-b-2 border-blue-500 text-blue-600 dark:text-blue-400 py-4 px-1 text-sm font-medium">
|
||||
API Keys
|
||||
</a>
|
||||
<a href="{{ url_for('settings.export') }}"
|
||||
class="border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 py-4 px-1 text-sm font-medium">
|
||||
Export Data
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">API Keys</h1>
|
||||
<button onclick="document.getElementById('create-key-modal').showModal()"
|
||||
|
||||
147
templates/dashboard/settings/export.html
Normal file
147
templates/dashboard/settings/export.html
Normal file
@@ -0,0 +1,147 @@
|
||||
{% extends 'dashboard.html' %}
|
||||
|
||||
{% block page %}
|
||||
<div class="p-6 max-w-4xl mx-auto">
|
||||
<!-- Settings Navigation -->
|
||||
<div class="mb-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<nav class="-mb-px flex space-x-8">
|
||||
<a href="{{ url_for('settings.api_keys') }}"
|
||||
class="border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 py-4 px-1 text-sm font-medium">
|
||||
API Keys
|
||||
</a>
|
||||
<a href="{{ url_for('settings.export') }}"
|
||||
class="border-b-2 border-blue-500 text-blue-600 dark:text-blue-400 py-4 px-1 text-sm font-medium">
|
||||
Export Data
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">Export Your Data</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400">Download all your data in JSON format for backup or migration
|
||||
purposes.</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">What's Included</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div class="flex items-start space-x-3">
|
||||
<svg class="w-5 h-5 text-green-500 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-medium text-gray-900 dark:text-white">HTTP Functions</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">All your HTTP functions with code and settings
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start space-x-3">
|
||||
<svg class="w-5 h-5 text-green-500 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-medium text-gray-900 dark:text-white">Timer Functions</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">All scheduled functions and their configurations
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start space-x-3">
|
||||
<svg class="w-5 h-5 text-green-500 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-medium text-gray-900 dark:text-white">Shared Environments</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Environment variables and configurations</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start space-x-3">
|
||||
<svg class="w-5 h-5 text-green-500 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-medium text-gray-900 dark:text-white">API Keys</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">API key names and scopes (keys are masked)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start space-x-3">
|
||||
<svg class="w-5 h-5 text-green-500 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-medium text-gray-900 dark:text-white">Invocation Logs</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Last 100 invocations per function</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start space-x-3">
|
||||
<svg class="w-5 h-5 text-green-500 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-medium text-gray-900 dark:text-white">Version History</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Complete version history for all functions</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4 mb-6">
|
||||
<div class="flex items-start space-x-3">
|
||||
<svg class="w-5 h-5 text-yellow-600 dark:text-yellow-500 mt-0.5" fill="currentColor"
|
||||
viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-medium text-yellow-900 dark:text-yellow-200 mb-1">Important Information</h3>
|
||||
<ul class="text-sm text-yellow-800 dark:text-yellow-300 space-y-1">
|
||||
<li>• The export file contains sensitive data. Store it securely.</li>
|
||||
<li>• API keys are partially masked for security (only first 8 characters shown).</li>
|
||||
<li>• The export is in JSON format for easy import/migration.</li>
|
||||
<li>• File size may be large if you have many functions and invocations.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<a href="{{ url_for('settings.export', download='true') }}"
|
||||
class="px-6 py-3 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 16v1a3 3 0 003 3h10a 3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
<span>Export My Data</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
|
||||
<h3 class="font-medium text-gray-900 dark:text-white mb-2">What to Do With Your Export</h3>
|
||||
<ul class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
|
||||
<li>• <strong>Backup:</strong> Save the file in a secure location for disaster recovery</li>
|
||||
<li>• <strong>Migration:</strong> Use the export to migrate to another instance or platform</li>
|
||||
<li>• <strong>Analysis:</strong> Parse the JSON data for auditing or analytics purposes</li>
|
||||
<li>• <strong>Version Control:</strong> Track changes to your functions over time</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user