hoàn thành trang settings

This commit is contained in:
2026-07-02 18:59:48 +07:00
parent 302785b60b
commit a7dbf52eea
13 changed files with 3927 additions and 5 deletions
@@ -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";
}
}
@@ -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";
}
}
@@ -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);
/**
* Find all pages authored by a specific user (login).
*/
List<Page> findByCreatedBy(String createdBy);
/**
* 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"})
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.
*/
@@ -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 "";
}
}
@@ -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;
}
}