updated authentication of instances
This commit is contained in:
Binary file not shown.
Binary file not shown.
24
migrations/versions/761908f0cacf_merge_heads.py
Normal file
24
migrations/versions/761908f0cacf_merge_heads.py
Normal file
@@ -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
|
||||||
24
migrations/versions/add_connection_token.py
Normal file
24
migrations/versions/add_connection_token.py
Normal file
@@ -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')
|
||||||
@@ -7,6 +7,7 @@ Create Date: 2024-03-19 11:00:00.000000
|
|||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
@@ -17,7 +18,18 @@ depends_on = None
|
|||||||
|
|
||||||
|
|
||||||
def upgrade():
|
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():
|
def downgrade():
|
||||||
|
|||||||
@@ -508,6 +508,7 @@ class Instance(db.Model):
|
|||||||
main_url = db.Column(db.String(255), unique=True, nullable=False)
|
main_url = db.Column(db.String(255), unique=True, nullable=False)
|
||||||
status = db.Column(db.String(20), nullable=False, default='inactive')
|
status = db.Column(db.String(20), nullable=False, default='inactive')
|
||||||
status_details = db.Column(db.Text, nullable=True)
|
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'))
|
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'))
|
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP'), onupdate=db.text('CURRENT_TIMESTAMP'))
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -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 functools import wraps
|
||||||
from models import (
|
from models import (
|
||||||
KeyValueSettings, User, Room, Conversation, RoomFile,
|
KeyValueSettings, User, Room, Conversation, RoomFile,
|
||||||
@@ -13,6 +13,25 @@ import secrets
|
|||||||
|
|
||||||
admin_api = Blueprint('admin_api', __name__)
|
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):
|
def token_required(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated(*args, **kwargs):
|
def decorated(*args, **kwargs):
|
||||||
|
|||||||
@@ -482,6 +482,27 @@ def init_routes(main_bp):
|
|||||||
|
|
||||||
return jsonify(status_info)
|
return jsonify(status_info)
|
||||||
|
|
||||||
|
@main_bp.route('/instances/<int:instance_id>/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'
|
UPLOAD_FOLDER = '/app/uploads/profile_pics'
|
||||||
if not os.path.exists(UPLOAD_FOLDER):
|
if not os.path.exists(UPLOAD_FOLDER):
|
||||||
os.makedirs(UPLOAD_FOLDER)
|
os.makedirs(UPLOAD_FOLDER)
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
<th>Payment Plan</th>
|
<th>Payment Plan</th>
|
||||||
<th>Main URL</th>
|
<th>Main URL</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
|
<th>Connection Token</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -64,6 +65,19 @@
|
|||||||
{{ instance.status|title }}
|
{{ instance.status|title }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if instance.connection_token %}
|
||||||
|
<span class="badge bg-success" data-bs-toggle="tooltip" title="Instance is authenticated">
|
||||||
|
Authenticated
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-warning" data-bs-toggle="tooltip" title="Click to authenticate instance">
|
||||||
|
<a href="#" class="text-dark text-decoration-none" onclick="showAuthModal('{{ instance.main_url }}', {{ instance.id }})">
|
||||||
|
Generate Token
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-sm btn-outline-primary" onclick="editInstance({{ instance.id }})">
|
<button class="btn btn-sm btn-outline-primary" onclick="editInstance({{ instance.id }})">
|
||||||
@@ -173,6 +187,37 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Auth Modal -->
|
||||||
|
<div class="modal fade" id="authModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Authenticate Instance</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="authForm">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
<input type="hidden" id="instance_url" name="instance_url">
|
||||||
|
<input type="hidden" id="instance_id" name="instance_id">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">Email</label>
|
||||||
|
<input type="email" class="form-control" id="email" name="email" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">Password</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="authenticateInstance()">Authenticate</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
@@ -181,11 +226,13 @@
|
|||||||
let addInstanceModal;
|
let addInstanceModal;
|
||||||
let editInstanceModal;
|
let editInstanceModal;
|
||||||
let addExistingInstanceModal;
|
let addExistingInstanceModal;
|
||||||
|
let authModal;
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
addInstanceModal = new bootstrap.Modal(document.getElementById('addInstanceModal'));
|
addInstanceModal = new bootstrap.Modal(document.getElementById('addInstanceModal'));
|
||||||
editInstanceModal = new bootstrap.Modal(document.getElementById('editInstanceModal'));
|
editInstanceModal = new bootstrap.Modal(document.getElementById('editInstanceModal'));
|
||||||
addExistingInstanceModal = new bootstrap.Modal(document.getElementById('addExistingInstanceModal'));
|
addExistingInstanceModal = new bootstrap.Modal(document.getElementById('addExistingInstanceModal'));
|
||||||
|
authModal = new bootstrap.Modal(document.getElementById('authModal'));
|
||||||
|
|
||||||
// Initialize tooltips
|
// Initialize tooltips
|
||||||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||||
@@ -409,5 +456,95 @@ async function submitAddExistingInstance() {
|
|||||||
alert('Error adding instance: ' + error.message);
|
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 = '<i class="fas fa-check"></i>';
|
||||||
|
setTimeout(() => {
|
||||||
|
button.innerHTML = originalText;
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user