Mar 04 2026: added enable/disable user functions
This commit is contained in:
@@ -16,10 +16,18 @@ class User(UserMixin, db.Model):
|
||||
password_hash = db.Column(db.String(255), nullable=False)
|
||||
role = db.Column(db.Enum('admin', 'supervisor', 'inspector'), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=now_eastern)
|
||||
active = db.Column(db.Boolean, default=True, nullable=False)
|
||||
|
||||
# Relationships
|
||||
inspections = db.relationship('Inspection', backref='inspector', lazy='dynamic')
|
||||
|
||||
# ── Flask-Login integration ────────────────────────────────────────────
|
||||
# Override UserMixin.is_active so that disabled accounts are rejected
|
||||
# automatically by login_required and login_user() without any extra code.
|
||||
@property
|
||||
def is_active(self):
|
||||
return self.active
|
||||
|
||||
def set_password(self, password):
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@ def login():
|
||||
user = User.query.filter_by(username=form.username.data).first()
|
||||
|
||||
if user and user.check_password(form.password.data):
|
||||
if not user.active:
|
||||
flash('Your account has been disabled. Please contact an administrator.', 'danger')
|
||||
return render_template('auth/login.html', form=form)
|
||||
login_user(user, remember=form.remember_me.data)
|
||||
# Use validated next URL — never redirect blindly to request.args['next']
|
||||
next_page = _safe_next(request.args.get('next'))
|
||||
@@ -203,3 +206,28 @@ def delete_user(user_id):
|
||||
log_action(ACTION_DELETE, 'User', user_id, username)
|
||||
flash(f'User {username} deleted successfully.', 'success')
|
||||
return redirect(url_for('auth.list_users'))
|
||||
|
||||
@bp.route('/users/<int:user_id>/toggle-active', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def toggle_active(user_id):
|
||||
user = User.query.get_or_404(user_id)
|
||||
|
||||
if user.id == current_user.id:
|
||||
flash('You cannot disable your own account.', 'danger')
|
||||
return redirect(url_for('auth.list_users'))
|
||||
|
||||
user.active = not user.active
|
||||
db.session.commit()
|
||||
|
||||
action_label = 'enabled' if user.active else 'disabled'
|
||||
logger.info(
|
||||
'AUTH | user_%s | admin_id=%s admin=%s target_user=%s',
|
||||
action_label, current_user.id, current_user.username, user.username,
|
||||
)
|
||||
log_action(
|
||||
ACTION_UPDATE, 'User', user.id, user.username,
|
||||
f'account {action_label} by {current_user.username}',
|
||||
)
|
||||
flash(f'User {user.username} has been {action_label}.', 'success')
|
||||
return redirect(request.referrer or url_for('auth.list_users'))
|
||||
@@ -10,6 +10,12 @@
|
||||
<h4 class="mb-0">{{ title }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if user and not user.active %}
|
||||
<div class="alert alert-warning d-flex align-items-center gap-2 mb-4">
|
||||
<i class="bi bi-person-slash fs-5 flex-shrink-0"></i>
|
||||
<span>This account is currently <strong>disabled</strong>. The user cannot log in.</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form method="POST">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
@@ -62,6 +68,33 @@
|
||||
{{ form.role(class="form-select") }}
|
||||
</div>
|
||||
|
||||
{% if user and user.id != current_user.id %}
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-semibold">Account Status</label>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<span class="badge fs-6 bg-{{ 'success' if user.active else 'secondary' }}">
|
||||
{{ 'Active' if user.active else 'Disabled' }}
|
||||
</span>
|
||||
<form method="POST" action="{{ url_for('auth.toggle_active', user_id=user.id) }}" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit"
|
||||
class="btn btn-sm {{ 'btn-outline-secondary' if user.active else 'btn-outline-success' }}"
|
||||
onclick="return confirm('{{ 'Disable' if user.active else 'Enable' }} user {{ user.username }}?')">
|
||||
<i class="bi bi-{{ 'person-slash' if user.active else 'person-check' }} me-1"></i>
|
||||
{{ 'Disable Account' if user.active else 'Enable Account' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
{% if user.active %}
|
||||
Disabling this account will immediately prevent the user from logging in.
|
||||
{% else %}
|
||||
Enabling this account will restore the user's ability to log in.
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-save"></i> Save User
|
||||
|
||||
@@ -24,12 +24,13 @@
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Created</th>
|
||||
<th width="150">Actions</th>
|
||||
<th>Status</th>
|
||||
<th width="180">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<tr class="{{ 'table-secondary text-muted' if not user.active else '' }}">
|
||||
<td><strong>{{ user.username }}</strong></td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>
|
||||
@@ -39,12 +40,29 @@
|
||||
</td>
|
||||
<td>{{ user.created_at.strftime('%Y-%m-%d') }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('auth.edit_user', user_id=user.id) }}" class="btn btn-sm btn-outline-primary">
|
||||
{% if user.active %}
|
||||
<span class="badge bg-success">Active</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Disabled</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('auth.edit_user', user_id=user.id) }}" class="btn btn-sm btn-outline-primary" title="Edit">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
{% if user.id != current_user.id %}
|
||||
<form method="POST" action="{{ url_for('auth.toggle_active', user_id=user.id) }}" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit"
|
||||
class="btn btn-sm {{ 'btn-outline-secondary' if user.active else 'btn-outline-success' }}"
|
||||
title="{{ 'Disable' if user.active else 'Enable' }}"
|
||||
onclick="return confirm('{{ 'Disable' if user.active else 'Enable' }} user {{ user.username }}?')">
|
||||
<i class="bi bi-{{ 'person-slash' if user.active else 'person-check' }}"></i>
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="{{ url_for('auth.delete_user', user_id=user.id) }}" class="d-inline" onsubmit="return confirm('Delete this user?');">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user