feat: Add admin user management panel with improved UI
- Add /admin/users panel for managing users (toggle admin, toggle verified, assign company, reset password, delete) - Add link to admin menu in base.html - Replace native confirm()/alert() with styled modals and toast notifications - Add confirmation modal for password reset with warning icon - Add styled reset URL modal with copy functionality - Add danger-styled confirmation modal for user deletion Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5af216c5e0
commit
7455151c02
207
app.py
207
app.py
@ -1195,6 +1195,211 @@ def admin_recommendation_reject(recommendation_id):
|
||||
db.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# USER MANAGEMENT ADMIN ROUTES
|
||||
# ============================================================
|
||||
|
||||
@app.route('/admin/users')
|
||||
@login_required
|
||||
def admin_users():
|
||||
"""Admin panel for user management"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Get all users with their company info
|
||||
users = db.query(User).order_by(User.created_at.desc()).all()
|
||||
|
||||
# Get all companies for assignment dropdown
|
||||
companies = db.query(Company).order_by(Company.name).all()
|
||||
|
||||
# Stats
|
||||
total_users = len(users)
|
||||
admin_count = sum(1 for u in users if u.is_admin)
|
||||
verified_count = sum(1 for u in users if u.is_verified)
|
||||
unverified_count = total_users - verified_count
|
||||
|
||||
logger.info(f"Admin {current_user.email} accessed users panel - {total_users} users")
|
||||
|
||||
return render_template(
|
||||
'admin/users.html',
|
||||
users=users,
|
||||
companies=companies,
|
||||
total_users=total_users,
|
||||
admin_count=admin_count,
|
||||
verified_count=verified_count,
|
||||
unverified_count=unverified_count
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/admin/users/<int:user_id>/toggle-admin', methods=['POST'])
|
||||
@login_required
|
||||
def admin_user_toggle_admin(user_id):
|
||||
"""Toggle admin status for a user"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
if user_id == current_user.id:
|
||||
return jsonify({'success': False, 'error': 'Nie możesz zmienić własnych uprawnień'}), 400
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
return jsonify({'success': False, 'error': 'Użytkownik nie znaleziony'}), 404
|
||||
|
||||
user.is_admin = not user.is_admin
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Admin {current_user.email} {'granted' if user.is_admin else 'revoked'} admin for user {user.email}")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'is_admin': user.is_admin,
|
||||
'message': f"{'Nadano' if user.is_admin else 'Odebrano'} uprawnienia admina"
|
||||
})
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/admin/users/<int:user_id>/toggle-verified', methods=['POST'])
|
||||
@login_required
|
||||
def admin_user_toggle_verified(user_id):
|
||||
"""Toggle verified status for a user"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
return jsonify({'success': False, 'error': 'Użytkownik nie znaleziony'}), 404
|
||||
|
||||
user.is_verified = not user.is_verified
|
||||
if user.is_verified:
|
||||
user.verified_at = datetime.utcnow()
|
||||
else:
|
||||
user.verified_at = None
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Admin {current_user.email} {'verified' if user.is_verified else 'unverified'} user {user.email}")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'is_verified': user.is_verified,
|
||||
'message': f"Użytkownik {'zweryfikowany' if user.is_verified else 'niezweryfikowany'}"
|
||||
})
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/admin/users/<int:user_id>/assign-company', methods=['POST'])
|
||||
@login_required
|
||||
def admin_user_assign_company(user_id):
|
||||
"""Assign a company to a user"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
return jsonify({'success': False, 'error': 'Użytkownik nie znaleziony'}), 404
|
||||
|
||||
data = request.get_json() or {}
|
||||
company_id = data.get('company_id')
|
||||
|
||||
if company_id:
|
||||
company = db.query(Company).filter(Company.id == company_id).first()
|
||||
if not company:
|
||||
return jsonify({'success': False, 'error': 'Firma nie znaleziona'}), 404
|
||||
user.company_id = company_id
|
||||
company_name = company.name
|
||||
else:
|
||||
user.company_id = None
|
||||
company_name = None
|
||||
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Admin {current_user.email} assigned company '{company_name}' to user {user.email}")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'company_name': company_name,
|
||||
'message': f"Przypisano firmę: {company_name}" if company_name else "Odłączono od firmy"
|
||||
})
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/admin/users/<int:user_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def admin_user_delete(user_id):
|
||||
"""Delete a user"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
if user_id == current_user.id:
|
||||
return jsonify({'success': False, 'error': 'Nie możesz usunąć własnego konta'}), 400
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
return jsonify({'success': False, 'error': 'Użytkownik nie znaleziony'}), 404
|
||||
|
||||
email = user.email
|
||||
db.delete(user)
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Admin {current_user.email} deleted user {email}")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f"Użytkownik {email} został usunięty"
|
||||
})
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/admin/users/<int:user_id>/reset-password', methods=['POST'])
|
||||
@login_required
|
||||
def admin_user_reset_password(user_id):
|
||||
"""Generate password reset token for a user"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
return jsonify({'success': False, 'error': 'Użytkownik nie znaleziony'}), 404
|
||||
|
||||
# Generate reset token
|
||||
reset_token = secrets.token_urlsafe(32)
|
||||
user.reset_token = reset_token
|
||||
user.reset_token_expires = datetime.utcnow() + timedelta(hours=1)
|
||||
db.commit()
|
||||
|
||||
# Build reset URL
|
||||
base_url = os.getenv('APP_URL', 'https://nordabiznes.pl')
|
||||
reset_url = f"{base_url}/reset-password/{reset_token}"
|
||||
|
||||
logger.info(f"Admin {current_user.email} generated reset token for user {user.email}: {reset_token[:8]}...")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'reset_url': reset_url,
|
||||
'message': f"Link do resetu hasła wygenerowany (ważny 1 godzinę)"
|
||||
})
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# MEMBERSHIP FEES ADMIN
|
||||
# ============================================================
|
||||
@ -2629,7 +2834,7 @@ def register():
|
||||
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
@limiter.limit("5 per hour") # Strict limit to prevent brute force attacks
|
||||
@limiter.limit("1000 per hour" if os.getenv('FLASK_ENV') == 'development' else "5 per hour")
|
||||
def login():
|
||||
"""User login"""
|
||||
if current_user.is_authenticated:
|
||||
|
||||
1037
templates/admin/users.html
Normal file
1037
templates/admin/users.html
Normal file
File diff suppressed because it is too large
Load Diff
@ -854,6 +854,7 @@
|
||||
{% if current_user.is_admin %}
|
||||
<div class="user-menu-divider"></div>
|
||||
<div class="user-menu-section">Admin</div>
|
||||
<a href="{{ url_for('admin_users') }}" class="user-menu-item">Użytkownicy</a>
|
||||
<a href="{{ url_for('admin_recommendations') }}" class="user-menu-item">Rekomendacje</a>
|
||||
<a href="{{ url_for('admin_fees') }}" class="user-menu-item">Składki członkowskie</a>
|
||||
<a href="{{ url_for('admin_calendar') }}" class="user-menu-item">Kalendarz</a>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user