Загрузка данных
# Dozor/modules/administrator/user_logic.py
import os
import sqlite3
import hashlib
import secrets
class UserLogic:
def __init__(self, db_path="db/users.db"):
self.db_path = db_path
if not os.path.exists(os.path.dirname(db_path)):
os.makedirs(os.path.dirname(db_path))
self._ensure_schema()
def _hash_password(self, password: str, salt: str) -> str:
return hashlib.sha256((salt + password).encode()).hexdigest()
def _generate_salt(self) -> str:
return secrets.token_hex(16)
def _ensure_schema(self):
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
# Проверяем структуру. Если колонки login нет — таблица старая.
cursor.execute("PRAGMA table_info(users)")
columns = [col[1] for col in cursor.fetchall()]
if "login" not in columns or len(columns) < 4:
print("[INFO] Структура users.db некорректна. Пересоздаем...")
cursor.execute("DROP TABLE IF EXISTS users")
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
login TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
salt TEXT NOT NULL,
role TEXT NOT NULL
)
''')
# Создаем дефолтного админа ТОЛЬКО если таблицы пуста
cursor.execute("SELECT COUNT(*) FROM users")
if cursor.fetchone()[0] == 0:
salt = secrets.token_hex(16)
pwd_hash = hashlib.sha256((salt + "admin").encode()).hexdigest()
cursor.execute(
"INSERT INTO users (login, password_hash, salt, role) VALUES (?, ?, ?, ?)",
("admin", pwd_hash, salt, "Администратор")
)
print("[INFO] Создан пользователь 'admin' с паролем 'admin'")
conn.commit()
def get_all_users(self):
try:
with sqlite3.connect(self.db_path) as conn:
return conn.execute("SELECT id, login, role FROM users ORDER BY id ASC").fetchall()
except Exception:
self._ensure_schema()
return [(1, "admin", "Администратор")]
def add_user(self, login: str, role: str, password: str):
valid_roles = ["Администратор", "Эксперт", "Пользователь"]
if role not in valid_roles:
raise ValueError(f"Недопустимая роль: {role}")
salt = self._generate_salt()
pwd_hash = self._hash_password(password, salt)
with sqlite3.connect(self.db_path) as conn:
conn.execute(
"INSERT INTO users (login, password_hash, salt, role) VALUES (?, ?, ?, ?)",
(login, pwd_hash, salt, role)
)
conn.commit()
def delete_user(self, user_id: int):
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute("DELETE FROM users WHERE id=?", (user_id,))
cursor.execute("UPDATE users SET id = id - 1 WHERE id > ?", (user_id,))
max_id = cursor.execute("SELECT MAX(id) FROM users").fetchone()[0] or 0
cursor.execute("UPDATE sqlite_sequence SET seq=? WHERE name='users'", (max_id,))
conn.commit()
def validate_credentials(self, login: str, password: str) -> bool:
"""
Проверяет пару логин/пароль по базе данных.
Возвращает True при успехе, False при неудаче.
"""
try:
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
# Ищем пользователя по логину
row = cursor.execute(
"SELECT password_hash, salt FROM users WHERE login=?", (login,)
).fetchone()
if not row:
return False # Пользователь не найден
stored_hash, salt = row
# Хэшируем введенный пароль с той же солью из базы
test_hash = hashlib.sha256((salt + password).encode()).hexdigest()
# Сравниваем хэши
return stored_hash == test_hash
except Exception as e:
print(f"[DB ERROR] Ошибка проверки пароля: {e}")
return False
# Dozor/modules/administrator/admin_user_inter.py
import tkinter as tk
from tkinter import ttk, messagebox, simpledialog
from modules.administrator.user_logic import UserLogic
class AdminUserInter(tk.Toplevel):
def __init__(self, parent, on_logout_callback=None):
super().__init__(parent)
self.parent_main = parent
self.on_logout_callback = on_logout_callback
self.title("Управление пользователями")
self.geometry("550x450")
self.resizable(True, True)
self.transient(parent)
self.grab_set()
container = ttk.Frame(self, padding="20")
container.pack(expand=True, fill="both")
left_panel = ttk.Frame(container)
left_panel.pack(side="left", fill="y", padx=(0, 10))
right_panel = ttk.Frame(container)
right_panel.pack(side="right", fill="both", expand=True)
actions = [
("Добавить пользователя", self.action_add),
("Сменить пользователя", self.action_change),
("Удалить пользователя", self.action_remove)
]
for text, cmd in actions:
ttk.Button(left_panel, text=text, command=cmd).pack(fill="x", pady=5)
columns = ("№", "Логин", "Роль")
self.users_tree = ttk.Treeview(right_panel, columns=columns, show="headings")
self.users_tree.heading("#1", text="№")
self.users_tree.column("#1", anchor="center", width=50)
self.users_tree.heading("#2", text="Логин")
self.users_tree.column("#2", anchor="w", width=120)
self.users_tree.heading("#3", text="Роль")
self.users_tree.column("#3", anchor="center", width=250)
self.users_tree.pack(fill="both", expand=True)
# Хранилище реального ID для удаления
self._currently_selected_real_id = None
# Отслеживание выбора строки
self.users_tree.bind("<<TreeviewSelect>>", self._on_tree_select)
self.user_manager = UserLogic()
self.load_data()
# Центрирование окна над главным приложением Dozor
self.update_idletasks() # Обновляем геометрию, чтобы узнать реальный размер окна
root_win = self.winfo_toplevel()
p_x = root_win.winfo_rootx()
p_y = root_win.winfo_rooty()
w = self.winfo_width()
h = self.winfo_height()
screen_w = self.winfo_screenwidth()
screen_h = self.winfo_screenheight()
x = int(p_x + (screen_w - w) / 2)
y = int(p_y + (screen_h - h) / 2)
# Устанавливаем позицию окна
self.geometry(f"+{x}+{y}")
def _on_tree_select(self, event):
selected_item = self.users_tree.selection()
if selected_item:
tags = self.users_tree.item(selected_item)['tags']
if tags:
try:
self._currently_selected_real_id = int(tags[0])
except ValueError:
self._currently_selected_real_id = None
def load_data(self):
for i in self.users_tree.get_children():
self.users_tree.delete(i)
users_from_db = self.user_manager.get_all_users()
for idx, user_row in enumerate(users_from_db, start=1):
login = user_row[1]
role = user_row[2]
real_id = user_row[0]
self.users_tree.insert("", "end", values=(idx, login, role), tags=(str(real_id),))
def action_add(self):
dialog = tk.Toplevel(self)
dialog.title("Новый пользователь")
dialog.geometry("300x220")
dialog.transient(self)
dialog.grab_set()
main_frame = ttk.Frame(dialog, padding="15")
main_frame.pack(expand=True, fill="both")
ttk.Label(main_frame, text="Логин:").grid(row=0, column=0, sticky="w", pady=5)
login_e = ttk.Entry(main_frame); login_e.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
ttk.Label(main_frame, text="Пароль:").grid(row=1, column=0, sticky="w", pady=5)
pass_e = ttk.Entry(main_frame, show="*"); pass_e.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
ttk.Label(main_frame, text="Роль:").grid(row=2, column=0, sticky="w", pady=5)
role_values = ["Администратор", "Эксперт", "Пользователь"]
role_combo = ttk.Combobox(main_frame, values=role_values, state="readonly")
role_combo.current(2)
role_combo.grid(row=2, column=1, padx=5, pady=5, sticky="ew")
btn_frame = ttk.Frame(main_frame)
btn_frame.grid(row=3, column=0, columnspan=2, pady=15)
def do_add():
try:
self.user_manager.add_user(login_e.get(), role_combo.get(), pass_e.get())
self.load_data()
dialog.destroy()
messagebox.showinfo("Успех", "Пользователь добавлен.")
except Exception as e:
messagebox.showerror("Ошибка", str(e))
ttk.Button(btn_frame, text="Создать", command=do_add).pack(side="left", padx=5)
main_frame.columnconfigure(1, weight=1)
login_e.focus()
def action_remove(self):
if self._currently_selected_real_id is None:
messagebox.showwarning("Ошибка", "Выберите пользователя для удаления!")
return
answer = messagebox.askyesno(
"Подтверждение",
f"УДАЛИТЬ пользователя с реальным ID={self._currently_selected_real_id}?\nЭто действие нельзя отменить."
)
if answer:
try:
self.user_manager.delete_user(self._currently_selected_real_id)
self._currently_selected_real_id = None
self.load_data()
messagebox.showinfo("Успех", "Пользователь удален.")
except Exception as e:
messagebox.showerror("Ошибка", str(e))
# === ИСПРАВЛЕННЫЙ МЕТОД СМЕНЫ ПОЛЬЗОВАТЕЛЯ ===
def action_change(self):
# 1. Сначала получаем ссылку на ГЛАВНОЕ ОКНО (оно еще живо)
root = self.parent_main.winfo_toplevel()
# 2. Останавливаем работу главного окна (его внутренний цикл Tcl)
try:
if hasattr(root, 'quit'):
root.quit()
except Exception:
pass
# 3. Закрываем само окно админки
self.destroy()
# 4. Теперь пытаемся уничтожить нативное окно ОС
try:
if root and root.winfo_exists():
root.destroy()
except Exception:
pass
# 5. Запускаем новый процесс
import sys, subprocess, os, shutil
script_path = os.path.abspath("main.py")
gui_python = shutil.which("pythonw.exe")
if not gui_python:
current_dir = os.path.dirname(sys.executable)
potential_path = os.path.join(current_dir, "pythonw.exe")
gui_python = potential_path if os.path.exists(potential_path) else sys.executable
try:
subprocess.Popen([gui_python, script_path], shell=True, creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0)
except Exception as e:
print(f"[FATAL] Не удалось запустить новую копию Dozor: {e}")
sys.exit(0)