password reset online test

This commit is contained in:
2025-06-20 13:18:13 +02:00
parent e25c7660b0
commit c9d1d7416b
3 changed files with 513 additions and 65 deletions

View File

@@ -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
@@ -527,3 +527,36 @@ def resend_setup_mail(current_user, user_id):
db.session.commit()
return jsonify({'message': 'Setup mail queued for resending'})
# Generate Password Reset Token
@admin_api.route('/generate-password-reset/<int:user_id>', 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
})

View File

@@ -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,6 +1128,33 @@ def update_admin_credentials():
admin_user_id = admin_user.get('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}",
@@ -1188,6 +1186,301 @@ def update_admin_credentials():
}
})
# 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={
'X-API-Key': instance.connection_token,
'Accept': 'application/json'
},
timeout=10
)
if jwt_response.status_code != 200:
return jsonify({'error': f'Failed to get JWT token: {jwt_response.text}'}), 400
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"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.header {{ background-color: #16767b; color: white; padding: 20px; text-align: center; border-radius: 5px 5px 0 0; }}
.content {{ background-color: #f9f9f9; padding: 20px; border-radius: 0 0 5px 5px; }}
.credentials {{ background-color: #e8f5e9; padding: 15px; border-radius: 5px; margin: 15px 0; }}
.button {{ display: inline-block; background-color: #16767b; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; margin: 10px 0; }}
.footer {{ margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd; font-size: 12px; color: #666; }}
.info-box {{ background-color: #e3f2fd; padding: 15px; border-radius: 5px; margin: 15px 0; }}
.security-notice {{ background-color: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 5px; margin: 15px 0; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎉 Your DocuPulse Instance is Ready!</h1>
</div>
<div class="content">
<p>Dear {company_data.get('name', 'Valued Customer')},</p>
<p>Great news! Your DocuPulse instance has been successfully deployed and configured.
You can now access your secure document management platform.</p>
<div class="info-box">
<h3>📋 Instance Details</h3>
<p><strong>Instance URL:</strong> <a href="{instance_url}">{instance_url}</a></p>
<p><strong>Company Name:</strong> {company_data.get('name', 'Not set')}</p>
<p><strong>Industry:</strong> {company_data.get('industry', 'Not set')}</p>
<p><strong>Deployment Date:</strong> {datetime.utcnow().strftime('%B %d, %Y at %I:%M %p UTC')}</p>
</div>
<div class="credentials">
<h3>🔐 Account Access</h3>
<p><strong>Email Address:</strong> {credentials_data.get('email', 'Not set')}</p>
<p><strong>Username:</strong> {credentials_data.get('username', 'administrator')}</p>
<div class="security-notice">
<h4>🔒 Security Setup Required</h4>
<p>For your security, you need to set up your password. Click the button below to create your secure password.</p>
<p><strong>Password Reset Link Expires:</strong> {expires_at}</p>
</div>
</div>
<div style="text-align: center; margin: 30px 0;">
<a href="{reset_url}" class="button">🔐 Set Up Your Password</a>
<br><br>
<a href="{instance_url}" class="button">🚀 Access Your Instance</a>
</div>
<h3>✅ What's Been Configured</h3>
<ul>
<li>✅ Secure SSL certificate for HTTPS access</li>
<li>✅ Company information and branding</li>
<li>✅ Custom color scheme</li>
<li>✅ Admin account created</li>
<li>✅ Document management system ready</li>
</ul>
<h3>🎯 Next Steps</h3>
<ol>
<li>Click the "Set Up Your Password" button above</li>
<li>Create your secure password</li>
<li>Return to your instance and log in</li>
<li>Explore your new DocuPulse platform</li>
<li>Start uploading and organizing your documents</li>
<li>Invite team members to collaborate</li>
</ol>
<div class="footer">
<p>If you have any questions or need assistance, please don't hesitate to contact our support team.</p>
<p>Thank you for choosing DocuPulse!</p>
</div>
</div>
</div>
</body>
</html>
"""
# 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 sending completion email: {str(e)}")
return jsonify({'error': str(e)}), 500

View File

@@ -171,6 +171,18 @@ function initializeSteps() {
</div>
`;
stepsContainer.appendChild(credentialsStep);
// Add Send Completion Email step
const emailStep = document.createElement('div');
emailStep.className = 'step-item';
emailStep.innerHTML = `
<div class="step-icon"><i class="fas fa-envelope"></i></div>
<div class="step-content">
<h5>Send Completion Email</h5>
<p class="step-status">Sending notification to client...</p>
</div>
`;
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');
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 = `
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">Admin Credentials Updated</h6>
<h6 class="card-title mb-3">${credentialsResult.data.already_updated ? 'Admin Credentials Status' : 'Admin Credentials Updated'}</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
@@ -667,12 +684,19 @@ async function startLaunch(data) {
<td>Role</td>
<td><span class="badge bg-primary">Administrator</span></td>
</tr>
<tr>
<td>Status</td>
<td><span class="badge bg-${credentialsResult.data.already_updated ? 'info' : 'success'}">${credentialsResult.data.already_updated ? 'Already Updated' : 'Updated'}</span></td>
</tr>
</tbody>
</table>
</div>
<div class="alert alert-warning mt-3">
<i class="fas fa-exclamation-triangle me-2"></i>
<strong>Important:</strong> Please save these credentials securely. The password will not be shown again.
<div class="alert alert-${credentialsResult.data.already_updated ? 'info' : 'warning'} mt-3">
<i class="fas fa-${credentialsResult.data.already_updated ? 'info-circle' : 'exclamation-triangle'} me-2"></i>
<strong>${credentialsResult.data.already_updated ? 'Note:' : 'Important:'}</strong>
${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.'}
</div>
<div class="alert alert-info mt-2">
<i class="fas fa-info-circle me-2"></i>
@@ -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 = `
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">Completion Email Sent</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Recipient</td>
<td>${data.company.email || 'Not set'}</td>
</tr>
<tr>
<td>Subject</td>
<td>Your DocuPulse Instance is Ready!</td>
</tr>
<tr>
<td>Status</td>
<td><span class="badge bg-success">Sent Successfully</span></td>
</tr>
<tr>
<td>Instance URL</td>
<td><a href="https://${data.webAddresses[0]}" target="_blank">https://${data.webAddresses[0]}</a></td>
</tr>
</tbody>
</table>
</div>
<div class="alert alert-success mt-3">
<i class="fas fa-check-circle me-2"></i>
<strong>Launch Complete!</strong> Your DocuPulse instance has been successfully deployed and configured.
The client has been notified via email with all necessary login information.
</div>
</div>
</div>
`;
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}%`;
@@ -1811,3 +1894,42 @@ async function updateAdminCredentials(instanceUrl, email) {
};
}
}
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
};
}
}