feat: роуты и UI — список, добавление, удаление, экспорт
This commit is contained in:
@@ -2,42 +2,100 @@ const express = require('express');
|
||||
const path = require('path');
|
||||
require('dotenv').config();
|
||||
const { checkConnection } = require('./src/db');
|
||||
const q = require('./src/queries');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const DEV = process.env.DEV_MODE === 'true';
|
||||
|
||||
// Шаблоны
|
||||
app.set('view engine', 'ejs');
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
|
||||
// Статика
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// Парсинг форм
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Health check — всегда 200 если сервер жив.
|
||||
// Статус БД проверяется отдельно.
|
||||
app.get('/healthz', (req, res) => {
|
||||
res.status(200).send('OK');
|
||||
});
|
||||
|
||||
// Главная — показывает реальный статус БД
|
||||
app.get('/', async (req, res) => {
|
||||
let dbStatus = 'неизвестно';
|
||||
try {
|
||||
await checkConnection();
|
||||
dbStatus = 'подключена';
|
||||
} catch (e) {
|
||||
dbStatus = 'ошибка: ' + e.message;
|
||||
// ── Auth middleware ──
|
||||
app.use((req, res, next) => {
|
||||
if (DEV) {
|
||||
req.user = { email: 'dev@test.local', clientId: 'WZ01325', companyId: null, companyName: 'DEV' };
|
||||
return next();
|
||||
}
|
||||
res.render('index', { title: 'IP WhiteList', dbStatus });
|
||||
const auth = req.headers.authorization || '';
|
||||
try {
|
||||
const payload = JSON.parse(Buffer.from(auth.replace('Bearer ', '').split('.')[1], 'base64').toString());
|
||||
req.user = {
|
||||
email: payload.email || 'unknown',
|
||||
clientId: payload.ClientID,
|
||||
companyId: payload.company_id,
|
||||
companyName: payload.company_name || payload.ClientID,
|
||||
};
|
||||
} catch { req.user = {}; }
|
||||
next();
|
||||
});
|
||||
|
||||
// Не падаем если БД недоступна — healthcheck покажет статус
|
||||
let dbOk = false;
|
||||
// ── Health ──
|
||||
app.get('/healthz', (req, res) => res.send('OK'));
|
||||
|
||||
// ── Главная ──
|
||||
app.get('/', async (req, res) => {
|
||||
const { clientId, companyName } = req.user;
|
||||
try {
|
||||
const company = await q.getOrCreateCompany(clientId, companyName);
|
||||
const limit = await q.getLimit(company);
|
||||
const entries = await q.listEntries(company.id);
|
||||
res.render('index', { entries, limit, used: entries.length, user: req.user, error: null, message: null, wasNormalized: false });
|
||||
} catch (e) {
|
||||
res.render('index', { entries: [], limit: 15, used: 0, user: req.user, error: e.message, message: null, wasNormalized: false });
|
||||
}
|
||||
});
|
||||
|
||||
// ── Создать ──
|
||||
app.post('/add', async (req, res) => {
|
||||
const { value, comment } = req.body;
|
||||
const { clientId, companyName, email } = req.user;
|
||||
try {
|
||||
const company = await q.getOrCreateCompany(clientId, companyName);
|
||||
const result = await q.createEntry(company.id, value, comment, email);
|
||||
const limit = await q.getLimit(company);
|
||||
const entries = await q.listEntries(company.id);
|
||||
res.render('index', {
|
||||
entries, limit, used: entries.length, user: req.user,
|
||||
message: result.wasNormalized ? `Адрес нормализован в ${result.entry.value_cidr}` : 'Добавлено',
|
||||
error: null, wasNormalized: result.wasNormalized,
|
||||
});
|
||||
} catch (e) {
|
||||
const company = await q.getOrCreateCompany(clientId, companyName).catch(() => null);
|
||||
const entries = company ? await q.listEntries(company.id).catch(() => []) : [];
|
||||
const limit = company ? await q.getLimit(company).catch(() => 15) : 15;
|
||||
res.render('index', { entries, limit, used: entries.length, user: req.user, error: e.message, message: null, wasNormalized: false });
|
||||
}
|
||||
});
|
||||
|
||||
// ── Удалить (soft) ──
|
||||
app.post('/delete/:id', async (req, res) => {
|
||||
const { clientId, companyName, email } = req.user;
|
||||
try {
|
||||
const company = await q.getOrCreateCompany(clientId, companyName);
|
||||
await q.deleteEntry(req.params.id, company.id, email);
|
||||
res.redirect('/');
|
||||
} catch (e) {
|
||||
res.redirect('/?error=' + encodeURIComponent(e.message));
|
||||
}
|
||||
});
|
||||
|
||||
// ── Экспорт ──
|
||||
app.get('/export', async (req, res) => {
|
||||
try {
|
||||
const cidrs = await q.getExportCIDRs();
|
||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||
res.send(cidrs.join('\n') + '\n');
|
||||
} catch (e) {
|
||||
res.status(500).send('Export error');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Старт ──
|
||||
checkConnection()
|
||||
.then(() => { dbOk = true; console.log('DB connected'); })
|
||||
.catch((e) => console.error('DB not ready:', e.message));
|
||||
.then(() => console.log('DB connected'))
|
||||
.catch(e => console.error('DB not ready:', e.message));
|
||||
|
||||
app.listen(PORT, () => console.log(`Server on port ${PORT}`));
|
||||
|
||||
+62
-5
@@ -2,12 +2,69 @@
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><%= title %></title>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
<title>IP WhiteList</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, sans-serif; max-width: 900px; margin: 2rem auto; padding: 0 1rem; color: #333; }
|
||||
h1 { margin-bottom: 0.5rem; }
|
||||
.limit { color: #666; margin-bottom: 1rem; }
|
||||
.limit .full { color: #c00; font-weight: bold; }
|
||||
table { width: 100%; border-collapse: collapse; margin-bottom: 1rem; }
|
||||
th, td { padding: 0.5rem; text-align: left; border-bottom: 1px solid #eee; }
|
||||
th { background: #f5f5f5; }
|
||||
.msg { padding: 0.5rem; margin-bottom: 1rem; border-radius: 4px; }
|
||||
.msg.ok { background: #d4edda; color: #155724; }
|
||||
.msg.err { background: #f8d7da; color: #721c24; }
|
||||
.msg.warn { background: #fff3cd; color: #856404; }
|
||||
form { display: flex; gap: 0.5rem; align-items: flex-end; flex-wrap: wrap; }
|
||||
input, button { padding: 0.5rem; font-size: 1rem; }
|
||||
input[name="value"] { width: 200px; }
|
||||
input[name="comment"] { width: 250px; }
|
||||
button { background: #007bff; color: #fff; border: none; border-radius: 4px; cursor: pointer; }
|
||||
button.del { background: #dc3545; font-size: 0.85rem; padding: 0.3rem 0.5rem; }
|
||||
button:hover { opacity: 0.85; }
|
||||
td small { color: #999; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1><%= title %></h1>
|
||||
<p>Сервис работает. БД: <strong><%= dbStatus %></strong></p>
|
||||
<h1>IP WhiteList</h1>
|
||||
<p><%= user.email %> | <%= user.clientId %> | <%= user.companyName %></p>
|
||||
<p class="limit">Использовано <strong class="<%= used >= limit ? 'full' : '' %>"><%= used %></strong> из <%= limit %></p>
|
||||
|
||||
<% if (message) { %><div class="msg <%= wasNormalized ? 'warn' : 'ok' %>"><%= message %></div><% } %>
|
||||
<% if (error) { %><div class="msg err"><%= error %></div><% } %>
|
||||
|
||||
<form method="POST" action="/add">
|
||||
<div>
|
||||
<label>IPv4 / CIDR</label><br>
|
||||
<input name="value" placeholder="203.0.113.0/24" required <%= used >= limit ? 'disabled' : '' %>>
|
||||
</div>
|
||||
<div>
|
||||
<label>Комментарий</label><br>
|
||||
<input name="comment" placeholder="Офис (необязательно)" maxlength="255">
|
||||
</div>
|
||||
<button type="submit" <%= used >= limit ? 'disabled' : '' %>>Добавить</button>
|
||||
</form>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Адрес / Подсеть</th><th>Комментарий</th><th>Кто</th><th>Когда</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% entries.forEach(e => { %>
|
||||
<tr>
|
||||
<td><code><%= e.value_cidr %></code></td>
|
||||
<td><%= e.comment || '' %></td>
|
||||
<td><%= e.created_by %></td>
|
||||
<td><small><%= new Date(e.created_at).toLocaleString('ru') %></small></td>
|
||||
<td>
|
||||
<form method="POST" action="/delete/<%= e.id %>" style="display:inline">
|
||||
<button class="del" onclick="return confirm('Удалить?')">Удалить</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user