feat: add versioned multi-component assessment registry
This commit is contained in:
@@ -428,6 +428,54 @@
|
||||
</div>
|
||||
|
||||
{% if user_info and user_info.role == 'admin' %}
|
||||
<div class="card" id="profile-registry">
|
||||
<div class="card-header"><h2 class="card-title">🧩 Registry Category → Component → Profile → Version</h2></div>
|
||||
<p style="color: var(--text-muted);">Các phiên bản là bất biến; chỉ phiên bản active được dùng khi audit. Mọi thao tác bên dưới chỉ dành cho admin.</p>
|
||||
<div class="form-row">
|
||||
<form class="registry-json-form" data-endpoint="{{ url_for('registry_create_category_v2') }}">
|
||||
<div class="form-group"><label>Category ID</label><input name="id" placeholder="database-platforms" required></div>
|
||||
<div class="form-group"><label>Tên category</label><input name="name" required></div>
|
||||
<button class="btn btn-primary" type="submit">Thêm category</button>
|
||||
</form>
|
||||
<form class="registry-json-form" data-endpoint="{{ url_for('registry_create_component_v2') }}">
|
||||
<div class="form-group"><label>Category ID</label><input name="category_id" required></div>
|
||||
<div class="form-group"><label>Component ID</label><input name="id" required></div>
|
||||
<div class="form-group"><label>Tên component</label><input name="name" required></div>
|
||||
<button class="btn btn-primary" type="submit">Thêm component</button>
|
||||
</form>
|
||||
<form class="registry-json-form" data-endpoint="{{ url_for('registry_create_profile_v2') }}">
|
||||
<div class="form-group"><label>Component ID</label><input name="component_id" required></div>
|
||||
<div class="form-group"><label>Profile ID</label><input name="id" required></div>
|
||||
<div class="form-group"><label>Tên profile</label><input name="name" required></div>
|
||||
<div class="form-group"><label>Aliases (phân cách dấu phẩy)</label><input name="aliases"></div>
|
||||
<button class="btn btn-primary" type="submit">Thêm profile</button>
|
||||
</form>
|
||||
</div>
|
||||
{% for category in registry.categories %}
|
||||
<h3>{{ category.name }} <code>{{ category.id }}</code></h3>
|
||||
{% for component in category.components %}
|
||||
<h4>{{ component.name }} <code>{{ component.id }}</code></h4>
|
||||
{% for profile in component.profiles %}
|
||||
<div style="border:1px solid var(--border-color);padding:14px;margin:10px 0;border-radius:8px;">
|
||||
<strong>{{ profile.name }}</strong> <code>{{ profile.id }}</code>
|
||||
<form action="{{ url_for('registry_upload_version_v2', profile_id=profile.id) }}" method="post" enctype="multipart/form-data" class="upload-form registry-action-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Version</label><input name="version" required></div>
|
||||
<div class="form-group"><label>Checklist XLSX</label><input type="file" name="checklist_file" accept=".xlsx" required></div>
|
||||
<div class="form-group"><label>Rule JSON</label><input type="file" name="config_file" accept=".json" required></div>
|
||||
<label><input type="checkbox" name="activate" value="true"> Kích hoạt ngay</label>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Upload version</button>
|
||||
</form>
|
||||
{% for version in profile.versions %}
|
||||
<span style="display:inline-block;margin:5px;"><code>{{ version.version }}</code>{% if version.active %} ✅ active{% else %}
|
||||
<form class="registry-action-form" style="display:inline" action="{{ url_for('registry_activate_v2', profile_id=profile.id, version=version.version) }}" method="post"><button class="btn" type="submit">Activate</button></form>{% endif %}</span>
|
||||
{% endfor %}
|
||||
<form class="registry-action-form" style="display:inline" action="{{ url_for('registry_rollback_v2', profile_id=profile.id) }}" method="post"><button class="btn" type="submit">Rollback</button></form>
|
||||
</div>
|
||||
{% endfor %}{% endfor %}{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Upload Checklist and Rule Configuration -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
@@ -681,14 +729,28 @@
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
['dragenter', 'dragover'].forEach(eventName => {
|
||||
uploadForm.addEventListener(eventName, () => uploadForm.classList.add('dragover'), false);
|
||||
});
|
||||
if (uploadForm) {
|
||||
['dragenter', 'dragover'].forEach(eventName => uploadForm.addEventListener(eventName, () => uploadForm.classList.add('dragover'), false));
|
||||
['dragleave', 'drop'].forEach(eventName => uploadForm.addEventListener(eventName, () => uploadForm.classList.remove('dragover'), false));
|
||||
}
|
||||
|
||||
['dragleave', 'drop'].forEach(eventName => {
|
||||
uploadForm.addEventListener(eventName, () => uploadForm.classList.remove('dragover'), false);
|
||||
});
|
||||
document.querySelectorAll('.registry-json-form').forEach(form => form.addEventListener('submit', async event => {
|
||||
event.preventDefault();
|
||||
const body = Object.fromEntries(new FormData(form).entries());
|
||||
if (body.aliases !== undefined) body.aliases = body.aliases.split(',').map(x => x.trim()).filter(Boolean);
|
||||
const response = await fetch(form.dataset.endpoint, {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body)});
|
||||
const result = await response.json();
|
||||
if (!response.ok) return alert(result.detail || 'Registry update failed');
|
||||
location.reload();
|
||||
}));
|
||||
document.querySelectorAll('.registry-action-form').forEach(form => form.addEventListener('submit', async event => {
|
||||
event.preventDefault();
|
||||
const response = await fetch(form.action, {method: 'POST', body: new FormData(form)});
|
||||
const result = await response.json();
|
||||
if (!response.ok) return alert(result.detail || 'Registry action failed');
|
||||
location.reload();
|
||||
}));
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -1309,6 +1309,29 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="profile_category">2. Loại thành phần đánh giá</label>
|
||||
<select id="profile_category">
|
||||
<option value="">Tất cả loại thành phần</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="profile_component">3. Thành phần</label>
|
||||
<select id="profile_component">
|
||||
<option value="">Tất cả thành phần</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="profile_id">4. Assessment Profile (tùy chọn)</label>
|
||||
<select id="profile_id" name="profile_id">
|
||||
<option value="">Dùng profile đang active theo OS tự động</option>
|
||||
{% for profile in active_profiles %}
|
||||
<option value="{{ profile.id }}" data-category="{{ profile.category_id }}" data-component="{{ profile.component_id }}">{{ profile.category }} → {{ profile.component }} → {{ profile.name }} ({{ profile.version }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="hint">Chọn profile cho OS, Database, Web Server, Network Device...; để trống vẫn giữ auto-detect OS tương thích cũ.</p>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn-submit" id="submitBtn" onclick="handleUploadClick()">
|
||||
⚡ Xử lý File
|
||||
</button>
|
||||
@@ -1453,11 +1476,45 @@
|
||||
const form = document.getElementById('uploadForm');
|
||||
const loadingOverlay = document.getElementById('loadingOverlay');
|
||||
const osSelect = document.getElementById('os_type');
|
||||
const profileSelect = document.getElementById('profile_id');
|
||||
const categorySelect = document.getElementById('profile_category');
|
||||
const componentSelect = document.getElementById('profile_component');
|
||||
const fileInput = document.getElementById('encrypted_files');
|
||||
const autoDetectInfo = document.getElementById('autoDetectInfo');
|
||||
const autoDetectIcon = document.getElementById('autoDetectIcon');
|
||||
const autoDetectMessage = document.getElementById('autoDetectMessage');
|
||||
|
||||
const profileOptions = [...profileSelect.options].filter(option => option.value).map(option => ({
|
||||
value: option.value,
|
||||
label: option.textContent,
|
||||
category: option.dataset.category,
|
||||
component: option.dataset.component
|
||||
}));
|
||||
const categories = [...new Set(profileOptions.map(option => option.category))];
|
||||
categories.forEach(category => categorySelect.add(new Option(category, category)));
|
||||
|
||||
function refreshComponents() {
|
||||
componentSelect.innerHTML = '<option value="">Tất cả thành phần</option>';
|
||||
const components = [...new Set(profileOptions
|
||||
.filter(option => !categorySelect.value || option.category === categorySelect.value)
|
||||
.map(option => option.component))];
|
||||
components.forEach(component => componentSelect.add(new Option(component, component)));
|
||||
refreshProfiles();
|
||||
}
|
||||
|
||||
function refreshProfiles() {
|
||||
const selected = profileSelect.value;
|
||||
profileSelect.innerHTML = '<option value="">Dùng profile đang active theo OS tự động</option>';
|
||||
profileOptions.filter(option =>
|
||||
(!categorySelect.value || option.category === categorySelect.value) &&
|
||||
(!componentSelect.value || option.component === componentSelect.value)
|
||||
).forEach(option => profileSelect.add(new Option(option.label, option.value)));
|
||||
if ([...profileSelect.options].some(option => option.value === selected)) profileSelect.value = selected;
|
||||
}
|
||||
categorySelect.addEventListener('change', refreshComponents);
|
||||
componentSelect.addEventListener('change', refreshProfiles);
|
||||
refreshComponents();
|
||||
|
||||
// OS name display map (mirrors OS_CONFIG)
|
||||
const osNames = {
|
||||
'centos': 'CentOS',
|
||||
@@ -1522,7 +1579,7 @@
|
||||
}
|
||||
|
||||
// Nếu người dùng đã chọn thủ công một OS trên form chính (không phải auto)
|
||||
if (osSelect.value !== 'auto') {
|
||||
if (profileSelect.value || osSelect.value !== 'auto') {
|
||||
// Trang trung gian 2 luôn (Xử lý ngay)
|
||||
startGeneratingReport();
|
||||
return;
|
||||
@@ -1656,4 +1713,4 @@
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user