Первый коммит: загрузка проекта SecretText
This commit is contained in:
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user