Первый коммит: загрузка проекта SecretText

This commit is contained in:
2026-09-05 15:35:25 +03:00
commit faa6bbc574
36 changed files with 5333 additions and 0 deletions
+365
View File
@@ -0,0 +1,365 @@
{% extends "base.html" %}
{% block content %}
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">Панель администратора</h2>
<div>
<span class="text-muted me-2">{{ session.username }}</span>
<a href="{{ url_for('admin_logout') }}" class="btn btn-sm btn-outline-secondary">Выйти</a>
</div>
</div>
<div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-3">
<div class="card">
<div class="card-body">
<div class="text-uppercase text-muted small">Всего секретов</div>
<div class="h5 mb-0">{{ stats.total }}</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card">
<div class="card-body">
<div class="text-uppercase text-muted small">Просмотрено</div>
<div class="h5 mb-0">{{ stats.viewed }} ({{ stats.viewed_percent }}%)</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card">
<div class="card-body">
<div class="text-uppercase text-muted small">Ожидают просмотра</div>
<div class="h5 mb-0">{{ stats.not_viewed }}</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card">
<div class="card-body">
<div class="text-uppercase text-muted small">Заблокированные IP</div>
<div class="h5 mb-0">{{ stats.blocked_ips }}</div>
</div>
</div>
</div>
</div>
<ul class="nav nav-tabs mb-4" id="adminTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="secrets-tab" data-bs-toggle="tab" data-bs-target="#secrets"
type="button" role="tab" aria-controls="secrets" aria-selected="true">
Секреты <span class="badge bg-secondary">{{ logs|length }}</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="users-tab" data-bs-toggle="tab" data-bs-target="#users" type="button"
role="tab" aria-controls="users" aria-selected="false">
Пользователи <span class="badge bg-secondary">{{ users|length }}</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="security-tab" data-bs-toggle="tab" data-bs-target="#security" type="button"
role="tab" aria-controls="security" aria-selected="false">
Безопасность
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="blocked-tab" data-bs-toggle="tab" data-bs-target="#blocked" type="button"
role="tab" aria-controls="blocked" aria-selected="false">
Заблокированные <span class="badge bg-secondary">{{ blocked_ips|length }}</span>
</button>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="secrets" role="tabpanel" aria-labelledby="secrets-tab">
<div class="card">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<span>Фильтр:</span>
<div>
<a href="{{ url_for('admin_panel') }}?filter=all"
class="btn btn-sm btn-outline-secondary {% if filter == 'all' or not filter %}active{% endif %}">Все</a>
<a href="{{ url_for('admin_panel') }}?filter=pending"
class="btn btn-sm btn-outline-secondary {% if filter == 'pending' %}active{% endif %}">Ожидают</a>
<a href="{{ url_for('admin_panel') }}?filter=viewed"
class="btn btn-sm btn-outline-secondary {% if filter == 'viewed' %}active{% endif %}">Просмотрены</a>
</div>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>ID</th>
<th>Создан</th>
<th>IP создания</th>
<th>Пользователь</th>
<th>Просмотрен</th>
<th>IP просмотра</th>
<th>Статус</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr>
<td><code class="bg-light p-1 rounded">{{ log.secret_id[:12] }}...</code></td>
<td>{{ log.created_at|format_datetime if log.created_at else '-' }}</td>
<td><code>{{ log.created_ip or '-' }}</code></td>
<td>
{% if log.user %}
<span class="badge bg-secondary">{{ log.user.username }}</span>
{% else %}
<span class="badge bg-secondary">Аноним</span>
{% endif %}
</td>
<td>{{ log.viewed_at|format_datetime if log.viewed_at else '-' }}</td>
<td><code>{{ log.viewed_ip or '-' }}</code></td>
<td>
{% if log.is_viewed %}
<span class="badge bg-secondary">Просмотрен</span>
{% else %}
<span class="badge bg-secondary">Ожидает</span>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="7" class="text-center py-4 text-muted">Нет записей</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if logs|length >= per_page %}
<div class="p-3">
<nav>
<ul class="pagination justify-content-center mb-0">
<li class="page-item {% if page <= 1 %}disabled{% endif %}">
<a class="page-link"
href="{{ url_for('admin_panel', page=page-1, filter=filter) if page > 1 else '#' }}">Назад</a>
</li>
<li class="page-item active">
<span class="page-link">{{ page }}</span>
</li>
<li class="page-item">
<a class="page-link"
href="{{ url_for('admin_panel', page=page+1, filter=filter) }}">Вперед</a>
</li>
</ul>
</nav>
</div>
{% endif %}
</div>
</div>
</div>
<div class="tab-pane fade" id="users" role="tabpanel" aria-labelledby="users-tab">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Управление пользователями</span>
<a href="{{ url_for('admin_create_user') }}" class="btn btn-sm btn-secondary">Создать
пользователя</a>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>ID</th>
<th>Имя пользователя</th>
<th>Роль</th>
<th>Создан</th>
<th>Последний вход</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.id }}</td>
<td><strong>{{ user.username }}</strong></td>
<td>
<form action="{{ url_for('admin_change_role', user_id=user.id) }}" method="POST"
class="d-flex">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<select name="role" class="form-select form-select-sm me-1"
style="width: auto;">
<option value="user" {% if user.role==
'user' %}selected{% endif %}>user</option>
<option value="admin" {% if user.role==
'admin' %}selected{% endif %}>admin</option>
</select>
<button type="submit" class="btn btn-sm btn-outline-secondary">Сохранить
</button>
</form>
</td>
<td>{{ user.created_at|format_datetime if user.created_at else '-' }}</td>
<td>{{ user.last_login|format_datetime if user.last_login else 'Никогда' }}</td>
<td>
{% if user.is_active %}
<span class="badge bg-secondary">Активен</span>
{% else %}
<span class="badge bg-secondary">Деактивирован</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm">
<a href="{{ url_for('admin_toggle_user', user_id=user.id) }}"
class="btn btn-outline-secondary">
{% if user.is_active %}Деактивировать{% else %}Активировать{% endif %}
</a>
<a href="{{ url_for('admin_delete_user', user_id=user.id) }}"
class="btn btn-outline-secondary"
onclick="return confirm('Удалить пользователя?')">Удалить</a>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="7" class="text-center py-4 text-muted">Нет пользователей</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="card mt-4">
<div class="card-header">Сменить пароль</div>
<div class="card-body">
<form action="{{ url_for('admin_change_password') }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="row">
<div class="col-md-4">
<div class="mb-3">
<label for="old_password" class="form-label small">Текущий пароль</label>
<input type="password" name="old_password" id="old_password"
class="form-control form-control-sm" required>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="new_password" class="form-label small">Новый пароль</label>
<input type="password" name="new_password" id="new_password"
class="form-control form-control-sm" required minlength="6">
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="confirm_password" class="form-label small">Подтвердите пароль</label>
<input type="password" name="confirm_password" id="confirm_password"
class="form-control form-control-sm" required>
</div>
</div>
</div>
<button type="submit" class="btn btn-sm btn-secondary">Сменить пароль</button>
</form>
</div>
</div>
</div>
<div class="tab-pane fade" id="security" role="tabpanel" aria-labelledby="security-tab">
<div class="card">
<div class="card-header">Журнал событий безопасности</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Время</th>
<th>IP</th>
<th>Тип события</th>
<th>Детали</th>
<th>Пользователь</th>
</tr>
</thead>
<tbody>
{% for log in security_logs %}
<tr>
<td>{{ log.timestamp|format_datetime_seconds if log.timestamp else '-' }}</td>
<td><code>{{ log.ip }}</code></td>
<td><span class="badge bg-secondary">{{ log.event_type }}</span></td>
<td>{{ log.details or '-' }}</td>
<td>
{% if log.user %}
<span class="badge bg-secondary">{{ log.user.username }}</span>
{% else %}
<span class="badge bg-secondary">Аноним</span>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="text-center py-4 text-muted">Нет событий безопасности</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="blocked" role="tabpanel" aria-labelledby="blocked-tab">
<div class="card">
<div class="card-header">Заблокированные IP-адреса</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>IP адрес</th>
<th>Тип блокировки</th>
<th>Осталось времени</th>
<th>Количество блокировок</th>
<th>Действие</th>
</tr>
</thead>
<tbody>
{% for ip in blocked_ips %}
<tr>
<td><code>{{ ip.ip }}</code></td>
<td>
{% if ip.permanent %}
<span class="badge bg-secondary">Постоянная</span>
{% else %}
<span class="badge bg-secondary">Временная</span>
{% endif %}
</td>
<td>
{% if ip.permanent %}
{% else %}
{{ ip.remaining_minutes }} мин.
{% endif %}
</td>
<td>{{ ip.block_count }}</td>
<td>
<div class="btn-group btn-group-sm">
<a href="{{ url_for('admin_unblock', ip=ip.ip) }}"
class="btn btn-outline-secondary">Разблокировать</a>
{% if not ip.permanent %}
<a href="{{ url_for('admin_block', ip=ip.ip) }}"
class="btn btn-outline-secondary"
onclick="return confirm('Заблокировать IP постоянно?')">Заблокировать</a>
{% endif %}
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="text-center py-4 text-muted">Нет заблокированных IP-адресов</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
+28
View File
@@ -0,0 +1,28 @@
{% extends "base.html" %}
{% block title %}Вход в админку{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-body">
<h5 class="card-title text-center">Вход в панель управления</h5>
<form method="POST" action="{{ url_for('admin_login') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="mb-3">
<label for="username" class="form-label">Логин</label>
<input type="text" class="form-control" id="username" name="username" required autofocus>
</div>
<div class="mb-3">
<label for="password" class="form-label">Пароль</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-secondary w-100">Войти</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
+51
View File
@@ -0,0 +1,51 @@
{% extends "base.html" %}
{% block title %}Создание пользователя{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-normal">Создание пользователя</h4>
<a href="{{ url_for('admin_panel') }}" class="btn btn-sm btn-outline-secondary">← Назад</a>
</div>
<div class="card">
<div class="card-body">
<form method="POST" action="{{ url_for('admin_create_user') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="mb-3">
<label for="username" class="form-label">Логин</label>
<input type="text" class="form-control" id="username" name="username"
placeholder="Введите логин (минимум 3 символа)" required minlength="3">
<div class="form-text text-secondary">Только буквы, цифры и знак подчеркивания</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">Пароль</label>
<input type="password" class="form-control" id="password" name="password"
placeholder="Введите пароль (минимум 6 символов)" required minlength="6">
<div class="form-text text-secondary">Минимум 6 символов</div>
</div>
<div class="mb-3">
<label for="confirm_password" class="form-label">Подтверждение пароля</label>
<input type="password" class="form-control" id="confirm_password" name="confirm_password"
placeholder="Повторите пароль" required>
</div>
<div class="mb-3">
<label for="role" class="form-label">Роль</label>
<select class="form-control" id="role" name="role">
<option value="user">Пользователь</option>
<option value="admin">Администратор</option>
</select>
<div class="form-text text-secondary">Администратор может управлять пользователями и просматривать логи
секретов.
</div>
</div>
<button type="submit" class="btn btn-secondary w-100">Создать пользователя</button>
</form>
</div>
</div>
{% endblock %}
+69
View File
@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}SecretText{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='icons/bootstrap-icons.css') }}">
<link rel="icon" type="image/svg+xml"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23333'%3E%3Cpath fill-rule='evenodd' d='m4.736 1.968-.892 3.269-.014.058C2.113 5.568 1 6.006 1 6.5 1 7.328 4.134 8 8 8s7-.672 7-1.5c0-.494-1.113-.932-2.83-1.205l-.014-.058-.892-3.27c-.146-.533-.698-.849-1.239-.734C9.411 1.363 8.62 1.5 8 1.5s-1.411-.136-2.025-.267c-.541-.115-1.093.2-1.239.735m.015 3.867a.25.25 0 0 1 .274-.224c.9.092 1.91.143 2.975.143a30 30 0 0 0 2.975-.143.25.25 0 0 1 .05.498c-.918.093-1.944.145-3.025.145s-2.107-.052-3.025-.145a.25.25 0 0 1-.224-.274M3.5 10h2a.5.5 0 0 1 .5.5v1a1.5 1.5 0 0 1-3 0v-1a.5.5 0 0 1 .5-.5m-1.5.5q.001-.264.085-.5H2a.5.5 0 0 1 0-1h3.5a1.5 1.5 0 0 1 1.488 1.312 3.5 3.5 0 0 1 2.024 0A1.5 1.5 0 0 1 10.5 9H14a.5.5 0 0 1 0 1h-.085q.084.236.085.5v1a2.5 2.5 0 0 1-5 0v-.14l-.21-.07a2.5 2.5 0 0 0-1.58 0l-.21.07v.14a2.5 2.5 0 0 1-5 0zm8.5-.5h2a.5.5 0 0 1 .5.5v1a1.5 1.5 0 0 1-3 0v-1a.5.5 0 0 1 .5-.5'/%3E%3C/svg%3E">
{% block extra_head %}{% endblock %}
</head>
<body>
<div class="container py-4">
<header class="pb-3 mb-4 border-bottom">
<div class="d-flex flex-wrap align-items-center justify-content-between">
<div>
<h1 class="display-6 fw-normal mb-0">
<a href="{{ url_for('index') }}" class="text-decoration-none text-dark">SecretText</a>
</h1>
<p class="text-secondary mb-0 small">Безопасная передача одноразовых секретов</p>
</div>
<nav class="d-flex flex-wrap align-items-center gap-2">
{% if is_authenticated %}
<a href="{{ url_for('index') }}" class="btn btn-sm btn-outline-secondary">Главная</a>
<a href="{{ url_for('my_secrets') }}" class="btn btn-sm btn-outline-secondary">Мои секреты</a>
<a href="{{ url_for('request_secret_form') }}" class="btn btn-sm btn-outline-secondary">Запросить
секрет</a>
<a href="{{ url_for('my_requests') }}" class="btn btn-sm btn-outline-secondary">Мои запросы</a>
{% if is_admin %}
<a href="{{ url_for('admin_panel') }}" class="btn btn-sm btn-secondary">Админка</a>
{% endif %}
<span class="text-secondary small mx-1">|</span>
<span class="text-secondary small me-1">{{ current_user }}</span>
<a href="{{ url_for('logout') }}" class="btn btn-sm btn-outline-danger">Выйти</a>
{% else %}
<a href="{{ url_for('login') }}" class="btn btn-sm btn-secondary">Войти</a>
{% endif %}
</nav>
</div>
</header>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
<footer class="pt-3 mt-4 border-top text-center text-secondary">
<small>Все данные шифруются и автоматически удаляются через 24 часа или после первого просмотра</small>
</footer>
</div>
{% block scripts %}{% endblock %}
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js.js') }}"
integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
crossorigin="anonymous"></script>
</body>
</html>
+21
View File
@@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block title %}Доступ заблокирован{% endblock %}
{% block content %}
<div class="card border-danger">
<div class="card-body text-center py-5">
<h3 class="text-danger fw-normal">{% if permanent %}Доступ запрещён{% else %}Слишком много попыток{% endif
%}</h3>
<p class="text-secondary my-4">{{ reason }}</p>
{% if not permanent and remaining %}
<p class="text-secondary">Подождите: <strong>{{ (remaining // 60) }} минут {{ (remaining % 60) }}
секунд</strong></p>
<p class="small text-secondary">Попробуйте обновить страницу после ожидания</p>
{% endif %}
<a href="{{ url_for('index') }}" class="btn btn-outline-secondary mt-3">Вернуться на главную</a>
{% if permanent %}<p class="small text-secondary mt-3">Для разблокировки обратитесь к администратору</p>{% endif
%}
</div>
</div>
{% endblock %}
+167
View File
@@ -0,0 +1,167 @@
{% extends "base.html" %}
{% block content %}
<div class="card border-secondary">
<div class="card-body text-center">
<h5 class="card-title">Секрет создан!</h5>
<p class="text-secondary">Ссылка для получения секрета готова.</p>
<div class="alert alert-secondary text-start position-relative">
<label class="form-label fw-bold small mb-1">
<i class="bi bi-clipboard me-1"></i> Скопируйте этот текст и отправьте получателю:
</label>
<div id="fullMessageBlock" class="bg-white p-2 rounded border"
style="font-family: monospace; font-size: 0.95rem; word-wrap: break-word; user-select: all; cursor: text; line-height: 1.6; white-space: pre-line;">Перейдите по ссылке, чтобы прочитать сообщение.
Ссылка действительна 24 часа.
После открытия доступ к данным будет потерян навсегда.
Повторно запросить или восстановить ссылку невозможно.
{{ link }}
</div>
<div class="mt-2 text-end">
<button class="btn btn-sm btn-outline-secondary js-copy-trigger" type="button"
data-target="fullMessageBlock">
<i class="bi bi-clipboard-plus"></i> Скопировать всё сообщение
</button>
</div>
</div>
<div class="alert alert-secondary text-start position-relative">
<label class="form-label fw-bold small mb-1">
<i class="bi bi-clipboard me-1"></i> Скопируйте этот текст и отправьте получателю:
</label>
<div id="fullMessageBlock1" class="bg-white p-2 rounded border"
style="font-family: monospace; font-size: 0.95rem; word-wrap: break-word; user-select: all; cursor: text; line-height: 1.6; white-space: pre-line;">Follow the link to read the message.
The link is valid for 24 hours.
Once opened, access to the data will be lost forever.
It is impossible to request the link again or restore it.
{{ link }}
</div>
<div class="mt-2 text-end">
<button class="btn btn-sm btn-outline-secondary js-copy-trigger" type="button"
data-target="fullMessageBlock1">
<i class="bi bi-clipboard-plus"></i> Скопировать всё сообщение
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label small text-secondary">Только ссылка:</label>
<div class="input-group">
<input type="text" class="form-control" id="secretLink" value="{{ link }}" readonly>
<button class="btn btn-secondary" type="button" id="copyBtn">
<i class="bi bi-clipboard"></i> Копировать
</button>
</div>
</div>
<div class="row g-2">
<div class="col">
<a href="{{ link }}" class="btn btn-secondary w-100" target="_blank" rel="noopener noreferrer">Открыть
секрет</a>
</div>
<div class="col">
<a href="{{ url_for('index') }}" class="btn btn-outline-secondary w-100">Создать ещё</a>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function () {
var copyTriggers = document.querySelectorAll('.js-copy-trigger');
copyTriggers.forEach(function (btn) {
btn.addEventListener('click', function () {
var targetId = this.getAttribute('data-target');
var targetContainer = document.getElementById(targetId);
if (targetContainer) {
var textToCopy = targetContainer.innerText || targetContainer.textContent;
textToCopy = textToCopy.trim();
var isEnglish = targetId.includes('1');
var successMsg = isEnglish ? 'English text copied!' : 'Русский текст скопирован!';
handleCopy(textToCopy, this, successMsg);
}
});
});
var copyLinkBtn = document.getElementById('copyBtn');
var input = document.getElementById('secretLink');
if (copyLinkBtn && input) {
copyLinkBtn.addEventListener('click', function () {
var textToCopy = input.value.trim();
handleCopy(textToCopy, this, 'Ссылка скопирована! / Link copied!');
});
}
if (!window.handleCopyRegistered) {
window.handleCopyRegistered = true;
window.handleCopy = function (text, buttonElement, successMessage) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function () {
processVisualSuccess(buttonElement, successMessage);
}).catch(function () {
executeFallback(text, buttonElement, successMessage);
});
} else {
executeFallback(text, buttonElement, successMessage);
}
}
window.executeFallback = function (text, buttonElement, successMessage) {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try {
var successful = document.execCommand('copy');
if (successful) {
processVisualSuccess(buttonElement, successMessage);
} else {
alert('Не удалось скопировать. Выделите текст руками и нажмите Ctrl+C');
}
} catch (err) {
alert('Не удалось скопировать. Выделите текст руками и нажмите Ctrl+C');
}
document.body.removeChild(ta);
}
window.processVisualSuccess = function (btn, message) {
if (typeof showToast === 'function') {
showToast(message, 'success');
}
var originalHtml = btn.innerHTML;
var isSmallBtn = btn.classList.contains('btn-sm');
btn.innerHTML = '<i class="bi bi-check-lg"></i> Скопировано!';
btn.classList.remove('btn-secondary', 'btn-outline-secondary');
btn.classList.add('btn-success');
setTimeout(function () {
btn.innerHTML = originalHtml;
btn.classList.remove('btn-success');
if (isSmallBtn) {
btn.classList.add('btn-outline-secondary');
} else {
btn.classList.add('btn-secondary');
}
}, 2000);
}
}
});
</script>
{% endblock %}
+11
View File
@@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block content %}
<div class="card border-danger">
<div class="card-body text-center py-5">
<h3 class="text-danger fw-normal">Ошибка</h3>
<p class="text-secondary my-4">{{ error or 'Произошла непредвиденная ошибка' }}</p>
<a href="{{ url_for('index') }}" class="btn btn-secondary">На главную</a>
</div>
</div>
{% endblock %}
+17
View File
@@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block content %}
<div class="card border-danger">
<div class="card-body text-center py-5">
<h3 class="text-danger fw-normal">Секрет недоступен</h3>
<div class="my-4 text-secondary">
<ul class="list-unstyled">
<li>Ссылка уже была использована</li>
<li>Истёк срок действия (24 часа)</li>
<li>Неверная ссылка</li>
</ul>
</div>
<a href="{{ url_for('index') }}" class="btn btn-secondary">Создать новый секрет</a>
</div>
</div>
{% endblock %}
+21
View File
@@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block content %}
<div class="card">
<div class="card-body">
<h5 class="card-title">Ваш секрет</h5>
<form method="POST" action="{{ url_for('create_secret') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="mb-3">
<textarea class="form-control" id="secret" name="secret" rows="6"
placeholder="" required></textarea>
<div class="form-text text-secondary">Максимум {{ config.MAX_SECRET_LENGTH }} символов. Автоудаление
через 24 часа или после просмотра.
</div>
</div>
<button type="submit" class="btn btn-secondary w-100">Создать ссылку</button>
</form>
</div>
</div>
{% endblock %}
+31
View File
@@ -0,0 +1,31 @@
{% extends "base.html" %}
{% block title %}Вход{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-body">
<h4 class="card-title text-center mb-4">Вход в систему</h4>
<p class="text-secondary text-center mb-3">Войдите под своей учётной записью</p>
<form method="POST" action="{{ url_for('login') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="mb-3">
<label for="username" class="form-label">Логин</label>
<input type="text" class="form-control" id="username" name="username" required autofocus>
</div>
<div class="mb-3">
<label for="password" class="form-label">Пароль</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-secondary w-100">Войти</button>
</form>
<div class="text-center mt-3">
<span class="text-secondary small">Если у вас нет аккаунта, обратитесь к администратору</span>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
+97
View File
@@ -0,0 +1,97 @@
{% extends "base.html" %}
{% block title %}Мои запросы{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-normal">Мои запросы на секрет</h4>
<a href="{{ url_for('request_secret_form') }}" class="btn btn-secondary">
<i class="bi"></i> Создать запрос
</a>
</div>
{% if requests %}
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>ID</th>
<th>Примечание</th>
<th>Создан</th>
<th>Срок действия</th>
<th>Статус</th>
<th class="text-center">Действия</th>
</tr>
</thead>
<tbody>
{% for req in requests %}
<tr>
<td>
<code class="text-muted small">{{ req.token[:18] }}...</code>
</td>
<td style="white-space: pre-wrap; word-break: break-word; max-width: 150px;">{% if req.note %}<span
class="text-muted small">{{ req.note }}</span>{% else %}<span class="text-muted fst-italic small">Без примечания</span>{%
endif %}
</td>
<td>
<span class="text-muted small">{{ req.created_at|format_datetime }}</span>
</td>
<td>
{% if req.ttl_seconds > 0 %}<span class="text-muted small">{{ req.ttl_hours }} ч.</span> {% else %}<span
class="text-muted small">Истек</span>{% endif %}
</td>
<td>
{% if req.ttl_seconds > 0 %}<span class="badge bg-success">Активен</span>{% else %}<span
class="badge bg-secondary">Истек</span>{% endif %}
</td>
<td class="text-center">
<div class="btn-group btn-group-sm"> {% if req.ttl_seconds > 0 %} <a href="{{ req.link }}"
target="_blank"
rel="noopener noreferrer"
class="btn btn-outline-secondary"
title="Открыть ссылку"> <i
class="bi bi-box-arrow-up-right"></i></a>
<button type="button" class="btn btn-outline-danger delete-request-btn"
data-url="{{ url_for('request_secret_delete', token=req.token) }}" title="Удалить запрос"><i
class="bi bi-trash"></i></button>
{% else %} <span class="text-muted small">Истек</span> {% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-inbox display-1 text-secondary"></i>
<p class="text-secondary mt-3">У вас пока нет запросов на секрет</p>
<a href="{{ url_for('request_secret_form') }}" class="btn btn-secondary">Создать первый запрос</a>
</div>
{% endif %}
<form id="globalDeleteForm" method="POST" style="display: none;">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
</form>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function () {
var deleteButtons = document.querySelectorAll('.delete-request-btn');
var globalForm = document.getElementById('globalDeleteForm');
deleteButtons.forEach(function (button) {
button.addEventListener('click', function () {
if (confirm('Вы уверены, что хотите удалить этот запрос?')) {
var deleteUrl = this.getAttribute('data-url');
if (deleteUrl && globalForm) {
globalForm.action = deleteUrl;
globalForm.submit();
}
}
});
});
});
</script>
{% endblock %}
+94
View File
@@ -0,0 +1,94 @@
{% extends "base.html" %}
{% block title %}Мои секреты{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-normal">Мои секреты</h4>
<a href="{{ url_for('index') }}" class="btn btn-secondary">
<i class="bi"></i> Создать секрет
</a>
</div>
{% if secrets %}
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>ID</th>
<th>Тип</th>
<th>Примечание</th>
<th>Создан</th>
<th>Статус</th>
<th>Просмотрен</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for secret in secrets %}
<tr>
<td>
<code class="text-muted small">{{ secret.secret_id[:12] }}...</code>
</td>
<td>
{% if secret.is_request %}
<span class="badge bg-warning text-dark">
<i class="bi bi-arrow-down-circle"></i> Получен
</span>
{% else %}
<span class="badge bg-info">
<i class="bi bi-arrow-up-circle"></i> Создан
</span>
{% endif %}
</td>
<td>
{% if secret.request_note %}
<span class="text-muted small">{{ secret.request_note|e if secret.request_note else '-' }}</span>
{% elif secret.is_request %}
<span class="text-muted fst-italic small">Без примечания</span>
{% else %}
<span class="text-muted fst-italic small"></span>
{% endif %}
</td>
<td>
<span class="text-muted small">{{ secret.created_at|format_datetime }}</span>
</td>
<td>
{% if secret.is_viewed %}
<span class="badge bg-secondary">Просмотрен</span>
{% else %}
<span class="badge bg-success">Активен</span>
{% endif %}
</td>
<td>
{% if secret.is_viewed %}
<span class="text-muted small">{{ secret.viewed_at|format_datetime }}</span>
{% else %}
<span class="text-muted small"></span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm">
{% if not secret.is_viewed %}
<a href="{{ url_for('view_secret', secret_id=secret.secret_id) }}"
class="btn btn-outline-secondary" title="Просмотреть секрет">
<i class="bi bi-eye"></i>
</a>
{% else %}
<span class="text-muted small">Просмотрен</span>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-inbox display-1 text-secondary"></i>
<p class="text-secondary mt-3">У вас пока нет секретов</p>
<a href="{{ url_for('index') }}" class="btn btn-secondary">Создать первый секрет</a>
</div>
{% endif %}
{% endblock %}
+41
View File
@@ -0,0 +1,41 @@
{% extends "base.html" %}
{% block title %}Запросить секрет{% endblock %}
{% block content %}
<div class="card">
<div class="card-body">
<h4 class="card-title text-center mb-3">Запросить секрет</h4>
<p class="text-secondary text-center mb-4">
Создайте ссылку, по которой кто-то сможет отправить вам секрет.
Секрет будет доступен для одноразового просмотра в вашем ЛК.
</p>
<div class="alert alert-secondary">
<strong>Как это работает:</strong>
<ol class="mb-0 small">
<li>Вы создаете ссылку-запрос</li>
<li>Отправляете ссылку получателю (по почте, в мессенджере)</li>
<li>Получатель переходит по ссылке и вводит секрет</li>
<li>Секрет появляется в ваших <strong>"Моих секретах"</strong> и доступен для одноразового просмотра
</li>
</ol>
</div>
<form method="POST" action="{{ url_for('request_secret_create') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="mb-3">
<label for="note" class="form-label">Примечание только для Вас (необязательно)</label>
<textarea class="form-control" id="note" name="note" rows="3"
placeholder="Например: пароль от замка велосипеда"></textarea>
<div class="form-text text-secondary">Максимум 500 символов.</div>
</div>
<button type="submit" class="btn btn-secondary w-100">Создать ссылку-запрос</button>
</form>
</div>
</div>
{% endblock %}
+166
View File
@@ -0,0 +1,166 @@
{% extends "base.html" %}
{% block content %}
<div class="card border-secondary">
<div class="card-body text-center">
<h5 class="card-title">Ссылка для запроса секрета создана!</h5>
<p class="text-secondary">Отправьте эту ссылку получателю. Ссылка действительна <strong>24 часа</strong>.</p>
<div class="alert alert-secondary">
<strong>Важно:</strong> Получатель сможет отправить вам секрет по этой ссылке только один раз.
</div>
<div class="alert alert-secondary text-start position-relative">
<label class="form-label fw-bold small mb-1">
<i class="bi bi-clipboard me-1"></i> Скопируйте этот текст и отправьте получателю:
</label>
<div id="fullMessageBlock" class="bg-white p-2 rounded border"
style="font-family: monospace; font-size: 0.95rem; word-wrap: break-word; user-select: all; cursor: text; line-height: 1.6; white-space: pre-line;">Для безопасной передачи данных перейдите по ссылке, введите текст и нажмите "Отправить".
Ссылка сработает один раз.
Данные будут удалены сразу после просмотра или через 24 часа.
{{ link }}
</div>
<div class="mt-2 text-end">
<button class="btn btn-sm btn-outline-secondary js-copy-trigger" type="button"
data-target="fullMessageBlock">
<i class="bi bi-clipboard-plus"></i> Скопировать всё сообщение
</button>
</div>
</div>
<div class="alert alert-secondary text-start position-relative">
<label class="form-label fw-bold small mb-1">
<i class="bi bi-clipboard me-1"></i> Скопируйте этот текст и отправьте получателю:
</label>
<div id="fullMessageBlock1" class="bg-white p-2 rounded border"
style="font-family: monospace; font-size: 0.95rem; word-wrap: break-word; user-select: all; cursor: text; line-height: 1.6; white-space: pre-line;">To securely transmit data, please follow the link, enter your text, and click "Submit".
This link is single-use.
The data will be erased immediately upon viewing or after 24 hours.
{{ link }}
</div>
<div class="mt-2 text-end">
<button class="btn btn-sm btn-outline-secondary js-copy-trigger" type="button"
data-target="fullMessageBlock1">
<i class="bi bi-clipboard-plus"></i> Скопировать всё сообщение
</button>
</div>
</div>
<div class="mb-3 text-start">
<label class="form-label small text-secondary">Только ссылка:</label>
<div class="input-group">
<input type="text" class="form-control" id="requestLink" value="{{ link }}" readonly>
<button class="btn btn-secondary" type="button" id="copyRequestLinkBtn">
<i class="bi bi-clipboard"></i> Копировать
</button>
</div>
</div>
<div class="row g-2">
<div class="col">
<a href="{{ link }}" class="btn btn-secondary w-100" target="_blank" rel="noopener noreferrer">Открыть
ссылку</a>
</div>
<div class="col">
<a href="{{ url_for('my_requests') }}" class="btn btn-outline-secondary w-100">Мои запросы</a>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function () {
var copyTriggers = document.querySelectorAll('.js-copy-trigger');
copyTriggers.forEach(function (btn) {
btn.addEventListener('click', function () {
var targetId = this.getAttribute('data-target');
var targetContainer = document.getElementById(targetId);
if (targetContainer) {
var textToCopy = targetContainer.innerText || targetContainer.textContent;
textToCopy = textToCopy.trim();
var isEnglish = targetId.includes('1');
var successMsg = isEnglish ? 'English text copied!' : 'Русский текст скопирован!';
handleCopy(textToCopy, this, successMsg);
}
});
});
var copyLinkBtn = document.getElementById('copyRequestLinkBtn') || document.getElementById('copyBtn');
var input = document.getElementById('requestLink') || document.getElementById('secretLink');
if (copyLinkBtn && input) {
copyLinkBtn.addEventListener('click', function () {
var textToCopy = input.value.trim();
handleCopy(textToCopy, this, 'Ссылка скопирована! / Link copied!');
});
}
function handleCopy(text, buttonElement, successMessage) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function () {
processVisualSuccess(buttonElement, successMessage);
}).catch(function () {
executeFallback(text, buttonElement, successMessage);
});
} else {
executeFallback(text, buttonElement, successMessage);
}
}
function executeFallback(text, buttonElement, successMessage) {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try {
var successful = document.execCommand('copy');
if (successful) {
processVisualSuccess(buttonElement, successMessage);
} else {
alert('Не удалось скопировать. Выделите текст руками и нажмите Ctrl+C');
}
} catch (err) {
alert('Не удалось скопировать. Выделите текст руками и нажмите Ctrl+C');
}
document.body.removeChild(ta);
}
function processVisualSuccess(btn, message) {
if (typeof showToast === 'function') {
showToast(message, 'success');
}
var originalHtml = btn.innerHTML;
var isSmallBtn = btn.classList.contains('btn-sm');
btn.innerHTML = '<i class="bi bi-check-lg"></i> Скопировано!';
btn.classList.remove('btn-secondary', 'btn-outline-secondary');
btn.classList.add('btn-success');
setTimeout(function () {
btn.innerHTML = originalHtml;
btn.classList.remove('btn-success');
if (isSmallBtn) {
btn.classList.add('btn-outline-secondary');
} else {
btn.classList.add('btn-secondary');
}
}, 2000);
}
});
</script>
{% endblock %}
+20
View File
@@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block title %}Ссылка недействительна{% endblock %}
{% block content %}
<div class="card border-danger">
<div class="card-body text-center py-5">
<h5 class="card-title text-danger">Ссылка недействительна</h5>
<p class="text-secondary">
Ссылка для запроса секрета недействительна или истекла.
</p>
<ul class="list-unstyled text-secondary small">
<li>Ссылка уже была использована</li>
<li>Истек срок действия (24 часа)</li>
<li>Запрос был удален</li>
</ul>
<a href="{{ url_for('index') }}" class="btn btn-secondary">На главную</a>
</div>
</div>
{% endblock %}
+34
View File
@@ -0,0 +1,34 @@
{% extends "base.html" %}
{% block title %}Отправить секрет{% endblock %}
{% block content %}
<div class="card">
<div class="card-body">
<h4 class="card-title text-center mb-3">Отправить секрет</h4>
<p class="text-secondary text-center mb-4">
Пользователь <strong>{{ recipient_name }}</strong> запрашивает у вас данные.
Введите их и отправьте.
</p>
<form method="POST" action="{{ url_for('request_secret_submit', token=token) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="mb-3">
<label for="secret" class="form-label">Секрет</label>
<textarea class="form-control" id="secret" name="secret" rows="6"
placeholder="" required></textarea>
<div class="form-text text-secondary">Максимум {{ config.MAX_SECRET_LENGTH }} символов. Автоудаление
через 24 часа или после просмотра.
</div>
</div>
<button type="submit" class="btn btn-secondary w-100">Отправить секрет</button>
</form>
</div>
</div>
{% endblock %}
+21
View File
@@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block title %}Секрет отправлен{% endblock %}
{% block content %}
<div class="card border-success">
<div class="card-body text-center py-5">
<h5 class="card-title">Секрет успешно отправлен!</h5>
<p class="text-secondary">
Ваш секрет получит <strong>{{ recipient_name }}</strong>.
</p>
<p class="text-secondary">
Секрет будет доступен получателю для <strong>одноразового просмотра</strong>.
</p>
<div class="mt-3">
<a href="{{ url_for('index') }}" class="btn btn-secondary">На главную</a>
</div>
</div>
</div>
{% endblock %}
+121
View File
@@ -0,0 +1,121 @@
{% extends "base.html" %}
{% block content %}
<div class="card">
<div class="card-body">
<div class="alert ">
<i class="bi me-2"></i>
<strong>Внимание!</strong> Данные удалены, после закрытия страницы она не откроется повторно.
<ul class="mb-0 mt-1 small">
<li>Обновление страницы или повторный переход по ссылке не откроют данные.</li>
<li>Восстановить информацию невозможно.</li>
<li>При необходимости <strong>сохраните или скопируйте</strong> информацию сейчас.</li>
</ul>
</div>
<div class="card bg-light mb-3">
<div class="card-header bg-secondary text-white d-flex justify-content-between align-items-center">
<span><i class="bi me-2"></i>Полученное сообщение</span>
<button class="btn btn-sm btn-light"
id="viewCopyBtn"
title="Копировать секрет">
<i class="bi bi-clipboard"></i> Копировать секрет
</button>
</div>
<div class="card-body">
<div class="bg-white p-4 rounded border"
id="secretContainer"
style="font-family: 'Courier New', monospace;
font-size: 1rem;
word-wrap: break-word;
max-height: 600px;
overflow-y: auto;
line-height: 1.8;
background-color: #f8f9fa;
border: 2px dashed #6c757d !important;
box-shadow: inset 0 0 10px rgba(0,0,0,0.05);
white-space: pre-wrap;">{{ secret | safe }}
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<div class="text-secondary small">
<i class="bi bi-info-circle me-1"></i>
Размер: {{ secret|length }} символов
</div>
</div>
</div>
</div>
{% if is_authenticated %}
<div class="text-center mt-3">
<a href="{{ url_for('index') }}" class="btn btn-secondary">
<i class="bi me-1"></i> Создать новый секрет
</a>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function () {
var copyBtn = document.getElementById('viewCopyBtn');
var container = document.getElementById('secretContainer');
if (!copyBtn || !container) return;
copyBtn.addEventListener('click', function () {
var textToCopy = container.innerText || container.textContent;
textToCopy = textToCopy.trim();
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(textToCopy).then(function () {
processVisualSuccess();
}).catch(function () {
executeFallback(textToCopy);
});
} else {
executeFallback(textToCopy);
}
});
function executeFallback(text) {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try {
var successful = document.execCommand('copy');
if (successful) {
processVisualSuccess();
} else {
alert('Не удалось скопировать автоматически. Выделите текст и нажмите Ctrl+C');
}
} catch (e) {
alert('Не удалось скопировать автоматически. Выделите текст и нажмите Ctrl+C');
}
document.body.removeChild(ta);
}
function processVisualSuccess() {
if (typeof showToast === 'function') {
showToast('Секрет успешно скопирован!', 'success');
}
var originalHtml = copyBtn.innerHTML;
copyBtn.innerHTML = '<i class="bi bi-check-lg"></i> Скопировано!';
copyBtn.classList.remove('btn-light');
copyBtn.classList.add('btn-success', 'text-white');
setTimeout(function () {
copyBtn.innerHTML = originalHtml;
copyBtn.classList.remove('btn-success', 'text-white');
copyBtn.classList.add('btn-light');
}, 2000);
}
});
</script>
{% endblock %}