Загрузка данных


# 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)