feat: harden content workflows and staging smoke tests
CI — Test & Build / 🧪 Run Tests & Generate Reports (push) Waiting to run
CI — Test & Build / 🏗️ Build Docker Image (push) Blocked by required conditions
CI — Test & Build / 🐳 Docker integration & API E2E (push) Blocked by required conditions
CI — Test & Build / 🌐 Staging Playwright smoke (push) Waiting to run
CI — Test & Build / 🧪 Run Tests & Generate Reports (push) Waiting to run
CI — Test & Build / 🏗️ Build Docker Image (push) Blocked by required conditions
CI — Test & Build / 🐳 Docker integration & API E2E (push) Blocked by required conditions
CI — Test & Build / 🌐 Staging Playwright smoke (push) Waiting to run
This commit is contained in:
@@ -8,6 +8,9 @@ on:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '17 4 * * *'
|
||||
|
||||
env:
|
||||
NODE_VERSION: '20'
|
||||
@@ -71,3 +74,87 @@ jobs:
|
||||
docker cp mozdit-app-ci:/app/.next/BUILD_ID ./build-id 2>/dev/null || \
|
||||
echo "BUILD_ID nem elérhető — build verification skip"
|
||||
docker rm mozdit-app-ci
|
||||
|
||||
docker-integration:
|
||||
name: 🐳 Docker integration & API E2E
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
steps:
|
||||
- name: 📥 Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 📦 Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
cache-dependency-path: proto/package-lock.json
|
||||
|
||||
- name: 📥 Install dependencies
|
||||
working-directory: ./proto
|
||||
run: npm ci
|
||||
|
||||
- name: 🐳 Start Docker development stack
|
||||
run: docker compose -f docker-compose.dev.yml up --build -d
|
||||
|
||||
- name: ⏳ Wait for application health
|
||||
run: |
|
||||
for attempt in $(seq 1 30); do
|
||||
if curl --fail --silent http://localhost:8080/api/health > /dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
docker compose -f docker-compose.dev.yml logs --tail=100
|
||||
exit 1
|
||||
|
||||
- name: 🔗 Run integration tests
|
||||
working-directory: ./proto
|
||||
run: npm run test:integration
|
||||
|
||||
- name: 🔄 Run API E2E tests
|
||||
working-directory: ./proto
|
||||
run: npm run test:e2e
|
||||
|
||||
- name: 🧹 Stop Docker development stack
|
||||
if: always()
|
||||
run: docker compose -f docker-compose.dev.yml down -v
|
||||
|
||||
staging-smoke:
|
||||
name: 🌐 Staging Playwright smoke
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
|
||||
steps:
|
||||
- name: 📥 Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 📦 Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
cache-dependency-path: proto/package-lock.json
|
||||
|
||||
- name: 📥 Install dependencies
|
||||
working-directory: ./proto
|
||||
run: npm ci
|
||||
|
||||
- name: 🌐 Install Chromium for Playwright
|
||||
working-directory: ./proto
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: 🧪 Run staging smoke suite
|
||||
working-directory: ./proto
|
||||
env:
|
||||
BASE_URL: https://stage.mozdit.hu
|
||||
run: npm run test:smoke:staging
|
||||
|
||||
- name: 📦 Upload Playwright artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: staging-smoke-artifacts
|
||||
path: |
|
||||
proto/playwright-report/
|
||||
proto/test-results/
|
||||
retention-days: 30
|
||||
|
||||
@@ -42,3 +42,8 @@ mcp.json
|
||||
|
||||
# Plane sync backup fájlok
|
||||
TODO.md.backup.*
|
||||
.content-backups/
|
||||
.content-editor-audit.jsonl
|
||||
.playwright-mcp/
|
||||
proto/playwright-report/
|
||||
proto/test-results/
|
||||
|
||||
+220
-51
@@ -12,9 +12,17 @@ const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { exec } = require('child_process');
|
||||
const crypto = require('crypto');
|
||||
const { validateContent } = require('./proto/src/content/schema');
|
||||
|
||||
const PORT = 4001;
|
||||
const CONTENT_DIR = path.join(__dirname, 'proto', 'src', 'content');
|
||||
const BACKUP_DIR = path.join(__dirname, '.content-backups');
|
||||
const MAX_REQUEST_BODY_BYTES = 256 * 1024;
|
||||
const AUDIT_LOG_FILE = path.join(__dirname, '.content-editor-audit.jsonl');
|
||||
const RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000;
|
||||
const AUTH_MAX_ATTEMPTS = 5;
|
||||
const PUBLISH_MAX_ATTEMPTS = 3;
|
||||
|
||||
const FILES = {
|
||||
common: path.join(CONTENT_DIR, 'common.json'),
|
||||
@@ -36,7 +44,7 @@ const FILE_LABELS = {
|
||||
hasznalatiFeltetelek: '⚖️ ÁSZF',
|
||||
};
|
||||
|
||||
const HTML = (activeFile, jsonData, message) => `<!DOCTYPE html>
|
||||
const HTML = (activeFile, jsonData, message, csrfToken) => `<!DOCTYPE html>
|
||||
<html lang="hu">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
@@ -145,6 +153,7 @@ ${message ? `<div class="toast ${message.type === 'ok' ? 'ok' : 'err'}">${messag
|
||||
<script>
|
||||
const DATA = JSON.parse(document.getElementById('page-data').textContent);
|
||||
const FILE = "${activeFile}";
|
||||
const CSRF_TOKEN = "${csrfToken}";
|
||||
|
||||
// ── Render ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -170,11 +179,20 @@ function renderPrimitive(path, val, container) {
|
||||
const isLong = String(val).length > 80 || String(val).includes('<');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'field';
|
||||
const type = val === null ? 'null' : typeof val;
|
||||
let control;
|
||||
if (type === 'boolean') {
|
||||
control = \`<input type="checkbox" data-path="\${path}" data-type="boolean" \${val ? 'checked' : ''}>\`;
|
||||
} else if (type === 'number') {
|
||||
control = \`<input type="number" data-path="\${path}" data-type="number" value="\${esc(val)}">\`;
|
||||
} else {
|
||||
control = isLong
|
||||
? \`<textarea data-path="\${path}" data-type="\${type}" rows="\${Math.min(8,Math.max(2,Math.ceil(String(val).length/80)))}">\${esc(val ?? '')}<\/textarea>\`
|
||||
: \`<input type="text" data-path="\${path}" data-type="\${type}" value="\${esc(val ?? '')}">\`;
|
||||
}
|
||||
div.innerHTML = \`
|
||||
<label>\${path}</label>
|
||||
\${isLong
|
||||
? \`<textarea data-path="\${path}" rows="\${Math.min(8,Math.max(2,Math.ceil(String(val).length/80)))}">\${esc(val)}</textarea>\`
|
||||
: \`<input type="text" data-path="\${path}" value="\${esc(val)}">\`}
|
||||
\${control}
|
||||
\`;
|
||||
container.appendChild(div);
|
||||
}
|
||||
@@ -212,7 +230,7 @@ function renderArray(key, arr, container, path) {
|
||||
addBtn.onclick = () => {
|
||||
const idx = items.children.length;
|
||||
if (isObj) {
|
||||
const blank = Object.fromEntries(Object.keys(sample).map(k => [k, '']));
|
||||
const blank = blankLike(sample);
|
||||
items.appendChild(makeObjCard(blank, idx, path));
|
||||
} else {
|
||||
items.appendChild(makeStrItem('', idx, path));
|
||||
@@ -223,13 +241,30 @@ function renderArray(key, arr, container, path) {
|
||||
container.appendChild(section);
|
||||
}
|
||||
|
||||
function blankLike(value) {
|
||||
if (Array.isArray(value)) return [];
|
||||
if (value && typeof value === 'object') {
|
||||
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, blankLike(child)]));
|
||||
}
|
||||
if (typeof value === 'boolean') return false;
|
||||
if (typeof value === 'number') return 0;
|
||||
return '';
|
||||
}
|
||||
|
||||
function makeStrItem(val, idx, path) {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'str-item';
|
||||
const ta = document.createElement('textarea');
|
||||
const type = val === null ? 'null' : typeof val;
|
||||
const ta = type === 'boolean' ? document.createElement('input') : document.createElement('textarea');
|
||||
ta.dataset.path = path + '[' + idx + ']';
|
||||
ta.value = val;
|
||||
ta.rows = Math.min(6, Math.max(2, Math.ceil(String(val).length / 80)));
|
||||
ta.dataset.type = type;
|
||||
if (type === 'boolean') {
|
||||
ta.type = 'checkbox';
|
||||
ta.checked = val;
|
||||
} else {
|
||||
ta.value = val ?? '';
|
||||
ta.rows = Math.min(6, Math.max(2, Math.ceil(String(val ?? '').length / 80)));
|
||||
}
|
||||
const del = document.createElement('button');
|
||||
del.className = 'btn-del';
|
||||
del.textContent = '❌';
|
||||
@@ -248,19 +283,7 @@ function makeObjCard(obj, idx, path) {
|
||||
hdr.textContent = path + '[' + idx + ']';
|
||||
card.appendChild(hdr);
|
||||
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
const fieldPath = path + '[' + idx + '].' + k;
|
||||
const isLong = String(v).length > 80 || String(v).includes('<');
|
||||
const fd = document.createElement('div');
|
||||
fd.className = 'inner-field';
|
||||
fd.innerHTML = \`
|
||||
<div class="inner-label">\${k}</div>
|
||||
\${isLong
|
||||
? \`<textarea data-path="\${fieldPath}" rows="\${Math.min(6,Math.max(2,Math.ceil(String(v).length/80)))}">\${esc(v)}</textarea>\`
|
||||
: \`<input type="text" data-path="\${fieldPath}" value="\${esc(v)}">\`}
|
||||
\`;
|
||||
card.appendChild(fd);
|
||||
}
|
||||
renderObject(obj, card, path + '[' + idx + ']');
|
||||
|
||||
const del = document.createElement('button');
|
||||
del.className = 'btn-del-card';
|
||||
@@ -299,11 +322,20 @@ function collect() {
|
||||
clearArrays(result);
|
||||
|
||||
document.querySelectorAll('[data-path]').forEach(el => {
|
||||
setPath(result, el.dataset.path, el.value);
|
||||
setPath(result, el.dataset.path, readValue(el));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function readValue(el) {
|
||||
switch (el.dataset.type) {
|
||||
case 'boolean': return el.checked;
|
||||
case 'number': return Number(el.value);
|
||||
case 'null': return el.value === '' ? null : el.value;
|
||||
default: return el.value;
|
||||
}
|
||||
}
|
||||
|
||||
function clearArrays(obj) {
|
||||
for (const k of Object.keys(obj)) {
|
||||
if (Array.isArray(obj[k])) obj[k] = [];
|
||||
@@ -312,24 +344,47 @@ function clearArrays(obj) {
|
||||
}
|
||||
|
||||
function setPath(obj, path, value) {
|
||||
// Parse "a.b[0].c" into parts
|
||||
const parts = path.replace(/\[(\d+)\]/g, '.$1').split('.');
|
||||
const parts = parsePath(path);
|
||||
let cur = obj;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const p = isNaN(parts[i]) ? parts[i] : +parts[i];
|
||||
const next = isNaN(parts[i+1]) ? {} : (cur[p] || []);
|
||||
if (cur[p] === undefined || cur[p] === null) cur[p] = next;
|
||||
cur = cur[p];
|
||||
const part = parts[i];
|
||||
if (cur[part] === undefined || cur[part] === null) {
|
||||
cur[part] = typeof parts[i + 1] === 'number' ? [] : {};
|
||||
}
|
||||
cur = cur[part];
|
||||
}
|
||||
const last = isNaN(parts[parts.length-1]) ? parts[parts.length-1] : +parts[parts.length-1];
|
||||
cur[last] = value;
|
||||
cur[parts[parts.length - 1]] = value;
|
||||
}
|
||||
|
||||
function parsePath(path) {
|
||||
const parts = [];
|
||||
let token = '';
|
||||
let inIndex = false;
|
||||
for (const char of path) {
|
||||
if (char === '.') {
|
||||
if (!inIndex && token) parts.push(token);
|
||||
token = '';
|
||||
} else if (char === '[') {
|
||||
if (token) parts.push(token);
|
||||
token = '';
|
||||
inIndex = true;
|
||||
} else if (char === ']') {
|
||||
parts.push(Number(token));
|
||||
token = '';
|
||||
inIndex = false;
|
||||
} else {
|
||||
token += char;
|
||||
}
|
||||
}
|
||||
if (token) parts.push(token);
|
||||
return parts;
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const data = collect();
|
||||
const res = await fetch('/save?file=' + FILE, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': CSRF_TOKEN },
|
||||
body: JSON.stringify(data, null, 2)
|
||||
});
|
||||
const json = await res.json();
|
||||
@@ -356,7 +411,7 @@ async function publish() {
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch('/publish', { method: 'POST' });
|
||||
const res = await fetch('/publish', { method: 'POST', headers: { 'X-CSRF-Token': CSRF_TOKEN } });
|
||||
const json = await res.json();
|
||||
|
||||
if (json.ok) {
|
||||
@@ -393,15 +448,75 @@ if (toast) setTimeout(() => toast.remove(), 3500);
|
||||
|
||||
// ── Server ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const CMS_USER = process.env.CMS_USER || 'admin';
|
||||
const CMS_PASS = process.env.CMS_PASS || 'mozdit2026';
|
||||
const CMS_USER = process.env.CMS_USER;
|
||||
const CMS_PASS = process.env.CMS_PASS;
|
||||
const CSRF_TOKEN = process.env.CMS_CSRF_TOKEN || crypto.randomBytes(32).toString('hex');
|
||||
const rateLimits = new Map();
|
||||
|
||||
function securityConfigIsValid() {
|
||||
return Boolean(CMS_USER && CMS_PASS);
|
||||
}
|
||||
|
||||
function getClientAddress(req) {
|
||||
return req.socket.remoteAddress || 'unknown';
|
||||
}
|
||||
|
||||
function exceedsRateLimit(key, limit) {
|
||||
const now = Date.now();
|
||||
const attempts = (rateLimits.get(key) || []).filter(time => now - time < RATE_LIMIT_WINDOW_MS);
|
||||
attempts.push(now);
|
||||
rateLimits.set(key, attempts);
|
||||
return attempts.length > limit;
|
||||
}
|
||||
|
||||
function hasValidCredentials(req) {
|
||||
const b64auth = (req.headers.authorization || '').split(' ')[1] || '';
|
||||
const [login = '', password = ''] = Buffer.from(b64auth, 'base64').toString().split(':');
|
||||
if (!CMS_USER || !CMS_PASS || login.length !== CMS_USER.length || password.length !== CMS_PASS.length) return false;
|
||||
return crypto.timingSafeEqual(Buffer.from(login), Buffer.from(CMS_USER))
|
||||
&& crypto.timingSafeEqual(Buffer.from(password), Buffer.from(CMS_PASS));
|
||||
}
|
||||
|
||||
function hasValidCsrfToken(req) {
|
||||
const token = req.headers['x-csrf-token'];
|
||||
return typeof token === 'string'
|
||||
&& token.length === CSRF_TOKEN.length
|
||||
&& crypto.timingSafeEqual(Buffer.from(token), Buffer.from(CSRF_TOKEN));
|
||||
}
|
||||
|
||||
function writeAudit(event, details = {}) {
|
||||
const record = { timestamp: new Date().toISOString(), event, ...details };
|
||||
fs.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(record) + '\n', { encoding: 'utf8', mode: 0o600 });
|
||||
}
|
||||
|
||||
function backupAndWriteAtomically(targetFile, data, backupDir = BACKUP_DIR) {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const backupName = `${path.basename(targetFile, '.json')}.${timestamp}.json`;
|
||||
const backupFile = path.join(backupDir, backupName);
|
||||
const tempFile = `${targetFile}.${process.pid}.${Date.now()}.tmp`;
|
||||
|
||||
fs.mkdirSync(backupDir, { recursive: true, mode: 0o700 });
|
||||
fs.copyFileSync(targetFile, backupFile);
|
||||
fs.writeFileSync(tempFile, JSON.stringify(data, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
|
||||
fs.renameSync(tempFile, targetFile);
|
||||
return backupFile;
|
||||
}
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
// Basic Auth verification
|
||||
const b64auth = (req.headers.authorization || '').split(' ')[1] || '';
|
||||
const [login, password] = Buffer.from(b64auth, 'base64').toString().split(':');
|
||||
|
||||
if (login !== CMS_USER || password !== CMS_PASS) {
|
||||
const clientAddress = getClientAddress(req);
|
||||
if (!securityConfigIsValid()) {
|
||||
res.writeHead(503, { 'Content-Type': 'text/plain; charset=utf-8' });
|
||||
res.end('Content Editor is disabled: CMS_USER and CMS_PASS must be configured.');
|
||||
return;
|
||||
}
|
||||
if (!hasValidCredentials(req)) {
|
||||
const limited = exceedsRateLimit(`auth:${clientAddress}`, AUTH_MAX_ATTEMPTS);
|
||||
writeAudit('authentication_failed', { clientAddress, limited });
|
||||
if (limited) {
|
||||
res.writeHead(429, { 'Retry-After': String(RATE_LIMIT_WINDOW_MS / 1000) });
|
||||
res.end('Too many authentication attempts');
|
||||
return;
|
||||
}
|
||||
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="mozdIT CMS"' });
|
||||
res.end('Access denied');
|
||||
return;
|
||||
@@ -411,17 +526,48 @@ const server = http.createServer(async (req, res) => {
|
||||
const fileKey = u.searchParams.get('file') || 'home';
|
||||
const activeFile = FILES[fileKey] ? fileKey : 'home';
|
||||
|
||||
if (req.method === 'POST' && !hasValidCsrfToken(req)) {
|
||||
writeAudit('csrf_rejected', { clientAddress, path: u.pathname, file: activeFile });
|
||||
res.writeHead(403, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: false, error: 'Érvénytelen vagy hiányzó CSRF token' }));
|
||||
return;
|
||||
}
|
||||
|
||||
// POST /save — JSON body
|
||||
if (req.method === 'POST' && u.pathname === '/save') {
|
||||
let body = '';
|
||||
req.on('data', c => body += c);
|
||||
let bodyTooLarge = false;
|
||||
let bodySize = 0;
|
||||
req.on('data', c => {
|
||||
bodySize += c.length;
|
||||
if (bodySize > MAX_REQUEST_BODY_BYTES) {
|
||||
bodyTooLarge = true;
|
||||
return;
|
||||
}
|
||||
body += c;
|
||||
});
|
||||
req.on('end', () => {
|
||||
try {
|
||||
if (bodyTooLarge) {
|
||||
writeAudit('content_saved', { clientAddress, user: CMS_USER, file: activeFile, result: 'request_too_large' });
|
||||
res.writeHead(413, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: false, error: `A kérés túl nagy (maximum ${MAX_REQUEST_BODY_BYTES} byte)` }));
|
||||
return;
|
||||
}
|
||||
const data = JSON.parse(body);
|
||||
fs.writeFileSync(FILES[activeFile], JSON.stringify(data, null, 2) + '\n', 'utf8');
|
||||
const validation = validateContent(activeFile, data);
|
||||
if (!validation.ok) {
|
||||
writeAudit('content_saved', { clientAddress, user: CMS_USER, file: activeFile, result: 'validation_failed' });
|
||||
res.writeHead(422, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: false, error: validation.errors.join('; '), errors: validation.errors }));
|
||||
return;
|
||||
}
|
||||
const backupFile = backupAndWriteAtomically(FILES[activeFile], data);
|
||||
writeAudit('content_saved', { clientAddress, user: CMS_USER, file: activeFile, result: 'ok' });
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
res.end(JSON.stringify({ ok: true, backup: path.relative(__dirname, backupFile) }));
|
||||
} catch (e) {
|
||||
writeAudit('content_saved', { clientAddress, user: CMS_USER, file: activeFile, result: 'error' });
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: false, error: e.message }));
|
||||
}
|
||||
@@ -431,18 +577,27 @@ const server = http.createServer(async (req, res) => {
|
||||
|
||||
// POST /publish — Git Commit & Push
|
||||
if (req.method === 'POST' && u.pathname === '/publish') {
|
||||
if (exceedsRateLimit(`publish:${clientAddress}`, PUBLISH_MAX_ATTEMPTS)) {
|
||||
writeAudit('publish_rate_limited', { clientAddress, user: CMS_USER });
|
||||
res.writeHead(429, { 'Content-Type': 'application/json', 'Retry-After': String(RATE_LIMIT_WINDOW_MS / 1000) });
|
||||
res.end(JSON.stringify({ ok: false, error: 'Túl sok publikálási kísérlet' }));
|
||||
return;
|
||||
}
|
||||
exec('git add . && git commit -m "content: frissítve a CMS-ből" && git push', { cwd: CONTENT_DIR }, (error, stdout, stderr) => {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
if (error) {
|
||||
// If there's nothing to commit, it's fine
|
||||
if (stdout.includes('nothing to commit') || stdout.includes('working tree clean')) {
|
||||
writeAudit('publish_finished', { clientAddress, user: CMS_USER, result: 'no_changes' });
|
||||
res.end(JSON.stringify({ ok: true, output: 'No changes to commit' }));
|
||||
} else {
|
||||
writeAudit('publish_finished', { clientAddress, user: CMS_USER, result: 'error' });
|
||||
res.end(JSON.stringify({ ok: false, error: stderr || stdout || error.message }));
|
||||
}
|
||||
} else {
|
||||
// If a deploy.sh script exists, run it optionally in background
|
||||
exec('cd ../../../ && ./deploy.sh production > deploy.log 2>&1 &');
|
||||
writeAudit('publish_finished', { clientAddress, user: CMS_USER, result: 'ok' });
|
||||
res.end(JSON.stringify({ ok: true, output: stdout }));
|
||||
}
|
||||
});
|
||||
@@ -459,15 +614,29 @@ const server = http.createServer(async (req, res) => {
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(HTML(activeFile, jsonData, message));
|
||||
res.end(HTML(activeFile, jsonData, message, CSRF_TOKEN));
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`\n✅ mozdIT Content Editor fut: http://localhost:${PORT}\n`);
|
||||
console.log(' Szerkeszthető fájlok:');
|
||||
Object.entries(FILE_LABELS).forEach(([k, l]) => {
|
||||
const rel = k === 'common' ? 'common.json' : `pages/${k}.json`;
|
||||
console.log(` • ${l}: proto/src/content/${rel}`);
|
||||
if (require.main === module) {
|
||||
if (!securityConfigIsValid()) {
|
||||
throw new Error('CMS_USER és CMS_PASS nélkül a Content Editor nem indítható el.');
|
||||
}
|
||||
server.listen(PORT, '127.0.0.1', () => {
|
||||
console.log(`\n✅ mozdIT Content Editor fut: http://localhost:${PORT}\n`);
|
||||
console.log(' Szerkeszthető fájlok:');
|
||||
Object.entries(FILE_LABELS).forEach(([k, l]) => {
|
||||
const rel = k === 'common' ? 'common.json' : `pages/${k}.json`;
|
||||
console.log(` • ${l}: proto/src/content/${rel}`);
|
||||
});
|
||||
console.log('\n Ctrl+C a leállításhoz\n');
|
||||
});
|
||||
console.log('\n Ctrl+C a leállításhoz\n');
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
backupAndWriteAtomically,
|
||||
validateContent,
|
||||
hasValidCredentials,
|
||||
hasValidCsrfToken,
|
||||
securityConfigIsValid,
|
||||
csrfToken: CSRF_TOKEN,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Kapcsolatfelvételi üzenetek perzisztenciája
|
||||
|
||||
## Működés
|
||||
|
||||
A `POST /api/contact` érvényes, nem spamnek minősített üzenetet a MongoDB `contact_submissions` kollekciójába ment. A rekord a következő, feldolgozáshoz szükséges mezőket tartalmazza:
|
||||
|
||||
- név, e-mail cím, tárgy és üzenet;
|
||||
- GDPR-hozzájárulás;
|
||||
- `new` feldolgozási státusz;
|
||||
- szerveroldali `createdAt` időbélyeg.
|
||||
|
||||
A végpont csak sikeres `insertOne` után ad 200-as választ, és visszaadja a `submissionId` azonosítót. Ha az adatbázis nem érhető el vagy az írás hibás, 500-as választ küld; a felhasználó nem kap hamis sikerjelzést.
|
||||
|
||||
## Ellenőrzések
|
||||
|
||||
- Unit teszt: sikeres mentésnél a beszúrandó dokumentum és válasz-ID ellenőrzése; MongoDB-hibánál 500-as válasz.
|
||||
- Docker integrációs teszt: a `POST /api/contact` után a teszt azonosító alapján visszaolvassa a MongoDB-dokumentumot.
|
||||
|
||||
A Docker integráció a `docker-compose.dev.yml` szerinti `http://localhost:8080` alkalmazás- és `localhost:27018` MongoDB-portot használja. A futtatáshoz Docker daemon szükséges:
|
||||
|
||||
```sh
|
||||
npm run docker:dev
|
||||
npm run test:integration
|
||||
```
|
||||
@@ -0,0 +1,79 @@
|
||||
# Content Editor – strukturális helyreállítás
|
||||
|
||||
## Cél és hatókör
|
||||
|
||||
Ez a dokumentum a `MITHOME-30` feltárását és a kezdeti adathelyreállítást rögzíti. A Content Editor a `proto/src/content/**/*.json` fájlokat írja, ezért a hiba a megjelenő weboldal-tartalmat közvetlenül érinti.
|
||||
|
||||
## Talált hiba
|
||||
|
||||
A böngészős mentési kód minden mező értékét szövegként írta vissza. Mentés előtt kiürítette az összes tömböt, majd a `a.b[0].c` útvonalat törékeny reguláris kifejezéssel dolgozta fel. Emiatt:
|
||||
|
||||
- a tömbök `[]` értékre ürültek, az elemek pedig hibás, például `items[0]` nevű objektumkulcsokba kerültek;
|
||||
- az objektumokat és beágyazott tömböket a felület `"[object Object]"` szöveggé alakíthatta;
|
||||
- a számok és logikai értékek szöveggé alakultak;
|
||||
- egyes kulcsok sérültek (`id` helyett `i.$1`, `description` helyett `0.$1escription`).
|
||||
|
||||
## Helyreállítás – 2026-08-17
|
||||
|
||||
Az alábbi fájlok strukturálisan helyreálltak:
|
||||
|
||||
- `pages/home.json` – a Git-történet utolsó ép változatából, a későbbi `https://mail.mozdit.hu` webmail-címet megtartva;
|
||||
- `pages/about.json` – a helyes tömbök és objektumok visszaállításával;
|
||||
- `pages/services.json` – a részletes szolgáltatás-specifikációk beágyazott objektumaival együtt;
|
||||
- `pages/adatvedelem.json` és `pages/hasznalati-feltetelek.json` – a későbbi, üzletileg releváns `Szigetszentmiklós` szöveg megtartásával.
|
||||
|
||||
Minden `src/content/pages/*.json` fájl JSON-szintaktikája és a korábbi sérülési mintákra végzett ellenőrzése rendben van. A teljes unit tesztcsomag 55 sikeres teszttel lefutott.
|
||||
|
||||
## A hiba megelőzése a kliensben
|
||||
|
||||
A `content-editor.js` mentési logikája most karakterenként elemzi az útvonalakat, ezért a HTML-sablon escape-elése nem módosíthatja a parser működését. A felület rekurzívan rendereli az objektumokat és a beágyazott tömböket; a boolean checkboxként, a szám number-inputként szerkeszthető. A mentés ezek típusát megőrzi.
|
||||
|
||||
A ténylegesen generált böngésző-kódot ellenőrző regressziós teszt:
|
||||
|
||||
```sh
|
||||
node scripts/test-content-editor-serializer.js
|
||||
```
|
||||
|
||||
A teszt egy beágyazott tömböket és objektumokat, valamint string-, boolean- és numerikus mezőket tartalmazó mintán ellenőrzi a mentési körutazást.
|
||||
|
||||
## Szerveroldali védelmi réteg
|
||||
|
||||
A `MITHOME-41` részeként a `proto/src/content/schema.js` az összes szerkeszthető content-fájl kötelező mezőit, típusait és megengedett kulcsait írja le. A Next.js loader és a Content Editor ezt az egy közös validátort használja.
|
||||
|
||||
A mentés csak sikeres validáció után történik. Az Editor legfeljebb 256 KiB-os request body-t fogad; hibás JSON vagy sémahiba `422` választ ad, és az eredeti fájlt érintetlenül hagyja. Érvényes mentéskor:
|
||||
|
||||
1. az eredeti JSON időbélyeges példánya a `.content-backups/` könyvtárba kerül;
|
||||
2. az új tartalom egy célfájllal azonos könyvtárban létrehozott ideiglenes fájlba íródik;
|
||||
3. az ideiglenes fájl atomikus átnevezéssel váltja fel a célfájlt.
|
||||
|
||||
A backup-könyvtár szándékosan nincs Gitben. A séma- és atomikus mentési tesztek:
|
||||
|
||||
```sh
|
||||
node scripts/test-content-schema.js
|
||||
node scripts/test-content-editor-save.js
|
||||
```
|
||||
|
||||
## Hozzáférésvédelem
|
||||
|
||||
A Content Editor csak akkor indul el, ha a környezetben mindkét változó meg van adva:
|
||||
|
||||
```sh
|
||||
CMS_USER=<egyedi-felhasználónév>
|
||||
CMS_PASS=<erős-egyedi-jelszó>
|
||||
```
|
||||
|
||||
Nincs beégetett alapértelmezett belépési adat. A szerver kizárólag a `127.0.0.1` címen figyel, ezért távoli használathoz Nginx/VPN vagy SSH tunnel szükséges. A böngésző minden módosító kéréshez szerver által kiadott CSRF-tokent küld.
|
||||
|
||||
Az ismételt hibás bejelentkezést és a publikálási kéréseket IP-alapú, 15 perces csúszóablakos limit védi. A mentések és publikálások minimális auditbejegyzést írnak a `.content-editor-audit.jsonl` fájlba (időpont, esemény, felhasználó, célfájl, eredmény); secretet és teljes contentet nem naplóz. Ez a fájl, valamint a backupok nincs Gitben.
|
||||
|
||||
A védelmi alapteszt:
|
||||
|
||||
```sh
|
||||
node scripts/test-content-editor-security.js
|
||||
```
|
||||
|
||||
## Következő védelmi lépések
|
||||
|
||||
1. A szerveroldali mentésnek sémaellenőrzést, atomikus írást és visszaállítható mentést kell használnia (`MITHOME-41`).
|
||||
2. A publikálási folyamatot a natív, systemd-alapú kiadási eljáráshoz kell igazítani (`MITHOME-42`).
|
||||
3. A Content Editor alapértelmezett hitelesítő adatait és a hiányzó CSRF-, audit- és rate-limit védelmet meg kell szüntetni (`MITHOME-43`).
|
||||
@@ -0,0 +1,64 @@
|
||||
# Staging UI smoke tesztterv
|
||||
|
||||
## Cél
|
||||
|
||||
A staging deploy után 2–3 perc alatt jelezze, ha a publikus weboldal alapvetően nem használható. A cél nem a teljes regresszió, hanem a kritikus útvonalak gyors, stabil ellenőrzése.
|
||||
|
||||
**Célkörnyezet:** `https://stage.mozdit.hu`
|
||||
**Futás:** minden staging deploy után, valamint manuálisan kiadás előtt.
|
||||
**Eszköz:** Playwright MCP feltáráshoz; az automatizált futtatáshoz Playwright Test alapú tesztfájl javasolt.
|
||||
|
||||
## Határok
|
||||
|
||||
- A smoke teszt csak a staging környezetet használja.
|
||||
- Nem ment tartalmat, nem publikál és nem küld valódi kapcsolatfelvételi üzenetet.
|
||||
- Nem támaszkodik a magyar marketing-szövegek teljes egyezésére; stabil szerepköröket, URL-eket, címeket és státuszkódokat ellenőriz.
|
||||
- Az olyan részletes üzleti folyamatok, mint az üzenet tartós MongoDB-mentése, külön integrációs/E2E tesztbe tartoznak.
|
||||
|
||||
## Kötelező ellenőrzések
|
||||
|
||||
| Azonosító | Forgatókönyv | Várható eredmény |
|
||||
| --- | --- | --- |
|
||||
| SMOKE-01 | Health endpoint | `GET /api/health` 200-as választ és `status: "ok"` értéket ad. |
|
||||
| SMOKE-02 | Kezdőlap renderelése | A `/` oldal betölt; látható a főcím, a fejléc navigációja, a kapcsolat CTA és a lábléc. Nincs böngésző-konzolhiba. |
|
||||
| SMOKE-03 | Fő navigáció | A Kezdőlap, Rólunk, Szolgáltatások és Kapcsolat linkek a megfelelő publikus útvonalra vezetnek; minden céloldalon egyetlen `h1` található. |
|
||||
| SMOKE-04 | Kritikus szolgáltatási tartalom | A kezdőlapon vagy a szolgáltatások oldalon látható legalább egy szolgáltatáskártya és a kapcsolat CTA. Ezzel korán észlelhető a content JSON vagy a Footer/Home integráció sérülése. |
|
||||
| SMOKE-05 | Kapcsolati űrlap kliensoldali védelme | Üres űrlap elküldésekor a kötelező mezők hibajelzései megjelennek, és nem történik hálózati `POST /api/contact` kérés. |
|
||||
|
||||
## Opcionális, nem blokkoló ellenőrzések
|
||||
|
||||
- A dark-mode kapcsoló működik és az oldal olvasható marad.
|
||||
- A webmail link külső URL-re mutat.
|
||||
- A jogi oldalak elérhetők és tartalmaznak főcímet.
|
||||
|
||||
## Automatikus futtatási szerződés
|
||||
|
||||
1. A teszt a `BASE_URL=https://stage.mozdit.hu` változót használja; lokális cím nincs beégetve.
|
||||
2. Az összes kötelező teszt legfeljebb 30 másodperc alatt lefut.
|
||||
3. Sikertelenségkor a futás ment accessibility snapshotot, konzolhibákat és Playwright trace-t.
|
||||
4. A CI a staging deploy után futtatja; sikertelenség esetén a staging kiadás hibásnak minősül, de production deployt csak explicit release szabály blokkol.
|
||||
5. A teszt a `MITHOME-35` CI/staging feladathoz tartozik.
|
||||
|
||||
## Első manuális eredmény
|
||||
|
||||
2026-08-17-én Playwright MCP-vel a `SMOKE-01` és `SMOKE-02` sikeresen lefutott a stagingen: a kezdőlap renderelt, nem volt konzolhiba, a health endpoint `status: "ok"` választ adott.
|
||||
|
||||
## Megvalósítás és első automatizált futás
|
||||
|
||||
A tesztcsomag helye: `proto/e2e/staging-smoke.spec.ts`; a konfiguráció: `proto/playwright.config.ts`.
|
||||
|
||||
```sh
|
||||
cd proto
|
||||
npm run test:smoke:staging
|
||||
```
|
||||
|
||||
Az első teljes automatizált futás 2026-08-17-én sikeres volt: mind az öt smoke teszt átment 12,2 másodperc alatt a `stage.mozdit.hu` környezeten.
|
||||
|
||||
## Gitea CI bekötés
|
||||
|
||||
A `.gitea/workflows/ci.yml` két további minőségi kaput tartalmaz:
|
||||
|
||||
- **Docker integration & API E2E:** minden normál CI futáskor elindítja a fejlesztői Compose stack-et, megvárja az alkalmazás health endpointját, majd futtatja az integration és API E2E Jest teszteket.
|
||||
- **Staging Playwright smoke:** csak kézi Gitea indításkor vagy napi ütemezéssel fut a `https://stage.mozdit.hu` ellen. A Playwright HTML riport és a trace/screenshot output artifactként megmarad.
|
||||
|
||||
Automatikus staging vagy production deploy nincs ebben a workflow-ban. A staging deploy továbbra is az elkülönített szerveroldali deploy eljárás feladata; annak sikeres befejezése után kell a staging smoke futást elindítani.
|
||||
@@ -0,0 +1,56 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test('SMOKE-01: health endpoint is available', async ({ request }) => {
|
||||
const response = await request.get('/api/health')
|
||||
expect(response.status()).toBe(200)
|
||||
await expect(response).toBeOK()
|
||||
await expect(response.json()).resolves.toMatchObject({ status: 'ok' })
|
||||
})
|
||||
|
||||
test('SMOKE-02: homepage renders its critical shell without console errors', async ({ page }) => {
|
||||
const consoleErrors: string[] = []
|
||||
page.on('console', message => {
|
||||
if (message.type() === 'error') consoleErrors.push(message.text())
|
||||
})
|
||||
|
||||
await page.goto('/')
|
||||
await expect(page.getByRole('heading', { level: 1 })).toBeVisible()
|
||||
await expect(page.getByRole('navigation')).toBeVisible()
|
||||
await expect(page.getByRole('link', { name: /beszéljük át/i })).toBeVisible()
|
||||
await expect(page.locator('footer')).toBeVisible()
|
||||
expect(consoleErrors).toEqual([])
|
||||
})
|
||||
|
||||
test('SMOKE-03: main navigation opens every public core page', async ({ page }) => {
|
||||
const targets = [
|
||||
['Kezdőlap', '/'],
|
||||
['Rólunk', '/rolunk'],
|
||||
['Szolgáltatások', '/szolgaltatasok'],
|
||||
['Kapcsolat', '/kapcsolat'],
|
||||
] as const
|
||||
|
||||
await page.goto('/')
|
||||
for (const [label, expectedPath] of targets) {
|
||||
await page.getByRole('navigation').getByRole('link', { name: label }).click()
|
||||
await expect(page).toHaveURL(new RegExp(`${expectedPath.replace('/', '\\/')}$`))
|
||||
await expect(page.getByRole('heading', { level: 1 })).toHaveCount(1)
|
||||
}
|
||||
})
|
||||
|
||||
test('SMOKE-04: service content and contact CTA are visible', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await expect(page.getByRole('heading', { name: 'Szolgáltatásaink' })).toBeVisible()
|
||||
await expect(page.getByRole('link', { name: /kérjen ajánlatot/i })).toHaveAttribute('href', '/kapcsolat')
|
||||
})
|
||||
|
||||
test('SMOKE-05: empty contact form is blocked client-side without API submission', async ({ page }) => {
|
||||
let contactPostCount = 0
|
||||
page.on('request', request => {
|
||||
if (request.method() === 'POST' && new URL(request.url()).pathname === '/api/contact') contactPostCount++
|
||||
})
|
||||
|
||||
await page.goto('/kapcsolat')
|
||||
await page.getByRole('button', { name: 'Üzenet küldése' }).click()
|
||||
await expect(page.getByRole('textbox', { name: 'Név *' })).toBeVisible()
|
||||
await expect.poll(() => contactPostCount).toBe(0)
|
||||
})
|
||||
Generated
+63
-22
@@ -16,6 +16,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@playwright/test": "^1.62.1",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@testing-library/jest-dom": "^6.5",
|
||||
"@testing-library/react": "^16.0",
|
||||
@@ -635,7 +636,6 @@
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz",
|
||||
"integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -657,7 +657,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
|
||||
"integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1844,7 +1843,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1861,7 +1859,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1878,7 +1875,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1895,7 +1891,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1912,7 +1907,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1929,7 +1923,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1946,7 +1939,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1963,7 +1955,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1980,7 +1971,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1997,7 +1987,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2014,7 +2003,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2031,7 +2019,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2048,7 +2035,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2065,7 +2051,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2082,7 +2067,6 @@
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -2096,7 +2080,6 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.3.tgz",
|
||||
"integrity": "sha512-rZxtMsLwjdXkMUGC3WwsPwLNVqVqnTJT6MNIB6e+5fhMcSCPP0AOsNWuMQ5mdCq6HNjs/ZeWAEchpqeprqBD2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -2112,7 +2095,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2129,7 +2111,6 @@
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2146,7 +2127,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2361,6 +2341,22 @@
|
||||
"node": ">=12.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz",
|
||||
"integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.62.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
@@ -2876,7 +2872,6 @@
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz",
|
||||
"integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -9556,6 +9551,52 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.62.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
|
||||
"integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.62.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.62.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
|
||||
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/possible-typed-array-names": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"test:browser": "jest --config jest.config.js src/__tests__/browser-integration.test.ts",
|
||||
"test:integration": "INTEGRATION_TESTS=1 jest --config jest.config.integration.js src/__tests__/integration.test.ts",
|
||||
"test:e2e": "E2E_TESTS=1 jest --config jest.config.integration.js src/__tests__/e2e-docker.test.ts",
|
||||
"test:smoke:staging": "playwright test --config=playwright.config.ts",
|
||||
"test:docker": "npm run test:integration && npm run test:e2e",
|
||||
"test:all": "npm run test:unit && npm run test:browser && npm run test:docker",
|
||||
"test:report": "npm test -- --json --outputFile=test-results.json --silent && node ../scripts/sync-test-management.js",
|
||||
@@ -37,6 +38,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@playwright/test": "^1.62.1",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@testing-library/jest-dom": "^6.5",
|
||||
"@testing-library/react": "^16.0",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
timeout: 30_000,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
reporter: [['list'], ['html', { open: 'never' }]],
|
||||
use: {
|
||||
baseURL: process.env.BASE_URL || 'https://stage.mozdit.hu',
|
||||
trace: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
},
|
||||
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
|
||||
})
|
||||
@@ -3,14 +3,14 @@
|
||||
* These tests run against the real services in the Docker stack
|
||||
*/
|
||||
|
||||
import { MongoClient } from 'mongodb'
|
||||
import { MongoClient, ObjectId } from 'mongodb'
|
||||
|
||||
const DOCKER_SERVICES = {
|
||||
app: 'http://localhost:3000',
|
||||
app: 'http://localhost:8080',
|
||||
mongoExpress: 'http://localhost:8081',
|
||||
grafana: 'http://localhost:3001',
|
||||
loki: 'http://localhost:3100',
|
||||
mongodb: 'mongodb://admin:password123@localhost:27017/admin'
|
||||
mongodb: 'mongodb://admin:password123@localhost:27018/mozdit?authSource=admin'
|
||||
}
|
||||
|
||||
describe('Docker Environment Integration Tests', () => {
|
||||
@@ -170,6 +170,24 @@ describe('Docker Environment Integration Tests', () => {
|
||||
if (response.status === 200) {
|
||||
expect(result).toHaveProperty('message', 'Üzenet sikeresen elküldve!')
|
||||
expect(result).toHaveProperty('timestamp')
|
||||
expect(result).toHaveProperty('submissionId')
|
||||
|
||||
if (!mongoClient) {
|
||||
mongoClient = new MongoClient(DOCKER_SERVICES.mongodb)
|
||||
await mongoClient.connect()
|
||||
}
|
||||
const savedSubmission = await mongoClient
|
||||
.db('mozdit')
|
||||
.collection('contact_submissions')
|
||||
.findOne({ _id: new ObjectId(result.submissionId) })
|
||||
expect(savedSubmission).toEqual(expect.objectContaining({
|
||||
name: contactData.name,
|
||||
email: contactData.email,
|
||||
subject: contactData.subject,
|
||||
message: contactData.message,
|
||||
gdprConsent: true,
|
||||
status: 'new',
|
||||
}))
|
||||
} else {
|
||||
expect(result).toHaveProperty('error')
|
||||
expect(result.error).toContain('Túl sok')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getCollection } from '@/lib/mongodb'
|
||||
|
||||
interface ContactFormData {
|
||||
name: string
|
||||
@@ -8,6 +9,16 @@ interface ContactFormData {
|
||||
gdprConsent: boolean
|
||||
}
|
||||
|
||||
interface ContactSubmission {
|
||||
name: string
|
||||
email: string
|
||||
subject: string
|
||||
message: string
|
||||
gdprConsent: true
|
||||
status: 'new'
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
// Simple spam protection - rate limiting by IP
|
||||
const rateLimitMap = new Map<string, { count: number; timestamp: number }>()
|
||||
const RATE_LIMIT_WINDOW = 60 * 1000 // 1 minute
|
||||
@@ -93,20 +104,20 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// Log the contact form submission (in production, this would be sent via email or saved to database)
|
||||
console.log('Contact form submission:', {
|
||||
const submissions = await getCollection<ContactSubmission>('contact_submissions')
|
||||
const submission: ContactSubmission = {
|
||||
...sanitizedData,
|
||||
timestamp: new Date().toISOString(),
|
||||
ip: ip
|
||||
})
|
||||
|
||||
// TODO: In production, implement actual email sending
|
||||
// For now, we'll just simulate success
|
||||
gdprConsent: true,
|
||||
status: 'new',
|
||||
createdAt: new Date(),
|
||||
}
|
||||
const result = await submissions.insertOne(submission)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: 'Üzenet sikeresen elküldve!',
|
||||
timestamp: new Date().toISOString()
|
||||
timestamp: submission.createdAt.toISOString(),
|
||||
submissionId: result.insertedId.toHexString()
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
* Unit tests for Contact API route
|
||||
* These tests focus on testing the business logic without complex mocking
|
||||
*/
|
||||
import { POST } from './route'
|
||||
|
||||
const mockInsertOne = jest.fn()
|
||||
|
||||
// Mock the logger to avoid complex setup
|
||||
jest.mock('@/lib/logger', () => ({
|
||||
@@ -12,7 +15,57 @@ jest.mock('@/lib/logger', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
jest.mock('@/lib/mongodb', () => ({
|
||||
getCollection: jest.fn(async () => ({ insertOne: mockInsertOne })),
|
||||
}))
|
||||
|
||||
describe('/api/contact Unit Tests', () => {
|
||||
beforeEach(() => {
|
||||
mockInsertOne.mockReset()
|
||||
})
|
||||
|
||||
describe('MongoDB persistence', () => {
|
||||
const validData = {
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
subject: 'Test Subject',
|
||||
message: 'This is a test message with enough content',
|
||||
gdprConsent: true,
|
||||
}
|
||||
|
||||
it('persists a valid submission before reporting success', async () => {
|
||||
mockInsertOne.mockResolvedValue({ insertedId: { toHexString: () => 'submission-123' } })
|
||||
const request = {
|
||||
headers: new Headers({ 'x-forwarded-for': 'persistence-success' }),
|
||||
json: async () => validData,
|
||||
} as any
|
||||
|
||||
const response = await POST(request)
|
||||
const body = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(body.submissionId).toBe('submission-123')
|
||||
expect(mockInsertOne).toHaveBeenCalledWith(expect.objectContaining({
|
||||
...validData,
|
||||
status: 'new',
|
||||
createdAt: expect.any(Date),
|
||||
}))
|
||||
})
|
||||
|
||||
it('returns a server error when persistence fails', async () => {
|
||||
mockInsertOne.mockRejectedValue(new Error('MongoDB unavailable'))
|
||||
const request = {
|
||||
headers: new Headers({ 'x-forwarded-for': 'persistence-failure' }),
|
||||
json: async () => validData,
|
||||
} as any
|
||||
|
||||
const response = await POST(request)
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
expect(mockInsertOne).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// TC-001: Email Format Validation Test (ZEE-48)
|
||||
describe('Input validation logic', () => {
|
||||
it('should validate required fields', () => {
|
||||
|
||||
@@ -145,7 +145,7 @@ export default function Home() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-5xl mx-auto">
|
||||
{pageContent.services.items.map((service: any, index: number) => (
|
||||
{pageContent.services.items.map((service, index) => (
|
||||
<div
|
||||
key={service.id}
|
||||
className="group card hover-lift"
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function ServicesPage() {
|
||||
{/* Services Grid */}
|
||||
<section className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{content.pages.home.services.items.map((service: any) => (
|
||||
{content.pages.home.services.items.map((service) => (
|
||||
<div key={service.id} className="bg-white rounded-xl shadow-sm border border-gray-200 p-8 hover:shadow-md transition-shadow">
|
||||
<div className="w-16 h-16 bg-blue-100 rounded-lg flex items-center justify-center mb-6">
|
||||
<span className="text-2xl">{service.icon}</span>
|
||||
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
ContactPageContent,
|
||||
LegalPageContent
|
||||
} from './types'
|
||||
import { validateContent } from './schema'
|
||||
|
||||
// Import JSON files
|
||||
import commonJson from './common.json'
|
||||
@@ -33,14 +34,22 @@ import contactJson from './pages/contact.json'
|
||||
import adatvedelemJson from './pages/adatvedelem.json'
|
||||
import hasznalatiFeltetelekJson from './pages/hasznalati-feltetelek.json'
|
||||
|
||||
// Type assertions for JSON imports
|
||||
const common: CommonContent = commonJson as CommonContent
|
||||
const home: HomePageContent = homeJson as HomePageContent
|
||||
const about: AboutPageContent = aboutJson as AboutPageContent
|
||||
const services: ServicesPageContent = servicesJson as ServicesPageContent
|
||||
const contact: ContactPageContent = contactJson as ContactPageContent
|
||||
const adatvedelem: LegalPageContent = adatvedelemJson as LegalPageContent
|
||||
const hasznalatiFeltetelek: LegalPageContent = hasznalatiFeltetelekJson as LegalPageContent
|
||||
function loadValidatedContent<T>(fileKey: string, value: unknown): T {
|
||||
const result = validateContent(fileKey, value)
|
||||
if (!result.ok) {
|
||||
throw new Error(`Érvénytelen content JSON (${fileKey}): ${result.errors.join('; ')}`)
|
||||
}
|
||||
return value as T
|
||||
}
|
||||
|
||||
// Runtime validation prevents malformed JSON from silently reaching the UI.
|
||||
const common = loadValidatedContent<CommonContent>('common', commonJson)
|
||||
const home = loadValidatedContent<HomePageContent>('home', homeJson)
|
||||
const about = loadValidatedContent<AboutPageContent>('about', aboutJson)
|
||||
const services = loadValidatedContent<ServicesPageContent>('services', servicesJson)
|
||||
const contact = loadValidatedContent<ContactPageContent>('contact', contactJson)
|
||||
const adatvedelem = loadValidatedContent<LegalPageContent>('adatvedelem', adatvedelemJson)
|
||||
const hasznalatiFeltetelek = loadValidatedContent<LegalPageContent>('hasznalatiFeltetelek', hasznalatiFeltetelekJson)
|
||||
|
||||
// Combined content object
|
||||
export const content: SiteContent = {
|
||||
|
||||
@@ -1,55 +1,49 @@
|
||||
{
|
||||
"meta": {
|
||||
"0": {
|
||||
"$1escription": "Ismerje meg a mozdIT Bt. történetét, küldetését és értékeit. Több mint 20 éve nyújtunk megbízható IT szolgáltatásokat."
|
||||
},
|
||||
"title": "Rólunk",
|
||||
"description": "Ismerje meg a mozdIT Bt. történetét, küldetését és értékeit. Több mint 20 éve nyújtunk megbízható IT szolgáltatásokat.",
|
||||
"ogDescription": "Ismerje meg a mozdIT Bt. történetét, küldetését és értékeit."
|
||||
},
|
||||
"hero": {
|
||||
"title": "Rólunk",
|
||||
"subtitle": "Több mint 20 éve biztosítunk megbízható IT infrastruktúrát."
|
||||
"subtitle": "Több mint 20 éve biztosítunk megbízható IT infrastruktúrát és személyes ügyfélszolgálatot"
|
||||
},
|
||||
"story": {
|
||||
"title": "Történetünk",
|
||||
"paragraphs": [],
|
||||
"paragraphs[0]": "Kezdetben néhány ügyfél weboldalának üzemeltetésével indultunk, ma pedig számos domain és email fiók működését biztosítjuk. Növekedésünk során mindig szem előtt tartottuk az alapelveinket: megbízhatóság, személyes kapcsolat és műszaki kiválóság.",
|
||||
"paragraphs[1]": "Ez egy kis cég, egy dedikált szerverünk van, és ezen szolgáltatunk webtárhelyet, domain-fenntartást és e-mail szolgáltatásokat kis- és középvállalkozásoknak, illetve magánszemélyeknek.",
|
||||
"paragraphs[2]": "Kis csapat, nagy elköteleződés. Saját dedikált infrastruktúránkon garantáljuk az adatbiztonságot és a folyamatos rendelkezésre állást. Nálunk nincs technoblabla: személyes ügyfélszolgálat, emberi lépték és mindig elérhető válaszok várják."
|
||||
"paragraphs": [
|
||||
"Kezdetben néhány ügyfél weboldalának üzemeltetésével indultunk, ma pedig számos domain és email fiók működését biztosítjuk. Növekedésünk során mindig szem előtt tartottuk az alapelveinket: megbízhatóság, személyes kapcsolat és műszaki kiválóság.",
|
||||
"Ez egy kis cég, egy dedikált szerverünk van, és ezen szolgáltatunk webtárhelyet, domain-fenntartást és e-mail szolgáltatásokat kis- és középvállalkozásoknak, illetve magánszemélyeknek.",
|
||||
"Kis csapat, nagy elköteleződés. Saját dedikált infrastruktúránkon garantáljuk az adatbiztonságot és a folyamatos rendelkezésre állást. Nálunk nincs technoblabla: személyes ügyfélszolgálat, emberi lépték és mindig elérhető válaszok várják."
|
||||
]
|
||||
},
|
||||
"mission": {
|
||||
"title": "Küldetésünk és értékeink",
|
||||
"subtitle": "Minden nap azért dolgozunk, hogy ügyfeleink digitális jelenléte biztonságos, stabil és hatékony legyen.",
|
||||
"values": [],
|
||||
"values[0]": {
|
||||
"0": {
|
||||
"$1escription": "99.9% uptime és 24/7 monitoring biztosítja, hogy szolgáltatásaink mindig elérhetők legyenek."
|
||||
"values": [
|
||||
{
|
||||
"title": "Megbízhatóság",
|
||||
"description": "99.9% uptime és 24/7 monitoring biztosítja, hogy szolgáltatásaink mindig elérhetők legyenek.",
|
||||
"icon": "🛡️"
|
||||
},
|
||||
"title": "Megbízhatóság",
|
||||
"icon": "🛡️"
|
||||
},
|
||||
"values[1]": {
|
||||
"0": {
|
||||
"$1escription": "Minden ügyfél számít számunkra. Személyre szabott megoldásokat kínálunk."
|
||||
{
|
||||
"title": "Személyes kapcsolat",
|
||||
"description": "Minden ügyfél számít számunkra. Személyre szabott megoldásokat kínálunk.",
|
||||
"icon": "👥"
|
||||
},
|
||||
"title": "Személyes kapcsolat",
|
||||
"icon": "👥"
|
||||
},
|
||||
"values[2]": {
|
||||
"0": {
|
||||
"$1escription": "Korszerű technológiák és bevált gyakorlatok alkalmazásával biztosítjuk a legmagasabb színvonalú szolgáltatást."
|
||||
},
|
||||
"title": "Műszaki kiválóság",
|
||||
"icon": "⚡"
|
||||
}
|
||||
{
|
||||
"title": "Műszaki kiválóság",
|
||||
"description": "Korszerű technológiák és bevált gyakorlatok alkalmazásával biztosítjuk a legmagasabb színvonalú szolgáltatást.",
|
||||
"icon": "⚡"
|
||||
}
|
||||
]
|
||||
},
|
||||
"team": {
|
||||
"title": "Szakértő csapat",
|
||||
"subtitle": "Tapasztalt IT szakemberek, akik elkötelezetten és megbízhatóan dolgoznak az ügyfeleink sikeréért.",
|
||||
"paragraphs": [],
|
||||
"paragraphs[0]": "Csapatunk tagjai több mint 20 éves tapasztalattal rendelkeznek a maguk területén, és folyamatosan követik a technológiai újdonságokat.",
|
||||
"paragraphs[1]": "Hiszünk abban, hogy a jó kommunikáció és a műszaki tudás együtt teremti meg a tökéletes ügyfélélményt."
|
||||
"paragraphs": [
|
||||
"Csapatunk tagjai több mint 20 éves tapasztalattal rendelkeznek a maguk területén, és folyamatosan követik a technológiai újdonságokat.",
|
||||
"Hiszünk abban, hogy a jó kommunikáció és a műszaki tudás együtt teremti meg a tökéletes ügyfélélményt."
|
||||
]
|
||||
},
|
||||
"cta": {
|
||||
"title": "Legyen Ön is elégedett ügyfelünk!",
|
||||
|
||||
@@ -1,45 +1,31 @@
|
||||
{
|
||||
"title": "Adatvédelmi Tájékoztató",
|
||||
"lastUpdated": "2026. április 26.",
|
||||
"sections": [],
|
||||
"lastUp": {
|
||||
"$1ate": {
|
||||
"$1": "2026. április 26."
|
||||
"sections": [
|
||||
{
|
||||
"id": "bevezetes",
|
||||
"title": "1. Bevezetés",
|
||||
"content": "A mozdIT Bt. (székhely: Szigetszentmiklós, Magyarország; e-mail: info@mozdit.hu) mint adatkezelő elkötelezett a felhasználók személyes adatainak védelme iránt. Jelen tájékoztató célja, hogy az Európai Unió Általános Adatvédelmi Rendelete (GDPR) előírásainak megfelelően átlátható módon bemutassa cégünk adatkezelési gyakorlatát."
|
||||
},
|
||||
{
|
||||
"id": "adatkezeles-celja",
|
||||
"title": "2. Milyen adatokat kezelünk, mi a céljuk és a jogalapjuk?",
|
||||
"content": "A szolgáltatásaink nyújtása során az alábbi adatokat kezeljük:\n\n• **Kapcsolatfelvétel és űrlapok:** Név, e-mail cím, telefonszám, a beküldött üzenet tartalma. (Cél: ügyfélkapcsolat, árajánlatadás. Jogalap: hozzájárulás és szerződést megelőző lépések.)\n• **Ügyfél-nyilvántartás és számlázás:** Számlázási név és cím, adószám, kapcsolattartó adatai. (Cél: szolgáltatás biztosítása, számlázás. Jogalap: jogi kötelezettség, szerződés teljesítése.)\n• **Webtárhely és e-mail szolgáltatás:** Technikai naplófájlok (IP címek, belépési időpontok), e-mail forgalmi adatok szűrése (spam, vírusok). (Cél: IT biztonság fenntartása, szolgáltatás biztosítása. Jogalap: szerződés teljesítése, jogos érdek.)\n• **Domain-regisztráció:** Tulajdonosi és kapcsolattartói adatok. (Cél: domain delegáció ügyintézése a regisztrátorok felé. Jogalap: szerződés teljesítése.)\n• **Sütik (Cookies):** Weboldalunk a megfelelő technikai működés érdekében alapvető (munkamenet) sütiket használ. Harmadik fél analitikai kódjait (pl. Meta pixel) jelenleg nem használjuk az alapvető nyomkövetéshez."
|
||||
},
|
||||
{
|
||||
"id": "adattovabbitas",
|
||||
"title": "3. Adattovábbítás és adatfeldolgozók",
|
||||
"content": "A személyes adatokat bizalmasan kezeljük, és alapvetően a mozdIT Bt. saját, dedikált infrastruktúráján tároljuk. Harmadik félnek csak jogszabályi kötelezettség alapján (pl. NAV részére könyvelési okokból), illetve a domain-regisztrációs szolgáltatások keretében továbbítunk adatokat az illetékes regisztrátorok (pl. ISZT) felé."
|
||||
},
|
||||
{
|
||||
"id": "adatmegorzes",
|
||||
"title": "4. Az adatkezelés időtartama",
|
||||
"content": "A számviteli bizonylatokat a Számviteli törvény értelmében 8 évig megőrizzük. A kapcsolatfelvételi űrlapon megadott, de szerződéssel nem végződő adatokat az ügy lezárását követő 1 év elteltével töröljük. A webszerverek technikai naplófájljait (logokat) a biztonsági és üzemeltetési feladatok ellátása céljából legfeljebb 30 napig őrizzük meg."
|
||||
},
|
||||
{
|
||||
"id": "jogok",
|
||||
"title": "5. Az érintettek jogai",
|
||||
"content": "Az érintettek bármikor tájékoztatást kérhetnek adataik kezeléséről, kérhetik azok helyesbítését, törlését (a kötelező adatkezelések kivételével) vagy zárolását. Kérésüket az info@mozdit.hu e-mail címre küldhetik el, amire indokolatlan késedelem nélkül, de legkésőbb egy hónapon belül válaszolunk. Jogosulatlan adatkezelés esetén a Nemzeti Adatvédelmi és Információszabadság Hatósághoz (NAIH) lehet fordulni panasszal."
|
||||
}
|
||||
},
|
||||
"sections[0]": {
|
||||
"i": {
|
||||
"$1": "bevezetes"
|
||||
},
|
||||
"title": "1. Bevezetés",
|
||||
"content": "A mozdIT Bt. (székhely: Szigetszentmiklós, Magyarország; e-mail: info@mozdit.hu) mint adatkezelő elkötelezett a felhasználók személyes adatainak védelme iránt. Jelen tájékoztató célja, hogy az Európai Unió Általános Adatvédelmi Rendelete (GDPR) előírásainak megfelelően átlátható módon bemutassa cégünk adatkezelési gyakorlatát."
|
||||
},
|
||||
"sections[1]": {
|
||||
"i": {
|
||||
"$1": "adatkezeles-celja"
|
||||
},
|
||||
"title": "2. Milyen adatokat kezelünk, mi a céljuk és a jogalapjuk?",
|
||||
"content": "A szolgáltatásaink nyújtása során az alábbi adatokat kezeljük:\n\n• **Kapcsolatfelvétel és űrlapok:** Név, e-mail cím, telefonszám, a beküldött üzenet tartalma. (Cél: ügyfélkapcsolat, árajánlatadás. Jogalap: hozzájárulás és szerződést megelőző lépések.)\n• **Ügyfél-nyilvántartás és számlázás:** Számlázási név és cím, adószám, kapcsolattartó adatai. (Cél: szolgáltatás biztosítása, számlázás. Jogalap: jogi kötelezettség, szerződés teljesítése.)\n• **Webtárhely és e-mail szolgáltatás:** Technikai naplófájlok (IP címek, belépési időpontok), e-mail forgalmi adatok szűrése (spam, vírusok). (Cél: IT biztonság fenntartása, szolgáltatás biztosítása. Jogalap: szerződés teljesítése, jogos érdek.)\n• **Domain-regisztráció:** Tulajdonosi és kapcsolattartói adatok. (Cél: domain delegáció ügyintézése a regisztrátorok felé. Jogalap: szerződés teljesítése.)\n• **Sütik (Cookies):** Weboldalunk a megfelelő technikai működés érdekében alapvető (munkamenet) sütiket használ. Harmadik fél analitikai kódjait (pl. Meta pixel) jelenleg nem használjuk az alapvető nyomkövetéshez."
|
||||
},
|
||||
"sections[2]": {
|
||||
"i": {
|
||||
"$1": "adattovabbitas"
|
||||
},
|
||||
"title": "3. Adattovábbítás és adatfeldolgozók",
|
||||
"content": "A személyes adatokat bizalmasan kezeljük, és alapvetően a mozdIT Bt. saját, dedikált infrastruktúráján tároljuk. Harmadik félnek csak jogszabályi kötelezettség alapján (pl. NAV részére könyvelési okokból), illetve a domain-regisztrációs szolgáltatások keretében továbbítunk adatokat az illetékes regisztrátorok (pl. ISZT) felé."
|
||||
},
|
||||
"sections[3]": {
|
||||
"i": {
|
||||
"$1": "adatmegorzes"
|
||||
},
|
||||
"title": "4. Az adatkezelés időtartama",
|
||||
"content": "A számviteli bizonylatokat a Számviteli törvény értelmében 8 évig megőrizzük. A kapcsolatfelvételi űrlapon megadott, de szerződéssel nem végződő adatokat az ügy lezárását követő 1 év elteltével töröljük. A webszerverek technikai naplófájljait (logokat) a biztonsági és üzemeltetési feladatok ellátása céljából legfeljebb 30 napig őrizzük meg."
|
||||
},
|
||||
"sections[4]": {
|
||||
"i": {
|
||||
"$1": "jogok"
|
||||
},
|
||||
"title": "5. Az érintettek jogai",
|
||||
"content": "Az érintettek bármikor tájékoztatást kérhetnek adataik kezeléséről, kérhetik azok helyesbítését, törlését (a kötelező adatkezelések kivételével) vagy zárolását. Kérésüket az info@mozdit.hu e-mail címre küldhetik el, amire indokolatlan késedelem nélkül, de legkésőbb egy hónapon belül válaszolunk. Jogosulatlan adatkezelés esetén a Nemzeti Adatvédelmi és Információszabadság Hatósághoz (NAIH) lehet fordulni panasszal."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,45 +1,31 @@
|
||||
{
|
||||
"title": "Használati Feltételek (ÁSZF)",
|
||||
"lastUpdated": "2026. április 26.",
|
||||
"sections": [],
|
||||
"lastUp": {
|
||||
"$1ate": {
|
||||
"$1": "2026. április 26."
|
||||
"sections": [
|
||||
{
|
||||
"id": "bevezetes",
|
||||
"title": "1. Általános rendelkezések",
|
||||
"content": "Jelen Általános Szerződési Feltételek (ÁSZF) a mozdIT Bt. (székhely: Szigetszentmiklós, Magyarország; e-mail: info@mozdit.hu) által nyújtott webtárhely, e-mail szolgáltatás és domain regisztráció szolgáltatásainak igénybevételére vonatkozó szabályokat tartalmazzák. A szolgáltatások megrendelésével és használatával az Ügyfél elfogadja jelen feltételeket."
|
||||
},
|
||||
{
|
||||
"id": "szolgaltatasok",
|
||||
"title": "2. Szolgáltatások leírása",
|
||||
"content": "A mozdIT Bt. dedikált szerverein nyújt webtárhely, e-mail fiók és DNS szolgáltatásokat. A szolgáltatások garantált rendelkezésre állása 99,9% éves szinten. A szolgáltató automatikus biztonsági mentéseket végez, de javasolja az Ügyfél számára is az adatai rendszeres mentését."
|
||||
},
|
||||
{
|
||||
"id": "ugyfel-kotelezettsegei",
|
||||
"title": "3. Az Ügyfél kötelezettségei",
|
||||
"content": "Az Ügyfél köteles a szolgáltatásokat rendeltetésszerűen használni. Szigorúan tilos a tárhelyen és e-mail fiókokban illegális, szerzői jogot sértő tartalmak elhelyezése, valamint spam (kéretlen reklámlevél) tömeges küldése. A Szolgáltató fenntartja a jogot, hogy jogsértés vagy hálózatot veszélyeztető tevékenység (pl. DDoS forrás) észlelése esetén a szolgáltatást előzetes értesítés nélkül azonnal szüneteltesse vagy megszüntesse."
|
||||
},
|
||||
{
|
||||
"id": "dijazas",
|
||||
"title": "4. Díjazás és fizetési feltételek",
|
||||
"content": "A szolgáltatások díjai a felek által megkötött egyedi megállapodás vagy elfogadott árajánlat alapján fizetendők. A számlázás elektronikus úton történik. A fizetési határidő elmulasztása esetén a Szolgáltató jogosult a szolgáltatás szüneteltetésére, melyről az Ügyfelet előzetesen e-mailben értesíti."
|
||||
},
|
||||
{
|
||||
"id": "felelosseg",
|
||||
"title": "5. Felelősségkorlátozás",
|
||||
"content": "A Szolgáltató mindent megtesz az adatok biztonsága és a folyamatos üzemeltetés érdekében, ugyanakkor nem vállal felelősséget a vis maior, illetve a harmadik felek által okozott (pl. hálózati) kimaradások miatti esetleges adatvesztésért vagy elmaradt haszonért."
|
||||
}
|
||||
},
|
||||
"sections[0]": {
|
||||
"i": {
|
||||
"$1": "bevezetes"
|
||||
},
|
||||
"title": "1. Általános rendelkezések",
|
||||
"content": "Jelen Általános Szerződési Feltételek (ÁSZF) a mozdIT Bt. (székhely: Szigetszentmiklós, Magyarország; e-mail: info@mozdit.hu) által nyújtott webtárhely, e-mail szolgáltatás és domain regisztráció szolgáltatásainak igénybevételére vonatkozó szabályokat tartalmazzák. A szolgáltatások megrendelésével és használatával az Ügyfél elfogadja jelen feltételeket."
|
||||
},
|
||||
"sections[1]": {
|
||||
"i": {
|
||||
"$1": "szolgaltatasok"
|
||||
},
|
||||
"title": "2. Szolgáltatások leírása",
|
||||
"content": "A mozdIT Bt. dedikált szerverein nyújt webtárhely, e-mail fiók és DNS szolgáltatásokat. A szolgáltatások garantált rendelkezésre állása 99,9% éves szinten. A szolgáltató automatikus biztonsági mentéseket végez, de javasolja az Ügyfél számára is az adatai rendszeres mentését."
|
||||
},
|
||||
"sections[2]": {
|
||||
"i": {
|
||||
"$1": "ugyfel-kotelezettsegei"
|
||||
},
|
||||
"title": "3. Az Ügyfél kötelezettségei",
|
||||
"content": "Az Ügyfél köteles a szolgáltatásokat rendeltetésszerűen használni. Szigorúan tilos a tárhelyen és e-mail fiókokban illegális, szerzői jogot sértő tartalmak elhelyezése, valamint spam (kéretlen reklámlevél) tömeges küldése. A Szolgáltató fenntartja a jogot, hogy jogsértés vagy hálózatot veszélyeztető tevékenység (pl. DDoS forrás) észlelése esetén a szolgáltatást előzetes értesítés nélkül azonnal szüneteltesse vagy megszüntesse."
|
||||
},
|
||||
"sections[3]": {
|
||||
"i": {
|
||||
"$1": "dijazas"
|
||||
},
|
||||
"title": "4. Díjazás és fizetési feltételek",
|
||||
"content": "A szolgáltatások díjai a felek által megkötött egyedi megállapodás vagy elfogadott árajánlat alapján fizetendők. A számlázás elektronikus úton történik. A fizetési határidő elmulasztása esetén a Szolgáltató jogosult a szolgáltatás szüneteltetésére, melyről az Ügyfelet előzetesen e-mailben értesíti."
|
||||
},
|
||||
"sections[4]": {
|
||||
"i": {
|
||||
"$1": "felelosseg"
|
||||
},
|
||||
"title": "5. Felelősségkorlátozás",
|
||||
"content": "A Szolgáltató mindent megtesz az adatok biztonsága és a folyamatos üzemeltetés érdekében, ugyanakkor nem vállal felelősséget a vis maior, illetve a harmadik felek által okozott (pl. hálózati) kimaradások miatti esetleges adatvesztésért vagy elmaradt haszonért."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,119 +1,99 @@
|
||||
{
|
||||
"hero": {
|
||||
"0": {
|
||||
"$1escription": "A mozdIT Bt. célja, hogy megbízható, támogató szolgáltatásokkal segítse ügyfeleit a digitális térben való sikerben."
|
||||
},
|
||||
"title": "Web és e-mail – testreszabott szolgáltatásokkal",
|
||||
"title": "Megbízható web‑ és email‑szolgáltatás személyre szabott támogatással",
|
||||
"subtitle": "Kis ügyfélkör, nagy figyelem: stabil tárhely, üzembiztos levelezés és DNS adminisztráció — gyors reakcióval.",
|
||||
"description": "A mozdIT Bt. célja, hogy megbízható, támogató szolgáltatásokkal segítse ügyfeleit a digitális térben való sikerben.",
|
||||
"trustBullets": [],
|
||||
"trustBullets": [
|
||||
"Saját dedikált infrastruktúra",
|
||||
"Webtárhely, domain, e-mail egy helyen",
|
||||
"Kifejezetten kisvállalkozásokra szabva"
|
||||
],
|
||||
"cta": {
|
||||
"primary": {
|
||||
"text": "Beszéljük át, mire van szüksége",
|
||||
"href": "/kapcsolat",
|
||||
"external": "false"
|
||||
"external": false
|
||||
},
|
||||
"secondary": {
|
||||
"text": "Webmail Ugrás",
|
||||
"href": "https://mail.mozdit.hu",
|
||||
"external": true
|
||||
},
|
||||
"secon": {
|
||||
"$1ary": {
|
||||
"text": "mozdIT Webmail",
|
||||
"href": "https://mail.mozdit.hu",
|
||||
"external": "true"
|
||||
}
|
||||
}
|
||||
},
|
||||
"trustBullets[0]": "Saját dedikált infrastruktúra",
|
||||
"trustBullets[1]": "Webtárhely, domain, e-mail egy helyen",
|
||||
"trustBullets[2]": "Kisvállalkozásokra szabva"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"title": "Miért érdemes minket választani?",
|
||||
"usps": [],
|
||||
"usps[0]": {
|
||||
"0": {
|
||||
"$1escription": "Nincs technoblabla. Átlátható és könnyen használható rendszereket biztosítunk, hogy Ön a saját üzletére fókuszálhasson."
|
||||
"usps": [
|
||||
{
|
||||
"id": "egyszeru",
|
||||
"title": "Egyszerűen kezelhető szolgáltatások",
|
||||
"description": "Nincs technoblabla. Átlátható és könnyen használható rendszereket biztosítunk, hogy Ön a saját üzletére fókuszálhasson.",
|
||||
"icon": "🎯"
|
||||
},
|
||||
"i": {
|
||||
"$1": "egyszeru"
|
||||
{
|
||||
"id": "nemtomeg",
|
||||
"title": "Nem tömegszolgáltatás",
|
||||
"description": "Egyedi figyelem és rugalmas, személyre szabott csomagok, amelyek tökéletesen igazodnak a vállalkozása igényeihez.",
|
||||
"icon": "👤"
|
||||
},
|
||||
"title": "Egyszerűen kezelhető szolgáltatások",
|
||||
"icon": "🎯"
|
||||
},
|
||||
"usps[1]": {
|
||||
"0": {
|
||||
"$1escription": "Egyedi figyelem és rugalmas, személyre szabott csomagok, amelyek tökéletesen igazodnak a vállalkozása igényeihez."
|
||||
{
|
||||
"id": "egykezben",
|
||||
"title": "Minden egy kézben",
|
||||
"description": "Domain, webtárhely és üzleti e-mail szolgáltatások zökkenőmentes kezelése, mindent egyetlen megbízható helyről.",
|
||||
"icon": "🧩"
|
||||
},
|
||||
"i": {
|
||||
"$1": "nemtomeg"
|
||||
},
|
||||
"title": "Nem tömegszolgáltatás",
|
||||
"icon": "👤"
|
||||
},
|
||||
"usps[2]": {
|
||||
"0": {
|
||||
"$1escription": "Domain, webtárhely és üzleti e-mail szolgáltatások zökkenőmentes kezelése, mindent egyetlen megbízható helyről."
|
||||
},
|
||||
"i": {
|
||||
"$1": "egykezben"
|
||||
},
|
||||
"title": "Minden egy kézben",
|
||||
"icon": "🧩"
|
||||
},
|
||||
"usps[3]": {
|
||||
"0": {
|
||||
"$1escription": "TLS/SSL védelemmel, modern biztonsági protokollokkal és gyors (SSD) kiszolgálással biztosítjuk a maximális teljesítményt."
|
||||
},
|
||||
"i": {
|
||||
"$1": "modern_tech"
|
||||
},
|
||||
"title": "Modern és Biztonságos",
|
||||
"icon": "🚀"
|
||||
}
|
||||
{
|
||||
"id": "modern_tech",
|
||||
"title": "Modern és Biztonságos",
|
||||
"description": "TLS/SSL védelemmel, modern biztonsági protokollokkal és gyors (SSD) kiszolgálással biztosítjuk a maximális teljesítményt.",
|
||||
"icon": "🚀"
|
||||
}
|
||||
]
|
||||
},
|
||||
"services": {
|
||||
"title": "Szolgáltatásaink",
|
||||
"subtitle": "Komplex megoldásokat kínálunk, amelyek tökéletesen igazodnak a vállalkozás igényeihez.",
|
||||
"items": [],
|
||||
"items[0]": {
|
||||
"0": {
|
||||
"$1escription": "Stabil és gyors tárhely szolgáltatás saját infrastruktúrán, biztonsági mentésekkel és magas rendelkezésre állással."
|
||||
"items": [
|
||||
{
|
||||
"id": "hosting",
|
||||
"title": "Webtárhely (Hosting)",
|
||||
"description": "Stabil és gyors tárhely szolgáltatás saját infrastruktúrán, biztonsági mentésekkel és magas rendelkezésre állással.",
|
||||
"icon": "🔥",
|
||||
"features": [
|
||||
"99.9% uptime garancia",
|
||||
"SSD alapú gyors kiszolgálás",
|
||||
"24/7 rendszeres monitoring",
|
||||
"Automatikus backup rendszer"
|
||||
],
|
||||
"ctaText": "További információk"
|
||||
},
|
||||
"i": {
|
||||
"$1": "hosting"
|
||||
{
|
||||
"id": "email",
|
||||
"title": "E-mail szolgáltatás",
|
||||
"description": "Üzleti, saját domain alapú levelezés magas szintű spamszűréssel, bárhonnan elérhető webmail felülettel.",
|
||||
"icon": "📧",
|
||||
"features": [
|
||||
"Saját domain alapú e-mail címek",
|
||||
"Biztonságos webmail és IMAP/POP3",
|
||||
"Erős spam- és vírusvédelem",
|
||||
"Nagy tárterület"
|
||||
],
|
||||
"ctaText": "További információk"
|
||||
},
|
||||
"title": "Webtárhely (Hosting)",
|
||||
"icon": "🔥",
|
||||
"features": "99.9% uptime garancia,SSD alapú gyors kiszolgálás,24/7 rendszeres monitoring,Automatikus backup rendszer",
|
||||
"ctaText": "További információk"
|
||||
},
|
||||
"items[1]": {
|
||||
"0": {
|
||||
"$1escription": "Üzleti, saját domain alapú levelezés magas szintű spamszűréssel, bárhonnan elérhető webmail felülettel."
|
||||
},
|
||||
"i": {
|
||||
"$1": "email"
|
||||
},
|
||||
"title": "E-mail szolgáltatás",
|
||||
"icon": "📧",
|
||||
"features": "Saját domain alapú e-mail címek,Biztonságos webmail és IMAP/POP3,Erős spam- és vírusvédelem,Nagy tárterület",
|
||||
"ctaText": "További információk"
|
||||
},
|
||||
"items[2]": {
|
||||
"0": {
|
||||
"$1escription": "Teljes körű domain regisztráció, kezelés és biztonságos névszerver (DNS) adminisztráció."
|
||||
},
|
||||
"i": {
|
||||
"$1": "domain"
|
||||
},
|
||||
"title": "Domain és DNS",
|
||||
"icon": "🌐",
|
||||
"features": "Magyar és nemzetközi domainek,Biztonságos DNS kezelés,Email routing (SPF, DKIM, DMARC),Gyors ügyintézés",
|
||||
"ctaText": "További információk"
|
||||
}
|
||||
{
|
||||
"id": "domain",
|
||||
"title": "Domain és DNS",
|
||||
"description": "Teljes körű domain regisztráció, kezelés és biztonságos névszerver (DNS) adminisztráció.",
|
||||
"icon": "🌐",
|
||||
"features": [
|
||||
"Magyar és nemzetközi domainek",
|
||||
"Biztonságos DNS kezelés",
|
||||
"Email routing (SPF, DKIM, DMARC)",
|
||||
"Gyors ügyintézés"
|
||||
],
|
||||
"ctaText": "További információk"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cta": {
|
||||
"title": "Kérjen személyre szabott ajánlatot",
|
||||
|
||||
@@ -1,41 +1,8 @@
|
||||
{
|
||||
"0": {
|
||||
"$1etails": {
|
||||
"title": "Részletes szolgáltatásleírás",
|
||||
"subtitle": "Minden szolgáltatásunk mögött évtizedes tapasztalat és modern technológia áll",
|
||||
"services[0]": {
|
||||
"0": {
|
||||
"$1escription": "Weboldalak biztonságos és gyors üzemeltetése SSD tárolással, automatikus biztonsági mentéssel és 24/7 monitoringgal. Támogatjuk a PHP, Python, Node.js technológiákat és MySQL/PostgreSQL adatbázisokat."
|
||||
},
|
||||
"icon": "🌐",
|
||||
"title": "Web Hosting részletesen",
|
||||
"specs": "[object Object]"
|
||||
},
|
||||
"services[1]": {
|
||||
"0": {
|
||||
"$1escription": "Professzionális email fiókok saját domain névvel, spam szűréssel és vírusvédelemmel. Webmail felület és IMAP/POP3/SMTP támogatás minden népszerű email klienssel."
|
||||
},
|
||||
"icon": "✉️",
|
||||
"title": "Email szolgáltatás részletesen",
|
||||
"specs": "[object Object]"
|
||||
},
|
||||
"services[2]": {
|
||||
"0": {
|
||||
"$1escription": "Teljes DNS kezelés domain regisztrációval, átvitellel és professzionális beállításokkal. Gyors propagáció és megbízható névszerverek világszerte."
|
||||
},
|
||||
"icon": "⚙️",
|
||||
"title": "DNS adminisztráció részletesen",
|
||||
"specs": "[object Object]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"0": {
|
||||
"$1escription": "Webhosting, email szolgáltatás és DNS adminisztráció. Ismerje meg részletes szolgáltatásainkat."
|
||||
},
|
||||
"title": "Szolgáltatások",
|
||||
"description": "Webhosting, email szolgáltatás és DNS adminisztráció professzionális szinten. Ismerje meg részletes szolgáltatásainkat.",
|
||||
"ogDescription": "Webhosting, email szolgáltatás és DNS adminisztráció."
|
||||
"ogDescription": "Webhosting, email szolgáltatás és DNS adminisztráció professzionális szinten."
|
||||
},
|
||||
"hero": {
|
||||
"title": "Szolgáltatásaink",
|
||||
@@ -44,41 +11,84 @@
|
||||
"details": {
|
||||
"title": "Részletes szolgáltatásleírás",
|
||||
"subtitle": "Minden szolgáltatásunk mögött évtizedes tapasztalat és modern technológia áll",
|
||||
"services": []
|
||||
"services": [
|
||||
{
|
||||
"icon": "🌐",
|
||||
"title": "Web Hosting részletesen",
|
||||
"description": "Weboldalak biztonságos és gyors üzemeltetése SSD tárolással, automatikus biztonsági mentéssel és 24/7 monitoringgal. Támogatjuk a PHP, Python, Node.js technológiákat és MySQL/PostgreSQL adatbázisokat.",
|
||||
"specs": {
|
||||
"title": "Technikai specifikációk:",
|
||||
"items": [
|
||||
"SSD tárhely 10GB-tól 500GB-ig",
|
||||
"Havi adatforgalom: korlátlan",
|
||||
"SSL tanúsítványok (Let's Encrypt vagy prémium)",
|
||||
"CDN integráció a gyorsabb betöltésért",
|
||||
"Automatikus napi biztonsági mentés",
|
||||
"cPanel vagy egyedi admin felület"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"icon": "✉️",
|
||||
"title": "Email szolgáltatás részletesen",
|
||||
"description": "Professzionális email fiókok saját domain névvel, spam szűréssel és vírusvédelemmel. Webmail felület és IMAP/POP3/SMTP támogatás minden népszerű email klienssel.",
|
||||
"specs": {
|
||||
"title": "Email funkciók:",
|
||||
"items": [
|
||||
"Korlátlan email fiókok létrehozása",
|
||||
"5GB-50GB tárhelyet fiókként",
|
||||
"Webmail hozzáférés (Roundcube/SOGo)",
|
||||
"Mobilalkalmazás szinkronizáció",
|
||||
"Spam és vírusszűrés",
|
||||
"Email továbbítás és automatikus válaszok",
|
||||
"Backup és archiválás"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"icon": "⚙️",
|
||||
"title": "DNS adminisztráció részletesen",
|
||||
"description": "Teljes DNS kezelés domain regisztrációval, átvitellel és professzionális beállításokkal. Gyors propagáció és megbízható névszerverek világszerte.",
|
||||
"specs": {
|
||||
"title": "DNS szolgáltatások:",
|
||||
"items": [
|
||||
"Domain regisztráció (.hu, .com, .eu, stb.)",
|
||||
"Domain átvitel más szolgáltatótól",
|
||||
"DNS rekord kezelés (A, CNAME, MX, TXT)",
|
||||
"Subdomain beállítások",
|
||||
"Redirect és forwarding szolgáltatások",
|
||||
"DNSSEC támogatás",
|
||||
"API hozzáférés fejlesztőknek"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"support": {
|
||||
"title": "Műszaki támogatás",
|
||||
"subtitle": "Minden szolgáltatásunkhoz teljes körű műszaki támogatást biztosítunk",
|
||||
"channels": [],
|
||||
"channels[0]": {
|
||||
"0": {
|
||||
"$1escription": "24 órán belüli válasz"
|
||||
"channels": [
|
||||
{
|
||||
"icon": "✉️",
|
||||
"title": "Email támogatás",
|
||||
"description": "24 órán belüli válasz"
|
||||
},
|
||||
"icon": "✉️",
|
||||
"title": "Email támogatás"
|
||||
},
|
||||
"channels[1]": {
|
||||
"0": {
|
||||
"$1escription": "Munkaidőben elérhető"
|
||||
{
|
||||
"icon": "📞",
|
||||
"title": "Telefonos segítség",
|
||||
"description": "Munkaidőben elérhető"
|
||||
},
|
||||
"icon": "📞",
|
||||
"title": "Telefonos segítség"
|
||||
},
|
||||
"channels[2]": {
|
||||
"0": {
|
||||
"$1escription": "Azonnali beavatkozás"
|
||||
},
|
||||
"icon": "🚨",
|
||||
"title": "Sürgős esetek"
|
||||
}
|
||||
{
|
||||
"icon": "🚨",
|
||||
"title": "Sürgős esetek",
|
||||
"description": "Azonnali beavatkozás"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cta": {
|
||||
"title": "Kezdjük el a közös munkát!",
|
||||
"subtitle": "Vegye fel velünk a kapcsolatot ingyenes konzultációért és egyedi ajánlatért.",
|
||||
"primaryButton": "Kapcsolatfelvétel",
|
||||
"secondaryButton": "Webmail belépés",
|
||||
"secon": {
|
||||
"$1aryButton": "Webmail belépés"
|
||||
}
|
||||
"secondaryButton": "Webmail belépés"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// Shared runtime schema for the content JSON files.
|
||||
// Kept dependency-free so it can run in both Next.js and content-editor.js.
|
||||
const string = { type: 'string' };
|
||||
const boolean = { type: 'boolean' };
|
||||
const array = items => ({ type: 'array', items });
|
||||
const object = properties => ({ type: 'object', properties, required: Object.keys(properties), additionalProperties: false });
|
||||
|
||||
const ctaLink = object({ text: string, href: string, external: boolean });
|
||||
const iconText = object({ icon: string, title: string, description: string });
|
||||
const legal = object({ id: string, title: string, content: string });
|
||||
|
||||
const schemas = {
|
||||
common: object({
|
||||
buttons: object({ contact: string, learnMore: string, webmail: string, sendMessage: string }),
|
||||
labels: object({ required: string, features: string }),
|
||||
validation: object({ required: string, invalidEmail: string, minLength: string }),
|
||||
}),
|
||||
home: object({
|
||||
hero: object({ title: string, subtitle: string, description: string, trustBullets: array(string), cta: object({ primary: ctaLink, secondary: ctaLink }) }),
|
||||
about: object({ title: string, usps: array(object({ id: string, title: string, description: string, icon: string })) }),
|
||||
services: object({ title: string, subtitle: string, items: array(object({ id: string, title: string, description: string, icon: string, features: array(string), ctaText: string })) }),
|
||||
cta: object({ title: string, subtitle: string, button: string }),
|
||||
serviceFeatures: object({ title: string }),
|
||||
}),
|
||||
about: object({
|
||||
meta: object({ title: string, description: string, ogDescription: string }),
|
||||
hero: object({ title: string, subtitle: string }),
|
||||
story: object({ title: string, paragraphs: array(string) }),
|
||||
mission: object({ title: string, subtitle: string, values: array(iconText) }),
|
||||
team: object({ title: string, subtitle: string, paragraphs: array(string) }),
|
||||
cta: object({ title: string, subtitle: string, button: string }),
|
||||
}),
|
||||
services: object({
|
||||
meta: object({ title: string, description: string, ogDescription: string }),
|
||||
hero: object({ title: string, subtitle: string }),
|
||||
details: object({ title: string, subtitle: string, services: array(object({ icon: string, title: string, description: string, specs: object({ title: string, items: array(string) }) })) }),
|
||||
support: object({ title: string, subtitle: string, channels: array(iconText) }),
|
||||
cta: object({ title: string, subtitle: string, primaryButton: string, secondaryButton: string }),
|
||||
}),
|
||||
contact: object({
|
||||
meta: object({ title: string, description: string }),
|
||||
hero: object({ title: string, subtitle: string }),
|
||||
form: object({
|
||||
title: string, successMessage: string, errorMessage: string,
|
||||
fields: object({
|
||||
name: object({ label: string, placeholder: string, error: string }),
|
||||
email: object({ label: string, placeholder: string, errorRequired: string, errorInvalid: string }),
|
||||
subject: object({ label: string, placeholder: string, error: string }),
|
||||
message: object({ label: string, placeholder: string, errorRequired: string, errorMinLength: string }),
|
||||
gdpr: object({ label: string, error: string }),
|
||||
}),
|
||||
submitButton: string, submittingButton: string,
|
||||
}),
|
||||
info: object({
|
||||
title: string,
|
||||
email: object({ title: string, responseTime: string }),
|
||||
company: object({ title: string }),
|
||||
webmail: object({ title: string, linkText: string, subtitle: string }),
|
||||
}),
|
||||
faq: object({ title: string, items: array(object({ question: string, answer: string })) }),
|
||||
}),
|
||||
adatvedelem: object({ title: string, lastUpdated: string, sections: array(legal) }),
|
||||
hasznalatiFeltetelek: object({ title: string, lastUpdated: string, sections: array(legal) }),
|
||||
};
|
||||
|
||||
function validateNode(value, schema, path, errors) {
|
||||
if (schema.type === 'string' || schema.type === 'boolean') {
|
||||
if (typeof value !== schema.type) errors.push(`${path}: ${schema.type} érték szükséges`);
|
||||
return;
|
||||
}
|
||||
if (schema.type === 'array') {
|
||||
if (!Array.isArray(value)) {
|
||||
errors.push(`${path}: tömb szükséges`);
|
||||
return;
|
||||
}
|
||||
value.forEach((item, index) => validateNode(item, schema.items, `${path}[${index}]`, errors));
|
||||
return;
|
||||
}
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
errors.push(`${path}: objektum szükséges`);
|
||||
return;
|
||||
}
|
||||
for (const key of schema.required) {
|
||||
if (!(key in value)) errors.push(`${path}.${key}: kötelező mező hiányzik`);
|
||||
}
|
||||
for (const key of Object.keys(value)) {
|
||||
if (!(key in schema.properties)) errors.push(`${path}.${key}: nem engedélyezett mező`);
|
||||
}
|
||||
for (const [key, child] of Object.entries(schema.properties)) {
|
||||
if (key in value) validateNode(value[key], child, `${path}.${key}`, errors);
|
||||
}
|
||||
}
|
||||
|
||||
function validateContent(fileKey, data) {
|
||||
const schema = schemas[fileKey];
|
||||
if (!schema) return { ok: false, errors: [`Ismeretlen content fájl: ${fileKey}`] };
|
||||
const errors = [];
|
||||
validateNode(data, schema, '$', errors);
|
||||
return { ok: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
module.exports = { schemas, validateContent };
|
||||
@@ -28,6 +28,32 @@ export interface TechSpec {
|
||||
|
||||
// Page-specific content types
|
||||
export interface HomePageContent {
|
||||
hero: {
|
||||
title: string
|
||||
subtitle: string
|
||||
description: string
|
||||
trustBullets: string[]
|
||||
cta: {
|
||||
primary: CTAContent
|
||||
secondary: CTAContent
|
||||
}
|
||||
}
|
||||
about: {
|
||||
title: string
|
||||
usps: Array<IconTextItem & { id: string }>
|
||||
}
|
||||
services: {
|
||||
title: string
|
||||
subtitle: string
|
||||
items: Array<{
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
icon: string
|
||||
features: string[]
|
||||
ctaText: string
|
||||
}>
|
||||
}
|
||||
cta: {
|
||||
title: string
|
||||
subtitle: string
|
||||
@@ -188,4 +214,3 @@ export interface SiteContent {
|
||||
hasznalatiFeltetelek: LegalPageContent
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MongoClient, Db } from 'mongodb'
|
||||
import { MongoClient, Db, Collection, Document } from 'mongodb'
|
||||
|
||||
if (!process.env.MONGODB_URI) {
|
||||
throw new Error('Please add MONGODB_URI to your environment variables')
|
||||
@@ -37,9 +37,9 @@ export async function getDb(): Promise<Db> {
|
||||
return client.db(process.env.MONGODB_DB || 'mozdit')
|
||||
}
|
||||
|
||||
export async function getCollection(collectionName: string) {
|
||||
export async function getCollection<T extends Document = Document>(collectionName: string): Promise<Collection<T>> {
|
||||
const db = await getDb()
|
||||
return db.collection(collectionName)
|
||||
return db.collection<T>(collectionName)
|
||||
}
|
||||
|
||||
// Health check for MongoDB connection
|
||||
@@ -52,4 +52,4 @@ export async function checkMongoConnection(): Promise<boolean> {
|
||||
console.error('MongoDB connection check failed:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const assert = require('assert/strict');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { backupAndWriteAtomically } = require('../content-editor');
|
||||
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mozdit-content-save-'));
|
||||
try {
|
||||
const target = path.join(tempDir, 'home.json');
|
||||
const backupDir = path.join(tempDir, 'backups');
|
||||
const original = { title: 'Eredeti', enabled: true };
|
||||
const replacement = { title: 'Új', enabled: false };
|
||||
fs.writeFileSync(target, JSON.stringify(original) + '\n');
|
||||
|
||||
const backupFile = backupAndWriteAtomically(target, replacement, backupDir);
|
||||
assert.deepEqual(JSON.parse(fs.readFileSync(target, 'utf8')), replacement);
|
||||
assert.deepEqual(JSON.parse(fs.readFileSync(backupFile, 'utf8')), original);
|
||||
assert.equal(fs.readdirSync(tempDir).some(name => name.endsWith('.tmp')), false);
|
||||
console.log('Content Editor atomic save test: OK');
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const assert = require('assert/strict');
|
||||
|
||||
process.env.CMS_USER = 'test-editor';
|
||||
process.env.CMS_PASS = 'not-a-real-secret';
|
||||
const {
|
||||
hasValidCredentials,
|
||||
hasValidCsrfToken,
|
||||
securityConfigIsValid,
|
||||
csrfToken,
|
||||
} = require('../content-editor');
|
||||
|
||||
const authorization = Buffer.from('test-editor:not-a-real-secret').toString('base64');
|
||||
assert.equal(securityConfigIsValid(), true);
|
||||
assert.equal(hasValidCredentials({ headers: { authorization: `Basic ${authorization}` } }), true);
|
||||
assert.equal(hasValidCredentials({ headers: { authorization: 'Basic invalid' } }), false);
|
||||
assert.equal(hasValidCredentials({ headers: {} }), false);
|
||||
assert.equal(hasValidCsrfToken({ headers: { 'x-csrf-token': csrfToken } }), true);
|
||||
assert.equal(hasValidCsrfToken({ headers: { 'x-csrf-token': 'invalid-token' } }), false);
|
||||
assert.equal(hasValidCsrfToken({ headers: {} }), false);
|
||||
|
||||
console.log('Content Editor security guard test: OK');
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Regressziós teszt a Content Editor böngészőben futó serializeréhez.
|
||||
* A szerver által generált tényleges JavaScriptet futtatja minimális DOM-mal,
|
||||
* így a szerkesztő és a teszt nem két eltérő implementációt vizsgál.
|
||||
*/
|
||||
const assert = require('assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
const { createRequire } = require('module');
|
||||
|
||||
const source = fs.readFileSync('content-editor.js', 'utf8')
|
||||
.replace('if (require.main === module) {', 'globalThis.renderContentEditor = HTML;\nif (false) {');
|
||||
const editorRequire = createRequire(path.join(process.cwd(), 'content-editor.js'));
|
||||
const serverContext = { require: editorRequire, console, process, Buffer, module: { exports: {} }, __dirname: process.cwd(), globalThis: {} };
|
||||
vm.createContext(serverContext);
|
||||
new vm.Script(source).runInContext(serverContext);
|
||||
|
||||
const fixture = {
|
||||
title: 'Teszt',
|
||||
enabled: true,
|
||||
limit: 42,
|
||||
sections: [{ id: 'first', items: ['egy', 'kettő'], settings: { visible: false, weight: 1 } }],
|
||||
};
|
||||
const html = serverContext.globalThis.renderContentEditor('home', JSON.stringify(fixture), null);
|
||||
const browserSource = [...html.matchAll(/<script(?: [^>]*)?>([\s\S]*?)<\/script>/g)].at(-1)[1]
|
||||
.replace("render(DATA, document.getElementById('editor'));", '')
|
||||
.replace("const toast = document.querySelector('.toast');", 'const toast = null;');
|
||||
|
||||
const fields = [
|
||||
{ dataset: { path: 'title', type: 'string' }, value: 'Módosított' },
|
||||
{ dataset: { path: 'enabled', type: 'boolean' }, checked: true },
|
||||
{ dataset: { path: 'limit', type: 'number' }, value: '99' },
|
||||
{ dataset: { path: 'sections[0].id', type: 'string' }, value: 'first' },
|
||||
{ dataset: { path: 'sections[0].items[0]', type: 'string' }, value: 'egy' },
|
||||
{ dataset: { path: 'sections[0].items[1]', type: 'string' }, value: 'kettő' },
|
||||
{ dataset: { path: 'sections[0].settings.visible', type: 'boolean' }, checked: false },
|
||||
{ dataset: { path: 'sections[0].settings.weight', type: 'number' }, value: '3' },
|
||||
];
|
||||
const document = {
|
||||
getElementById: id => id === 'page-data' ? { textContent: JSON.stringify(fixture) } : {},
|
||||
querySelectorAll: selector => selector === '[data-path]' ? fields : [],
|
||||
querySelector: () => null,
|
||||
};
|
||||
const browserContext = { document, console, setTimeout, fetch: async () => ({ json: async () => ({ ok: true }) }) };
|
||||
vm.createContext(browserContext);
|
||||
new vm.Script(`${browserSource}\nglobalThis.__collect = collect;`).runInContext(browserContext);
|
||||
|
||||
assert.deepStrictEqual(JSON.parse(JSON.stringify(browserContext.__collect())), {
|
||||
title: 'Módosított',
|
||||
enabled: true,
|
||||
limit: 99,
|
||||
sections: [{ id: 'first', items: ['egy', 'kettő'], settings: { visible: false, weight: 3 } }],
|
||||
});
|
||||
console.log('Content Editor serializer regression test: OK');
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const assert = require('assert/strict');
|
||||
const fs = require('fs');
|
||||
const { validateContent } = require('../proto/src/content/schema');
|
||||
|
||||
const files = {
|
||||
common: 'proto/src/content/common.json',
|
||||
home: 'proto/src/content/pages/home.json',
|
||||
about: 'proto/src/content/pages/about.json',
|
||||
services: 'proto/src/content/pages/services.json',
|
||||
contact: 'proto/src/content/pages/contact.json',
|
||||
adatvedelem: 'proto/src/content/pages/adatvedelem.json',
|
||||
hasznalatiFeltetelek: 'proto/src/content/pages/hasznalati-feltetelek.json',
|
||||
};
|
||||
|
||||
for (const [key, file] of Object.entries(files)) {
|
||||
const result = validateContent(key, JSON.parse(fs.readFileSync(file, 'utf8')));
|
||||
assert.equal(result.ok, true, `${key}: ${result.errors.join('; ')}`);
|
||||
}
|
||||
|
||||
const invalidHome = JSON.parse(fs.readFileSync(files.home, 'utf8'));
|
||||
invalidHome.hero.trustBullets = [];
|
||||
invalidHome.services['items[0]'] = invalidHome.services.items[0];
|
||||
assert.deepEqual(validateContent('home', invalidHome).ok, false);
|
||||
assert.match(validateContent('home', invalidHome).errors.join(' '), /nem engedélyezett mező/);
|
||||
|
||||
console.log('Content schema validation test: OK');
|
||||
Reference in New Issue
Block a user