diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc index 0244db8..6ca4e8a 100644 Binary files a/__pycache__/app.cpython-313.pyc and b/__pycache__/app.cpython-313.pyc differ diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc index bda7478..32cd4ed 100644 Binary files a/__pycache__/models.cpython-313.pyc and b/__pycache__/models.cpython-313.pyc differ diff --git a/migrations/versions/761908f0cacf_merge_heads.py b/migrations/versions/761908f0cacf_merge_heads.py new file mode 100644 index 0000000..d9907ec --- /dev/null +++ b/migrations/versions/761908f0cacf_merge_heads.py @@ -0,0 +1,24 @@ +"""merge heads + +Revision ID: 761908f0cacf +Revises: 4ee23cb29001, add_connection_token +Create Date: 2025-06-09 13:57:17.650231 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '761908f0cacf' +down_revision = ('4ee23cb29001', 'add_connection_token') +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/migrations/versions/add_connection_token.py b/migrations/versions/add_connection_token.py new file mode 100644 index 0000000..6070679 --- /dev/null +++ b/migrations/versions/add_connection_token.py @@ -0,0 +1,24 @@ +"""add connection_token column + +Revision ID: add_connection_token +Revises: fix_updated_at_trigger +Create Date: 2024-03-19 13:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'add_connection_token' +down_revision = 'fix_updated_at_trigger' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('instances', sa.Column('connection_token', sa.String(64), nullable=True, unique=True)) + + +def downgrade(): + op.drop_column('instances', 'connection_token') \ No newline at end of file diff --git a/migrations/versions/add_status_details.py b/migrations/versions/add_status_details.py index c073ac6..34b1f4a 100644 --- a/migrations/versions/add_status_details.py +++ b/migrations/versions/add_status_details.py @@ -7,6 +7,7 @@ Create Date: 2024-03-19 11:00:00.000000 """ from alembic import op import sqlalchemy as sa +from sqlalchemy import text # revision identifiers, used by Alembic. @@ -17,7 +18,18 @@ depends_on = None def upgrade(): - op.add_column('instances', sa.Column('status_details', sa.Text(), nullable=True)) + # Check if column exists before adding + conn = op.get_bind() + result = conn.execute(text(""" + SELECT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_name = 'instances' AND column_name = 'status_details' + ); + """)) + exists = result.scalar() + + if not exists: + op.add_column('instances', sa.Column('status_details', sa.Text(), nullable=True)) def downgrade(): diff --git a/models.py b/models.py index 727f100..4fd334f 100644 --- a/models.py +++ b/models.py @@ -508,6 +508,7 @@ class Instance(db.Model): main_url = db.Column(db.String(255), unique=True, nullable=False) status = db.Column(db.String(20), nullable=False, default='inactive') status_details = db.Column(db.Text, nullable=True) + connection_token = db.Column(db.String(64), unique=True, nullable=True) created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP')) updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP'), onupdate=db.text('CURRENT_TIMESTAMP')) diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index 53844d3..2163a11 100644 Binary files a/routes/__pycache__/main.cpython-313.pyc and b/routes/__pycache__/main.cpython-313.pyc differ diff --git a/routes/admin_api.py b/routes/admin_api.py index 6688902..0a8b606 100644 --- a/routes/admin_api.py +++ b/routes/admin_api.py @@ -1,4 +1,4 @@ -from flask import Blueprint, jsonify, request, current_app +from flask import Blueprint, jsonify, request, current_app, make_response from functools import wraps from models import ( KeyValueSettings, User, Room, Conversation, RoomFile, @@ -13,6 +13,25 @@ import secrets admin_api = Blueprint('admin_api', __name__) +def add_cors_headers(response): + """Add CORS headers to the response""" + response.headers['Access-Control-Allow-Origin'] = '*' + response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS' + response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, X-API-Key, X-CSRF-Token' + return response + +@admin_api.before_request +def handle_preflight(): + """Handle preflight requests""" + if request.method == 'OPTIONS': + response = make_response() + return add_cors_headers(response) + +@admin_api.after_request +def after_request(response): + """Add CORS headers to all responses""" + return add_cors_headers(response) + def token_required(f): @wraps(f) def decorated(*args, **kwargs): diff --git a/routes/main.py b/routes/main.py index 9d05298..89cba90 100644 --- a/routes/main.py +++ b/routes/main.py @@ -482,6 +482,27 @@ def init_routes(main_bp): return jsonify(status_info) + @main_bp.route('/instances//save-token', methods=['POST']) + @login_required + @require_password_change + def save_instance_token(instance_id): + if not os.environ.get('MASTER', 'false').lower() == 'true': + return jsonify({'error': 'Unauthorized'}), 403 + + instance = Instance.query.get_or_404(instance_id) + data = request.get_json() + + if not data or 'token' not in data: + return jsonify({'error': 'Token is required'}), 400 + + try: + instance.connection_token = data['token'] + db.session.commit() + return jsonify({'message': 'Token saved successfully'}) + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 400 + UPLOAD_FOLDER = '/app/uploads/profile_pics' if not os.path.exists(UPLOAD_FOLDER): os.makedirs(UPLOAD_FOLDER) diff --git a/templates/main/instances.html b/templates/main/instances.html index 8da9ff8..642f4ed 100644 --- a/templates/main/instances.html +++ b/templates/main/instances.html @@ -43,6 +43,7 @@ Payment Plan Main URL Status + Connection Token Actions @@ -64,6 +65,19 @@ {{ instance.status|title }} + + {% if instance.connection_token %} + + Authenticated + + {% else %} + + + Generate Token + + + {% endif %} +
+ + + {% endblock %} {% block extra_js %} @@ -181,11 +226,13 @@ let addInstanceModal; let editInstanceModal; let addExistingInstanceModal; +let authModal; document.addEventListener('DOMContentLoaded', function() { addInstanceModal = new bootstrap.Modal(document.getElementById('addInstanceModal')); editInstanceModal = new bootstrap.Modal(document.getElementById('editInstanceModal')); addExistingInstanceModal = new bootstrap.Modal(document.getElementById('addExistingInstanceModal')); + authModal = new bootstrap.Modal(document.getElementById('authModal')); // Initialize tooltips const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); @@ -409,5 +456,95 @@ async function submitAddExistingInstance() { alert('Error adding instance: ' + error.message); } } + +function showAuthModal(instanceUrl, instanceId) { + document.getElementById('instance_url').value = instanceUrl; + document.getElementById('instance_id').value = instanceId; + document.getElementById('authForm').reset(); + authModal.show(); +} + +async function authenticateInstance() { + const form = document.getElementById('authForm'); + const formData = new FormData(form); + const instanceUrl = formData.get('instance_url').replace(/\/+$/, ''); // Remove trailing slashes + const instanceId = formData.get('instance_id'); + const email = formData.get('email'); + const password = formData.get('password'); + + try { + // First login to get token + const loginResponse = await fetch(`${instanceUrl}/admin/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ email, password }) + }); + + if (!loginResponse.ok) { + const errorData = await loginResponse.json().catch(() => ({})); + throw new Error(errorData.message || 'Login failed'); + } + + const { token } = await loginResponse.json(); + + // Then create management API key + const keyResponse = await fetch(`${instanceUrl}/admin/management-api-key`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ + name: `Connection from ${window.location.hostname}` + }) + }); + + if (!keyResponse.ok) { + const errorData = await keyResponse.json().catch(() => ({})); + throw new Error(errorData.message || 'Failed to create API key'); + } + + const { api_key } = await keyResponse.json(); + + // Save the token to our database + const saveResponse = await fetch(`/instances/${instanceId}/save-token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': formData.get('csrf_token') + }, + body: JSON.stringify({ token: api_key }) + }); + + if (!saveResponse.ok) { + const errorData = await saveResponse.json().catch(() => ({})); + throw new Error(errorData.message || 'Failed to save token'); + } + + // Show success and refresh + authModal.hide(); + location.reload(); + } catch (error) { + console.error('Authentication error:', error); + alert('Error: ' + error.message); + } +} + +function copyConnectionToken(button) { + const input = button.previousElementSibling; + input.select(); + document.execCommand('copy'); + + // Show feedback + const originalText = button.innerHTML; + button.innerHTML = ''; + setTimeout(() => { + button.innerHTML = originalText; + }, 2000); +} {% endblock %} \ No newline at end of file