/
OlgaG
/
empty
Обзор
Документация
Войти
/
OlgaG
/
empty
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
main.py
711 строк
30 KB
olga_goncharova
trying to add code
13 окт 2025, 11:19
13 окт 2025, 11:19
3d953e9
Код
Авторство
О чём код?
#!/usr/bin/env python3 # -*- coding: utf-8 -*- import tkinter as tk from tkinter import ttk, messagebox, simpledialog import os, json, time, threading, hashlib, hmac, binascii, platform, math, ast, operator from datetime import datetime # ----------------------------- Конфигурация / пути ----------------------------- BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DATA_DIR = os.path.join(BASE_DIR, "data") USERS_FILE = os.path.join(DATA_DIR, "users.json") ATTEMPTS_FILE = os.path.join(DATA_DIR, "attempts.json") SECURITY_FILE = os.path.join(DATA_DIR, "security.json") HISTORY_DIR = os.path.join(DATA_DIR, "history") MEM_DIR = os.path.join(DATA_DIR, "memory") os.makedirs(DATA_DIR, exist_ok=True) os.makedirs(HISTORY_DIR, exist_ok=True) os.makedirs(MEM_DIR, exist_ok=True) # ----------------------------- Утилиты и безопасное хеширование ----------------------------- def current_ts(): return time.time() def now_str(): return datetime.now().strftime("%Y-%m-%d %H:%M:%S") def pbkdf2_hash(password: str, salt_bytes: bytes = None, iterations: int = 200_000): """ Если salt_bytes не задана — сгенерировать. """ if salt_bytes is None: salt_bytes = os.urandom(16) dk = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt_bytes, iterations) return binascii.hexlify(salt_bytes).decode(), binascii.hexlify(dk).decode(), iterations def pbkdf2_verify(password: str, salt_hex: str, hash_hex: str, iterations: int): salt = binascii.unhexlify(salt_hex.encode()) dk = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, iterations) return hmac.compare_digest(binascii.hexlify(dk).decode(), hash_hex) # ----------------------------- Хранилища: пользователи, попытки, безопасность ----------------------------- class UserStore: """Управление пользователями (хранение в JSON).""" def __init__(self, path=USERS_FILE): self.path = path self._load() # если база пуста — создаём демонстрационного админа if not self.users: # demo admin: admin / admin1234 try: self.create_user('admin', 'admin1234', role='admin', force=True) print("Demo admin created: admin / admin1234") except Exception: pass def _load(self): if os.path.exists(self.path): with open(self.path, 'r', encoding='utf-8') as f: self.users = json.load(f) else: self.users = {} def _save(self): with open(self.path, 'w', encoding='utf-8') as f: json.dump(self.users, f, ensure_ascii=False, indent=2) def list_users(self): return list(self.users.keys()) def create_user(self, username, password, role='user', force=False): username = username.strip() if not username: raise ValueError("Логин не может быть пустым") if username in self.users and not force: raise ValueError("Логин уже существует") if not force: if len(password) < 4: raise ValueError("Пароль должен быть не менее 4 символов") if username == password: raise ValueError("Логин и пароль не могут совпадать") salt_hex, hash_hex, iterations = pbkdf2_hash(password) self.users[username] = { "salt": salt_hex, "hash": hash_hex, "iterations": iterations, "role": role, "created_at": current_ts() } self._save() def verify_user(self, username, password) -> bool: if username not in self.users: return False rec = self.users[username] return pbkdf2_verify(password, rec['salt'], rec['hash'], rec['iterations']) def delete_user(self, username): if username in self.users: del self.users[username] self._save() # удалить историю и память hist = os.path.join(HISTORY_DIR, f"{username}.json") mem = os.path.join(MEM_DIR, f"{username}.json") if os.path.exists(hist): os.remove(hist) if os.path.exists(mem): os.remove(mem) def set_role(self, username, role): if username in self.users: self.users[username]['role'] = role self._save() def get_role(self, username): return self.users.get(username, {}).get('role') def get_user_meta(self, username): return self.users.get(username) class AttemptStore: """Учёт попыток входа и блокировок (в JSON). Блокируем на 10 секунд после 3 неудач.""" def __init__(self, path=ATTEMPTS_FILE): self.path = path self._load() def _load(self): if os.path.exists(self.path): with open(self.path, 'r', encoding='utf-8') as f: self.attempts = json.load(f) else: self.attempts = {} def _save(self): with open(self.path, 'w', encoding='utf-8') as f: json.dump(self.attempts, f, ensure_ascii=False, indent=2) def is_locked(self, username): rec = self.attempts.get(username) if not rec: return False, 0 locked_until = rec.get('locked_until', 0) if locked_until and time.time() < locked_until: return True, int(locked_until - time.time()) return False, 0 def record_failure(self, username, max_tries=3, lock_seconds=10): rec = self.attempts.get(username, {"tries": 0, "locked_until": 0}) rec['tries'] = rec.get('tries', 0) + 1 blocked_now = False if rec['tries'] >= max_tries: rec['locked_until'] = time.time() + lock_seconds rec['tries'] = 0 blocked_now = True self.attempts[username] = rec self._save() left = max_tries - rec['tries'] if not blocked_now else 0 return blocked_now, left def reset(self, username): if username in self.attempts: self.attempts[username] = {"tries": 0, "locked_until": 0} self._save() class SecurityManager: """ Слежение за последовательными блокировками разных пользователей. Если подряд блокируются 2 разных пользователя - показываем big alert. Храним prev_blocked и consec_blocks в security.json. """ def __init__(self, path=SECURITY_FILE): self.path = path self._load() def _load(self): if os.path.exists(self.path): with open(self.path, 'r', encoding='utf-8') as f: self.data = json.load(f) else: self.data = {"prev_blocked": None, "consec_blocks": 0} def _save(self): with open(self.path, 'w', encoding='utf-8') as f: json.dump(self.data, f, ensure_ascii=False, indent=2) def record_block(self, username): prev = self.data.get("prev_blocked") if prev and prev != username: self.data["consec_blocks"] = self.data.get("consec_blocks", 0) + 1 else: self.data["consec_blocks"] = 1 self.data["prev_blocked"] = username self._save() return self.data["consec_blocks"] def reset(self): self.data["prev_blocked"] = None self.data["consec_blocks"] = 0 self._save() # ----------------------------- Разрешение на вычесление ----------------------------- # Разрешаем только арифметику: + - * / **, унарный -, числа, скобки, sqrt(x) ALLOWED_BINOPS = { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, ast.Pow: operator.pow, ast.Mod: operator.mod } ALLOWED_UNARYOPS = { ast.UAdd: lambda x: x, ast.USub: operator.neg } def safe_eval(expr: str): """ Вычисляет арифметическое выражение с поддержкой sqrt(...). Выбрасывает ValueError при недопустимом выражении. """ expr = expr.strip() if not expr: raise ValueError("Пустое выражение") node = ast.parse(expr, mode='eval').body def _eval(n): if isinstance(n, ast.BinOp): op_type = type(n.op) if op_type not in ALLOWED_BINOPS: raise ValueError("Операция не разрешена") left = _eval(n.left) right = _eval(n.right) return ALLOWED_BINOPS[op_type](left, right) elif isinstance(n, ast.UnaryOp): op_type = type(n.op) if op_type not in ALLOWED_UNARYOPS: raise ValueError("Унарная операция не разрешена") return ALLOWED_UNARYOPS[op_type](_eval(n.operand)) elif isinstance(n, ast.Num): # Python <3.8 return n.n elif isinstance(n, ast.Constant): # Python 3.8+ if isinstance(n.value, (int, float)): return n.value else: raise ValueError("Нельзя использовать константу этого типа") elif isinstance(n, ast.Call): # разрешаем только sqrt(x) if isinstance(n.func, ast.Name) and n.func.id == "sqrt": if len(n.args) != 1: raise ValueError("sqrt принимает один аргумент") arg = _eval(n.args[0]) if arg < 0: raise ValueError("Корень из отрицательного числа") return math.sqrt(arg) else: raise ValueError("Вызовы функций запрещены, кроме sqrt") else: raise ValueError("Недопустимый элемент выражения") return _eval(node) # ----------------------------- Звук / оповещение ----------------------------- def play_double_beep(root=None): """Проигрывает двойной короткий сигнал.""" def _worker(): try: if platform.system() == "Windows": import winsound for _ in range(2): winsound.Beep(800, 180) time.sleep(0.18) else: for _ in range(2): if root: try: root.bell() except: pass else: print("\a", end="", flush=True) time.sleep(0.18) except Exception: try: if root: root.bell() except: pass threading.Thread(target=_worker, daemon=True).start() # ----------------------------- Работа с историей и памятью пользователя ----------------------------- def save_history(username, entry): path = os.path.join(HISTORY_DIR, f"{username}.json") if os.path.exists(path): with open(path, 'r', encoding='utf-8') as f: arr = json.load(f) else: arr = [] arr.insert(0, f"{now_str()}: {entry}") arr = arr[:200] # ограничение длины with open(path, 'w', encoding='utf-8') as f: json.dump(arr, f, ensure_ascii=False, indent=2) def load_history(username): path = os.path.join(HISTORY_DIR, f"{username}.json") if os.path.exists(path): with open(path, 'r', encoding='utf-8') as f: return json.load(f) return [] def save_memory(username, value): path = os.path.join(MEM_DIR, f"{username}.json") with open(path, 'w', encoding='utf-8') as f: json.dump({"value": value}, f, ensure_ascii=False, indent=2) def load_memory(username): path = os.path.join(MEM_DIR, f"{username}.json") if os.path.exists(path): with open(path, 'r', encoding='utf-8') as f: return json.load(f).get("value") return None # ----------------------------- GUI: фреймы и окна ----------------------------- class CalculatorAppUI: def __init__(self, root): self.root = root self.root.title("Калькулятор — Авторизация") self.root.geometry("820x520") self.user_store = UserStore() self.attempt_store = AttemptStore() self.security = SecurityManager() self.current_user = None # контейнер для смены экранов self.container = tk.Frame(root) self.container.pack(fill="both", expand=True) self.login_frame = None self.register_frame = None self.calc_frame = None self.show_login_frame() def clear_container(self): for w in self.container.winfo_children(): w.destroy() # ----------------- Login ----------------- def show_login_frame(self): self.clear_container() f = tk.Frame(self.container, padx=12, pady=12) f.pack(fill="both", expand=True) header = tk.Label(f, text="Введите доступ к калькулятору", font=("Arial", 16)) header.pack(pady=(0,12)) frm = tk.Frame(f) frm.pack(pady=6) tk.Label(frm, text="Логин:").grid(row=0, column=0, sticky="e", padx=4, pady=4) self.login_entry = tk.Entry(frm, width=30) self.login_entry.grid(row=0, column=1, padx=4, pady=4) tk.Label(frm, text="Пароль:").grid(row=1, column=0, sticky="e", padx=4, pady=4) self.pass_entry = tk.Entry(frm, width=30, show="*") self.pass_entry.grid(row=1, column=1, padx=4, pady=4) btn_frame = tk.Frame(f) btn_frame.pack(pady=10) tk.Button(btn_frame, text="Войти", width=12, command=self.handle_login).grid(row=0, column=0, padx=6) tk.Button(btn_frame, text="Регистрация", width=12, command=self.show_register_frame).grid(row=0, column=1, padx=6) info = tk.Label(f, text="Если новый пользователь — нажмите Регистрация.", fg="#333") info.pack(pady=(8,0)) def handle_login(self): username = self.login_entry.get().strip() password = self.pass_entry.get().strip() if not username: messagebox.showwarning("Ввод", "Введите логин") return users = self.user_store.users if username not in users: # если пользователь не зарегистрирован — предложим регистрацию if messagebox.askyesno("Пользователь не найден", "Пользователь не найден. Зарегистрировать?"): self.show_register_frame(prefill=username) return locked, sec = self.attempt_store.is_locked(username) if locked: messagebox.showerror("Заблокирован", f"Вход для пользователя заблокирован ещё {sec} сек.") return if self.user_store.verify_user(username, password): # успешный вход self.attempt_store.reset(username) self.security.reset() # успешный вход прерывает блокировки messagebox.showinfo("Успех", f"Добро пожаловать, {username}!") self.current_user = username self.show_calc_frame() else: blocked_now, left = self.attempt_store.record_failure(username) if blocked_now: # если блокирован зарегистрированный пользователь consec = self.security.record_block(username) msg = "Неверная пара логин/пароль. Аккаунт заблокирован на 10 сек." messagebox.showerror("Блокировка", msg) if consec >= 2: # показываем большое предупреждение + двойной звук self.show_big_alert() else: # звуковой сигнал одиночный play_double_beep(self.root) # всё равно даём звук else: messagebox.showerror("Ошибка", f"Неверная пара логин/пароль. Осталось попыток: {left}") # ----------------- Registration ----------------- def show_register_frame(self, prefill=None): self.clear_container() f = tk.Frame(self.container, padx=12, pady=12) f.pack(fill="both", expand=True) tk.Label(f, text="Регистрация нового пользователя", font=("Arial", 16)).pack(pady=(0,12)) frm = tk.Frame(f) frm.pack(pady=6) tk.Label(frm, text="Желаемый логин:").grid(row=0, column=0, sticky="e", padx=4, pady=4) self.reg_login = tk.Entry(frm, width=30) self.reg_login.grid(row=0, column=1, padx=4, pady=4) if prefill: self.reg_login.insert(0, prefill) tk.Label(frm, text="Пароль:").grid(row=1, column=0, sticky="e", padx=4, pady=4) self.reg_pass = tk.Entry(frm, width=30, show="*") self.reg_pass.grid(row=1, column=1, padx=4, pady=4) tk.Label(frm, text="Повторите пароль:").grid(row=2, column=0, sticky="e", padx=4, pady=4) self.reg_pass2 = tk.Entry(frm, width=30, show="*") self.reg_pass2.grid(row=2, column=1, padx=4, pady=4) tk.Label(frm, text="Роль:").grid(row=3, column=0, sticky="e", padx=4, pady=4) self.role_var = tk.StringVar(value="user") role_menu = ttk.Combobox(frm, textvariable=self.role_var, values=["user","power","admin"], state="readonly", width=27) role_menu.grid(row=3, column=1, padx=4, pady=4) note = tk.Label(f, text="Роли:\n user — базовый (только базовые операции)\n power — доп. функции (корень, степень, память, история)\n admin — всё + управление пользователями", justify="left") note.pack(pady=8) btn_frame = tk.Frame(f) btn_frame.pack(pady=8) tk.Button(btn_frame, text="Сохранить", width=12, command=self.handle_register).grid(row=0, column=0, padx=6) tk.Button(btn_frame, text="Отмена", width=12, command=self.show_login_frame).grid(row=0, column=1, padx=6) def handle_register(self): username = self.reg_login.get().strip() p1 = self.reg_pass.get() p2 = self.reg_pass2.get() role = self.role_var.get() or "user" try: if p1 != p2: raise ValueError("Пароли не совпадают") self.user_store.create_user(username, p1, role=role) messagebox.showinfo("Готово", "Регистрация прошла успешно. Введите логин/пароль для входа.") self.show_login_frame() except Exception as e: messagebox.showerror("Ошибка регистрации", str(e)) # ----------------- Большое предупреждение ----------------- def show_big_alert(self): alert = tk.Toplevel(self.root) alert.title("Несанкционированный доступ") alert.geometry("520x220") alert.transient(self.root) alert.grab_set() lbl = tk.Label(alert, text="НЕСАНКЦИОНИРОВАННЫЙ ДОСТУП ОБНАРУЖЕН", font=("Arial", 16), fg="red") lbl.pack(pady=18) txt = tk.Label(alert, text="Зафиксированы подряд блокировки разных пользователей — возможно подбор паролей.\nСработала система защиты.", justify="center") txt.pack(pady=6) btn = tk.Button(alert, text="Закрыть", width=12, command=lambda: (alert.destroy(), self.security.reset())) btn.pack(pady=12) # двойной звуковой сигнал в фоновом потоке play_double_beep(self.root) # ----------------- Calculator ----------------- def show_calc_frame(self): self.clear_container() username = self.current_user role = self.user_store.get_role(username) self.root.title(f"Калькулятор — {username} ({role})") main = tk.Frame(self.container, padx=8, pady=8) main.pack(fill="both", expand=True) # слева — калькулятор left = tk.Frame(main) left.pack(side="left", fill="both", expand=True) display_frame = tk.Frame(left) display_frame.pack(fill="x", pady=(0,6)) self.expr_var = tk.StringVar(value="") self.display = tk.Entry(display_frame, textvariable=self.expr_var, font=("Consolas", 18), justify="right") self.display.pack(fill="x", padx=2, pady=2) # кнопки калькулятора btns = [ ('7', '8', '9', '/'), ('4', '5', '6', '*'), ('1', '2', '3', '-'), ('0', '.', '=', '+'), ] for r, row in enumerate(btns): row_frame = tk.Frame(left) row_frame.pack(fill="x") for c, val in enumerate(row): action = (lambda v=val: self.on_button_click(v)) tk.Button(row_frame, text=val, width=8, height=2, command=action).pack(side="left", padx=2, pady=2) # вспомогательные кнопки aux_frame = tk.Frame(left) aux_frame.pack(pady=6) tk.Button(aux_frame, text="C", width=6, command=self.clear_all).pack(side="left", padx=4) tk.Button(aux_frame, text="⌫", width=6, command=self.backspace).pack(side="left", padx=4) tk.Button(aux_frame, text="CE", width=6, command=self.clear_entry).pack(side="left", padx=4) # role-specific controls if role in ("power", "admin"): extra_frame = tk.Frame(left) extra_frame.pack(pady=6) tk.Button(extra_frame, text="√", width=6, command=self.btn_sqrt).pack(side="left", padx=4) tk.Button(extra_frame, text="^", width=6, command=lambda: self.append_text("**")).pack(side="left", padx=4) tk.Button(extra_frame, text="M+", width=6, command=self.mem_store).pack(side="left", padx=4) tk.Button(extra_frame, text="MR", width=6, command=self.mem_recall).pack(side="left", padx=4) tk.Button(extra_frame, text="MC", width=6, command=self.mem_clear).pack(side="left", padx=4) # справа — история и админ right = tk.Frame(main, width=300) right.pack(side="right", fill="y", padx=(8,0)) hist_panel = tk.Frame(right, relief="groove", bd=1, padx=6, pady=6) hist_panel.pack(fill="both", pady=(0,8), expand=True) tk.Label(hist_panel, text="История", font=("Arial", 12)).pack(anchor="w") self.history_listbox = tk.Listbox(hist_panel, height=15) self.history_listbox.pack(fill="both", expand=True, pady=(6,0)) self.reload_history() if role == "admin": admin_panel = tk.Frame(right, relief="groove", bd=1, padx=6, pady=6) admin_panel.pack(fill="x") tk.Label(admin_panel, text="Админ-панель", font=("Arial", 12)).pack(anchor="w") tk.Button(admin_panel, text="Список пользователей", width=18, command=self.admin_manage).pack(pady=6) # кнопка выйти bottom = tk.Frame(main) bottom.pack(fill="x", pady=8) tk.Button(bottom, text="Выйти", width=10, command=self.logout).pack(side="right", padx=8) def on_button_click(self, val): if val == "=": self.evaluate_expr() else: self.append_text(val) def append_text(self, s): cur = self.expr_var.get() self.expr_var.set(cur + s) def clear_all(self): self.expr_var.set("") def clear_entry(self): self.expr_var.set("") def backspace(self): cur = self.expr_var.get() if cur: self.expr_var.set(cur[:-1]) def evaluate_expr(self): expr = self.expr_var.get().strip() if not expr: return try: # поддерживаем sqrt(...) через safe_eval result = safe_eval(expr) self.expr_var.set(str(result)) save_history(self.current_user, f"{expr} = {result}") self.reload_history() except Exception as e: messagebox.showerror("Ошибка вычисления", str(e)) def btn_sqrt(self): expr = self.expr_var.get().strip() if not expr: return try: # преобразуем sqrt(x) # если expr — число, вычислим корень, иначе подставим sqrt(expr) try: val = float(expr) res = math.sqrt(val) self.expr_var.set(str(res)) save_history(self.current_user, f"sqrt({val}) = {res}") self.reload_history() except Exception: self.append_text("sqrt(") except Exception as e: messagebox.showerror("Ошибка", str(e)) def reload_history(self): self.history_listbox.delete(0, tk.END) arr = load_history(self.current_user) for line in arr[:200]: self.history_listbox.insert(tk.END, line) # memory functions def mem_store(self): val = self.expr_var.get().strip() if not val: messagebox.showinfo("Память", "Нечего сохранить") return save_memory(self.current_user, val) messagebox.showinfo("Память", "Сохранено в память") def mem_recall(self): val = load_memory(self.current_user) if val is None: messagebox.showinfo("Память", "Память пуста") return self.append_text(str(val)) def mem_clear(self): save_memory(self.current_user, "") messagebox.showinfo("Память", "Память очищена") # ----------------- Admin: управление пользователями ----------------- def admin_manage(self): top = tk.Toplevel(self.root) top.title("Управление пользователями") top.geometry("520x360") frm = tk.Frame(top, padx=8, pady=8) frm.pack(fill="both", expand=True) cols = ('login', 'role', 'created') tree = ttk.Treeview(frm, columns=cols, show='headings', height=12) tree.heading('login', text='Логин') tree.heading('role', text='Роль') tree.heading('created', text='Создан') tree.pack(fill="both", expand=True) def load(): for i in tree.get_children(): tree.delete(i) for u, meta in self.user_store.users.items(): created = now_str() try: created = datetime.fromtimestamp(meta.get('created_at', time.time())).strftime("%Y-%m-%d %H:%M") except: pass tree.insert('', tk.END, values=(u, meta.get('role'), created)) def delete_user(): sel = tree.selection() if not sel: return login = tree.item(sel[0])['values'][0] if login == 'admin': messagebox.showwarning("Запрещено", "Нельзя удалить admin") return if messagebox.askyesno("Удалить", f"Удалить пользователя {login}?"): self.user_store.delete_user(login) self.attempt_store.reset(login) load() messagebox.showinfo("Удалено", f"Пользователь {login} удалён") def change_role(): sel = tree.selection() if not sel: return login = tree.item(sel[0])['values'][0] role = simpledialog.askstring("Роль", "Укажите новую роль (user/power/admin):", initialvalue=self.user_store.get_role(login)) if role not in ("user", "power", "admin"): messagebox.showwarning("Роль", "Недопустимая роль") return self.user_store.set_role(login, role) load() messagebox.showinfo("Готово", "Роль изменена") btns = tk.Frame(frm) btns.pack(fill="x", pady=6) tk.Button(btns, text="Удалить", command=delete_user).pack(side="left", padx=6) tk.Button(btns, text="Изменить роль", command=change_role).pack(side="left", padx=6) tk.Button(btns, text="Закрыть", command=top.destroy).pack(side="right", padx=6) load() # ----------------- Logout ----------------- def logout(self): # при выходе — сбрасывает текущего пользователя и возврат на экран логина self.current_user = None self.security.reset() self.root.title("Калькулятор — Авторизация") self.show_login_frame() # ----------------------------- Запуск приложения ----------------------------- def main(): root = tk.Tk() app = CalculatorAppUI(root) root.mainloop() if __name__ == "__main__": main()