bổ sung thêm các chức năng còn thiếu của appearance

This commit is contained in:
2026-07-02 18:48:00 +07:00
parent e91e607d14
commit 302785b60b
28 changed files with 1265 additions and 29 deletions
@@ -15,10 +15,12 @@ import java.util.Map;
public class GlobalControllerAdvice {
private final SettingService settingService;
private final com.sisvietnamvn.web.service.MenuService menuService;
private final PathMatchingResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
public GlobalControllerAdvice(SettingService settingService) {
public GlobalControllerAdvice(SettingService settingService, com.sisvietnamvn.web.service.MenuService menuService) {
this.settingService = settingService;
this.menuService = menuService;
}
/**
@@ -41,4 +43,48 @@ public class GlobalControllerAdvice {
return activeTheme;
}
@ModelAttribute("primaryMenu")
public com.sisvietnamvn.web.domain.Menu getPrimaryMenu() {
return menuService.findByLocation("PRIMARY").orElse(null);
}
@ModelAttribute("footerMenu")
public com.sisvietnamvn.web.domain.Menu getFooterMenu() {
return menuService.findByLocation("FOOTER").orElse(null);
}
@ModelAttribute("sidebarWidgets")
public java.util.List<com.sisvietnamvn.web.service.dto.WidgetDto> getSidebarWidgets() {
return getWidgets("sidebar");
}
@ModelAttribute("footerWidgets")
public java.util.List<com.sisvietnamvn.web.service.dto.WidgetDto> getFooterWidgets() {
return getWidgets("footer");
}
@ModelAttribute("themeModPrimaryColor")
public String getThemeModPrimaryColor() {
return settingService.getValue("theme_mod_primaryColor", "#007bff");
}
@ModelAttribute("themeModFontFamily")
public String getThemeModFontFamily() {
return settingService.getValue("theme_mod_fontFamily", "sans-serif");
}
@ModelAttribute("themeModCustomCss")
public String getThemeModCustomCss() {
return settingService.getValue("theme_mod_customCss", "");
}
private java.util.List<com.sisvietnamvn.web.service.dto.WidgetDto> getWidgets(String area) {
String json = settingService.getValue("theme_widgets_" + area, "[]");
try {
return new com.fasterxml.jackson.databind.ObjectMapper().readValue(json, new com.fasterxml.jackson.core.type.TypeReference<java.util.List<com.sisvietnamvn.web.service.dto.WidgetDto>>() {});
} catch (com.fasterxml.jackson.core.JsonProcessingException e) {
return new java.util.ArrayList<>();
}
}
}
@@ -0,0 +1,44 @@
package com.sisvietnamvn.web.controller.manage;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
import com.sisvietnamvn.web.service.SettingService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
/**
* Controller for the Theme Customizer in the Admin panel.
*/
@Controller
@RequestMapping("/manage/customize")
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\")")
public class ManageCustomizeController {
private final SettingService settingService;
public ManageCustomizeController(SettingService settingService) {
this.settingService = settingService;
}
@GetMapping
public String customizerView(Model model) {
// Load current customizer settings
model.addAttribute("primaryColor", settingService.getValue("theme_mod_primaryColor", "#007bff"));
model.addAttribute("fontFamily", settingService.getValue("theme_mod_fontFamily", "sans-serif"));
model.addAttribute("customCss", settingService.getValue("theme_mod_customCss", ""));
return "manage/customize/index";
}
@PostMapping("/save")
public String saveCustomizer(@RequestParam String primaryColor, @RequestParam String fontFamily, @RequestParam String customCss, RedirectAttributes redirectAttributes) {
settingService.setValue("theme_mod_primaryColor", primaryColor);
settingService.setValue("theme_mod_fontFamily", fontFamily);
settingService.setValue("theme_mod_customCss", customCss);
redirectAttributes.addFlashAttribute("successMessage", "Customizations saved securely.");
return "redirect:/manage/customize";
}
}
@@ -0,0 +1,72 @@
package com.sisvietnamvn.web.controller.manage;
import com.sisvietnamvn.web.domain.Menu;
import com.sisvietnamvn.web.domain.MenuItem;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
import com.sisvietnamvn.web.service.MenuService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
/**
* Controller for managing Navigation Menus in the Admin panel.
*/
@Controller
@RequestMapping("/manage/menus")
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\")")
public class ManageMenuController {
private final MenuService menuService;
public ManageMenuController(MenuService menuService) {
this.menuService = menuService;
}
@GetMapping
public String listMenus(Model model) {
model.addAttribute("menus", menuService.findAll());
return "manage/menus/list";
}
@PostMapping("/create")
public String createMenu(@RequestParam String name, @RequestParam(required = false) String location, RedirectAttributes redirectAttributes) {
Menu menu = new Menu();
menu.setName(name);
menu.setLocation(location);
menuService.save(menu);
redirectAttributes.addFlashAttribute("successMessage", "Menu created successfully.");
return "redirect:/manage/menus";
}
@GetMapping("/{id}/edit")
public String editMenu(@PathVariable Long id, Model model) {
Menu menu = menuService.findById(id).orElseThrow(() -> new IllegalArgumentException("Invalid menu Id"));
model.addAttribute("menu", menu);
return "manage/menus/edit";
}
@PostMapping("/{id}/items/add")
public String addMenuItem(@PathVariable Long id, @RequestParam String label, @RequestParam String url, RedirectAttributes redirectAttributes) {
Menu menu = menuService.findById(id).orElseThrow(() -> new IllegalArgumentException("Invalid menu Id"));
MenuItem item = new MenuItem();
item.setLabel(label);
item.setUrl(url);
item.setMenu(menu);
item.setDisplayOrder(menu.getItems().size());
menu.getItems().add(item);
menuService.save(menu);
redirectAttributes.addFlashAttribute("successMessage", "Menu item added.");
return "redirect:/manage/menus/" + id + "/edit";
}
@PostMapping("/{id}/delete")
public String deleteMenu(@PathVariable Long id, RedirectAttributes redirectAttributes) {
menuService.deleteById(id);
redirectAttributes.addFlashAttribute("successMessage", "Menu deleted successfully.");
return "redirect:/manage/menus";
}
}
@@ -95,4 +95,72 @@ public class ManageThemeController {
redirectAttributes.addFlashAttribute("successMessage", "New theme activated successfully.");
return "redirect:/manage/themes";
}
@PostMapping("/delete")
public String deleteTheme(@RequestParam("themeKey") String themeKey, RedirectAttributes redirectAttributes) {
String activeTheme = settingService.getValue("active_theme", "default");
if (themeKey.equals(activeTheme)) {
redirectAttributes.addFlashAttribute("errorMessage", "Cannot delete the active theme.");
return "redirect:/manage/themes";
}
try {
java.nio.file.Path themePath = java.nio.file.Paths.get("src/main/resources/templates/themes/", themeKey);
if (java.nio.file.Files.exists(themePath)) {
// Delete directory recursively
org.springframework.util.FileSystemUtils.deleteRecursively(themePath);
redirectAttributes.addFlashAttribute("successMessage", "Theme deleted successfully.");
} else {
redirectAttributes.addFlashAttribute("errorMessage", "Theme folder not found.");
}
} catch (java.io.IOException e) {
redirectAttributes.addFlashAttribute("errorMessage", "Error deleting theme: " + e.getMessage());
}
return "redirect:/manage/themes";
}
@PostMapping("/upload")
public String uploadTheme(@RequestParam("file") org.springframework.web.multipart.MultipartFile file, RedirectAttributes redirectAttributes) {
if (file.isEmpty() || !file.getOriginalFilename().endsWith(".zip")) {
redirectAttributes.addFlashAttribute("errorMessage", "Please select a valid .zip file.");
return "redirect:/manage/themes";
}
try {
java.nio.file.Path targetDir = java.nio.file.Paths.get("src/main/resources/templates/themes/");
java.util.zip.ZipInputStream zis = new java.util.zip.ZipInputStream(file.getInputStream());
java.util.zip.ZipEntry zipEntry = zis.getNextEntry();
while (zipEntry != null) {
java.nio.file.Path newPath = zipSlipProtect(zipEntry, targetDir);
if (zipEntry.isDirectory()) {
java.nio.file.Files.createDirectories(newPath);
} else {
if (newPath.getParent() != null) {
if (java.nio.file.Files.notExists(newPath.getParent())) {
java.nio.file.Files.createDirectories(newPath.getParent());
}
}
java.nio.file.Files.copy(zis, newPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
zipEntry = zis.getNextEntry();
}
zis.closeEntry();
zis.close();
redirectAttributes.addFlashAttribute("successMessage", "Theme uploaded and extracted successfully.");
} catch (Exception e) {
redirectAttributes.addFlashAttribute("errorMessage", "Failed to upload theme: " + e.getMessage());
}
return "redirect:/manage/themes";
}
private java.nio.file.Path zipSlipProtect(java.util.zip.ZipEntry zipEntry, java.nio.file.Path targetDir) throws java.io.IOException {
java.nio.file.Path targetDirResolved = targetDir.resolve(zipEntry.getName());
java.nio.file.Path normalizePath = targetDirResolved.normalize();
if (!normalizePath.startsWith(targetDir)) {
throw new java.io.IOException("Bad zip entry: " + zipEntry.getName());
}
return normalizePath;
}
}
@@ -0,0 +1,66 @@
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.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.io.IOException;
import java.nio.file.*;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Controller for the Theme File Editor.
*/
@Controller
@RequestMapping("/manage/theme-editor")
@PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")")
public class ManageThemeEditorController {
private final Path themesBaseDir = Paths.get("src/main/resources/templates/themes").toAbsolutePath().normalize();
@GetMapping
public String editorView(@RequestParam(required = false) String file, Model model) {
try (Stream<Path> walk = Files.walk(themesBaseDir)) {
List<String> fileTree = walk.filter(Files::isRegularFile)
.map(path -> themesBaseDir.relativize(path).toString())
.collect(Collectors.toList());
model.addAttribute("files", fileTree);
if (file != null && !file.isEmpty()) {
Path targetPath = themesBaseDir.resolve(file).normalize();
if (targetPath.startsWith(themesBaseDir) && Files.exists(targetPath)) {
String content = Files.readString(targetPath);
model.addAttribute("selectedFile", file);
model.addAttribute("fileContent", content);
} else {
model.addAttribute("errorMessage", "Invalid file path.");
}
}
} catch (IOException e) {
model.addAttribute("errorMessage", "Error loading theme files: " + e.getMessage());
}
return "manage/themes/editor";
}
@PostMapping("/save")
public String saveFile(@RequestParam("selectedFile") String file, @RequestParam("fileContent") String content, RedirectAttributes redirectAttributes) {
try {
Path targetPath = themesBaseDir.resolve(file).normalize();
if (targetPath.startsWith(themesBaseDir) && Files.exists(targetPath)) {
Files.writeString(targetPath, content, StandardOpenOption.TRUNCATE_EXISTING);
redirectAttributes.addFlashAttribute("successMessage", "File saved successfully.");
return "redirect:/manage/theme-editor?file=" + file;
} else {
redirectAttributes.addFlashAttribute("errorMessage", "Security violation: Invalid file path.");
}
} catch (IOException e) {
redirectAttributes.addFlashAttribute("errorMessage", "Error saving file: " + e.getMessage());
}
return "redirect:/manage/theme-editor";
}
}
@@ -0,0 +1,85 @@
package com.sisvietnamvn.web.controller.manage;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
import com.sisvietnamvn.web.service.SettingService;
import com.sisvietnamvn.web.service.dto.WidgetDto;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Controller for managing Theme Widgets in the Admin panel.
*/
@Controller
@RequestMapping("/manage/widgets")
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\")")
public class ManageWidgetController {
private final SettingService settingService;
private final ObjectMapper objectMapper;
public ManageWidgetController(SettingService settingService, ObjectMapper objectMapper) {
this.settingService = settingService;
this.objectMapper = objectMapper;
}
private List<WidgetDto> getWidgets(String area) {
String json = settingService.getValue("theme_widgets_" + area, "[]");
try {
return objectMapper.readValue(json, new TypeReference<List<WidgetDto>>() {});
} catch (JsonProcessingException e) {
return new ArrayList<>();
}
}
private void saveWidgets(String area, List<WidgetDto> widgets) {
try {
String json = objectMapper.writeValueAsString(widgets);
settingService.setValue("theme_widgets_" + area, json);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
@GetMapping
public String listWidgets(Model model) {
model.addAttribute("sidebarWidgets", getWidgets("sidebar"));
model.addAttribute("footerWidgets", getWidgets("footer"));
return "manage/widgets/list";
}
@PostMapping("/add")
public String addWidget(@RequestParam String area, @RequestParam String type, @RequestParam String title, @RequestParam String content, RedirectAttributes redirectAttributes) {
List<WidgetDto> widgets = getWidgets(area);
WidgetDto widget = new WidgetDto();
widget.setId(UUID.randomUUID().toString());
widget.setType(type);
widget.setTitle(title);
widget.setContent(content);
widgets.add(widget);
saveWidgets(area, widgets);
redirectAttributes.addFlashAttribute("successMessage", "Widget added to " + area);
return "redirect:/manage/widgets";
}
@PostMapping("/delete")
public String deleteWidget(@RequestParam String area, @RequestParam String id, RedirectAttributes redirectAttributes) {
List<WidgetDto> widgets = getWidgets(area);
widgets.removeIf(w -> w.getId().equals(id));
saveWidgets(area, widgets);
redirectAttributes.addFlashAttribute("successMessage", "Widget removed.");
return "redirect:/manage/widgets";
}
}
@@ -0,0 +1,85 @@
package com.sisvietnamvn.web.domain;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.io.Serial;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* A Menu entity for managing navigation menus.
* Maps to the "sis_menu" database table.
*/
@Entity
@Table(name = "sis_menu")
public class Menu extends AbstractAuditingEntity<Long> {
@Serial
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
@SequenceGenerator(name = "sequenceGenerator")
private Long id;
@NotNull
@Size(max = 255)
@Column(name = "name", length = 255, nullable = false)
private String name;
@Size(max = 100)
@Column(name = "location", length = 100)
private String location;
@OneToMany(mappedBy = "menu", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy("displayOrder ASC")
private List<MenuItem> items = new ArrayList<>();
@Override
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public List<MenuItem> getItems() {
return items;
}
public void setItems(List<MenuItem> items) {
this.items = items;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Menu menu = (Menu) o;
return Objects.equals(id, menu.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
@@ -0,0 +1,106 @@
package com.sisvietnamvn.web.domain;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.io.Serial;
import java.util.Objects;
/**
* A MenuItem entity representing a single link in a Menu.
* Maps to the "sis_menu_item" database table.
*/
@Entity
@Table(name = "sis_menu_item")
public class MenuItem extends AbstractAuditingEntity<Long> {
@Serial
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
@SequenceGenerator(name = "sequenceGenerator")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "menu_id", nullable = false)
private Menu menu;
@NotNull
@Size(max = 255)
@Column(name = "label", length = 255, nullable = false)
private String label;
@Size(max = 500)
@Column(name = "url", length = 500)
private String url;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private MenuItem parent;
@Column(name = "display_order")
private Integer displayOrder = 0;
@Override
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Menu getMenu() {
return menu;
}
public void setMenu(Menu menu) {
this.menu = menu;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public MenuItem getParent() {
return parent;
}
public void setParent(MenuItem parent) {
this.parent = parent;
}
public Integer getDisplayOrder() {
return displayOrder;
}
public void setDisplayOrder(Integer displayOrder) {
this.displayOrder = displayOrder;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MenuItem menuItem = (MenuItem) o;
return Objects.equals(id, menuItem.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
@@ -0,0 +1,12 @@
package com.sisvietnamvn.web.repository;
import com.sisvietnamvn.web.domain.MenuItem;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* Spring Data JPA repository for the MenuItem entity.
*/
@Repository
public interface MenuItemRepository extends JpaRepository<MenuItem, Long> {
}
@@ -0,0 +1,15 @@
package com.sisvietnamvn.web.repository;
import com.sisvietnamvn.web.domain.Menu;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/**
* Spring Data JPA repository for the Menu entity.
*/
@Repository
public interface MenuRepository extends JpaRepository<Menu, Long> {
Optional<Menu> findByLocation(String location);
}
@@ -0,0 +1,43 @@
package com.sisvietnamvn.web.service;
import com.sisvietnamvn.web.domain.Menu;
import com.sisvietnamvn.web.repository.MenuRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@Transactional
public class MenuService {
private final MenuRepository menuRepository;
public MenuService(MenuRepository menuRepository) {
this.menuRepository = menuRepository;
}
@Transactional(readOnly = true)
public List<Menu> findAll() {
return menuRepository.findAll();
}
@Transactional(readOnly = true)
public Optional<Menu> findById(Long id) {
return menuRepository.findById(id);
}
@Transactional(readOnly = true)
public Optional<Menu> findByLocation(String location) {
return menuRepository.findByLocation(location);
}
public Menu save(Menu menu) {
return menuRepository.save(menu);
}
public void deleteById(Long id) {
menuRepository.deleteById(id);
}
}
@@ -0,0 +1,40 @@
package com.sisvietnamvn.web.service.dto;
public class WidgetDto {
private String id;
private String type; // "HTML", "TEXT"
private String title;
private String content;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}