Files
WebChecker/views/admin_users_view.py
T

299 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
views/admin_users_view.py — Admin panel: User Management tab.
"""
import tkinter as tk
from tkinter import ttk
import logging
from utils.ui_helpers import (
COLOURS, FONT, FONT_BOLD, FONT_HEADING,
show_error, show_info, confirm_delete
)
logger = logging.getLogger("admin_users_view")
class AdminUsersView(ttk.Frame):
def __init__(self, parent, current_user: dict):
super().__init__(parent)
self.current_user = current_user
self.configure(style="TFrame")
self._selected_user_id = None
self._build_ui()
self._load_users()
# ─── Layout ───────────────────────────────────────────────────────────────
def _build_ui(self):
# Top toolbar
toolbar = ttk.Frame(self)
toolbar.pack(fill="x", pady=(0, 10))
ttk.Label(toolbar, text="User Management", style="Heading.TLabel").pack(side="left")
ttk.Button(toolbar, text=" Add User",
command=self._open_add_dialog).pack(side="right", padx=(4, 0))
ttk.Button(toolbar, text="✎ Edit",
style="Ghost.TButton",
command=self._open_edit_dialog).pack(side="right", padx=(4, 0))
ttk.Button(toolbar, text="✕ Delete",
style="Danger.TButton",
command=self._delete_selected).pack(side="right")
ttk.Button(toolbar, text="🔑 Reset Password",
style="Ghost.TButton",
command=self._reset_password).pack(side="right", padx=(0, 8))
# Treeview
cols = ("ID", "Username", "Full Name", "Role", "Active", "Created")
self.tree = ttk.Treeview(self, columns=cols, show="headings", selectmode="browse")
widths = [40, 140, 180, 80, 60, 160]
for col, w in zip(cols, widths):
self.tree.heading(col, text=col)
self.tree.column(col, width=w, anchor="center" if w < 120 else "w")
self.tree.pack(fill="both", expand=True)
self.tree.bind("<Double-1>", lambda _: self._open_edit_dialog())
vsb = ttk.Scrollbar(self, orient="vertical", command=self.tree.yview)
self.tree.configure(yscrollcommand=vsb.set)
vsb.place(relx=1, rely=0, relheight=1, anchor="ne")
# ─── Data ─────────────────────────────────────────────────────────────────
def _load_users(self):
from models import get_all_users
self.tree.delete(*self.tree.get_children())
try:
for u in get_all_users():
active = "✔" if u["is_active"] else "✘"
created = str(u["created_at"])[:16] if u["created_at"] else ""
self.tree.insert("", "end", iid=str(u["id"]),
values=(u["id"], u["username"],
u["full_name"] or "", u["role"],
active, created))
except Exception as e:
show_error(f"Failed to load users:\n{e}")
def _get_selected_id(self):
sel = self.tree.selection()
return int(sel[0]) if sel else None
# ─── Dialogs ──────────────────────────────────────────────────────────────
def _reset_password(self):
"""
Generate a random temporary password for the selected user, set it in
the database, copy it to the clipboard, and log the action.
The admin must communicate the temporary password to the user out-of-band;
the user should change it immediately via Change Password.
"""
import random
import string
uid = self._get_selected_id()
if not uid:
show_error("Please select a user to reset.")
return
if uid == self.current_user["id"]:
show_error("You cannot reset your own password here.\nUse Change Password instead.")
return
vals = self.tree.item(uid, "values")
username = vals[1] if vals else str(uid)
# Confirm before proceeding
from tkinter import messagebox
if not messagebox.askyesno(
"Reset Password",
f"Generate a new temporary password for '{username}'?\n\n"
"The temporary password will be copied to your clipboard.",
icon="warning",
):
return
# Build a strong random password that meets the existing policy:
# 8+ chars, uppercase, digit, special character
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
while True:
pwd = "".join(random.SystemRandom().choices(alphabet, k=16))
if (any(c.isupper() for c in pwd)
and any(c.isdigit() for c in pwd)
and any(c in "!@#$%^&*" for c in pwd)):
break
try:
from models import update_user, get_user_by_id, log_action
user_data = get_user_by_id(uid)
if not user_data:
show_error("User not found.")
return
update_user(
self.current_user["id"],
uid,
user_data["username"],
user_data["role"],
user_data["full_name"],
user_data["is_active"],
password=pwd,
)
log_action(
self.current_user["id"], "RESET_PASSWORD", "users", uid,
f"Admin reset password for user '{username}' (id={uid})."
)
logger.info(
f"Password reset for user id={uid} '{username}' "
f"by admin '{self.current_user['username']}'."
)
# Copy to clipboard
self.clipboard_clear()
self.clipboard_append(pwd)
show_info(
f"Temporary password for '{username}' has been set and "
f"copied to your clipboard:\n\n{pwd}\n\n"
"Please share it with the user securely.\n"
"The user should change it immediately after logging in."
)
except Exception as e:
show_error(f"Password reset failed:\n{e}")
def _open_add_dialog(self):
UserDialog(self, self.current_user, user_data=None,
on_save=self._load_users)
def _open_edit_dialog(self):
uid = self._get_selected_id()
if not uid:
show_error("Please select a user to edit.")
return
from models import get_user_by_id
user_data = get_user_by_id(uid)
UserDialog(self, self.current_user, user_data=user_data,
on_save=self._load_users)
def _delete_selected(self):
uid = self._get_selected_id()
if not uid:
show_error("Please select a user to delete.")
return
if uid == self.current_user["id"]:
show_error("You cannot delete your own account.")
return
vals = self.tree.item(uid, "values")
username = vals[1] if vals else str(uid)
if confirm_delete(username):
try:
from models import delete_user
delete_user(self.current_user["id"], uid)
logger.info(f"Admin {self.current_user['username']} deleted user id={uid}.")
show_info(f"User '{username}' deleted successfully.")
self._load_users()
except Exception as e:
show_error(f"Delete failed:\n{e}")
# ─── User Dialog (Add / Edit) ─────────────────────────────────────────────────
class UserDialog(tk.Toplevel):
def __init__(self, parent, current_user, user_data, on_save):
super().__init__(parent)
self.current_user = current_user
self.user_data = user_data
self.on_save = on_save
self.is_edit = user_data is not None
self.title("Edit User" if self.is_edit else "Add User")
self.resizable(False, False)
self.configure(bg=COLOURS["bg"])
self.grab_set()
self._build_ui()
self._centre()
def _centre(self):
self.update_idletasks()
w, h = 420, 420
x = (self.winfo_screenwidth() - w) // 2
y = (self.winfo_screenheight() - h) // 2
self.geometry(f"{w}x{h}+{x}+{y}")
def _build_ui(self):
pad = {"padx": 24, "pady": 8}
ttk.Label(self,
text="Edit User" if self.is_edit else "New User",
style="Heading.TLabel").pack(anchor="w", padx=24, pady=(20, 4))
form = ttk.Frame(self)
form.pack(fill="x", padx=24, pady=8)
form.columnconfigure(1, weight=1)
def row(label, row_n, show=None):
ttk.Label(form, text=label).grid(row=row_n, column=0,
sticky="w", padx=(0, 10), pady=6)
var = tk.StringVar()
ent = ttk.Entry(form, textvariable=var, show=show or "")
ent.grid(row=row_n, column=1, sticky="ew", pady=6)
return var, ent
self.full_name_var, _ = row("Full Name", 0)
self.username_var, _ = row("Username", 1)
self.password_var, _ = row("Password", 2, show="•")
pw_hint = "(leave blank to keep unchanged)" if self.is_edit else ""
ttk.Label(form, text=pw_hint, style="Dim.TLabel").grid(
row=3, column=1, sticky="w")
ttk.Label(form, text="Role").grid(row=4, column=0, sticky="w",
padx=(0, 10), pady=6)
self.role_var = tk.StringVar(value="user")
role_cb = ttk.Combobox(form, textvariable=self.role_var,
values=["admin", "user"], state="readonly")
role_cb.grid(row=4, column=1, sticky="ew", pady=6)
ttk.Label(form, text="Active").grid(row=5, column=0, sticky="w",
padx=(0, 10), pady=6)
self.active_var = tk.BooleanVar(value=True)
ttk.Checkbutton(form, variable=self.active_var).grid(row=5, column=1,
sticky="w", pady=6)
if self.is_edit:
d = self.user_data
self.full_name_var.set(d.get("full_name") or "")
self.username_var.set(d.get("username") or "")
self.role_var.set(d.get("role") or "user")
self.active_var.set(bool(d.get("is_active", 1)))
btn_frame = ttk.Frame(self)
btn_frame.pack(fill="x", padx=24, pady=(16, 20))
ttk.Button(btn_frame, text="Save", command=self._save).pack(side="right", padx=(6, 0))
ttk.Button(btn_frame, text="Cancel", style="Ghost.TButton",
command=self.destroy).pack(side="right")
def _save(self):
full_name = self.full_name_var.get().strip()
username = self.username_var.get().strip()
password = self.password_var.get()
role = self.role_var.get()
is_active = int(self.active_var.get())
if not username:
show_error("Username is required.")
return
if not self.is_edit and not password:
show_error("Password is required for new users.")
return
try:
if self.is_edit:
from models import update_user
update_user(self.current_user["id"],
self.user_data["id"],
username, role, full_name, is_active,
password if password else None)
logger.info(f"User id={self.user_data['id']} updated by admin.")
show_info("User updated successfully.")
else:
from models import create_user
create_user(self.current_user["id"], username, password, role, full_name)
logger.info(f"New user '{username}' created by admin.")
show_info("User created successfully.")
self.on_save()
self.destroy()
except Exception as e:
show_error(f"Save failed:\n{e}")