feat: add password reset button in User Insights for batch-imported accounts
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Adds a "Send reset" action button in the Problems tab and user profile page, allowing admins to send password reset emails directly from User Insights dashboard. Each reset requires manual confirmation via dialog. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
774ce6fca6
commit
80b9bf1fea
@ -10,10 +10,12 @@ import csv
|
||||
import io
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import secrets
|
||||
from datetime import date, timedelta, datetime
|
||||
|
||||
from flask import render_template, request, redirect, url_for, flash, Response
|
||||
from flask_login import login_required
|
||||
from flask import render_template, request, redirect, url_for, flash, Response, jsonify
|
||||
from flask_login import login_required, current_user
|
||||
from sqlalchemy import func, desc, text, or_
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
@ -1584,3 +1586,70 @@ def user_insights_export():
|
||||
return redirect(url_for('admin.user_insights'))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@bp.route('/user-insights/send-reset/<int:user_id>', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def user_insights_send_reset(user_id):
|
||||
"""Send password reset email to user from User Insights."""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
user = db.query(User).get(user_id)
|
||||
if not user or not user.is_active:
|
||||
return jsonify({'error': 'Użytkownik nie znaleziony lub nieaktywny'}), 404
|
||||
|
||||
# Generate reset token (24h validity)
|
||||
token = secrets.token_urlsafe(32)
|
||||
user.reset_token = token
|
||||
user.reset_token_expires = datetime.now() + timedelta(hours=24)
|
||||
db.commit()
|
||||
|
||||
# Build reset URL
|
||||
base_url = os.getenv('APP_URL', 'https://nordabiznes.pl')
|
||||
reset_url = f"{base_url}/reset-password/{token}"
|
||||
|
||||
# Send email
|
||||
import email_service
|
||||
if email_service.is_configured():
|
||||
success = email_service.send_password_reset_email(user.email, reset_url)
|
||||
else:
|
||||
return jsonify({'error': 'Email service nie skonfigurowany'}), 500
|
||||
|
||||
if not success:
|
||||
return jsonify({'error': 'Błąd wysyłki emaila'}), 500
|
||||
|
||||
# Log to email_logs
|
||||
email_log = EmailLog(
|
||||
recipient_email=user.email,
|
||||
recipient_name=user.name,
|
||||
email_type='password_reset',
|
||||
subject='Reset hasła - Norda Biznes Partner',
|
||||
status='sent',
|
||||
user_id=user.id,
|
||||
sender_email=current_user.email,
|
||||
)
|
||||
db.add(email_log)
|
||||
|
||||
# Audit log
|
||||
audit = AuditLog(
|
||||
user_id=current_user.id,
|
||||
user_email=current_user.email,
|
||||
action='user.password_reset_sent',
|
||||
entity_type='user',
|
||||
entity_id=user.id,
|
||||
entity_name=user.email,
|
||||
ip_address=request.remote_addr,
|
||||
request_path=request.path,
|
||||
)
|
||||
db.add(audit)
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Admin {current_user.email} sent password reset to {user.email} (user {user.id})")
|
||||
return jsonify({'success': True, 'message': f'Reset wysłany do {user.email}'})
|
||||
except Exception as e:
|
||||
logger.error(f"Send reset error for user {user_id}: {e}")
|
||||
db.rollback()
|
||||
return jsonify({'error': 'Błąd wysyłki'}), 500
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@ -139,6 +139,12 @@
|
||||
details[open] summary span:first-child { transform: rotate(90deg); }
|
||||
details summary::-webkit-details-marker { display: none; }
|
||||
|
||||
/* Reset password button */
|
||||
.btn-reset-pw { padding: 4px 10px; background: white; border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: var(--font-size-xs); cursor: pointer; white-space: nowrap; transition: var(--transition); }
|
||||
.btn-reset-pw:hover { background: var(--background); border-color: var(--primary); }
|
||||
.btn-reset-pw.sent { background: #dcfce7; color: #166534; border-color: #86efac; cursor: default; }
|
||||
.btn-reset-pw.error { background: #fef2f2; color: #991b1b; border-color: #fca5a5; }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.insights-header { flex-direction: column; align-items: flex-start; }
|
||||
@ -332,6 +338,7 @@
|
||||
<th>Wolne strony</th>
|
||||
<th>🔒</th>
|
||||
<th>Ostatni login</th>
|
||||
<th>Akcja</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -355,6 +362,9 @@
|
||||
<td>{{ p.slow_pages }}</td>
|
||||
<td>{% if p.is_locked %}<span class="badge badge-critical">Tak</span>{% endif %}</td>
|
||||
<td>{{ p.last_login.strftime('%d.%m %H:%M') if p.last_login else 'Nigdy' }}</td>
|
||||
<td>
|
||||
<button class="btn-reset-pw" data-uid="{{ p.user.id }}" data-name="{{ p.user.name or p.user.email }}" data-email="{{ p.user.email }}">📧 Reset</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@ -872,4 +882,37 @@
|
||||
}
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
// Password reset buttons
|
||||
document.querySelectorAll('.btn-reset-pw').forEach(function(btn) {
|
||||
btn.addEventListener('click', async function() {
|
||||
const uid = this.dataset.uid;
|
||||
const name = this.dataset.name;
|
||||
const email = this.dataset.email;
|
||||
if (!confirm('Wysłać reset hasła do ' + name + ' (' + email + ')?')) return;
|
||||
|
||||
this.disabled = true;
|
||||
this.textContent = '⏳...';
|
||||
try {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
const resp = await fetch('/admin/user-insights/send-reset/' + uid, {
|
||||
method: 'POST',
|
||||
headers: {'X-CSRFToken': csrfToken}
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
this.textContent = '✓ Wysłano';
|
||||
this.classList.add('sent');
|
||||
} else {
|
||||
this.textContent = '✗ ' + (data.error || 'Błąd');
|
||||
this.classList.add('error');
|
||||
this.disabled = false;
|
||||
}
|
||||
} catch (e) {
|
||||
this.textContent = '✗ Błąd';
|
||||
this.classList.add('error');
|
||||
this.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
{% endblock %}
|
||||
|
||||
@ -110,6 +110,12 @@
|
||||
/* Chart */
|
||||
.chart-container { position: relative; height: 250px; }
|
||||
|
||||
/* Reset password button */
|
||||
.btn-reset-pw { padding: 4px 10px; background: white; border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: var(--font-size-xs); cursor: pointer; white-space: nowrap; transition: var(--transition); vertical-align: middle; }
|
||||
.btn-reset-pw:hover { background: var(--background); border-color: var(--primary); }
|
||||
.btn-reset-pw.sent { background: #dcfce7; color: #166534; border-color: #86efac; cursor: default; }
|
||||
.btn-reset-pw.error { background: #fef2f2; color: #991b1b; border-color: #fca5a5; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.profile-header { flex-direction: column; }
|
||||
.gauges-row { flex-direction: row; gap: var(--spacing-md); }
|
||||
@ -129,7 +135,10 @@
|
||||
<div class="profile-header">
|
||||
<div class="profile-info">
|
||||
<div class="profile-name">{{ user.name or 'Bez nazwy' }}</div>
|
||||
<div class="profile-email">{{ user.email }}</div>
|
||||
<div class="profile-email">
|
||||
{{ user.email }}
|
||||
<button class="btn-reset-pw" data-uid="{{ user.id }}" data-name="{{ user.name or user.email }}" data-email="{{ user.email }}" style="margin-left: 12px;">📧 Wyślij reset hasła</button>
|
||||
</div>
|
||||
<div class="profile-meta">
|
||||
{% if user.company %}
|
||||
<div class="profile-meta-item">
|
||||
@ -435,4 +444,37 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Password reset button
|
||||
document.querySelectorAll('.btn-reset-pw').forEach(function(btn) {
|
||||
btn.addEventListener('click', async function() {
|
||||
const uid = this.dataset.uid;
|
||||
const name = this.dataset.name;
|
||||
const email = this.dataset.email;
|
||||
if (!confirm('Wysłać reset hasła do ' + name + ' (' + email + ')?')) return;
|
||||
|
||||
this.disabled = true;
|
||||
this.textContent = '⏳...';
|
||||
try {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
const resp = await fetch('/admin/user-insights/send-reset/' + uid, {
|
||||
method: 'POST',
|
||||
headers: {'X-CSRFToken': csrfToken}
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
this.textContent = '✓ Wysłano';
|
||||
this.classList.add('sent');
|
||||
} else {
|
||||
this.textContent = '✗ ' + (data.error || 'Błąd');
|
||||
this.classList.add('error');
|
||||
this.disabled = false;
|
||||
}
|
||||
} catch (e) {
|
||||
this.textContent = '✗ Błąd';
|
||||
this.classList.add('error');
|
||||
this.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
{% endblock %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user