From c9d1d7416b6083a15bcf68af230816c06a0811fc Mon Sep 17 00:00:00 2001 From: Kobe Date: Fri, 20 Jun 2025 13:18:13 +0200 Subject: [PATCH] password reset online test --- routes/admin_api.py | 37 +++- routes/launch_api.py | 403 ++++++++++++++++++++++++++++++----- static/js/launch_progress.js | 138 +++++++++++- 3 files changed, 513 insertions(+), 65 deletions(-) diff --git a/routes/admin_api.py b/routes/admin_api.py index 6837007..35490ea 100644 --- a/routes/admin_api.py +++ b/routes/admin_api.py @@ -2,7 +2,7 @@ from flask import Blueprint, jsonify, request, current_app, make_response, flash from functools import wraps from models import ( KeyValueSettings, User, Room, Conversation, RoomFile, - SiteSettings, DocuPulseSettings, Event, Mail, ManagementAPIKey + SiteSettings, DocuPulseSettings, Event, Mail, ManagementAPIKey, PasswordSetupToken, PasswordResetToken ) from extensions import db, csrf from datetime import datetime, timedelta @@ -526,4 +526,37 @@ def resend_setup_mail(current_user, user_id): db.session.add(mail) db.session.commit() - return jsonify({'message': 'Setup mail queued for resending'}) \ No newline at end of file + return jsonify({'message': 'Setup mail queued for resending'}) + +# Generate Password Reset Token +@admin_api.route('/generate-password-reset/', methods=['POST']) +@csrf.exempt +@token_required +def generate_password_reset_token(current_user, user_id): + user = User.query.get(user_id) + if not user: + return jsonify({'message': 'User not found'}), 404 + + # Generate a secure token for password reset + token = secrets.token_urlsafe(32) + + # Create password reset token + reset_token = PasswordResetToken( + user_id=user.id, + token=token, + expires_at=datetime.utcnow() + timedelta(hours=24), # 24 hour expiration + ip_address=request.remote_addr + ) + db.session.add(reset_token) + db.session.commit() + + # Return the token and reset URL + reset_url = f"{request.host_url.rstrip('/')}/reset-password/{token}" + + return jsonify({ + 'message': 'Password reset token generated successfully', + 'token': token, + 'reset_url': reset_url, + 'expires_at': reset_token.expires_at.isoformat(), + 'user_email': user.email + }) \ No newline at end of file diff --git a/routes/launch_api.py b/routes/launch_api.py index 21d2d84..0717099 100644 --- a/routes/launch_api.py +++ b/routes/launch_api.py @@ -1101,40 +1101,11 @@ def update_admin_credentials(): if not jwt_token: return jsonify({'error': 'No JWT token received'}), 400 - # Generate a secure password - import secrets - import string - alphabet = string.ascii_letters + string.digits + "!@#$%^&*" - new_password = ''.join(secrets.choice(alphabet) for i in range(16)) - - # First, login with default credentials to get admin token - login_response = requests.post( - f"{instance_url.rstrip('/')}/api/admin/login", - headers={ - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }, - json={ - 'email': 'administrator@docupulse.com', - 'password': 'changeme' - }, - timeout=10 - ) - - if login_response.status_code != 200: - return jsonify({'error': f'Failed to login with default credentials: {login_response.text}'}), 400 - - login_data = login_response.json() - if login_data.get('status') != 'success' or not login_data.get('token'): - return jsonify({'error': 'Failed to get admin token'}), 400 - - admin_token = login_data.get('token') - # Get the admin user ID first users_response = requests.get( f"{instance_url.rstrip('/')}/api/admin/contacts", headers={ - 'Authorization': f'Bearer {admin_token}', + 'Authorization': f'Bearer {jwt_token}', 'Accept': 'application/json' }, timeout=10 @@ -1148,7 +1119,7 @@ def update_admin_credentials(): # Find the administrator user for user in users_data: - if user.get('email') == 'administrator@docupulse.com': + if user.get('email') == email: admin_user = user break @@ -1157,37 +1128,359 @@ def update_admin_credentials(): admin_user_id = admin_user.get('id') - # Update the admin user with new email and password - update_response = requests.put( - f"{instance_url.rstrip('/')}/api/admin/contacts/{admin_user_id}", + # Try to login with default credentials first + try: + login_response = requests.post( + f"{instance_url.rstrip('/')}/api/admin/login", + headers={ + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + json={ + 'email': 'administrator@docupulse.com', + 'password': 'changeme' + }, + timeout=10 + ) + + # If login with default credentials succeeds, update the credentials + if login_response.status_code == 200: + login_data = login_response.json() + if login_data.get('status') == 'success' and login_data.get('token'): + admin_token = login_data.get('token') + + # Generate a secure password + import secrets + import string + alphabet = string.ascii_letters + string.digits + "!@#$%^&*" + new_password = ''.join(secrets.choice(alphabet) for i in range(16)) + + # Update the admin user with new email and password + update_response = requests.put( + f"{instance_url.rstrip('/')}/api/admin/contacts/{admin_user_id}", + headers={ + 'Authorization': f'Bearer {admin_token}', + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + json={ + 'email': email, + 'password': new_password, + 'username': 'administrator', + 'last_name': 'Administrator', + 'role': 'admin' + }, + timeout=10 + ) + + if update_response.status_code != 200: + return jsonify({'error': f'Failed to update admin credentials: {update_response.text}'}), 400 + + return jsonify({ + 'message': 'Admin credentials updated successfully', + 'data': { + 'email': email, + 'password': new_password, + 'username': 'administrator', + 'instance_url': instance_url + } + }) + + # If login with default credentials fails, check if email is already updated + else: + # Check if the email is already the target email + if admin_user.get('email') == email: + return jsonify({ + 'message': 'Admin credentials already updated', + 'data': { + 'email': email, + 'password': 'Already set', + 'username': 'administrator', + 'instance_url': instance_url, + 'already_updated': True + } + }) + else: + return jsonify({'error': 'Failed to login with default credentials and email not yet updated'}), 400 + + except Exception as login_error: + # If there's an error with login, check if email is already updated + if admin_user.get('email') == email: + return jsonify({ + 'message': 'Admin credentials already updated', + 'data': { + 'email': email, + 'password': 'Already set', + 'username': 'administrator', + 'instance_url': instance_url, + 'already_updated': True + } + }) + else: + return jsonify({'error': f'Login error and email not yet updated: {str(login_error)}'}), 400 + + except Exception as e: + current_app.logger.error(f"Error updating admin credentials: {str(e)}") + return jsonify({'error': str(e)}), 500 + +@launch_api.route('/send-completion-email', methods=['POST']) +@csrf.exempt +def send_completion_email(): + """Send completion email to client with instance details and password reset link""" + try: + if not request.is_json: + return jsonify({'error': 'Request must be JSON'}), 400 + + data = request.get_json() + required_fields = ['instance_url', 'company_data', 'credentials_data'] + + if not all(field in data for field in required_fields): + missing_fields = [field for field in required_fields if field not in data] + return jsonify({'error': f'Missing required fields: {", ".join(missing_fields)}'}), 400 + + instance_url = data['instance_url'] + company_data = data['company_data'] + credentials_data = data['credentials_data'] + + # Get SMTP settings from master instance + smtp_settings = KeyValueSettings.get_value('smtp_settings') + if not smtp_settings: + return jsonify({'error': 'SMTP settings not configured'}), 400 + + # Get the instance from database to get the connection token + instance = Instance.query.filter_by(main_url=instance_url).first() + + if not instance: + return jsonify({'error': 'Instance not found in database'}), 404 + + if not instance.connection_token: + return jsonify({'error': 'Instance not authenticated'}), 400 + + # Get JWT token from the launched instance using management API key + jwt_response = requests.post( + f"{instance_url.rstrip('/')}/api/admin/management-token", headers={ - 'Authorization': f'Bearer {admin_token}', - 'Content-Type': 'application/json', + 'X-API-Key': instance.connection_token, 'Accept': 'application/json' }, - json={ - 'email': email, - 'password': new_password, - 'username': 'administrator', - 'last_name': 'Administrator', - 'role': 'admin' - }, timeout=10 ) - if update_response.status_code != 200: - return jsonify({'error': f'Failed to update admin credentials: {update_response.text}'}), 400 + if jwt_response.status_code != 200: + return jsonify({'error': f'Failed to get JWT token: {jwt_response.text}'}), 400 - return jsonify({ - 'message': 'Admin credentials updated successfully', - 'data': { - 'email': email, - 'password': new_password, - 'username': 'administrator', - 'instance_url': instance_url - } - }) + jwt_data = jwt_response.json() + jwt_token = jwt_data.get('token') + + if not jwt_token: + return jsonify({'error': 'No JWT token received'}), 400 + + # Get the admin user ID from the launched instance + users_response = requests.get( + f"{instance_url.rstrip('/')}/api/admin/contacts", + headers={ + 'Authorization': f'Bearer {jwt_token}', + 'Accept': 'application/json' + }, + timeout=10 + ) + + if users_response.status_code != 200: + return jsonify({'error': f'Failed to get users: {users_response.text}'}), 400 + + users_data = users_response.json() + admin_user = None + + # Find the administrator user by email + for user in users_data: + if user.get('email') == credentials_data.get('email'): + admin_user = user + break + + if not admin_user: + return jsonify({'error': 'Administrator user not found'}), 404 + + admin_user_id = admin_user.get('id') + + # Generate password reset token for the launched instance using management API + reset_response = requests.post( + f"{instance_url.rstrip('/')}/api/admin/generate-password-reset/{admin_user_id}", + headers={ + 'Authorization': f'Bearer {jwt_token}', + 'Accept': 'application/json' + }, + timeout=10 + ) + + if reset_response.status_code != 200: + return jsonify({'error': f'Failed to generate password reset: {reset_response.text}'}), 400 + + reset_data = reset_response.json() + reset_url = reset_data.get('reset_url') + expires_at = reset_data.get('expires_at') + + if not reset_url: + return jsonify({'error': 'No reset URL received from instance'}), 400 + + # Create email content + subject = "Your DocuPulse Instance is Ready!" + + # Build HTML email content + html_content = f""" + + + + + + + +
+
+

