fix: гонки (транзакции), валидатор (суперсеть, parseInt, слэши), auth (401, DEV_MODE, export), схема (UNIQUE, CHECK, FK), client-валидация

This commit is contained in:
2026-05-30 08:28:21 +03:00
parent 60972169c5
commit 51f2f4ddaf
5 changed files with 164 additions and 108 deletions
+26 -20
View File
@@ -6,12 +6,17 @@ const q = require('./src/queries');
const app = express();
const PORT = process.env.PORT || 3000;
const DEV = process.env.DEV_MODE === 'true';
const DEV = process.env.DEV_MODE === 'true' && process.env.NODE_ENV !== 'production';
if (DEV) console.warn('⚠ DEV_MODE активен — аутентификация отключена');
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 }));
app.use(express.urlencoded({ extended: true, limit: '32kb' }));
// ── Health (выше auth — k8s probe не должна требовать токен) ──
app.get('/healthz', (req, res) => res.send('OK'));
// ── Auth middleware ──
app.use((req, res, next) => {
@@ -29,12 +34,11 @@ app.use((req, res, next) => {
companyName: payload.company_name || payload.ClientID,
};
} catch { req.user = {}; }
// Без clientId — 401, анонимы не должны делить NULL-компанию
if (!req.user.clientId) return res.status(401).send('Unauthorized');
next();
});
// ── Health ──
app.get('/healthz', (req, res) => res.send('OK'));
// ── Главная ──
app.get('/', async (req, res) => {
const { clientId, companyName } = req.user;
@@ -42,9 +46,14 @@ app.get('/', async (req, res) => {
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 });
res.render('index', {
entries, limit, used: entries.length, user: req.user,
error: req.query.error || null,
message: req.query.message || null,
wasNormalized: req.query.wasNormalized === '1',
});
} catch (e) {
res.render('index', { entries: [], limit: 15, used: 0, user: req.user, error: e.message, message: null, wasNormalized: false });
res.render('index', { entries: [], limit: 15, used: 0, user: req.user, error: 'Ошибка загрузки', message: null, wasNormalized: false });
}
});
@@ -55,18 +64,13 @@ app.post('/add', async (req, res) => {
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,
});
const message = result.wasNormalized
? `Адрес нормализован в ${result.entry.value_cidr}`
: 'Добавлено';
const wasNormalized = result.wasNormalized ? '1' : '0';
res.redirect(`/?message=${encodeURIComponent(message)}&wasNormalized=${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 });
res.redirect('/?error=' + encodeURIComponent(e.message));
}
});
@@ -82,10 +86,12 @@ app.post('/delete/:id', async (req, res) => {
}
});
// ── Экспорт ──
// ── Экспорт (только для своей компании) ──
app.get('/export', async (req, res) => {
try {
const cidrs = await q.getExportCIDRs();
const { clientId, companyName } = req.user;
const company = await q.getOrCreateCompany(clientId, companyName);
const cidrs = await q.getExportCIDRs(company.id);
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.send(cidrs.join('\n') + '\n');
} catch (e) {