04/23 Enhance app functionalities 2
This commit is contained in:
+171
-10
@@ -6,7 +6,7 @@ import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import logging
|
||||
from utils.ui_helpers import (
|
||||
COLOURS, FONT, FONT_BOLD, FONT_HEADING,
|
||||
COLOURS, FONT, FONT_BOLD, FONT_HEADING, FONT_SMALL,
|
||||
show_error, show_info, confirm_delete
|
||||
)
|
||||
|
||||
@@ -143,15 +143,9 @@ class AdminUsersView(ttk.Frame):
|
||||
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."
|
||||
)
|
||||
# Show the temporary password in a purpose-built secure dialog
|
||||
# instead of show_info (which leaves the password visible indefinitely)
|
||||
_PasswordResetDialog(self, username=username, password=pwd)
|
||||
except Exception as e:
|
||||
show_error(f"Password reset failed:\n{e}")
|
||||
|
||||
@@ -296,3 +290,170 @@ class UserDialog(tk.Toplevel):
|
||||
self.destroy()
|
||||
except Exception as e:
|
||||
show_error(f"Save failed:\n{e}")
|
||||
|
||||
|
||||
# ─── Secure Password Reset Dialog ─────────────────────────────────────────────
|
||||
|
||||
class _PasswordResetDialog(tk.Toplevel):
|
||||
"""
|
||||
Purpose-built dialog for displaying a freshly-generated temporary password.
|
||||
|
||||
Features:
|
||||
- Password field is masked by default; admin can reveal with 👁 toggle.
|
||||
- 📋 Copy button copies to clipboard with a 2-second "Copied!" flash.
|
||||
- Clipboard is auto-cleared after 30 seconds for security.
|
||||
- Dialog auto-closes after 120 seconds with a live countdown so the
|
||||
password cannot sit on screen indefinitely.
|
||||
"""
|
||||
|
||||
_AUTO_CLOSE_S = 120 # seconds before auto-close
|
||||
_CLIP_CLEAR_S = 30 # seconds before clipboard is wiped
|
||||
|
||||
def __init__(self, parent, username: str, password: str):
|
||||
super().__init__(parent)
|
||||
self._password = password
|
||||
self._username = username
|
||||
self._remaining = self._AUTO_CLOSE_S
|
||||
self._clip_job = None
|
||||
self._tick_job = None
|
||||
self._revealed = False
|
||||
|
||||
self.title("Temporary Password")
|
||||
self.configure(bg=COLOURS["bg"])
|
||||
self.resizable(False, False)
|
||||
self.grab_set()
|
||||
self.protocol("WM_DELETE_WINDOW", self._close)
|
||||
self._build_ui()
|
||||
self._centre()
|
||||
self._tick()
|
||||
|
||||
def _centre(self):
|
||||
self.update_idletasks()
|
||||
w, h = 480, 300
|
||||
x = (self.winfo_screenwidth() - w) // 2
|
||||
y = (self.winfo_screenheight() - h) // 2
|
||||
self.geometry(f"{w}x{h}+{x}+{y}")
|
||||
|
||||
def _build_ui(self):
|
||||
C = COLOURS
|
||||
pad = dict(padx=24, pady=8)
|
||||
|
||||
tk.Label(self, text="🔑 Password Reset",
|
||||
bg=C["bg"], fg=C["text"],
|
||||
font=FONT_BOLD).pack(anchor="w", **pad)
|
||||
|
||||
tk.Label(
|
||||
self,
|
||||
text=f"A temporary password has been generated for '{self._username}'.\n"
|
||||
"Share it securely — the user must change it on first login.",
|
||||
bg=C["bg"], fg=C["text_dim"],
|
||||
font=FONT_SMALL, justify="left", wraplength=430,
|
||||
).pack(anchor="w", padx=24, pady=(0, 8))
|
||||
|
||||
# Password row
|
||||
pw_frame = tk.Frame(self, bg=C["surface2"], padx=8, pady=8)
|
||||
pw_frame.pack(fill="x", padx=24, pady=(0, 8))
|
||||
|
||||
self._pw_var = tk.StringVar(value=self._password)
|
||||
self._pw_entry = tk.Entry(
|
||||
pw_frame, textvariable=self._pw_var,
|
||||
show="•", state="readonly",
|
||||
readonlybackground=C["surface2"], fg=C["text"],
|
||||
relief="flat", font=(FONT_BOLD[0], 14),
|
||||
)
|
||||
self._pw_entry.pack(side="left", fill="x", expand=True)
|
||||
|
||||
tk.Button(
|
||||
pw_frame, text="👁",
|
||||
command=self._toggle_reveal,
|
||||
bg=C["surface2"], fg=C["text_dim"],
|
||||
activebackground=C["surface"], activeforeground=C["text"],
|
||||
relief="flat", font=FONT_SMALL, cursor="hand2", padx=6,
|
||||
).pack(side="left", padx=(6, 0))
|
||||
|
||||
self._copy_btn = tk.Button(
|
||||
pw_frame, text="📋 Copy",
|
||||
command=self._copy,
|
||||
bg=C["accent"], fg=C["white"],
|
||||
activebackground=C["accent_hover"], activeforeground=C["white"],
|
||||
relief="flat", font=FONT_SMALL, cursor="hand2",
|
||||
padx=10, pady=4,
|
||||
)
|
||||
self._copy_btn.pack(side="left", padx=(6, 0))
|
||||
|
||||
# Countdown label
|
||||
self._countdown_lbl = tk.Label(
|
||||
self, text="",
|
||||
bg=C["bg"], fg=C["text_dim"], font=FONT_SMALL,
|
||||
)
|
||||
self._countdown_lbl.pack(pady=(0, 4))
|
||||
|
||||
# Warning
|
||||
tk.Label(
|
||||
self,
|
||||
text="⚠ This dialog will close automatically. "
|
||||
"The clipboard is cleared after 30 seconds.",
|
||||
bg=C["bg"], fg=C["warning"],
|
||||
font=FONT_SMALL, wraplength=430, justify="left",
|
||||
).pack(anchor="w", padx=24, pady=(0, 8))
|
||||
|
||||
# Close button
|
||||
tk.Button(
|
||||
self, text="Close",
|
||||
command=self._close,
|
||||
bg=C["surface2"], fg=C["text"],
|
||||
activebackground=C["danger"], activeforeground=C["white"],
|
||||
relief="flat", font=FONT_SMALL, cursor="hand2",
|
||||
padx=12, pady=6,
|
||||
).pack(pady=(0, 16))
|
||||
|
||||
def _toggle_reveal(self):
|
||||
self._revealed = not self._revealed
|
||||
self._pw_entry.config(show="" if self._revealed else "•")
|
||||
|
||||
def _copy(self):
|
||||
self.clipboard_clear()
|
||||
self.clipboard_append(self._password)
|
||||
self._copy_btn.config(text="✔ Copied!", bg=COLOURS["success"])
|
||||
self.after(2000, lambda: self._copy_btn.config(
|
||||
text="📋 Copy", bg=COLOURS["accent"]))
|
||||
# Schedule clipboard wipe
|
||||
if self._clip_job:
|
||||
try:
|
||||
self.after_cancel(self._clip_job)
|
||||
except Exception:
|
||||
pass
|
||||
self._clip_job = self.after(
|
||||
self._CLIP_CLEAR_S * 1000, self._clear_clipboard)
|
||||
|
||||
def _clear_clipboard(self):
|
||||
try:
|
||||
self.clipboard_clear()
|
||||
self.clipboard_append("")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _tick(self):
|
||||
if not self.winfo_exists():
|
||||
return
|
||||
self._countdown_lbl.config(
|
||||
text=f"Auto-closes in {self._remaining}s")
|
||||
if self._remaining <= 0:
|
||||
self._close()
|
||||
return
|
||||
self._remaining -= 1
|
||||
self._tick_job = self.after(1000, self._tick)
|
||||
|
||||
def _close(self):
|
||||
# Cancel any pending jobs
|
||||
for job in (self._clip_job, self._tick_job):
|
||||
if job:
|
||||
try:
|
||||
self.after_cancel(job)
|
||||
except Exception:
|
||||
pass
|
||||
self._clear_clipboard()
|
||||
try:
|
||||
self.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
Reference in New Issue
Block a user