🎉 Your DocuPulse Instance is Ready!

+
+
+

Dear {company_data.get('name', 'Valued Customer')},

+ +

Great news! Your DocuPulse instance has been successfully deployed and configured. + You can now access your secure document management platform.

+ +
+

📋 Instance Details

+

Instance URL: {instance_url}

+

Company Name: {company_data.get('name', 'Not set')}

+

Industry: {company_data.get('industry', 'Not set')}

+

Deployment Date: {datetime.utcnow().strftime('%B %d, %Y at %I:%M %p UTC')}

+
+ +
+

🔐 Account Access

+

Email Address: {credentials_data.get('email', 'Not set')}

+

Username: {credentials_data.get('username', 'administrator')}

+ +
+

🔒 Security Setup Required

+

For your security, you need to set up your password. Click the button below to create your secure password.

+

Password Reset Link Expires: {expires_at}

+
+
+ +
+ 🔐 Set Up Your Password +

+ 🚀 Access Your Instance +
+ +

✅ What's Been Configured

+
    +
  • ✅ Secure SSL certificate for HTTPS access
  • +
  • ✅ Company information and branding
  • +
  • ✅ Custom color scheme
  • +
  • ✅ Admin account created
  • +
  • ✅ Document management system ready
  • +
+ +

🎯 Next Steps

