Initial commit

This commit is contained in:
AVS
2026-06-26 16:42:07 +05:00
commit da6b107066
17 changed files with 1448 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TNProxy - API Docs</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; padding: 20px; }
.container { max-width: 900px; margin: 0 auto; }
.nav { margin-bottom: 20px; }
.nav a { margin-right: 15px; color: #0088cc; text-decoration: none; }
.nav a:hover { text-decoration: underline; }
h1 { color: #333; margin-bottom: 20px; }
h2 { color: #555; margin-top: 30px; margin-bottom: 15px; }
.card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }
code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-family: monospace; }
pre { background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 5px; overflow-x: auto; }
.method { display: inline-block; padding: 4px 8px; border-radius: 4px; font-weight: bold; font-size: 12px; }
.method.get { background: #61affe; color: white; }
.method.post { background: #49cc90; color: white; }
.method.delete { background: #f93e3e; color: white; }
</style>
</head>
<body>
<div class="container">
<div class="nav">
<a href="/admin">Приложения</a>
<a href="/admin/api">API Docs</a>
</div>
<h1>API Документация</h1>
<div class="card">
<h2>Отправка сообщения</h2>
<p><span class="method post">POST</span> <code>/send</code></p>
<p><strong>Headers:</strong></p>
<ul>
<li><code>Content-Type: application/json</code></li>
<li><code>X-App-Token: &lt;ваш_токен_приложения&gt;</code></li>
</ul>
<p><strong>Body:</strong></p>
<pre>{"text": "Hello, World!"}</pre>
<h3>Пример PowerShell:</h3>
<pre>$body = @{text = "Alert!"} | ConvertTo-Json
Invoke-RestMethod -Uri "http://localhost:8080/send" -Method Post -Body $body -Headers @{"Content-Type"="application/json"; "X-App-Token"="YOUR_APP_TOKEN"}</pre>
<h3>Пример cURL:</h3>
<pre>curl -X POST http://localhost:8080/send \
-H "Content-Type: application/json" \
-H "X-App-Token: YOUR_APP_TOKEN" \
-d '{"text": "Hello!"}'</pre>
</div>
<div class="card">
<h2>Проверка здоровья</h2>
<p><span class="method get">GET</span> <code>/health</code></p>
<pre>{"status": "ok"}</pre>
</div>
</div>
</body>
</html>
+188
View File
@@ -0,0 +1,188 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TNProxy - Telegram Notification Proxy</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; padding: 20px; }
.container { max-width: 900px; margin: 0 auto; }
h1 { color: #333; margin-bottom: 20px; }
.btn { display: inline-block; padding: 10px 20px; background: #0088cc; color: white; text-decoration: none; border-radius: 5px; border: none; cursor: pointer; font-size: 14px; }
.btn:hover { background: #006699; }
.btn-secondary { background: #666; }
.btn-danger { background: #dc3545; }
.btn-danger:hover { background: #c82333; }
.btn-sm { padding: 5px 10px; font-size: 12px; }
table { width: 100%; background: white; border-collapse: collapse; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; font-weight: 600; }
.status { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 12px; }
.status.active { background: #d4edda; color: #155724; }
.status.inactive { background: #f8d7da; color: #721c24; }
.token { font-family: monospace; background: #f8f9fa; padding: 4px 8px; border-radius: 4px; font-size: 12px; }
.card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: 500; }
input[type="text"] { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; }
.modal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); align-items: center; justify-content: center; }
.modal.show { display: flex; }
.modal-content { background: white; padding: 20px; border-radius: 8px; width: 400px; }
.modal-actions { margin-top: 15px; text-align: right; }
.nav { margin-bottom: 20px; }
.nav a { margin-right: 15px; color: #0088cc; text-decoration: none; }
.nav a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="container">
<h1>TNProxy - Dashboard</h1>
<div class="nav">
<a href="/adtn">Приложения</a>
<a href="/adtn/logs">Логи</a>
<a href="/adtn/api">API Docs</a>
<a href="/adtn/password">Пароль</a>
<a href="/adtn/export">Экспорт</a>
<a href="#" id="importBtn">Импорт</a>
<a href="#" id="logout">Выход</a>
</div>
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2>Приложения</h2>
<button class="btn" onclick="showModal()">+ Добавить приложение</button>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Название</th>
<th>Token</th>
<th>Chat ID</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for app in apps %}
<tr>
<td>{{ app.id }}</td>
<td>{{ app.name }}</td>
<td><span class="token">{{ app.token }}</span></td>
<td>{{ app.telegram_chat_id }}</td>
<td>
<span class="status {{ 'active' if app.is_active else 'inactive' }}">
{{ 'Активно' if app.is_active else 'Отключено' }}
</span>
</td>
<td>
<button class="btn btn-secondary btn-sm" onclick="toggleApp({{ app.id }})">
{{ 'Отключить' if app.is_active else 'Включить' }}
</button>
<button class="btn btn-danger btn-sm" onclick="deleteApp({{ app.id }})">Удалить</button>
</td>
</tr>
{% else %}
<tr>
<td colspan="6" style="text-align: center; color: #666;">Нет приложений</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="modal" id="addModal">
<div class="modal-content">
<h3>Добавить приложение</h3>
<form id="addForm">
<div class="form-group">
<label>Название</label>
<input type="text" name="name" required>
</div>
<div class="form-group">
<label>Telegram Bot Token</label>
<input type="text" name="telegram_bot_token" required>
</div>
<div class="form-group">
<label>Telegram Chat ID</label>
<input type="text" name="telegram_chat_id" required>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" onclick="hideModal()">Отмена</button>
<button type="submit" class="btn">Создать</button>
</div>
</form>
</div>
</div>
<script>
function showModal() { document.getElementById('addModal').classList.add('show'); }
function hideModal() { document.getElementById('addModal').classList.remove('show'); }
document.getElementById('addForm').onsubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData);
const res = await fetch('/adtn/applications', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
const result = await res.json();
if (result.token) {
alert('Приложение создано!\n\nToken: ' + result.token + '\n\nСкопируйте токен - он больше не будет показан!');
hideModal();
location.reload();
}
};
async function deleteApp(id) {
if (!confirm('Удалить приложение?')) return;
await fetch('/adtn/applications/' + id, { method: 'DELETE' });
location.reload();
}
async function toggleApp(id) {
await fetch('/adtn/applications/' + id + '/toggle', { method: 'POST' });
location.reload();
}
document.getElementById('logout').onclick = async (e) => {
e.preventDefault();
await fetch('/logout', { method: 'POST' });
window.location.href = '/logtn';
};
document.getElementById('importBtn').onclick = async (e) => {
e.preventDefault();
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = async (ev) => {
const file = ev.target.files[0];
if (!file) return;
const text = await file.text();
const res = await fetch('/adtn/import', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: text
});
const result = await res.json();
if (result.success) {
alert('Настройки импортированы!');
location.reload();
} else {
alert('Ошибка: ' + result.error);
}
};
input.click();
};
</script>
</body>
</html>
+96
View File
@@ -0,0 +1,96 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TNProxy - Настройки логов</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; padding: 20px; }
.container { max-width: 600px; margin: 0 auto; }
h1 { color: #333; margin-bottom: 20px; }
.btn { display: inline-block; padding: 10px 20px; background: #0088cc; color: white; text-decoration: none; border-radius: 5px; border: none; cursor: pointer; font-size: 14px; }
.btn:hover { background: #006699; }
.btn-secondary { background: #666; }
.nav { margin-bottom: 20px; }
.nav a { margin-right: 15px; color: #0088cc; text-decoration: none; }
.nav a:hover { text-decoration: underline; }
.card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: 500; }
input, select { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; }
.help { font-size: 12px; color: #666; margin-top: 4px; }
.log-path { font-family: monospace; background: #f8f9fa; padding: 4px 8px; border-radius: 4px; }
</style>
</head>
<body>
<div class="container">
<div class="nav">
<a href="/adtn">Приложения</a>
<a href="/adtn/logs">Логи</a>
<a href="/adtn/api">API Docs</a>
</div>
<h1>Настройки логирования</h1>
<div class="card">
<h2>Log Rotate</h2>
<form id="logForm">
<div class="form-group">
<label>Уровень логирования</label>
<select name="log_level">
<option value="DEBUG" {% if settings.log_level == 'DEBUG' %}selected{% endif %}>DEBUG</option>
<option value="INFO" {% if settings.log_level == 'INFO' %}selected{% endif %}>INFO</option>
<option value="WARNING" {% if settings.log_level == 'WARNING' %}selected{% endif %}>WARNING</option>
<option value="ERROR" {% if settings.log_level == 'ERROR' %}selected{% endif %}>ERROR</option>
</select>
</div>
<div class="form-group">
<label>Максимальный размер файла (байт)</label>
<input type="number" name="max_bytes" value="{{ settings.max_bytes }}" min="1024">
<div class="help">Текущее значение: {{ settings.max_bytes }} байт ({{ (settings.max_bytes / 1024 / 1024)|round(2) }} MB)</div>
</div>
<div class="form-group">
<label>Количество backup файлов</label>
<input type="number" name="backup_count" value="{{ settings.backup_count }}" min="1" max="100">
<div class="help">Сколько архивных файлов хранить</div>
</div>
<button type="submit" class="btn">Сохранить</button>
</form>
</div>
<div class="card">
<h2>Файл логов</h2>
<p>Путь: <span class="log-path">/app/logs/tnproxy.log</span></p>
<p class="help">В Docker подключите volume <code>./logs:/app/logs</code> для доступа к файлам</p>
</div>
</div>
<script>
document.getElementById('logForm').onsubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = {
log_level: formData.get('log_level'),
max_bytes: parseInt(formData.get('max_bytes')),
backup_count: parseInt(formData.get('backup_count'))
};
const res = await fetch('/adtn/logs', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
if (res.ok) {
alert('Настройки сохранены!');
} else {
alert('Ошибка сохранения');
}
};
</script>
</body>
</html>
+63
View File
@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TNProxy - Вход</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
.login-box { background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); width: 100%; max-width: 400px; }
h1 { color: #333; margin-bottom: 20px; text-align: center; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: 500; }
input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; }
input:focus { outline: none; border-color: #0088cc; }
.btn { width: 100%; padding: 12px; background: #0088cc; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; }
.btn:hover { background: #006699; }
.error { color: #dc3545; margin-bottom: 15px; text-align: center; }
</style>
</head>
<body>
<div class="login-box">
<h1>TNProxy - Вход</h1>
<div id="error" class="error" {% if not error %}style="display:none"{% endif %}>{{ error }}</div>
<form id="loginForm">
<div class="form-group">
<label>Логин</label>
<input type="text" name="username" required>
</div>
<div class="form-group">
<label>Пароль</label>
<input type="password" name="password" required>
</div>
<button type="submit" class="btn">Войти</button>
</form>
</div>
<script>
document.getElementById('loginForm').onsubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = {
username: formData.get('username'),
password: formData.get('password')
};
const res = await fetch('/logtn', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
const result = await res.json();
if (result.success) {
window.location.href = result.redirect;
} else {
document.getElementById('error').textContent = result.error;
document.getElementById('error').style.display = 'block';
}
};
</script>
</body>
</html>
+95
View File
@@ -0,0 +1,95 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TNProxy - Смена пароля</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; padding: 20px; }
.container { max-width: 500px; margin: 0 auto; }
h1 { color: #333; margin-bottom: 20px; }
.btn { display: inline-block; padding: 10px 20px; background: #0088cc; color: white; text-decoration: none; border-radius: 5px; border: none; cursor: pointer; font-size: 14px; }
.btn:hover { background: #006699; }
.btn-secondary { background: #666; }
.nav { margin-bottom: 20px; }
.nav a { margin-right: 15px; color: #0088cc; text-decoration: none; }
.card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: 500; }
input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; }
.success { color: #28a745; margin-bottom: 15px; }
.error { color: #dc3545; margin-bottom: 15px; }
</style>
</head>
<body>
<div class="container">
<h1>TNProxy</h1>
<div class="nav">
<a href="/adtn">Приложения</a>
<a href="/adtn/logs">Логи</a>
<a href="/adtn/api">API Docs</a>
<a href="/adtn/password">Пароль</a>
<a href="#" id="logout">Выход</a>
</div>
<div class="card">
<h2>Смена пароля</h2>
<div id="message"></div>
<form id="passwordForm">
<div class="form-group">
<label>Текущий пароль</label>
<input type="password" name="old_password" required>
</div>
<div class="form-group">
<label>Новый пароль</label>
<input type="password" name="new_password" required minlength="6">
</div>
<div class="form-group">
<label>Подтверждение пароля</label>
<input type="password" name="confirm_password" required>
</div>
<button type="submit" class="btn">Изменить пароль</button>
</form>
</div>
</div>
<script>
document.getElementById('logout').onclick = async (e) => {
e.preventDefault();
await fetch('/logout', { method: 'POST' });
window.location.href = '/login';
};
document.getElementById('passwordForm').onsubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
if (formData.get('new_password') !== formData.get('confirm_password')) {
document.getElementById('message').innerHTML = '<div class="error">Пароли не совпадают</div>';
return;
}
const data = {
old_password: formData.get('old_password'),
new_password: formData.get('new_password')
};
const res = await fetch('/adtn/password', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
const result = await res.json();
if (result.success) {
document.getElementById('message').innerHTML = '<div class="success">Пароль изменен!</div>';
e.target.reset();
} else {
document.getElementById('message').innerHTML = '<div class="error">' + result.error + '</div>';
}
};
</script>
</body>
</html>