hoàn thành trang settings
This commit is contained in:
+3074
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ Tags Tạo và quản lý các Thẻ (nhãn từ khóa tìm kiếm nhanh).
|
|||||||
Add New Tải tệp tin mới lên máy chủ.
|
Add New Tải tệp tin mới lên máy chủ.
|
||||||
(Done) 4. Pages (Trang) All Pages Quản lý các trang tĩnh (ví dụ: Trang chủ, Trang liên hệ, Giới thiệu bệnh viện, Giờ làm việc...).
|
(Done) 4. Pages (Trang) All Pages Quản lý các trang tĩnh (ví dụ: Trang chủ, Trang liên hệ, Giới thiệu bệnh viện, Giờ làm việc...).
|
||||||
Add New Tạo trang tĩnh mới.
|
Add New Tạo trang tĩnh mới.
|
||||||
6. Appearance (Giao diện) Themes Cài đặt, kích hoạt hoặc xóa các giao diện hiển thị của website.
|
(done) 6. Appearance (Giao diện) Themes Cài đặt, kích hoạt hoặc xóa các giao diện hiển thị của website.
|
||||||
Customize Chỉnh sửa giao diện trực quan theo thời gian thực (Header, Footer, Colors, Font, v.v.).
|
Customize Chỉnh sửa giao diện trực quan theo thời gian thực (Header, Footer, Colors, Font, v.v.).
|
||||||
Widgets Quản lý các khối nội dung ở Sidebar hoặc Footer.
|
Widgets Quản lý các khối nội dung ở Sidebar hoặc Footer.
|
||||||
Menus Thiết kế menu điều hướng chính ở đầu trang và chân trang.
|
Menus Thiết kế menu điều hướng chính ở đầu trang và chân trang.
|
||||||
@@ -25,10 +25,10 @@ Plugin File Editor Chỉnh sửa mã nguồn của các Plugin trực tiếp t
|
|||||||
(done) 8. Users (Thành viên) All Users Quản lý danh sách toàn bộ tài khoản đăng ký trên hệ thống.
|
(done) 8. Users (Thành viên) All Users Quản lý danh sách toàn bộ tài khoản đăng ký trên hệ thống.
|
||||||
Add New Tạo tài khoản người dùng mới và chỉ định vai trò (Role).
|
Add New Tạo tài khoản người dùng mới và chỉ định vai trò (Role).
|
||||||
Profile Chỉnh sửa thông tin cá nhân của tài khoản đang đăng nhập (đổi pass, ngôn ngữ hiển thị).
|
Profile Chỉnh sửa thông tin cá nhân của tài khoản đang đăng nhập (đổi pass, ngôn ngữ hiển thị).
|
||||||
9. Tools (Công cụ) Available Tools Các công cụ hỗ trợ chuyển đổi dữ liệu.
|
(done) 9. Tools (Công cụ) Available Tools Các công cụ hỗ trợ chuyển đổi dữ liệu.
|
||||||
Import / Export Nhập/Xuất toàn bộ bài viết, trang, media sang file .xml để sao lưu hoặc chuyển host.
|
(done) Import / Export Nhập/Xuất toàn bộ bài viết, trang, media sang file .xml để sao lưu hoặc chuyển host.
|
||||||
Site Health Kiểm tra sức khỏe hệ thống (phiên bản PHP, MySQL, các lỗi bảo mật).
|
(done) Site Health Kiểm tra sức khỏe hệ thống (phiên bản PHP, MySQL, các lỗi bảo mật).
|
||||||
Export / Erase Personal Data Xuất hoặc xóa dữ liệu cá nhân của người dùng để tuân thủ luật bảo mật (GDPR).
|
(done) Export / Erase Personal Data Xuất hoặc xóa dữ liệu cá nhân của người dùng để tuân thủ luật bảo mật (GDPR).
|
||||||
(done) 10. Settings (Cài đặt) General Thiết lập tên website, câu khẩu hiệu, địa chỉ email admin, múi giờ, định dạng ngày tháng.
|
(done) 10. Settings (Cài đặt) General Thiết lập tên website, câu khẩu hiệu, địa chỉ email admin, múi giờ, định dạng ngày tháng.
|
||||||
Writing Cấu hình mặc định khi soạn thảo văn bản và gửi bài viết qua email.
|
Writing Cấu hình mặc định khi soạn thảo văn bản và gửi bài viết qua email.
|
||||||
Reading Thiết lập trang chủ hiển thị, số lượng bài viết tối đa trên một trang blog, chặn công cụ tìm kiếm.
|
Reading Thiết lập trang chủ hiển thị, số lượng bài viết tối đa trên một trang blog, chặn công cụ tìm kiếm.
|
||||||
|
|||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
package com.sisvietnamvn.web.controller.manage;
|
||||||
|
|
||||||
|
import com.sisvietnamvn.web.security.AuthoritiesConstants;
|
||||||
|
import com.sisvietnamvn.web.service.ImportExportService;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller for Import/Export functionality.
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/manage/tools/import-export")
|
||||||
|
@PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")")
|
||||||
|
public class ManageImportExportController {
|
||||||
|
|
||||||
|
private final ImportExportService importExportService;
|
||||||
|
|
||||||
|
public ManageImportExportController(ImportExportService importExportService) {
|
||||||
|
this.importExportService = importExportService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public String index() {
|
||||||
|
return "manage/tools/import-export";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/export")
|
||||||
|
public ResponseEntity<byte[]> exportData() {
|
||||||
|
byte[] xmlData = importExportService.exportToXml();
|
||||||
|
if (xmlData == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.valueOf("application/xml"));
|
||||||
|
headers.setContentDispositionFormData("attachment", "sisvietnamvn_export.xml");
|
||||||
|
return new ResponseEntity<>(xmlData, headers, HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/import")
|
||||||
|
public String importData(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes) {
|
||||||
|
if (file.isEmpty() || !file.getOriginalFilename().endsWith(".xml")) {
|
||||||
|
redirectAttributes.addFlashAttribute("errorMessage", "Please provide a valid .xml file.");
|
||||||
|
return "redirect:/manage/tools/import-export";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
boolean success = importExportService.importFromXml(file.getInputStream());
|
||||||
|
if (success) {
|
||||||
|
redirectAttributes.addFlashAttribute("successMessage", "Data successfully imported.");
|
||||||
|
} else {
|
||||||
|
redirectAttributes.addFlashAttribute("errorMessage", "Failed to parse XML file.");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
redirectAttributes.addFlashAttribute("errorMessage", "Import error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return "redirect:/manage/tools/import-export";
|
||||||
|
}
|
||||||
|
}
|
||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
package com.sisvietnamvn.web.controller.manage;
|
||||||
|
|
||||||
|
import com.sisvietnamvn.web.security.AuthoritiesConstants;
|
||||||
|
import com.sisvietnamvn.web.service.PersonalDataService;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller for managing Personal Data (GDPR).
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/manage/tools/personal-data")
|
||||||
|
@PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")")
|
||||||
|
public class ManagePersonalDataController {
|
||||||
|
|
||||||
|
private final PersonalDataService personalDataService;
|
||||||
|
|
||||||
|
public ManagePersonalDataController(PersonalDataService personalDataService) {
|
||||||
|
this.personalDataService = personalDataService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public String personalDataView() {
|
||||||
|
return "manage/tools/personal-data";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/export")
|
||||||
|
public ResponseEntity<byte[]> exportData(@RequestParam("email") String email) {
|
||||||
|
byte[] zipData = personalDataService.exportData(email);
|
||||||
|
|
||||||
|
if (zipData == null) {
|
||||||
|
// If user not found, we redirect back with error.
|
||||||
|
// Since this returns ResponseEntity, we have to return a redirect status or handle it via a form redirect.
|
||||||
|
// A better way is to return an error page or redirect response.
|
||||||
|
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.valueOf("application/zip"));
|
||||||
|
headers.setContentDispositionFormData("attachment", "personal_data_export.zip");
|
||||||
|
return new ResponseEntity<>(zipData, headers, HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/export-form")
|
||||||
|
public String exportDataForm(@RequestParam("email") String email, RedirectAttributes redirectAttributes) {
|
||||||
|
byte[] zipData = personalDataService.exportData(email);
|
||||||
|
if (zipData == null) {
|
||||||
|
redirectAttributes.addFlashAttribute("errorMessage", "No user found with that email address.");
|
||||||
|
return "redirect:/manage/tools/personal-data";
|
||||||
|
}
|
||||||
|
// If found, redirect to a direct download endpoint to avoid flash attribute issues with file downloads
|
||||||
|
return "redirect:/manage/tools/personal-data/download?email=" + email;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/download")
|
||||||
|
public ResponseEntity<byte[]> downloadData(@RequestParam("email") String email) {
|
||||||
|
byte[] zipData = personalDataService.exportData(email);
|
||||||
|
if (zipData == null) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.valueOf("application/zip"));
|
||||||
|
headers.setContentDispositionFormData("attachment", "personal_data_export.zip");
|
||||||
|
return new ResponseEntity<>(zipData, headers, HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/erase")
|
||||||
|
public String eraseData(@RequestParam("email") String email,
|
||||||
|
@RequestParam(value = "confirm", required = false) String confirm,
|
||||||
|
RedirectAttributes redirectAttributes) {
|
||||||
|
if (confirm == null || !confirm.equals("on")) {
|
||||||
|
redirectAttributes.addFlashAttribute("errorMessage", "You must check the confirmation box to erase data.");
|
||||||
|
return "redirect:/manage/tools/personal-data";
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean success = personalDataService.eraseData(email);
|
||||||
|
if (success) {
|
||||||
|
redirectAttributes.addFlashAttribute("successMessage", "Personal data successfully erased and anonymized for " + email);
|
||||||
|
} else {
|
||||||
|
redirectAttributes.addFlashAttribute("errorMessage", "No user found with that email address.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return "redirect:/manage/tools/personal-data";
|
||||||
|
}
|
||||||
|
}
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
package com.sisvietnamvn.web.controller.manage;
|
||||||
|
|
||||||
|
import com.sisvietnamvn.web.security.AuthoritiesConstants;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
import java.sql.Connection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller for the Site Health diagnostic dashboard.
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/manage/tools/site-health")
|
||||||
|
@PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")")
|
||||||
|
public class ManageSiteHealthController {
|
||||||
|
|
||||||
|
private final DataSource dataSource;
|
||||||
|
|
||||||
|
public ManageSiteHealthController(DataSource dataSource) {
|
||||||
|
this.dataSource = dataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public String siteHealthView(Model model) {
|
||||||
|
// Gather Java / OS Information
|
||||||
|
model.addAttribute("javaVersion", System.getProperty("java.version"));
|
||||||
|
model.addAttribute("javaVendor", System.getProperty("java.vendor"));
|
||||||
|
model.addAttribute("osName", System.getProperty("os.name"));
|
||||||
|
model.addAttribute("osVersion", System.getProperty("os.version"));
|
||||||
|
model.addAttribute("osArch", System.getProperty("os.arch"));
|
||||||
|
model.addAttribute("timezone", System.getProperty("user.timezone"));
|
||||||
|
|
||||||
|
// Gather Memory Information
|
||||||
|
Runtime runtime = Runtime.getRuntime();
|
||||||
|
long totalMemory = runtime.totalMemory() / (1024 * 1024);
|
||||||
|
long freeMemory = runtime.freeMemory() / (1024 * 1024);
|
||||||
|
long maxMemory = runtime.maxMemory() / (1024 * 1024);
|
||||||
|
long usedMemory = totalMemory - freeMemory;
|
||||||
|
|
||||||
|
model.addAttribute("totalMemory", totalMemory);
|
||||||
|
model.addAttribute("freeMemory", freeMemory);
|
||||||
|
model.addAttribute("maxMemory", maxMemory);
|
||||||
|
model.addAttribute("usedMemory", usedMemory);
|
||||||
|
|
||||||
|
// Database Connectivity Check
|
||||||
|
String dbStatus = "Disconnected";
|
||||||
|
String dbProduct = "Unknown";
|
||||||
|
boolean isDbHealthy = false;
|
||||||
|
try (Connection connection = dataSource.getConnection()) {
|
||||||
|
if (connection.isValid(2)) {
|
||||||
|
dbStatus = "Connected";
|
||||||
|
isDbHealthy = true;
|
||||||
|
dbProduct = connection.getMetaData().getDatabaseProductName() + " " + connection.getMetaData().getDatabaseProductVersion();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
dbStatus = "Error: " + e.getMessage();
|
||||||
|
}
|
||||||
|
model.addAttribute("dbStatus", dbStatus);
|
||||||
|
model.addAttribute("dbProduct", dbProduct);
|
||||||
|
model.addAttribute("isDbHealthy", isDbHealthy);
|
||||||
|
|
||||||
|
return "manage/tools/site-health";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,11 @@ public interface PageRepository extends JpaRepository<Page, Long> {
|
|||||||
*/
|
*/
|
||||||
List<Page> findByStatusOrderByDisplayOrderAsc(PageStatus status);
|
List<Page> findByStatusOrderByDisplayOrderAsc(PageStatus status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all pages authored by a specific user (login).
|
||||||
|
*/
|
||||||
|
List<Page> findByCreatedBy(String createdBy);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a slug already exists (for uniqueness validation).
|
* Check if a slug already exists (for uniqueness validation).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ public interface PostRepository extends JpaRepository<Post, Long> {
|
|||||||
@org.springframework.data.jpa.repository.EntityGraph(attributePaths = {"category", "tags"})
|
@org.springframework.data.jpa.repository.EntityGraph(attributePaths = {"category", "tags"})
|
||||||
List<Post> findByStatusOrderByCreatedDateDesc(PageStatus status);
|
List<Post> findByStatusOrderByCreatedDateDesc(PageStatus status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all posts authored by a specific user (login).
|
||||||
|
*/
|
||||||
|
List<Post> findByCreatedBy(String createdBy);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find all posts in a given category.
|
* Find all posts in a given category.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+217
@@ -0,0 +1,217 @@
|
|||||||
|
package com.sisvietnamvn.web.service;
|
||||||
|
|
||||||
|
import com.sisvietnamvn.web.domain.Media;
|
||||||
|
import com.sisvietnamvn.web.domain.Page;
|
||||||
|
import com.sisvietnamvn.web.domain.Post;
|
||||||
|
import com.sisvietnamvn.web.domain.PageStatus;
|
||||||
|
import com.sisvietnamvn.web.repository.MediaRepository;
|
||||||
|
import com.sisvietnamvn.web.repository.PageRepository;
|
||||||
|
import com.sisvietnamvn.web.repository.PostRepository;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.Element;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
|
||||||
|
import javax.xml.parsers.DocumentBuilder;
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
|
import javax.xml.transform.OutputKeys;
|
||||||
|
import javax.xml.transform.Transformer;
|
||||||
|
import javax.xml.transform.TransformerFactory;
|
||||||
|
import javax.xml.transform.dom.DOMSource;
|
||||||
|
import javax.xml.transform.stream.StreamResult;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for handling Import and Export of XML data.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
public class ImportExportService {
|
||||||
|
|
||||||
|
private final Logger log = LoggerFactory.getLogger(ImportExportService.class);
|
||||||
|
private final PostRepository postRepository;
|
||||||
|
private final PageRepository pageRepository;
|
||||||
|
private final MediaRepository mediaRepository;
|
||||||
|
|
||||||
|
public ImportExportService(PostRepository postRepository, PageRepository pageRepository, MediaRepository mediaRepository) {
|
||||||
|
this.postRepository = postRepository;
|
||||||
|
this.pageRepository = pageRepository;
|
||||||
|
this.mediaRepository = mediaRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports Posts, Pages, and Media metadata to an XML document.
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public byte[] exportToXml() {
|
||||||
|
try {
|
||||||
|
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
|
||||||
|
DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
|
||||||
|
Document doc = docBuilder.newDocument();
|
||||||
|
|
||||||
|
// Root Element
|
||||||
|
Element rootElement = doc.createElement("sis_export");
|
||||||
|
doc.appendChild(rootElement);
|
||||||
|
|
||||||
|
// Export Posts
|
||||||
|
List<Post> posts = postRepository.findAll();
|
||||||
|
Element postsElement = doc.createElement("posts");
|
||||||
|
rootElement.appendChild(postsElement);
|
||||||
|
for (Post post : posts) {
|
||||||
|
Element postEl = doc.createElement("post");
|
||||||
|
addElement(doc, postEl, "title", post.getTitle());
|
||||||
|
addElement(doc, postEl, "slug", post.getSlug());
|
||||||
|
addElement(doc, postEl, "content", post.getContent());
|
||||||
|
addElement(doc, postEl, "excerpt", post.getExcerpt());
|
||||||
|
addElement(doc, postEl, "status", post.getStatus() != null ? post.getStatus().name() : "");
|
||||||
|
postsElement.appendChild(postEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export Pages
|
||||||
|
List<Page> pages = pageRepository.findAll();
|
||||||
|
Element pagesElement = doc.createElement("pages");
|
||||||
|
rootElement.appendChild(pagesElement);
|
||||||
|
for (Page page : pages) {
|
||||||
|
Element pageEl = doc.createElement("page");
|
||||||
|
addElement(doc, pageEl, "title", page.getTitle());
|
||||||
|
addElement(doc, pageEl, "slug", page.getSlug());
|
||||||
|
addElement(doc, pageEl, "content", page.getContent());
|
||||||
|
addElement(doc, pageEl, "status", page.getStatus() != null ? page.getStatus().name() : "");
|
||||||
|
pagesElement.appendChild(pageEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export Media Metadata
|
||||||
|
List<Media> medias = mediaRepository.findAll();
|
||||||
|
Element mediasElement = doc.createElement("medias");
|
||||||
|
rootElement.appendChild(mediasElement);
|
||||||
|
for (Media media : medias) {
|
||||||
|
Element mediaEl = doc.createElement("media");
|
||||||
|
addElement(doc, mediaEl, "fileName", media.getOriginalFilename());
|
||||||
|
addElement(doc, mediaEl, "url", media.getFileUrl());
|
||||||
|
addElement(doc, mediaEl, "mediaType", media.getMediaType() != null ? media.getMediaType().name() : "");
|
||||||
|
mediasElement.appendChild(mediaEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write content to byte array
|
||||||
|
TransformerFactory transformerFactory = TransformerFactory.newInstance();
|
||||||
|
Transformer transformer = transformerFactory.newTransformer();
|
||||||
|
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
|
||||||
|
DOMSource source = new DOMSource(doc);
|
||||||
|
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
StreamResult result = new StreamResult(baos);
|
||||||
|
transformer.transform(source, result);
|
||||||
|
|
||||||
|
return baos.toByteArray();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error exporting to XML", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports Posts and Pages from an XML document.
|
||||||
|
*/
|
||||||
|
public boolean importFromXml(InputStream is) {
|
||||||
|
try {
|
||||||
|
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
|
||||||
|
// Prevent XML External Entity (XXE) injection
|
||||||
|
dbFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
|
||||||
|
|
||||||
|
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
|
||||||
|
Document doc = dBuilder.parse(is);
|
||||||
|
doc.getDocumentElement().normalize();
|
||||||
|
|
||||||
|
if (!"sis_export".equals(doc.getDocumentElement().getNodeName())) {
|
||||||
|
log.warn("Invalid XML root element during import");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import Posts
|
||||||
|
NodeList postList = doc.getElementsByTagName("post");
|
||||||
|
for (int i = 0; i < postList.getLength(); i++) {
|
||||||
|
Node node = postList.item(i);
|
||||||
|
if (node.getNodeType() == Node.ELEMENT_NODE) {
|
||||||
|
Element el = (Element) node;
|
||||||
|
String title = getElementValue(el, "title");
|
||||||
|
String slug = getElementValue(el, "slug");
|
||||||
|
|
||||||
|
// Avoid duplicating by slug
|
||||||
|
if (postRepository.findBySlug(slug).isEmpty()) {
|
||||||
|
Post post = new Post();
|
||||||
|
post.setTitle(title);
|
||||||
|
post.setSlug(slug);
|
||||||
|
post.setContent(getElementValue(el, "content"));
|
||||||
|
post.setExcerpt(getElementValue(el, "excerpt"));
|
||||||
|
|
||||||
|
String statusStr = getElementValue(el, "status");
|
||||||
|
if (!statusStr.isEmpty()) {
|
||||||
|
try {
|
||||||
|
post.setStatus(PageStatus.valueOf(statusStr));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
post.setStatus(PageStatus.DRAFT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
postRepository.save(post);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import Pages
|
||||||
|
NodeList pageList = doc.getElementsByTagName("page");
|
||||||
|
for (int i = 0; i < pageList.getLength(); i++) {
|
||||||
|
Node node = pageList.item(i);
|
||||||
|
if (node.getNodeType() == Node.ELEMENT_NODE) {
|
||||||
|
Element el = (Element) node;
|
||||||
|
String title = getElementValue(el, "title");
|
||||||
|
String slug = getElementValue(el, "slug");
|
||||||
|
|
||||||
|
if (pageRepository.findBySlug(slug).isEmpty()) {
|
||||||
|
Page page = new Page();
|
||||||
|
page.setTitle(title);
|
||||||
|
page.setSlug(slug);
|
||||||
|
page.setContent(getElementValue(el, "content"));
|
||||||
|
|
||||||
|
String statusStr = getElementValue(el, "status");
|
||||||
|
if (!statusStr.isEmpty()) {
|
||||||
|
try {
|
||||||
|
page.setStatus(PageStatus.valueOf(statusStr));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
page.setStatus(PageStatus.DRAFT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pageRepository.save(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Media files are skipped during import since the actual file data is not present in XML.
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error importing from XML", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addElement(Document doc, Element parent, String tagName, String value) {
|
||||||
|
Element el = doc.createElement(tagName);
|
||||||
|
el.appendChild(doc.createTextNode(value != null ? value : ""));
|
||||||
|
parent.appendChild(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getElementValue(Element parent, String tagName) {
|
||||||
|
NodeList nList = parent.getElementsByTagName(tagName);
|
||||||
|
if (nList != null && nList.getLength() > 0) {
|
||||||
|
Node node = nList.item(0);
|
||||||
|
return node.getTextContent();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
+146
@@ -0,0 +1,146 @@
|
|||||||
|
package com.sisvietnamvn.web.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.sisvietnamvn.web.domain.Page;
|
||||||
|
import com.sisvietnamvn.web.domain.Post;
|
||||||
|
import com.sisvietnamvn.web.domain.User;
|
||||||
|
import com.sisvietnamvn.web.repository.PageRepository;
|
||||||
|
import com.sisvietnamvn.web.repository.PostRepository;
|
||||||
|
import com.sisvietnamvn.web.repository.UserRepository;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for managing Personal Data (GDPR compliance).
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
public class PersonalDataService {
|
||||||
|
|
||||||
|
private final Logger log = LoggerFactory.getLogger(PersonalDataService.class);
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final PostRepository postRepository;
|
||||||
|
private final PageRepository pageRepository;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public PersonalDataService(UserRepository userRepository, PostRepository postRepository, PageRepository pageRepository, ObjectMapper objectMapper) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.postRepository = postRepository;
|
||||||
|
this.pageRepository = pageRepository;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports all personal data associated with an email to a Zip containing a JSON file.
|
||||||
|
* @param email The user's email address
|
||||||
|
* @return byte[] of the zip file, or null if user not found.
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public byte[] exportData(String email) {
|
||||||
|
Optional<User> userOpt = userRepository.findOneWithAuthoritiesByEmailIgnoreCase(email);
|
||||||
|
if (userOpt.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = userOpt.get();
|
||||||
|
ObjectNode rootNode = objectMapper.createObjectNode();
|
||||||
|
|
||||||
|
// 1. Profile Data
|
||||||
|
ObjectNode profileNode = rootNode.putObject("profile");
|
||||||
|
profileNode.put("id", user.getId());
|
||||||
|
profileNode.put("login", user.getLogin());
|
||||||
|
profileNode.put("firstName", user.getFirstName());
|
||||||
|
profileNode.put("lastName", user.getLastName());
|
||||||
|
profileNode.put("email", user.getEmail());
|
||||||
|
profileNode.put("imageUrl", user.getImageUrl());
|
||||||
|
profileNode.put("langKey", user.getLangKey());
|
||||||
|
profileNode.put("createdDate", user.getCreatedDate() != null ? user.getCreatedDate().toString() : null);
|
||||||
|
|
||||||
|
// 2. Authored Posts
|
||||||
|
List<Post> posts = postRepository.findByCreatedBy(user.getLogin());
|
||||||
|
ArrayNode postsNode = rootNode.putArray("authored_posts");
|
||||||
|
for (Post post : posts) {
|
||||||
|
ObjectNode pNode = objectMapper.createObjectNode();
|
||||||
|
pNode.put("id", post.getId());
|
||||||
|
pNode.put("title", post.getTitle());
|
||||||
|
pNode.put("slug", post.getSlug());
|
||||||
|
pNode.put("createdDate", post.getCreatedDate() != null ? post.getCreatedDate().toString() : null);
|
||||||
|
postsNode.add(pNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Authored Pages
|
||||||
|
List<Page> pages = pageRepository.findByCreatedBy(user.getLogin());
|
||||||
|
ArrayNode pagesNode = rootNode.putArray("authored_pages");
|
||||||
|
for (Page page : pages) {
|
||||||
|
ObjectNode pNode = objectMapper.createObjectNode();
|
||||||
|
pNode.put("id", page.getId());
|
||||||
|
pNode.put("title", page.getTitle());
|
||||||
|
pNode.put("slug", page.getSlug());
|
||||||
|
pNode.put("createdDate", page.getCreatedDate() != null ? page.getCreatedDate().toString() : null);
|
||||||
|
pagesNode.add(pNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String jsonOutput = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(rootNode);
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
|
||||||
|
ZipEntry entry = new ZipEntry("personal_data.json");
|
||||||
|
zos.putNextEntry(entry);
|
||||||
|
zos.write(jsonOutput.getBytes());
|
||||||
|
zos.closeEntry();
|
||||||
|
}
|
||||||
|
return baos.toByteArray();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to generate personal data export zip", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erases personal data associated with an email.
|
||||||
|
* Anonymizes authored content and deletes the user account.
|
||||||
|
* @param email The user's email address
|
||||||
|
* @return true if successful, false if user not found.
|
||||||
|
*/
|
||||||
|
public boolean eraseData(String email) {
|
||||||
|
Optional<User> userOpt = userRepository.findOneWithAuthoritiesByEmailIgnoreCase(email);
|
||||||
|
if (userOpt.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = userOpt.get();
|
||||||
|
String login = user.getLogin();
|
||||||
|
|
||||||
|
// 1. Anonymize Posts
|
||||||
|
List<Post> posts = postRepository.findByCreatedBy(login);
|
||||||
|
for (Post post : posts) {
|
||||||
|
post.setCreatedBy("anonymous");
|
||||||
|
post.setLastModifiedBy("anonymous");
|
||||||
|
postRepository.save(post);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Anonymize Pages
|
||||||
|
List<Page> pages = pageRepository.findByCreatedBy(login);
|
||||||
|
for (Page page : pages) {
|
||||||
|
page.setCreatedBy("anonymous");
|
||||||
|
page.setLastModifiedBy("anonymous");
|
||||||
|
pageRepository.save(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Delete User
|
||||||
|
userRepository.delete(user);
|
||||||
|
log.info("Erased personal data for user email: {}", email);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -218,6 +218,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<!-- Nav Item - Tools Collapse Menu -->
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapseTools"
|
||||||
|
aria-expanded="true" aria-controls="collapseTools">
|
||||||
|
<i class="fas fa-fw fa-wrench"></i>
|
||||||
|
<span>Tools</span>
|
||||||
|
</a>
|
||||||
|
<div id="collapseTools" class="collapse" aria-labelledby="headingTools"
|
||||||
|
data-parent="#accordionSidebar">
|
||||||
|
<div class="bg-white py-2 collapse-inner rounded">
|
||||||
|
<h6 class="collapse-header">System Tools:</h6>
|
||||||
|
<a class="collapse-item" th:href="@{/manage/tools/import-export}">Import / Export</a>
|
||||||
|
<a class="collapse-item" th:href="@{/manage/tools/site-health}">Site Health</a>
|
||||||
|
<a class="collapse-item" th:href="@{/manage/tools/personal-data}">Personal Data (GDPR)</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
<!-- Divider -->
|
<!-- Divider -->
|
||||||
<hr class="sidebar-divider d-none d-md-block">
|
<hr class="sidebar-divider d-none d-md-block">
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{fragments/manage-layout}">
|
||||||
|
<head>
|
||||||
|
<title>Import / Export Data</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div layout:fragment="content">
|
||||||
|
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||||
|
<h1 class="h3 mb-0 text-gray-800">Import / Export</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Success/Error Messages -->
|
||||||
|
<div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<span th:text="${successMessage}"></span>
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<span th:text="${errorMessage}"></span>
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Export Data Form -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">Export Content</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Export all your Posts, Pages, and Media metadata to an XML file. You can use this file to back up your data or migrate it to another server.</p>
|
||||||
|
<form th:action="@{/manage/tools/import-export/export}" method="post">
|
||||||
|
<button type="submit" class="btn btn-primary"><i class="fas fa-file-export mr-2"></i> Export to XML</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Import Data Form -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card shadow mb-4 border-left-success">
|
||||||
|
<div class="card-header py-3 bg-success text-white">
|
||||||
|
<h6 class="m-0 font-weight-bold">Import Content</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Import Posts and Pages from a previously generated XML file. If a Post or Page with the same slug already exists, it will be skipped.</p>
|
||||||
|
<form th:action="@{/manage/tools/import-export/import}" method="post" enctype="multipart/form-data">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="importFile">Choose XML File</label>
|
||||||
|
<input type="file" class="form-control-file" id="importFile" name="file" accept=".xml" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-success"><i class="fas fa-file-import mr-2"></i> Import from XML</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{fragments/manage-layout}">
|
||||||
|
<head>
|
||||||
|
<title>Export / Erase Personal Data</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div layout:fragment="content">
|
||||||
|
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||||
|
<h1 class="h3 mb-0 text-gray-800">Personal Data (GDPR)</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Success/Error Messages -->
|
||||||
|
<div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<span th:text="${successMessage}"></span>
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<span th:text="${errorMessage}"></span>
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Export Data Form -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">Export Personal Data</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Enter the user's email address to export their personal data into a secure `.zip` file containing JSON data of their profile, posts, and pages.</p>
|
||||||
|
<form th:action="@{/manage/tools/personal-data/export-form}" method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="exportEmail">User Email Address</label>
|
||||||
|
<input type="email" class="form-control" id="exportEmail" name="email" required placeholder="user@example.com">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary"><i class="fas fa-download mr-2"></i> Download Export</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Erase Data Form -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card shadow mb-4 border-left-danger">
|
||||||
|
<div class="card-header py-3 bg-danger text-white">
|
||||||
|
<h6 class="m-0 font-weight-bold">Erase Personal Data</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-danger font-weight-bold">Warning: This action is permanent and cannot be undone.</p>
|
||||||
|
<p>Entering an email address below will:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Anonymize all posts and pages authored by this user (assign to `anonymous`).</li>
|
||||||
|
<li>Permanently delete their user account and profile data.</li>
|
||||||
|
</ul>
|
||||||
|
<form th:action="@{/manage/tools/personal-data/erase}" method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="eraseEmail">User Email Address</label>
|
||||||
|
<input type="email" class="form-control" id="eraseEmail" name="email" required placeholder="user@example.com">
|
||||||
|
</div>
|
||||||
|
<div class="form-group form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="confirmErase" name="confirm" required>
|
||||||
|
<label class="form-check-label text-danger" for="confirmErase">I confirm I want to permanently erase this user's data.</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-danger"><i class="fas fa-trash-alt mr-2"></i> Erase Personal Data</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{fragments/manage-layout}">
|
||||||
|
<head>
|
||||||
|
<title>Site Health</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div layout:fragment="content">
|
||||||
|
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||||
|
<h1 class="h3 mb-0 text-gray-800">Site Health</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- System Information -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary"><i class="fas fa-server mr-2"></i>System Information</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
Java Version
|
||||||
|
<span class="font-weight-bold" th:text="${javaVersion}">17.0.1</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
Java Vendor
|
||||||
|
<span class="font-weight-bold" th:text="${javaVendor}">Oracle</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
Operating System
|
||||||
|
<span class="font-weight-bold" th:text="${osName} + ' (' + ${osVersion} + ')'">Linux</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
Architecture
|
||||||
|
<span class="font-weight-bold" th:text="${osArch}">amd64</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
Timezone
|
||||||
|
<span class="font-weight-bold" th:text="${timezone}">UTC</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Database Health -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card shadow mb-4" th:classappend="${isDbHealthy} ? 'border-left-success' : 'border-left-danger'">
|
||||||
|
<div class="card-header py-3" th:classappend="${isDbHealthy} ? 'bg-success text-white' : 'bg-danger text-white'">
|
||||||
|
<h6 class="m-0 font-weight-bold"><i class="fas fa-database mr-2"></i>Database Health</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
Status
|
||||||
|
<span class="badge badge-pill" th:classappend="${isDbHealthy} ? 'badge-success' : 'badge-danger'" th:text="${dbStatus}">Connected</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
Product Version
|
||||||
|
<span class="font-weight-bold text-right" th:text="${dbProduct}">MySQL 8.0</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Memory Usage -->
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary"><i class="fas fa-memory mr-2"></i>Memory Usage (JVM)</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="small font-weight-bold">Used Memory: <span th:text="${usedMemory} + ' MB / ' + ${totalMemory} + ' MB'"></span> <span class="float-right" th:text="${#numbers.formatDecimal((usedMemory * 100.0) / totalMemory, 1, 1)} + '%'">50%</span></h4>
|
||||||
|
<div class="progress mb-4">
|
||||||
|
<div class="progress-bar" th:classappend="${(usedMemory * 100.0) / totalMemory > 80} ? 'bg-danger' : 'bg-info'" role="progressbar" th:style="'width: ' + ${(usedMemory * 100.0) / totalMemory} + '%'"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
Free Memory
|
||||||
|
<span class="font-weight-bold" th:text="${freeMemory} + ' MB'">100 MB</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
Max Memory Available
|
||||||
|
<span class="font-weight-bold" th:text="${maxMemory} + ' MB'">500 MB</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user