""" 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("", 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}")