+
    +
  1. Click the "Set Up Your Password" button above
  2. +
  3. Create your secure password
  4. +
  5. Return to your instance and log in
  6. +
  7. Explore your new DocuPulse platform
  8. +
  9. Start uploading and organizing your documents
  10. +
  11. Invite team members to collaborate
  12. +
+ + +
+
+ + + """ + + # Build plain text version + text_content = f""" +Your DocuPulse Instance is Ready! + +Dear {company_data.get('name', 'Valued Customer')}, + +Great news! Your DocuPulse instance has been successfully deployed and configured. + +INSTANCE DETAILS: +- Instance URL: {instance_url} +- Company Name: {company_data.get('name', 'Not set')} +- Industry: {company_data.get('industry', 'Not set')} +- Deployment Date: {datetime.utcnow().strftime('%B %d, %Y at %I:%M %p UTC')} + +ACCOUNT ACCESS: +- Email Address: {credentials_data.get('email', 'Not set')} +- Username: {credentials_data.get('username', 'administrator')} + +SECURITY SETUP REQUIRED: +For your security, you need to set up your password. + +Password Reset Link: {reset_url} +Password Reset Link Expires: {expires_at} + +WHAT'S BEEN CONFIGURED: +✓ Secure SSL certificate for HTTPS access +✓ Company information and branding +✓ Custom color scheme +✓ Admin account created +✓ Document management system ready + +NEXT STEPS: +1. Click the password reset link above +2. Create your secure password +3. Return to your instance and log in +4. Explore your new DocuPulse platform +5. Start uploading and organizing your documents +6. Invite team members to collaborate + +If you have any questions or need assistance, please don't hesitate to contact our support team. + +Thank you for choosing DocuPulse! + """ + + # Send email using master instance's email system + from utils.email_templates import send_email + + try: + send_email( + to_email=company_data.get('email'), + subject=subject, + html_content=html_content, + text_content=text_content + ) + + # Log the email sending + current_app.logger.info(f"Completion email sent to {company_data.get('email')} for instance {instance_url}") + + return jsonify({ + 'message': 'Completion email sent successfully', + 'data': { + 'recipient': company_data.get('email'), + 'subject': subject, + 'instance_url': instance_url, + 'password_reset_sent': True + } + }) + + except Exception as email_error: + current_app.logger.error(f"Failed to send completion email: {str(email_error)}") + return jsonify({'error': f'Failed to send email: {str(email_error)}'}), 500 except Exception as e: - current_app.logger.error(f"Error updating admin credentials: {str(e)}") + current_app.logger.error(f"Error sending completion email: {str(e)}") return jsonify({'error': str(e)}), 500 \ No newline at end of file diff --git a/static/js/launch_progress.js b/static/js/launch_progress.js index 84b28ff..4230d30 100644 --- a/static/js/launch_progress.js +++ b/static/js/launch_progress.js @@ -171,6 +171,18 @@ function initializeSteps() { `; stepsContainer.appendChild(credentialsStep); + + // Add Send Completion Email step + const emailStep = document.createElement('div'); + emailStep.className = 'step-item'; + emailStep.innerHTML = ` +
+
+
Send Completion Email
+

Sending notification to client...

+
+ `; + stepsContainer.appendChild(emailStep); } async function startLaunch(data) { @@ -633,7 +645,12 @@ async function startLaunch(data) { const credentialsStep = document.querySelectorAll('.step-item')[12]; credentialsStep.classList.remove('active'); credentialsStep.classList.add('completed'); - credentialsStep.querySelector('.step-status').textContent = 'Successfully updated admin credentials'; + + if (credentialsResult.data.already_updated) { + credentialsStep.querySelector('.step-status').textContent = 'Admin credentials already updated'; + } else { + credentialsStep.querySelector('.step-status').textContent = 'Successfully updated admin credentials'; + } // Add credentials details const credentialsDetails = document.createElement('div'); @@ -641,7 +658,7 @@ async function startLaunch(data) { credentialsDetails.innerHTML = `
-
Admin Credentials Updated
+
${credentialsResult.data.already_updated ? 'Admin Credentials Status' : 'Admin Credentials Updated'}
@@ -667,12 +684,19 @@ async function startLaunch(data) { + + + +
Role Administrator
Status${credentialsResult.data.already_updated ? 'Already Updated' : 'Updated'}
-
- - Important: Please save these credentials securely. The password will not be shown again. +
+ + ${credentialsResult.data.already_updated ? 'Note:' : 'Important:'} + ${credentialsResult.data.already_updated ? + 'Admin credentials were already updated from the default settings.' : + 'Please save these credentials securely. The password will not be shown again.'}
@@ -683,9 +707,68 @@ async function startLaunch(data) { `; credentialsStep.querySelector('.step-content').appendChild(credentialsDetails); + // Step 14: Send Completion Email + await updateStep(14, 'Send Completion Email', 'Sending notification to client...'); + const emailResult = await sendCompletionEmail(`https://${data.webAddresses[0]}`, data.company, credentialsResult.data); + + if (!emailResult.success) { + throw new Error(`Email sending failed: ${emailResult.error}`); + } + + // Update the email step to show success + const emailStep = document.querySelectorAll('.step-item')[13]; + emailStep.classList.remove('active'); + emailStep.classList.add('completed'); + emailStep.querySelector('.step-status').textContent = 'Successfully sent completion email'; + + // Add email details + const emailDetails = document.createElement('div'); + emailDetails.className = 'mt-3'; + emailDetails.innerHTML = ` +
+
+
Completion Email Sent
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
Recipient${data.company.email || 'Not set'}
SubjectYour DocuPulse Instance is Ready!
StatusSent Successfully
Instance URLhttps://${data.webAddresses[0]}
+
+
+ + Launch Complete! Your DocuPulse instance has been successfully deployed and configured. + The client has been notified via email with all necessary login information. +
+
+
+ `; + emailStep.querySelector('.step-content').appendChild(emailDetails); + } catch (error) { console.error('Launch failed:', error); - await updateStep(13, 'Update Admin Credentials', `Error: ${error.message}`); + await updateStep(14, 'Send Completion Email', `Error: ${error.message}`); showError(error.message); } } @@ -1291,8 +1374,8 @@ function updateStep(stepNumber, title, description) { document.getElementById('currentStep').textContent = title; document.getElementById('stepDescription').textContent = description; - // Calculate progress based on total number of steps (13 steps total) - const totalSteps = 13; + // Calculate progress based on total number of steps (14 steps total) + const totalSteps = 14; const progress = ((stepNumber - 1) / (totalSteps - 1)) * 100; const progressBar = document.getElementById('launchProgress'); progressBar.style.width = `${progress}%`; @@ -1810,4 +1893,43 @@ async function updateAdminCredentials(instanceUrl, email) { error: error.message }; } +} + +async function sendCompletionEmail(instanceUrl, company, credentials) { + try { + console.log('Sending completion email to:', company.email); + + const response = await fetch('/api/admin/send-completion-email', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content + }, + body: JSON.stringify({ + instance_url: instanceUrl, + company_data: company, + credentials_data: credentials + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Failed to send completion email: ${errorText}`); + } + + const result = await response.json(); + console.log('Email sent successfully:', result); + + return { + success: true, + message: result.message, + data: result.data + }; + } catch (error) { + console.error('Error sending completion email:', error); + return { + success: false, + error: error.message + }; + } } \ No newline at end of file