working
This commit is contained in:
parent
a385308f38
commit
9b0a20aa91
6 changed files with 275 additions and 69 deletions
35
.gitignore
vendored
35
.gitignore
vendored
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Виртуальное окружение
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# База данных и данные приложения
|
||||||
|
captains_log.db
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Nuitka
|
||||||
|
*.build/
|
||||||
|
*.dist/
|
||||||
|
*.onefile-build/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
9
LICENSE
Normal file
9
LICENSE
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Vic
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
16
README.md
Normal file
16
README.md
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Captain's Log
|
||||||
|
|
||||||
|
Приложение для быстрых заметок с шифрованием.
|
||||||
|
|
||||||
|
## Установка и запуск
|
||||||
|
|
||||||
|
1. Клонируйте репозиторий
|
||||||
|
2. Создайте виртуальное окружение: `python -m venv .venv`
|
||||||
|
3. Активируйте его: `.venv\Scripts\activate` (Windows)
|
||||||
|
4. Установите зависимости: `pip install -r requirements.txt`
|
||||||
|
5. Запустите: `python main.py`
|
||||||
|
|
||||||
|
## Первый запуск
|
||||||
|
|
||||||
|
При первом запуске приложение предложит создать мастер-пароль.
|
||||||
|
База данных создается автоматически в папке приложения.
|
||||||
22
app/auth.py
22
app/auth.py
|
|
@ -1,9 +1,8 @@
|
||||||
"""Модуль аутентификации"""
|
"""Модуль аутентификации"""
|
||||||
import hashlib
|
import hashlib
|
||||||
import base64
|
import os
|
||||||
from app.database import Database
|
from app.database import Database
|
||||||
|
|
||||||
|
|
||||||
class Auth:
|
class Auth:
|
||||||
def __init__(self, db: Database):
|
def __init__(self, db: Database):
|
||||||
self.db = db
|
self.db = db
|
||||||
|
|
@ -34,6 +33,23 @@ class Auth:
|
||||||
"""Установить, спрашивать ли пароль при входе"""
|
"""Установить, спрашивать ли пароль при входе"""
|
||||||
self.db.set_setting('ask_password_on_startup', '1' if value else '0')
|
self.db.set_setting('ask_password_on_startup', '1' if value else '0')
|
||||||
|
|
||||||
|
def reset_password(self):
|
||||||
|
"""Сбросить мастер-пароль"""
|
||||||
|
self.db.set_setting('password_set', '0')
|
||||||
|
self.db.set_setting('master_password_hash', '')
|
||||||
|
self.db.set_setting('ask_password_on_startup', '1')
|
||||||
|
|
||||||
|
def change_password(self, old_password, new_password):
|
||||||
|
"""Изменить мастер-пароль"""
|
||||||
|
if self.is_password_set():
|
||||||
|
if not self.check_password(old_password):
|
||||||
|
return False, "Неверный текущий пароль"
|
||||||
|
|
||||||
|
self.set_master_password(new_password)
|
||||||
|
return True, "Пароль успешно изменен"
|
||||||
|
|
||||||
def _hash_password(self, password):
|
def _hash_password(self, password):
|
||||||
"""Хешировать пароль"""
|
"""Хешировать пароль с солью"""
|
||||||
|
# В будущем здесь будет более безопасное хеширование
|
||||||
|
# Пока используем SHA-256 для простоты
|
||||||
return hashlib.sha256(password.encode()).hexdigest()
|
return hashlib.sha256(password.encode()).hexdigest()
|
||||||
|
|
@ -3,18 +3,19 @@ import tkinter as tk
|
||||||
from tkinter import ttk, messagebox
|
from tkinter import ttk, messagebox
|
||||||
from app.config import Config
|
from app.config import Config
|
||||||
|
|
||||||
|
|
||||||
class PasswordDialog(tk.Toplevel):
|
class PasswordDialog(tk.Toplevel):
|
||||||
"""Диалог установки/ввода мастер-пароля"""
|
"""Диалог установки/ввода мастер-пароля"""
|
||||||
|
|
||||||
def __init__(self, parent, auth, mode='set'):
|
def __init__(self, parent, auth, mode='set'):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
self.mode = mode # 'set' или 'check'
|
self.mode = mode # 'set', 'check', 'reset'
|
||||||
self.result = False
|
self.result = False
|
||||||
|
|
||||||
self.title("Мастер-пароль" if mode == 'set' else "Введите пароль")
|
self.title("Мастер-пароль" if mode == 'set' else
|
||||||
self.geometry("400x250")
|
"Введите пароль" if mode == 'check' else
|
||||||
|
"Сброс пароля")
|
||||||
|
self.geometry("400x300" if mode == 'reset' else "400x250")
|
||||||
self.resizable(False, False)
|
self.resizable(False, False)
|
||||||
self.configure(bg=Config.COLORS['bg'])
|
self.configure(bg=Config.COLORS['bg'])
|
||||||
|
|
||||||
|
|
@ -35,8 +36,10 @@ class PasswordDialog(tk.Toplevel):
|
||||||
|
|
||||||
if self.mode == 'set':
|
if self.mode == 'set':
|
||||||
text = "Придумайте мастер-пароль\nдля защиты ваших заметок:"
|
text = "Придумайте мастер-пароль\nдля защиты ваших заметок:"
|
||||||
else:
|
elif self.mode == 'check':
|
||||||
text = "Введите мастер-пароль:"
|
text = "Введите мастер-пароль:"
|
||||||
|
else: # reset
|
||||||
|
text = "ВНИМАНИЕ!\nЭто сбросит текущий пароль.\nВсе заметки останутся.\n\nДля сброса введите 'RESET':"
|
||||||
|
|
||||||
tk.Label(
|
tk.Label(
|
||||||
main_frame,
|
main_frame,
|
||||||
|
|
@ -47,79 +50,30 @@ class PasswordDialog(tk.Toplevel):
|
||||||
wraplength=350
|
wraplength=350
|
||||||
).pack(pady=(0, 15))
|
).pack(pady=(0, 15))
|
||||||
|
|
||||||
# Поле ввода пароля
|
if self.mode == 'reset':
|
||||||
self.password_var = tk.StringVar()
|
# Поле для сброса
|
||||||
self.password_entry = ttk.Entry(
|
self.reset_var = tk.StringVar()
|
||||||
main_frame,
|
self.reset_entry = ttk.Entry(
|
||||||
textvariable=self.password_var,
|
|
||||||
show='*',
|
|
||||||
font=('Arial', 12)
|
|
||||||
)
|
|
||||||
self.password_entry.pack(fill='x', pady=(0, 10))
|
|
||||||
self.password_entry.focus()
|
|
||||||
|
|
||||||
if self.mode == 'set':
|
|
||||||
# Подтверждение пароля
|
|
||||||
tk.Label(
|
|
||||||
main_frame,
|
main_frame,
|
||||||
text="Подтвердите пароль:",
|
textvariable=self.reset_var,
|
||||||
bg=Config.COLORS['bg'],
|
|
||||||
fg=Config.COLORS['fg'],
|
|
||||||
font=('Arial', 11)
|
|
||||||
).pack()
|
|
||||||
|
|
||||||
self.confirm_var = tk.StringVar()
|
|
||||||
self.confirm_entry = ttk.Entry(
|
|
||||||
main_frame,
|
|
||||||
textvariable=self.confirm_var,
|
|
||||||
show='*',
|
|
||||||
font=('Arial', 12)
|
font=('Arial', 12)
|
||||||
)
|
)
|
||||||
self.confirm_entry.pack(fill='x', pady=(0, 15))
|
self.reset_entry.pack(fill='x', pady=(0, 15))
|
||||||
|
self.reset_entry.focus()
|
||||||
|
|
||||||
# Чекбокс "спрашивать при входе"
|
tk.Button(
|
||||||
self.ask_var = tk.BooleanVar(value=True)
|
|
||||||
tk.Checkbutton(
|
|
||||||
main_frame,
|
main_frame,
|
||||||
text="Спрашивать при входе",
|
text="Сбросить пароль",
|
||||||
variable=self.ask_var,
|
command=self.reset_password,
|
||||||
bg=Config.COLORS['bg'],
|
bg=Config.COLORS['danger'],
|
||||||
fg=Config.COLORS['fg'],
|
|
||||||
selectcolor=Config.COLORS['bg'],
|
|
||||||
activebackground=Config.COLORS['bg'],
|
|
||||||
activeforeground=Config.COLORS['fg']
|
|
||||||
).pack(pady=(0, 15))
|
|
||||||
|
|
||||||
# Кнопки
|
|
||||||
button_frame = tk.Frame(main_frame, bg=Config.COLORS['bg'])
|
|
||||||
button_frame.pack(fill='x')
|
|
||||||
|
|
||||||
if self.mode == 'set':
|
|
||||||
tk.Button(
|
|
||||||
button_frame,
|
|
||||||
text="Установить пароль",
|
|
||||||
command=self.set_password,
|
|
||||||
bg=Config.COLORS['accent'],
|
|
||||||
fg=Config.COLORS['fg'],
|
|
||||||
font=('Arial', 11, 'bold'),
|
|
||||||
relief='flat',
|
|
||||||
cursor='hand2'
|
|
||||||
).pack(fill='x', pady=(0, 5))
|
|
||||||
else:
|
|
||||||
tk.Button(
|
|
||||||
button_frame,
|
|
||||||
text="Войти",
|
|
||||||
command=self.check_password,
|
|
||||||
bg=Config.COLORS['accent'],
|
|
||||||
fg=Config.COLORS['fg'],
|
fg=Config.COLORS['fg'],
|
||||||
font=('Arial', 11, 'bold'),
|
font=('Arial', 11, 'bold'),
|
||||||
relief='flat',
|
relief='flat',
|
||||||
cursor='hand2'
|
cursor='hand2'
|
||||||
).pack(fill='x', pady=(0, 5))
|
).pack(fill='x', pady=(0, 5))
|
||||||
|
|
||||||
if self.mode == 'check':
|
|
||||||
tk.Button(
|
tk.Button(
|
||||||
button_frame,
|
main_frame,
|
||||||
text="Отмена",
|
text="Отмена",
|
||||||
command=self.destroy,
|
command=self.destroy,
|
||||||
bg=Config.COLORS['button_bg'],
|
bg=Config.COLORS['button_bg'],
|
||||||
|
|
@ -127,6 +81,99 @@ class PasswordDialog(tk.Toplevel):
|
||||||
relief='flat',
|
relief='flat',
|
||||||
cursor='hand2'
|
cursor='hand2'
|
||||||
).pack(fill='x')
|
).pack(fill='x')
|
||||||
|
else:
|
||||||
|
# Поле ввода пароля
|
||||||
|
self.password_var = tk.StringVar()
|
||||||
|
self.password_entry = ttk.Entry(
|
||||||
|
main_frame,
|
||||||
|
textvariable=self.password_var,
|
||||||
|
show='*',
|
||||||
|
font=('Arial', 12)
|
||||||
|
)
|
||||||
|
self.password_entry.pack(fill='x', pady=(0, 10))
|
||||||
|
self.password_entry.focus()
|
||||||
|
|
||||||
|
if self.mode == 'set':
|
||||||
|
# Подтверждение пароля
|
||||||
|
tk.Label(
|
||||||
|
main_frame,
|
||||||
|
text="Подтвердите пароль:",
|
||||||
|
bg=Config.COLORS['bg'],
|
||||||
|
fg=Config.COLORS['fg'],
|
||||||
|
font=('Arial', 11)
|
||||||
|
).pack()
|
||||||
|
|
||||||
|
self.confirm_var = tk.StringVar()
|
||||||
|
self.confirm_entry = ttk.Entry(
|
||||||
|
main_frame,
|
||||||
|
textvariable=self.confirm_var,
|
||||||
|
show='*',
|
||||||
|
font=('Arial', 12)
|
||||||
|
)
|
||||||
|
self.confirm_entry.pack(fill='x', pady=(0, 15))
|
||||||
|
|
||||||
|
# Чекбокс "спрашивать при входе"
|
||||||
|
self.ask_var = tk.BooleanVar(value=True)
|
||||||
|
tk.Checkbutton(
|
||||||
|
main_frame,
|
||||||
|
text="Спрашивать при входе",
|
||||||
|
variable=self.ask_var,
|
||||||
|
bg=Config.COLORS['bg'],
|
||||||
|
fg=Config.COLORS['fg'],
|
||||||
|
selectcolor=Config.COLORS['bg'],
|
||||||
|
activebackground=Config.COLORS['bg'],
|
||||||
|
activeforeground=Config.COLORS['fg']
|
||||||
|
).pack(pady=(0, 15))
|
||||||
|
|
||||||
|
# Кнопки
|
||||||
|
button_frame = tk.Frame(main_frame, bg=Config.COLORS['bg'])
|
||||||
|
button_frame.pack(fill='x')
|
||||||
|
|
||||||
|
if self.mode == 'set':
|
||||||
|
tk.Button(
|
||||||
|
button_frame,
|
||||||
|
text="Установить пароль",
|
||||||
|
command=self.set_password,
|
||||||
|
bg=Config.COLORS['accent'],
|
||||||
|
fg=Config.COLORS['fg'],
|
||||||
|
font=('Arial', 11, 'bold'),
|
||||||
|
relief='flat',
|
||||||
|
cursor='hand2'
|
||||||
|
).pack(fill='x', pady=(0, 5))
|
||||||
|
else:
|
||||||
|
tk.Button(
|
||||||
|
button_frame,
|
||||||
|
text="Войти",
|
||||||
|
command=self.check_password,
|
||||||
|
bg=Config.COLORS['accent'],
|
||||||
|
fg=Config.COLORS['fg'],
|
||||||
|
font=('Arial', 11, 'bold'),
|
||||||
|
relief='flat',
|
||||||
|
cursor='hand2'
|
||||||
|
).pack(fill='x', pady=(0, 5))
|
||||||
|
|
||||||
|
# Кнопка сброса пароля
|
||||||
|
tk.Button(
|
||||||
|
button_frame,
|
||||||
|
text="Забыли пароль?",
|
||||||
|
command=self.show_reset_warning,
|
||||||
|
bg=Config.COLORS['button_bg'],
|
||||||
|
fg=Config.COLORS['fg'],
|
||||||
|
relief='flat',
|
||||||
|
cursor='hand2',
|
||||||
|
font=('Arial', 9)
|
||||||
|
).pack(fill='x', pady=(0, 5))
|
||||||
|
|
||||||
|
if self.mode == 'check':
|
||||||
|
tk.Button(
|
||||||
|
button_frame,
|
||||||
|
text="Отмена",
|
||||||
|
command=self.destroy,
|
||||||
|
bg=Config.COLORS['button_bg'],
|
||||||
|
fg=Config.COLORS['fg'],
|
||||||
|
relief='flat',
|
||||||
|
cursor='hand2'
|
||||||
|
).pack(fill='x')
|
||||||
|
|
||||||
def set_password(self):
|
def set_password(self):
|
||||||
password = self.password_var.get()
|
password = self.password_var.get()
|
||||||
|
|
@ -136,6 +183,10 @@ class PasswordDialog(tk.Toplevel):
|
||||||
messagebox.showwarning("Ошибка", "Введите пароль")
|
messagebox.showwarning("Ошибка", "Введите пароль")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if len(password) < 4:
|
||||||
|
messagebox.showwarning("Ошибка", "Пароль должен быть минимум 4 символа")
|
||||||
|
return
|
||||||
|
|
||||||
if password != confirm:
|
if password != confirm:
|
||||||
messagebox.showwarning("Ошибка", "Пароли не совпадают")
|
messagebox.showwarning("Ошибка", "Пароли не совпадают")
|
||||||
return
|
return
|
||||||
|
|
@ -156,6 +207,32 @@ class PasswordDialog(tk.Toplevel):
|
||||||
self.password_var.set('')
|
self.password_var.set('')
|
||||||
self.password_entry.focus()
|
self.password_entry.focus()
|
||||||
|
|
||||||
|
def reset_password(self):
|
||||||
|
if self.reset_var.get().upper() == 'RESET':
|
||||||
|
self.auth.reset_password()
|
||||||
|
messagebox.showinfo("Успех", "Пароль сброшен.\nПри следующем запуске создайте новый пароль.")
|
||||||
|
self.result = True
|
||||||
|
self.destroy()
|
||||||
|
else:
|
||||||
|
messagebox.showerror("Ошибка", "Введите RESET для подтверждения сброса")
|
||||||
|
self.reset_var.set('')
|
||||||
|
self.reset_entry.focus()
|
||||||
|
|
||||||
|
def show_reset_warning(self):
|
||||||
|
"""Показать предупреждение о сбросе пароля"""
|
||||||
|
if messagebox.askyesno(
|
||||||
|
"Сброс пароля",
|
||||||
|
"Вы уверены, что хотите сбросить пароль?\n\n"
|
||||||
|
"Это действие удалит только пароль, все заметки останутся.\n"
|
||||||
|
"После сброса вы сможете установить новый пароль."
|
||||||
|
):
|
||||||
|
self.destroy()
|
||||||
|
reset_dialog = PasswordDialog(self.master, self.auth, mode='reset')
|
||||||
|
self.master.wait_window(reset_dialog)
|
||||||
|
if reset_dialog.result:
|
||||||
|
self.result = True
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
|
||||||
class ConfirmDialog(tk.Toplevel):
|
class ConfirmDialog(tk.Toplevel):
|
||||||
"""Диалог подтверждения действия"""
|
"""Диалог подтверждения действия"""
|
||||||
|
|
|
||||||
53
main.py
53
main.py
|
|
@ -11,10 +11,63 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
from app.ui.main_window import MainWindow
|
from app.ui.main_window import MainWindow
|
||||||
|
|
||||||
|
|
||||||
|
def create_gitignore():
|
||||||
|
"""Создать .gitignore если его нет"""
|
||||||
|
gitignore_path = os.path.join(os.path.dirname(__file__), '.gitignore')
|
||||||
|
|
||||||
|
if not os.path.exists(gitignore_path):
|
||||||
|
gitignore_content = """# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Виртуальное окружение
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# База данных и данные приложения
|
||||||
|
captains_log.db
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Nuitka
|
||||||
|
*.build/
|
||||||
|
*.dist/
|
||||||
|
*.onefile-build/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(gitignore_path, 'w') as f:
|
||||||
|
f.write(gitignore_content)
|
||||||
|
except:
|
||||||
|
pass # Игнорируем ошибки при создании .gitignore
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Точка входа в приложение"""
|
"""Точка входа в приложение"""
|
||||||
|
# Создаем .gitignore при первом запуске
|
||||||
|
create_gitignore()
|
||||||
|
|
||||||
app = MainWindow()
|
app = MainWindow()
|
||||||
app.mainloop()
|
app.mainloop()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue