diff --git a/.gitignore b/.gitignore index 761a6e4..514e680 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,7 @@ instance/ /.deploy_cache.db /.env /sync.py +/.idea +/collect_code.py +/project_summary.txt +exclude.txt diff --git a/database.py b/database.py index a35bb61..799499a 100644 --- a/database.py +++ b/database.py @@ -21,6 +21,15 @@ class User(db.Model): last_login = Column(DateTime, nullable=True) is_active = Column(Boolean, default=True) + # ТОЛЬКО ЭТИ ПОЛЯ ДЛЯ ПРОФИЛЯ + email = Column(String(255), nullable=True) + jabber = Column(String(255), nullable=True) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + # Настройки уведомлений + notify_email = Column(Boolean, default=False) + notify_jabber = Column(Boolean, default=False) + secrets = relationship('Secret', back_populates='user', cascade='all, delete-orphan') security_logs = relationship('SecurityLog', back_populates='user', cascade='all, delete-orphan') @@ -40,7 +49,10 @@ class User(db.Model): 'role': self.role, 'created_at': self.created_at.isoformat() if self.created_at else None, 'last_login': self.last_login.isoformat() if self.last_login else None, - 'is_active': self.is_active + 'is_active': self.is_active, + 'email': self.email, + 'jabber': self.jabber, + 'updated_at': self.updated_at.isoformat() if self.updated_at else None, } @@ -174,7 +186,7 @@ def set_user_role(user_id: int, role: str): db.session.commit() -def log_creation(secret_id: str, client_info: dict, user_id: int = None, request_note: str = None): +def log_creation(secret_id: str, client_info: dict, user_id: int = None, request_note: str = None, user_note: str = None): secret = Secret( secret_id=secret_id, user_id=user_id, diff --git a/mailer.py b/mailer.py new file mode 100644 index 0000000..54fd2d4 --- /dev/null +++ b/mailer.py @@ -0,0 +1,210 @@ +import os +import smtplib +import ssl +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from email.utils import formataddr +from threading import Thread +from flask import render_template_string, render_template +from dotenv import load_dotenv + +load_dotenv() + + +class Mailer: + def __init__(self): + self.enabled = os.environ.get('MAIL_ENABLED', 'false').lower() == 'true' + self.smtp_server = os.environ.get('MAIL_SERVER', 'smtp.gmail.com') + self.smtp_port = int(os.environ.get('MAIL_PORT', 587)) + self.use_tls = os.environ.get('MAIL_USE_TLS', 'true').lower() == 'true' + self.use_ssl = os.environ.get('MAIL_USE_SSL', 'false').lower() == 'true' + self.username = os.environ.get('MAIL_USERNAME', '') + self.password = os.environ.get('MAIL_PASSWORD', '') + self.default_sender = os.environ.get('MAIL_DEFAULT_SENDER', self.username) + + def is_enabled(self) -> bool: + if not self.enabled: + return False + if not self.username or not self.password: + print("Mail: Отсутствуют логин или пароль") + return False + return True + + def _get_connection(self): + if self.use_ssl: + context = ssl.create_default_context() + return smtplib.SMTP_SSL(self.smtp_server, self.smtp_port, context=context) + else: + server = smtplib.SMTP(self.smtp_server, self.smtp_port) + if self.use_tls: + server.starttls() + return server + + def _send(self, to_emails, subject, body, html_body=None, from_email=None, from_name='SecretText'): + if not self.is_enabled(): + print(f"Mail: Пропущено письмо (отключено)") + return False + + if not to_emails: + print("Mail: Не указаны получатели") + return False + + if isinstance(to_emails, str): + to_emails = [to_emails] + + from_email = from_email or self.default_sender + + try: + msg = MIMEMultipart('alternative') + msg['Subject'] = subject + msg['From'] = formataddr((from_name, from_email)) + msg['To'] = ', '.join(to_emails) + + msg.attach(MIMEText(body, 'plain', 'utf-8')) + + if html_body: + msg.attach(MIMEText(html_body, 'html', 'utf-8')) + + with self._get_connection() as server: + if self.username and self.password: + server.login(self.username, self.password) + server.send_message(msg) + + print(f"Mail: Письмо отправлено на {', '.join(to_emails)}") + return True + + except Exception as e: + print(f"Mail: Ошибка отправки: {e}") + return False + + def send_email(self, to_emails, subject, body, html_body=None, from_email=None, from_name='SecretText'): + if not self.is_enabled(): + print(f"Mail: Письмо не отправлено (отключено)") + return None + + thread = Thread( + target=self._send, + args=(to_emails, subject, body, html_body, from_email, from_name), + daemon=True + ) + thread.start() + return thread + + + def send_email_with_template(self, to_emails, subject, template_name, context=None, from_email=None, + from_name='SecretText'): + from main import app + context = context or {} + + with app.app_context(): + html_body = render_template(template_name, **context) + + body = f"Письмо от SecretText. Пожалуйста, используйте почтовый клиент с поддержкой HTML." + + return self.send_email(to_emails, subject, body, html_body, from_email, from_name) + + def send_user_created_email(self, to_email, username, view_url): + subject = f"Ваш аккаунт в SecretText создан!" + context = { + 'username': username, + 'view_url': view_url, + 'base_url': self._get_base_url() + } + return self.send_email_with_template( + to_email, + subject, + 'emails/user_created.html', + context + ) + + def _get_base_url(self): + from main import app + return app.config.get('APP_URL', 'http://localhost:5000') + + def send_welcome_email(self, to_email, username): + subject = f"Добро пожаловать в SecretText, {username}!" + context = { + 'username': username, + 'base_url': self._get_base_url() + } + return self.send_email_with_template( + to_email, + subject, + 'emails/welcome.html', + context + ) + + def send_secret_notification(self, to_email, username, secret_id=None): + subject = f"Новый секрет для вас!" + context = { + 'username': username, + 'base_url': self._get_base_url() + } + return self.send_email_with_template( + to_email, + subject, + 'emails/secret_notification.html', + context + ) + + def send_secret_viewed_notification(self, to_email, username): + subject = f"Ваш секрет просмотрен!" + context = { + 'username': username, + 'base_url': self._get_base_url() + } + return self.send_email_with_template( + to_email, + subject, + 'emails/secret_viewed.html', + context + ) + + # mailer.py + + def send_secret_received_email(self, to_email, username, note=None): + subject = f"Вам отправили секрет по запросу!" + context = { + 'username': username, + 'note': note, + 'base_url': self._get_base_url() + } + return self.send_email_with_template( + to_email, + subject, + 'emails/secret_received.html', + context + ) + + + +mailer = Mailer() + + + +def send_mail(to_emails, subject, body, html_body=None): + return mailer.send_email(to_emails, subject, body, html_body) + + +def send_mail_template(to_emails, subject, template_name, context=None): + return mailer.send_email_with_template(to_emails, subject, template_name, context) + + +def send_user_created_email(to_email, username, view_url): + return mailer.send_user_created_email(to_email, username, view_url) + + +def send_welcome_email(to_email, username): + return mailer.send_welcome_email(to_email, username) + + +def send_secret_notification(to_email, username, secret_id=None): + return mailer.send_secret_notification(to_email, username, secret_id) + + +def send_secret_viewed_notification(to_email, username): + return mailer.send_secret_viewed_notification(to_email, username) + + +def send_secret_received_email(to_email, username, note=None): + return mailer.send_secret_received_email(to_email, username, note) \ No newline at end of file diff --git a/main.py b/main.py index 5cf3c65..c53e52f 100644 --- a/main.py +++ b/main.py @@ -3,6 +3,10 @@ import base64 import secrets import re import bleach +import secrets +import string +from mailer import mailer, send_mail +from migrate import run_migrations from datetime import datetime, timedelta from urllib.parse import quote_plus from bleach import Linker @@ -27,6 +31,7 @@ load_dotenv() class Config: + APP_URL = os.environ.get('APP_URL', 'http://localhost:5000') SECRET_KEY = os.environ.get('SECRET_KEY', secrets.token_hex(32)) REDIS_HOST = os.environ.get('REDIS_HOST', 'localhost') REDIS_PORT = int(os.environ.get('REDIS_PORT', 6379)) @@ -115,7 +120,7 @@ def create_admin(): print(f" ВАЖНО: Очистите куки браузера перед входом!") print(f"{'=' * 60}\n") else: - print(f"❌ Ошибка при создании администратора") + print(f"Ошибка при создании администратора") else: print(f"\n{'=' * 60}") print(f" Пользователи уже существуют") @@ -123,6 +128,17 @@ def create_admin(): print(f"{'=' * 60}\n") +def get_base_url() -> str: + return app.config.get( + 'APP_URL') or f"{app.config.get('APP_SCHEME', 'http')}://{app.config.get('APP_HOST', 'localhost')}:{app.config.get('APP_PORT', 5000)}" + + +def absolute_url(endpoint: str, **kwargs) -> str: + base_url = get_base_url() + relative_url = url_for(endpoint, **kwargs) + return f"{base_url}{relative_url}" + + def custom_linkify(text): linker = Linker( callbacks=[ @@ -138,10 +154,6 @@ def custom_linkify(text): database.db.init_app(app) -'''with app.app_context(): - database.init_db() - create_admin()''' - csrf = CSRFProtect(app) limiter = Limiter( app=app, @@ -151,6 +163,29 @@ limiter = Limiter( ) +def generate_secure_password(min_len: int = 12, max_len: int = 16) -> str: + lowercase = string.ascii_lowercase + uppercase = string.ascii_uppercase + digits = string.digits + special = "!@#$%^&*()-_=+" + + password = [ + secrets.choice(lowercase), + secrets.choice(uppercase), + secrets.choice(digits), + secrets.choice(special) + ] + + all_chars = lowercase + uppercase + digits + special + remaining_length = secrets.randbelow(max_len - min_len + 1) + (min_len - 4) + + for _ in range(remaining_length): + password.append(secrets.choice(all_chars)) + + secrets.SystemRandom().shuffle(password) + return ''.join(password) + + @app.template_filter('format_datetime') def format_datetime(value, format='%Y-%m-%d %H:%M'): if value is None: @@ -231,7 +266,6 @@ def is_ip_allowed(ip: str) -> bool: return True except: pass - return False @@ -336,13 +370,17 @@ def add_security_headers(response): "form-action 'self'" ) response.headers['Content-Security-Policy'] = csp - return response @app.context_processor def utility_processor(): - return {'now': datetime.now(), 'config': app.config} + return { + 'now': datetime.now(), + 'config': app.config, + 'base_url': get_base_url, + 'absolute_url': absolute_url + } redis_client = Redis( @@ -379,7 +417,8 @@ def generate_secret_id() -> str: return secrets.token_urlsafe(app.config['SECRET_ID_LENGTH']) -def store_secret(plaintext: str, client_info: dict, user_id: int = None, request_note: str = None) -> str: +def store_secret(plaintext: str, client_info: dict, user_id: int = None, request_note: str = None, + user_note: str = None) -> str: secret_id = generate_secret_id() encrypted = cipher.encrypt(plaintext.encode()) @@ -391,13 +430,19 @@ def store_secret(plaintext: str, client_info: dict, user_id: int = None, request if not user_exists: user_id = None - clean_note = None + clean_request_note = None if request_note: - clean_note = sanitize_input(request_note) - if clean_note and len(clean_note) > 500: - clean_note = clean_note[:500] + clean_request_note = sanitize_input(request_note) + if clean_request_note and len(clean_request_note) > 500: + clean_request_note = clean_request_note[:500] - database.log_creation(secret_id, client_info, user_id, clean_note) + clean_user_note = None + if user_note: + clean_user_note = sanitize_input(user_note) + if clean_user_note and len(clean_user_note) > 500: + clean_user_note = clean_user_note[:500] + + database.log_creation(secret_id, client_info, user_id, clean_request_note, clean_user_note) return secret_id @@ -519,7 +564,6 @@ def login(): f'Неудачная попытка входа: {username}' ) flash('Неверный логин или пароль', 'danger') - return render_template('login.html') @@ -538,11 +582,98 @@ def logout(): return redirect(url_for('index')) +@app.route('/profile', methods=['GET', 'POST']) +@login_required +def profile(): + user_id = session.get('user_id') + user = database.get_user_by_id(user_id) + + if not user: + session.clear() + flash('Ваша сессия устарела. Пожалуйста, войдите заново.', 'warning') + return redirect(url_for('login')) + + if request.method == 'POST': + email = request.form.get('email', '').strip() + jabber = request.form.get('jabber', '').strip() + + if email and not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', email): + flash('Неверный формат email', 'danger') + return render_template('profile.html', user=user) + + if jabber and not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', jabber): + flash('Неверный формат Jabber ID', 'danger') + return render_template('profile.html', user=user) + + user.email = email if email else None + user.jabber = jabber if jabber else None + user.updated_at = datetime.now() + + user.notify_email = 'notify_email' in request.form + user.notify_jabber = 'notify_jabber' in request.form + + database.db.session.commit() + + database.log_security_event( + request.remote_addr, + 'profile_updated', + f'Обновлен профиль пользователя {user.username}', + user_id=user.id + ) + + flash('Профиль успешно обновлен!', 'success') + return redirect(url_for('profile')) + + return render_template('profile.html', user=user) + + +@app.route('/profile/change-password', methods=['POST']) +@login_required +def profile_change_password(): + user_id = session.get('user_id') + user = database.get_user_by_id(user_id) + + if not user: + session.clear() + flash('Ваша сессия устарела.', 'warning') + return redirect(url_for('login')) + + old_password = request.form.get('old_password', '') + new_password = request.form.get('new_password', '') + confirm_password = request.form.get('confirm_password', '') + + if not user.check_password(old_password): + flash('Неверный текущий пароль', 'danger') + return redirect(url_for('profile')) + + if len(new_password) < 6: + flash('Новый пароль должен содержать минимум 6 символов', 'danger') + return redirect(url_for('profile')) + + if new_password != confirm_password: + flash('Пароли не совпадают', 'danger') + return redirect(url_for('profile')) + + user.set_password(new_password) + database.db.session.commit() + + database.log_security_event( + request.remote_addr, + 'password_change', + f'Смена пароля пользователем {user.username}', + user_id=user.id + ) + + flash('Пароль успешно изменен!', 'success') + return redirect(url_for('profile')) + + @app.route('/create', methods=['POST']) @login_required @limiter.limit("30 per minute") def create_secret(): secret_text = request.form.get('secret', '').strip() + user_note = request.form.get('user_note', '').strip() if not secret_text: flash('Пожалуйста, введите секрет', 'danger') @@ -566,7 +697,7 @@ def create_secret(): flash('Ваша сессия устарела. Пожалуйста, войдите заново.', 'warning') return redirect(url_for('login')) - secret_id = store_secret(secret_text, client_info, user_id) + secret_id = store_secret(secret_text, client_info, user_id, request_note=user_note) view_url = url_for('view_secret', secret_id=secret_id, _external=True) database.log_security_event( @@ -750,7 +881,7 @@ def request_secret_submit(token): if clean_note and len(clean_note) > 500: clean_note = clean_note[:500] - secret_id = store_secret(secret_text, client_info, int(user_id), clean_note) + secret_id = store_secret(secret_text, client_info, int(user_id), request_note=clean_note) view_url = url_for('view_secret', secret_id=secret_id, _external=True) redis_client.delete(request_key) @@ -764,6 +895,30 @@ def request_secret_submit(token): user_id=int(user_id) ) + if user and user.email and mailer.is_enabled() and user.notify_email: + try: + from mailer import send_secret_received_email + send_secret_received_email( + to_email=user.email, + username=user.username, + note=clean_note + ) + print(f"Уведомление о секрете отправлено на {user.email}") + except Exception as e: + print(f"Ошибка отправки уведомления: {e}") + else: + # Логируем причину, почему письмо не отправлено + reasons = [] + if not user: + reasons.append("пользователь не найден") + elif not user.email: + reasons.append("email не указан") + elif not mailer.is_enabled(): + reasons.append("почта отключена") + elif not user.notify_email: + reasons.append("отключены уведомления в профиле") + print(f"ℹ️ Письмо не отправлено: {', '.join(reasons)}") + flash('Секрет успешно отправлен! Получатель сможет его просмотреть.', 'success') return render_template('request_secret_submit_done.html', @@ -831,7 +986,6 @@ def my_requests(): return render_template('my_requests.html', requests=requests) - @app.route('/request//delete', methods=['POST']) @login_required def request_secret_delete(token): @@ -979,8 +1133,7 @@ def admin_panel(): def admin_create_user(): if request.method == 'POST': username = request.form.get('username', '').strip() - password = request.form.get('password', '') - confirm_password = request.form.get('confirm_password', '') + email = request.form.get('email', '').strip() role = request.form.get('role', 'user') if not username or len(username) < 3: @@ -991,12 +1144,16 @@ def admin_create_user(): flash('Логин может содержать только буквы, цифры и знак подчеркивания', 'danger') return render_template('admin_user_create.html') - if not password or len(password) < 6: - flash('Пароль должен содержать минимум 6 символов', 'danger') + if not email: + flash('Email обязателен для заполнения', 'danger') return render_template('admin_user_create.html') - if password != confirm_password: - flash('Пароли не совпадают', 'danger') + if not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', email): + flash('Неверный формат email', 'danger') + return render_template('admin_user_create.html') + + if role not in ['user', 'admin']: + flash('Неверная роль', 'danger') return render_template('admin_user_create.html') existing = database.get_user(username) @@ -1004,17 +1161,69 @@ def admin_create_user(): flash(f'Пользователь "{username}" уже существует', 'danger') return render_template('admin_user_create.html') - if database.create_user(username, password, role): - database.log_security_event( - request.remote_addr, - 'user_created', - f'Создан пользователь: {username} (role: {role}) администратором {session.get("username")}', - user_id=session.get('user_id') - ) - flash(f'Пользователь "{username}" успешно создан с ролью {role}', 'success') - return redirect(url_for('admin_panel')) - else: + password = generate_secure_password() + print(f"Сгенерирован пароль для {username}: {password}") + + if not database.create_user(username, password, role): flash('Ошибка при создании пользователя', 'danger') + return render_template('admin_user_create.html') + + new_user = database.get_user(username) + new_user_id = new_user.id if new_user else None + print(f"✅ Пользователь {username} создан с ID: {new_user_id}") + + admin_id = session.get('user_id') + admin_username = session.get('username', 'admin') + + secret_text = f"""Пароль для {username}: + +{password} + +Логин: {username} + +Отправлено на: {email} + +ВАЖНО: +• Пароль был отправлен пользователю в виде одноразовой ссылки +• После прочтения ссылка станет недействительной + +Создано: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} +Администратор: {admin_username} + """ + + client_info = { + 'ip': request.remote_addr, + 'user_agent': request.headers.get('User-Agent', 'Unknown'), + 'session_id': session.get('session_id', '') + } + + secret_id = store_secret( + plaintext=secret_text, + client_info=client_info, + user_id=admin_id, # ← ID администратора! + request_note=f"Пароль для {username} (отправлен на {email})" + ) + + view_url = url_for('view_secret', secret_id=secret_id, _external=True) + + if mailer.is_enabled(): + try: + mailer.send_user_created_email(email, username, view_url) + flash(f'Письмо с паролем отправлено на {email}', 'success') + except Exception as e: + flash(f'Ошибка отправки письма: {str(e)}', 'danger') + print(f"Пароль для {username}: {password}") + else: + flash(f'Почта отключена. Пароль для {username}: {password} (сохраните его!)', 'warning') + + database.log_security_event( + request.remote_addr, + 'user_created_with_email', + f'Создан пользователь: {username} (role: {role}) администратором {admin_username}. Пароль отправлен на {email}', + user_id=admin_id + ) + flash(f'Пользователь "{username}" успешно создан! Пароль отправлен на email.', 'success') + return redirect(url_for('admin_panel')) return render_template('admin_user_create.html') @@ -1250,6 +1459,10 @@ def health_check(): }), 500 +with app.app_context(): + run_migrations() + create_admin() + if __name__ == '__main__': app.run( debug=app.config['DEBUG'], diff --git a/migrate.py b/migrate.py new file mode 100644 index 0000000..1068979 --- /dev/null +++ b/migrate.py @@ -0,0 +1,42 @@ +from sqlalchemy import inspect, text +from database import db + + +def run_migrations(): + db.create_all() + print("Таблицы созданы (или уже существуют)") + _add_profile_columns() + print("Все миграции применены!") + + +def _add_profile_columns(): + inspector = inspect(db.engine) + existing_columns = [col['name'] for col in inspector.get_columns('users')] + + columns_to_add = { + 'email': 'VARCHAR(255) NULL', + 'jabber': 'VARCHAR(255) NULL', + 'updated_at': 'DATETIME NULL', + 'notify_email': 'BOOLEAN DEFAULT 0', + 'notify_jabber': 'BOOLEAN DEFAULT 0', + } + + added = [] + for col_name, col_type in columns_to_add.items(): + if col_name not in existing_columns: + print(f"Добавляем колонку: {col_name}") + db.session.execute(text(f"ALTER TABLE users ADD COLUMN {col_name} {col_type}")) + added.append(col_name) + + if added: + db.session.commit() + print(f"Добавлены колонки: {', '.join(added)}") + else: + print("ℹВсе колонки профиля уже существуют") + + +if __name__ == '__main__': + from main import app + + with app.app_context(): + run_migrations() \ No newline at end of file diff --git a/templates/admin.html b/templates/admin.html index 30d0e7b..2cf1687 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -170,6 +170,7 @@ ID Имя пользователя + Email Роль Создан Последний вход @@ -182,6 +183,7 @@ {{ user.id }} {{ user.username }} + {{ user.email or '-' }}
@@ -227,6 +229,9 @@ + + +
diff --git a/templates/admin_user_create.html b/templates/admin_user_create.html index 798d608..b8da051 100644 --- a/templates/admin_user_create.html +++ b/templates/admin_user_create.html @@ -14,23 +14,17 @@
- +
Только буквы, цифры и знак подчеркивания
- - -
Минимум 6 символов
-
- -
- - + + +
На этот адрес будет отправлен пароль
@@ -39,9 +33,13 @@ -
Администратор может управлять пользователями и просматривать логи - секретов. -
+
Администратор может управлять пользователями и просматривать логи секретов.
+
+ +
+ + Пароль будет сгенерирован автоматически и отправлен на указанный email + в виде секретной ссылки для безопасной передачи.
diff --git a/templates/base.html b/templates/base.html index de9daea..37193e5 100644 --- a/templates/base.html +++ b/templates/base.html @@ -35,7 +35,9 @@ Админка {% endif %} | - {{ current_user }} + + {{ current_user }} + Выйти {% else %} Войти @@ -57,7 +59,9 @@ {% block content %}{% endblock %}
diff --git a/templates/emails/base_email.html b/templates/emails/base_email.html new file mode 100644 index 0000000..aa027a7 --- /dev/null +++ b/templates/emails/base_email.html @@ -0,0 +1,38 @@ + + + + + {% block title %}SecretText{% endblock %} + + + + + + + +
+ + + + + + + + + + + + + + +
+ SECRETTEXT +
+ {% block content %}{% endblock %} +
+ SecretText +
+
+ + + \ No newline at end of file diff --git a/templates/emails/secret_notification.html b/templates/emails/secret_notification.html new file mode 100644 index 0000000..28dba33 --- /dev/null +++ b/templates/emails/secret_notification.html @@ -0,0 +1,21 @@ +{% extends "emails/base_email.html" %} + +{% block content %} + +
+ Кто-то отправил вам секрет. +
+ +
+ МОИ СЕКРЕТЫ +
+ +
+ {{ base_url }}/my-secrets +
+ +
+ Секрет доступен для одноразового просмотра. +
+ +{% endblock %} \ No newline at end of file diff --git a/templates/emails/secret_received.html b/templates/emails/secret_received.html new file mode 100644 index 0000000..bb57a71 --- /dev/null +++ b/templates/emails/secret_received.html @@ -0,0 +1,27 @@ +{% extends "emails/base_email.html" %} + +{% block content %} + +
+ Вам отправили секрет по запросу. +
+ +{% if note %} +
+ Примечание: {{ note }} +
+{% endif %} + +
+ МОИ СЕКРЕТЫ +
+ +
+ {{ base_url }}/my-secrets +
+ +
+ Секрет доступен для одноразового просмотра. +
+ +{% endblock %} \ No newline at end of file diff --git a/templates/emails/secret_viewed.html b/templates/emails/secret_viewed.html new file mode 100644 index 0000000..462443d --- /dev/null +++ b/templates/emails/secret_viewed.html @@ -0,0 +1,17 @@ +{% extends "emails/base_email.html" %} + +{% block content %} + +
+ Ваш секрет был просмотрен. +
+ +
+ СОЗДАТЬ НОВЫЙ +
+ +
+ {{ base_url }} +
+ +{% endblock %} \ No newline at end of file diff --git a/templates/emails/user_created.html b/templates/emails/user_created.html new file mode 100644 index 0000000..3ac40dd --- /dev/null +++ b/templates/emails/user_created.html @@ -0,0 +1,29 @@ +{% extends "emails/base_email.html" %} + +{% block content %} + +
+ Для вас создан аккаунт в SecretText. +
+ +
+ Логин: {{ username }} +
+ +
+ Ссылка для получения пароля: +
+ +
+ ПОЛУЧИТЬ ПАРОЛЬ +
+ +
+ {{ view_url }} +
+ +
+ Ссылка действительна 24 часа. +
+ +{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 23789bb..4257cf8 100644 --- a/templates/index.html +++ b/templates/index.html @@ -8,14 +8,22 @@
+ placeholder="Введите ваш секрет..." required>
Максимум {{ config.MAX_SECRET_LENGTH }} символов. Автоудаление через 24 часа или после просмотра.
+ + +
+ + +
Не обязательно.
+
+
- {% endblock %} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html index a1ad56e..17cd97f 100644 --- a/templates/login.html +++ b/templates/login.html @@ -8,7 +8,7 @@

Вход в систему

-

Войдите под своей учётной записью

+
@@ -22,7 +22,7 @@
- Если у вас нет аккаунта, обратитесь к администратору +
diff --git a/templates/profile.html b/templates/profile.html new file mode 100644 index 0000000..e607d88 --- /dev/null +++ b/templates/profile.html @@ -0,0 +1,127 @@ +{% extends "base.html" %} + +{% block title %}Профиль пользователя{% endblock %} + +{% block content %} +
+
+
+
+
+ +
+ +
{{ user.username }}
+

Роль: {{ user.role }}

+ +
+ {% if user.is_active %} + Активен + {% else %} + Деактивирован + {% endif %} +
+ +
+
Регистрация: {{ user.created_at|format_datetime }}
+
Последний вход: {{ user.last_login|format_datetime or 'Никогда' }}
+ {% if user.updated_at %} +
Обновлен: {{ user.updated_at|format_datetime }}
+ {% endif %} +
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+ + +
+ + + +
+ +
+
Настройки уведомлений
+ +
+
+ + +
+
+ + + +
+
+ + +
+
+ + +
+ Для безопасности используйте сложный пароль (не менее 8 символов, + с буквами разного регистра и цифрами) +
+ +
+ + +
+ +
+ + +
Минимум 6 символов
+
+ +
+ + +
+ + +
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file