master #1
+3
-2
@@ -59,9 +59,10 @@ CREATE INDEX IF NOT EXISTS idx_group_photos_group_id ON group_photos(group_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS project_files (
|
||||
id SERIAL PRIMARY KEY,
|
||||
entry_id INT NOT NULL REFERENCES entries(id) ON DELETE CASCADE,
|
||||
entry_id INT REFERENCES entries(id) ON DELETE CASCADE,
|
||||
token VARCHAR(64) UNIQUE NOT NULL,
|
||||
path VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
detached_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
@@ -45,6 +45,9 @@ UPDATE project_files SET token = md5(random()::text || clock_timestamp()::text)
|
||||
ALTER TABLE project_files ALTER COLUMN token SET NOT NULL;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS project_files_token_key ON project_files(token);
|
||||
|
||||
ALTER TABLE project_files ALTER COLUMN entry_id DROP NOT NULL;
|
||||
ALTER TABLE project_files ADD COLUMN IF NOT EXISTS detached_at TIMESTAMPTZ;
|
||||
|
||||
ALTER TABLE entries ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS group_photos (
|
||||
|
||||
@@ -46,9 +46,20 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b
|
||||
.card .desc{color:var(--muted);font-size:.85rem;margin-top:8px;line-height:1.4;white-space:pre-wrap}
|
||||
.card .time{color:var(--muted);font-size:.75rem;margin-top:8px}
|
||||
.entry-files{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
|
||||
.entry-files .f-item{display:inline-flex;align-items:center;gap:2px;background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:2px}
|
||||
.entry-files .f-item:hover{border-color:var(--accent)}
|
||||
.entry-files .f-item a{display:inline-flex;align-items:center;gap:4px;color:var(--text);text-decoration:none;padding:2px 6px;font-size:.75rem;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.entry-files .f-item a:hover{color:var(--accent)}
|
||||
.entry-files .f-item a::before{content:'📎'}
|
||||
.entry-files .f-detach{background:none;border:none;color:var(--muted);cursor:pointer;font-size:.75rem;line-height:1;padding:2px 5px;border-radius:4px}
|
||||
.entry-files .f-detach:hover{color:#ef4444;background:rgba(239,68,68,.1)}
|
||||
.entry-files a{display:inline-flex;align-items:center;gap:4px;background:var(--bg);border:1px solid var(--border);color:var(--text);text-decoration:none;border-radius:6px;padding:4px 8px;font-size:.75rem;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.entry-files a:hover{border-color:var(--accent);color:var(--accent)}
|
||||
.entry-files a::before{content:'📎'}
|
||||
.f-actions{display:flex;align-items:center;flex-shrink:0}
|
||||
.f-action{background:none;border:1px solid var(--border);color:var(--muted);cursor:pointer;border-radius:8px;padding:6px 10px;font-size:.75rem;white-space:nowrap}
|
||||
.f-action:hover{color:var(--accent);border-color:var(--accent)}
|
||||
.f-action.f-del:hover{color:#ef4444;border-color:#ef4444}
|
||||
.card .del{background:none;border:none;color:var(--muted);cursor:pointer;font-size:1.1rem;padding:4px;flex-shrink:0}
|
||||
.card .del:hover{color:#ef4444}
|
||||
.card .edit{background:none;border:none;color:var(--muted);cursor:pointer;font-size:1.05rem;padding:4px;flex-shrink:0}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Ошибка — WhatIDo</title>
|
||||
<link rel="stylesheet" href="admin.css">
|
||||
<style>
|
||||
.error-page{display:flex;align-items:center;justify-content:center;min-height:100dvh;padding:24px;text-align:center}
|
||||
.error-card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:48px 40px;max-width:400px;width:100%}
|
||||
.error-code{font-size:6rem;font-weight:700;color:var(--accent);line-height:1;margin-bottom:8px}
|
||||
.error-title{font-size:1.5rem;font-weight:600;color:var(--text);margin-bottom:12px}
|
||||
.error-message{color:var(--muted);font-size:1rem;line-height:1.6;margin-bottom:24px}
|
||||
.error-actions{display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
|
||||
.error-actions a,.error-actions button{background:var(--accent);color:#fff;border:none;border-radius:8px;padding:12px 24px;font-size:.9rem;font-weight:600;cursor:pointer;text-decoration:none;transition:opacity .15s}
|
||||
.error-actions a:hover,.error-actions button:hover{opacity:.85}
|
||||
.error-actions .ghost{background:var(--card);border:1px solid var(--border);color:var(--accent)}
|
||||
.error-actions .ghost:hover{border-color:var(--accent);background:rgba(37,99,235,.06)}
|
||||
.error-details{margin-top:24px;padding-top:24px;border-top:1px solid var(--border);font-size:.75rem;color:var(--muted);text-align:left}
|
||||
.error-details pre{white-space:pre-wrap;word-break:break-word;font-family:monospace;background:var(--bg);padding:12px;border-radius:8px;margin-top:8px;max-height:200px;overflow:auto}
|
||||
@media(max-width:480px){.error-code{font-size:4rem}.error-card{padding:32px 20px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-page">
|
||||
<div class="error-card">
|
||||
<div class="error-code" id="errorCode">404</div>
|
||||
<h1 class="error-title" id="errorTitle">Страница не найдена</h1>
|
||||
<p class="error-message" id="errorMessage">Запрашиваемая страница не существует или была перемещена.</p>
|
||||
<div class="error-actions">
|
||||
<a href="/" class="ghost">На главную</a>
|
||||
<button onclick="history.back()">Назад</button>
|
||||
</div>
|
||||
<div class="error-details" id="errorDetails" style="display:none">
|
||||
<strong>Детали:</strong>
|
||||
<pre id="errorStack"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
const params = new URLSearchParams(location.search);
|
||||
const code = params.get('code') || '404';
|
||||
const title = params.get('title') || '';
|
||||
const message = params.get('message') || '';
|
||||
const details = params.get('details') || '';
|
||||
document.getElementById('errorCode').textContent = code;
|
||||
document.getElementById('errorTitle').textContent = title || (code === '404' ? 'Страница не найдена' : 'Ошибка');
|
||||
document.getElementById('errorMessage').textContent = message || (code === '404' ? 'Запрашиваемая страница не существует или была перемещена.' : 'Произошла ошибка на сервере.');
|
||||
if (details) {
|
||||
document.getElementById('errorDetails').style.display = 'block';
|
||||
document.getElementById('errorStack').textContent = details;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+61
-13
@@ -23,6 +23,12 @@
|
||||
<div><label>Группа</label><select id="groupFilter"><option value="">Все группы</option></select></div>
|
||||
<div><label>С</label><input type="date" id="dateFrom"></div>
|
||||
<div><label>По</label><input type="date" id="dateTo"></div>
|
||||
<div><label>Тип</label>
|
||||
<div class="view-toggle" id="tabToggle">
|
||||
<button data-mode="attached" onclick="setMode('attached')">Прикреплённые</button>
|
||||
<button data-mode="detached" onclick="setMode('detached')">Откреплённые</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid" id="grid"><div class="empty">Загрузка...</div></div>
|
||||
<div class="pager" id="pager"></div>
|
||||
@@ -43,6 +49,20 @@ let currentFiles = [];
|
||||
let groupsLoaded = false;
|
||||
let studentsLoaded = false;
|
||||
let searchTimer = null;
|
||||
let mode = 'attached';
|
||||
|
||||
function setMode(m) {
|
||||
mode = m === 'detached' ? 'detached' : 'attached';
|
||||
document.querySelectorAll('#tabToggle button').forEach(b => {
|
||||
b.classList.toggle('active', b.dataset.mode === mode);
|
||||
});
|
||||
const show = mode === 'attached';
|
||||
const keep = ['searchInput', 'studentFilter', 'groupFilter', 'dateFrom', 'dateTo'].map(id => document.getElementById(id));
|
||||
if (!show) { keep.forEach(el => { el.value = ''; }); }
|
||||
keep.forEach(el => { el.parentElement.style.display = show ? '' : 'none'; });
|
||||
page = 1;
|
||||
loadFiles();
|
||||
}
|
||||
|
||||
function isImageFile(name) {
|
||||
return /\.(jpe?g|png|gif|webp|bmp|avif|svg|ico)$/i.test(name || '');
|
||||
@@ -63,14 +83,21 @@ function fileCardHTML(f) {
|
||||
const target = isImageFile(f.name)
|
||||
? `href="#" onclick="showImg('${url}');return false;"`
|
||||
: `href="${url}" download`;
|
||||
const meta = mode === 'detached'
|
||||
? `<div class="group" style="color:#f59e0b">Откреплён от записи, не привязан</div>`
|
||||
: `<div class="group">${esc(f.student_name)} · ${esc(f.group_name)}</div>`;
|
||||
const action = mode === 'detached'
|
||||
? `<button class="f-action f-del" onclick="deleteFile(${f.id})" title="Удалить файл навсегда">Удалить</button>`
|
||||
: `<button class="f-action" onclick="detachFile(${f.id})" title="Открепить файл от записи">Открепить</button>`;
|
||||
return `
|
||||
<div class="card">
|
||||
${thumb}
|
||||
<div class="info">
|
||||
<div class="file-name"><a ${target}>${esc(f.name)}</a></div>
|
||||
<div class="group">${esc(f.student_name)} · ${esc(f.group_name)}</div>
|
||||
${meta}
|
||||
<div class="time">${new Date(f.created_at).toLocaleString('ru')}${f.size ? ' · ' + humanSize(f.size) : ''}</div>
|
||||
</div>
|
||||
<div class="f-actions">${action}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -79,13 +106,29 @@ function renderGrid() {
|
||||
const grid = document.getElementById('grid');
|
||||
if (!currentFiles.length) {
|
||||
grid.className = 'grid';
|
||||
grid.innerHTML = '<div class="empty">Файлов не найдено</div>';
|
||||
grid.innerHTML = mode === 'detached'
|
||||
? '<div class="empty">Откреплённых файлов нет</div>'
|
||||
: '<div class="empty">Файлов не найдено</div>';
|
||||
return;
|
||||
}
|
||||
grid.className = 'grid';
|
||||
grid.innerHTML = currentFiles.map(fileCardHTML).join('');
|
||||
}
|
||||
|
||||
async function detachFile(id) {
|
||||
if (!confirm('Открепить файл от записи? Он переместится во вкладку «Откреплённые».')) return;
|
||||
const res = await fetch(`${API}/api/files/${id}/detach`, { method: 'POST', headers: hdr() });
|
||||
if (res.ok) { showToast('Файл откреплён'); loadFiles(); }
|
||||
else { const err = await res.json().catch(() => ({})); alert(err.error || 'Ошибка'); }
|
||||
}
|
||||
|
||||
async function deleteFile(id) {
|
||||
if (!confirm('Удалить файл навсегда? Отменить это действие нельзя.')) return;
|
||||
const res = await fetch(`${API}/api/files/${id}`, { method: 'DELETE', headers: hdr() });
|
||||
if (res.ok) { showToast('Файл удалён'); loadFiles(); }
|
||||
else { const err = await res.json().catch(() => ({})); alert(err.error || 'Ошибка'); }
|
||||
}
|
||||
|
||||
async function loadGroupOptions() {
|
||||
if (groupsLoaded) return;
|
||||
const sel = document.getElementById('groupFilter');
|
||||
@@ -111,19 +154,21 @@ async function loadStudentOptions() {
|
||||
|
||||
async function loadFiles() {
|
||||
const p = new URLSearchParams();
|
||||
const q = document.getElementById('searchInput').value;
|
||||
const sn = document.getElementById('studentFilter').value;
|
||||
const gid = document.getElementById('groupFilter').value;
|
||||
const df = document.getElementById('dateFrom').value;
|
||||
const dt = document.getElementById('dateTo').value;
|
||||
if (q) p.set('search', q);
|
||||
if (sn) p.set('student_name', sn);
|
||||
if (gid) p.set('group_id', gid);
|
||||
if (df) p.set('date_from', df);
|
||||
if (dt) p.set('date_to', dt);
|
||||
if (mode === 'attached') {
|
||||
const q = document.getElementById('searchInput').value;
|
||||
const sn = document.getElementById('studentFilter').value;
|
||||
const gid = document.getElementById('groupFilter').value;
|
||||
const df = document.getElementById('dateFrom').value;
|
||||
const dt = document.getElementById('dateTo').value;
|
||||
if (q) p.set('search', q);
|
||||
if (sn) p.set('student_name', sn);
|
||||
if (gid) p.set('group_id', gid);
|
||||
if (df) p.set('date_from', df);
|
||||
if (dt) p.set('date_to', dt);
|
||||
}
|
||||
p.set('limit', PAGE_SIZE);
|
||||
p.set('offset', (page - 1) * PAGE_SIZE);
|
||||
const res = await fetch(`${API}/api/files?${p}`, { headers: hdr() });
|
||||
const res = await fetch(`${API}/api/files${mode === 'detached' ? '/detached' : ''}?${p}`, { headers: hdr() });
|
||||
const data = await res.json();
|
||||
currentFiles = data.files;
|
||||
totalFiles = data.total;
|
||||
@@ -148,6 +193,9 @@ document.getElementById('searchInput').addEventListener('input', () => {
|
||||
(async () => {
|
||||
if (await checkAuth()) {
|
||||
buildSidebar(document.body.dataset.page);
|
||||
document.querySelectorAll('#tabToggle button').forEach(b => {
|
||||
b.classList.toggle('active', b.dataset.mode === mode);
|
||||
});
|
||||
loadGroupOptions();
|
||||
loadStudentOptions();
|
||||
loadFiles();
|
||||
|
||||
+27
-4
@@ -113,11 +113,34 @@ function isImageFile(name) {
|
||||
|
||||
function filesHTML(files) {
|
||||
if (!files || !files.length) return '';
|
||||
return `<div class="entry-files">${files.map(f =>
|
||||
isImageFile(f.name)
|
||||
return `<div class="entry-files">${files.map(f => {
|
||||
const link = isImageFile(f.name)
|
||||
? `<a class="f-link" href="#" onclick="showImg('${API}/api/files/${f.token}');return false;" title="Открыть">${esc(f.name)}</a>`
|
||||
: `<a class="f-link" href="${API}/api/files/${f.token}" download title="Скачать">${esc(f.name)}</a>`
|
||||
).join('')}</div>`;
|
||||
: `<a class="f-link" href="${API}/api/files/${f.token}" download title="Скачать">${esc(f.name)}</a>`;
|
||||
return `<span class="f-item">${link}<button class="f-detach" onclick="detachFile(${f.id})" title="Открепить файл">✕</button></span>`;
|
||||
}).join('')}</div>`;
|
||||
}
|
||||
|
||||
function findFile(id) {
|
||||
for (const e of currentEntries) {
|
||||
const f = (e.files || []).find(x => x.id === id);
|
||||
if (f) return f;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function detachFile(id) {
|
||||
const f = findFile(id);
|
||||
if (!f) return;
|
||||
if (!confirm(`Открепить файл «${f.name}» от записи? Он переместится в «Файлы → Откреплённые».`)) return;
|
||||
const res = await fetch(`${API}/api/files/${id}/detach`, { method: 'POST', headers: hdr() });
|
||||
if (res.ok) {
|
||||
showToast('Файл откреплён');
|
||||
loadEntries();
|
||||
} else {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
alert(err.error || 'Ошибка');
|
||||
}
|
||||
}
|
||||
|
||||
function gridEntryHTML(e) {
|
||||
|
||||
@@ -25,6 +25,14 @@ function requireAdmin(req, res, next) {
|
||||
next();
|
||||
}
|
||||
|
||||
function fixFilename(str) {
|
||||
try {
|
||||
return Buffer.from(str, 'latin1').toString('utf8');
|
||||
} catch {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
const upload = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: (_, __, cb) => {
|
||||
@@ -32,12 +40,14 @@ const upload = multer({
|
||||
cb(null, 'uploads');
|
||||
},
|
||||
filename: (_, file, cb) => {
|
||||
const ext = path.extname(file.originalname) || '.jpg';
|
||||
const original = fixFilename(file.originalname);
|
||||
const ext = path.extname(original) || '.jpg';
|
||||
cb(null, `${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext}`);
|
||||
},
|
||||
}),
|
||||
limits: { fileSize: 10 * 1024 * 1024 },
|
||||
fileFilter: (req, file, cb) => {
|
||||
file.originalname = fixFilename(file.originalname);
|
||||
if (file.fieldname === 'photo' && !file.mimetype.startsWith('image/')) cb(new Error('Only images'));
|
||||
else cb(null, true);
|
||||
},
|
||||
@@ -609,7 +619,7 @@ app.get('/api/entries', requireAdmin, async (req, res) => {
|
||||
if (rows.length) {
|
||||
const ids = rows.map(r => r.id);
|
||||
const fRes = await pool.query(
|
||||
'SELECT entry_id, token, name FROM project_files WHERE entry_id = ANY($1) ORDER BY id',
|
||||
'SELECT id, entry_id, token, name FROM project_files WHERE entry_id = ANY($1) ORDER BY id',
|
||||
[ids]
|
||||
);
|
||||
files = {};
|
||||
@@ -623,7 +633,7 @@ app.get('/api/entries', requireAdmin, async (req, res) => {
|
||||
|
||||
app.get('/api/entries/:id/files', requireAdmin, async (req, res) => {
|
||||
const { rows } = await pool.query(
|
||||
'SELECT token, name FROM project_files WHERE entry_id = $1 ORDER BY id',
|
||||
'SELECT id, token, name FROM project_files WHERE entry_id = $1 ORDER BY id',
|
||||
[req.params.id]
|
||||
);
|
||||
res.json(rows);
|
||||
@@ -668,6 +678,51 @@ app.get('/api/files', requireAdmin, async (req, res) => {
|
||||
res.json({ files, total });
|
||||
});
|
||||
|
||||
app.get('/api/files/detached', requireAdmin, async (req, res) => {
|
||||
const { search, limit, offset } = req.query;
|
||||
const conditions = [];
|
||||
const params = [];
|
||||
conditions.push('entry_id IS NULL');
|
||||
if (search) { params.push(`%${search}%`); conditions.push(`name ILIKE $${params.length}`); }
|
||||
const where = conditions.join(' AND ');
|
||||
const { rows: crows } = await pool.query(
|
||||
`SELECT count(*)::int AS n FROM project_files WHERE ${where}`,
|
||||
params
|
||||
);
|
||||
const total = crows[0].n;
|
||||
let q = `SELECT id, token, name, path, created_at FROM project_files
|
||||
WHERE ${where} ORDER BY created_at DESC`;
|
||||
const qparams = params.slice();
|
||||
const lim = parseInt(limit, 10);
|
||||
if (lim > 0) { qparams.push(lim); q += ` LIMIT $${qparams.length}`; }
|
||||
const off = parseInt(offset, 10);
|
||||
if (off > 0) { qparams.push(off); q += ` OFFSET $${qparams.length}`; }
|
||||
const { rows } = await pool.query(q, qparams);
|
||||
const files = rows.map(r => {
|
||||
let size = 0;
|
||||
try { size = fs.statSync(path.join(__dirname, r.path)).size; } catch {}
|
||||
return { id: r.id, token: r.token, name: r.name, created_at: r.created_at, size };
|
||||
});
|
||||
res.json({ files, total });
|
||||
});
|
||||
|
||||
app.post('/api/files/:id/detach', requireAdmin, async (req, res) => {
|
||||
const { rows } = await pool.query(
|
||||
'UPDATE project_files SET entry_id = NULL, detached_at = now() WHERE id = $1 RETURNING *',
|
||||
[req.params.id]
|
||||
);
|
||||
if (!rows.length) return res.status(404).json({ error: 'Не найдено' });
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
app.delete('/api/files/:id', requireAdmin, async (req, res) => {
|
||||
const { rows } = await pool.query('SELECT path FROM project_files WHERE id = $1', [req.params.id]);
|
||||
if (!rows.length) return res.status(404).json({ error: 'Не найдено' });
|
||||
safeUnlink(rows[0].path);
|
||||
await pool.query('DELETE FROM project_files WHERE id = $1', [req.params.id]);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
app.get('/api/files/:token', async (req, res) => {
|
||||
const { rows } = await pool.query('SELECT path, name FROM project_files WHERE token = $1', [req.params.token]);
|
||||
if (!rows.length) return res.status(404).json({ error: 'Not found' });
|
||||
@@ -866,7 +921,7 @@ app.get('/api/trash', requireAdmin, async (req, res) => {
|
||||
let files = {};
|
||||
if (rows.length) {
|
||||
const fRes = await pool.query(
|
||||
'SELECT entry_id, token, name FROM project_files WHERE entry_id = ANY($1) ORDER BY id',
|
||||
'SELECT id, entry_id, token, name FROM project_files WHERE entry_id = ANY($1) ORDER BY id',
|
||||
[rows.map(r => r.id)]
|
||||
);
|
||||
fRes.rows.forEach(f => { (files[f.entry_id] = files[f.entry_id] || []).push(f); });
|
||||
@@ -887,6 +942,43 @@ app.delete('/api/trash', requireAdmin, async (req, res) => {
|
||||
res.json({ ok: true, deleted: d.rowCount });
|
||||
});
|
||||
|
||||
// --- Error handlers ---
|
||||
const ERROR_HTML = fs.readFileSync(path.join(__dirname, 'public', 'error.html'), 'utf8');
|
||||
|
||||
function isApiRoute(req) {
|
||||
return req.path.startsWith('/api/') || req.path.startsWith('/s/');
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function renderErrorPage(code, title, message, details) {
|
||||
return ERROR_HTML
|
||||
.replace('id="errorCode">404', `id="errorCode">${code}`)
|
||||
.replace('id="errorTitle">Страница не найдена', `id="errorTitle">${title}`)
|
||||
.replace('id="errorMessage">Запрашиваемая страница не существует или была перемещена.', `id="errorMessage">${message}`)
|
||||
.replace('style="display:none"', details ? '' : 'style="display:none"')
|
||||
.replace('<pre id="errorStack"></pre>', details ? `<pre id="errorStack">${escapeHtml(details)}</pre>` : '<pre id="errorStack"></pre>');
|
||||
}
|
||||
|
||||
app.use((req, res, next) => {
|
||||
if (isApiRoute(req)) {
|
||||
return res.status(404).json({ error: 'Not found' });
|
||||
}
|
||||
res.status(404).send(renderErrorPage(404, 'Страница не найдена', 'Запрашиваемая страница не существует или была перемещена.'));
|
||||
});
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('Error:', err);
|
||||
if (isApiRoute(req)) {
|
||||
return res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
const msg = process.env.NODE_ENV === 'production' ? 'Произошла ошибка на сервере.' : (err?.message || 'Internal server error');
|
||||
const details = process.env.NODE_ENV === 'production' ? '' : (err?.stack || '');
|
||||
res.status(500).send(renderErrorPage(500, 'Ошибка сервера', msg, details));
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const HTTPS_PORT = process.env.HTTPS_PORT || 3443;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user