Mar 04 2026: added enable/disable user functions
This commit is contained in:
+14
-6
@@ -10,16 +10,24 @@ def load_user(user_id):
|
|||||||
class User(UserMixin, db.Model):
|
class User(UserMixin, db.Model):
|
||||||
__tablename__ = 'users'
|
__tablename__ = 'users'
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(100), unique=True, nullable=False, index=True)
|
username = db.Column(db.String(100), unique=True, nullable=False, index=True)
|
||||||
email = db.Column(db.String(255), unique=True, nullable=False, index=True)
|
email = db.Column(db.String(255), unique=True, nullable=False, index=True)
|
||||||
password_hash = db.Column(db.String(255), nullable=False)
|
password_hash = db.Column(db.String(255), nullable=False)
|
||||||
role = db.Column(db.Enum('admin', 'supervisor', 'inspector'), nullable=False)
|
role = db.Column(db.Enum('admin', 'supervisor', 'inspector'), nullable=False)
|
||||||
created_at = db.Column(db.DateTime, default=now_eastern)
|
created_at = db.Column(db.DateTime, default=now_eastern)
|
||||||
|
active = db.Column(db.Boolean, default=True, nullable=False)
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
inspections = db.relationship('Inspection', backref='inspector', lazy='dynamic')
|
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):
|
def set_password(self, password):
|
||||||
self.password_hash = generate_password_hash(password)
|
self.password_hash = generate_password_hash(password)
|
||||||
|
|
||||||
@@ -27,4 +35,4 @@ class User(UserMixin, db.Model):
|
|||||||
return check_password_hash(self.password_hash, password)
|
return check_password_hash(self.password_hash, password)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<User {self.username}>'
|
return f'<User {self.username}>'
|
||||||
@@ -39,6 +39,9 @@ def login():
|
|||||||
user = User.query.filter_by(username=form.username.data).first()
|
user = User.query.filter_by(username=form.username.data).first()
|
||||||
|
|
||||||
if user and user.check_password(form.password.data):
|
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)
|
login_user(user, remember=form.remember_me.data)
|
||||||
# Use validated next URL — never redirect blindly to request.args['next']
|
# Use validated next URL — never redirect blindly to request.args['next']
|
||||||
next_page = _safe_next(request.args.get('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)
|
log_action(ACTION_DELETE, 'User', user_id, username)
|
||||||
flash(f'User {username} deleted successfully.', 'success')
|
flash(f'User {username} deleted successfully.', 'success')
|
||||||
return redirect(url_for('auth.list_users'))
|
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>
|
<h4 class="mb-0">{{ title }}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<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 method="POST">
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
@@ -62,6 +68,33 @@
|
|||||||
{{ form.role(class="form-select") }}
|
{{ form.role(class="form-select") }}
|
||||||
</div>
|
</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">
|
<div class="d-flex gap-2">
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
<i class="bi bi-save"></i> Save User
|
<i class="bi bi-save"></i> Save User
|
||||||
|
|||||||
@@ -24,12 +24,13 @@
|
|||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
<th>Role</th>
|
<th>Role</th>
|
||||||
<th>Created</th>
|
<th>Created</th>
|
||||||
<th width="150">Actions</th>
|
<th>Status</th>
|
||||||
|
<th width="180">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<tr>
|
<tr class="{{ 'table-secondary text-muted' if not user.active else '' }}">
|
||||||
<td><strong>{{ user.username }}</strong></td>
|
<td><strong>{{ user.username }}</strong></td>
|
||||||
<td>{{ user.email }}</td>
|
<td>{{ user.email }}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -39,12 +40,29 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>{{ user.created_at.strftime('%Y-%m-%d') }}</td>
|
<td>{{ user.created_at.strftime('%Y-%m-%d') }}</td>
|
||||||
<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>
|
<i class="bi bi-pencil"></i>
|
||||||
</a>
|
</a>
|
||||||
{% if user.id != current_user.id %}
|
{% 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?');">
|
<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>
|
<i class="bi bi-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user