Compare commits
2 Commits
388a34837f
...
e8f36222e7
| Author | SHA1 | Date | |
|---|---|---|---|
| e8f36222e7 | |||
| e24b41f53c |
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.5</version>
|
||||
<version>3.5.13</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
<groupId>com.yaoyuan</groupId>
|
||||
@@ -73,7 +73,7 @@
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.6.0</version>
|
||||
<version>2.8.9</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
|
||||
@@ -12,7 +12,6 @@ import org.springframework.security.authentication.dao.DaoAuthenticationProvider
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
@@ -52,17 +51,6 @@ public class WebSecurityConfig {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exclude truly static assets from the security filter chain entirely.
|
||||
* H2 console and Druid monitoring are NOT excluded here — they are secured via filterChain.
|
||||
*/
|
||||
@Bean
|
||||
public WebSecurityCustomizer webSecurityCustomizer() {
|
||||
return web -> web.ignoring().requestMatchers(
|
||||
"/css/**", "/js/**", "/images/**", "/static/**", "/favicon.ico"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main security filter chain.
|
||||
*
|
||||
@@ -98,7 +86,8 @@ public class WebSecurityConfig {
|
||||
http.authorizeHttpRequests(auth -> {
|
||||
// Publicly accessible
|
||||
auth.requestMatchers(
|
||||
"/login/**", "/register", "/registerDo", "/initUserData",
|
||||
"/css/**", "/js/**", "/images/**", "/static/**", "/favicon.ico",
|
||||
"/login/**", "/register", "/registerDo", "/initUserData",
|
||||
"/", "/main", "/index", "/getdiscussionsbyid",
|
||||
// SpringDoc OpenAPI UI and spec endpoint (developer tools, no sensitive data)
|
||||
"/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**"
|
||||
|
||||
@@ -1,42 +1,365 @@
|
||||
package com.yaoyuan.jiscuss.controller;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.Discussion;
|
||||
import com.yaoyuan.jiscuss.entity.Post;
|
||||
import com.yaoyuan.jiscuss.entity.Role;
|
||||
import com.yaoyuan.jiscuss.entity.UpgradeLog;
|
||||
import com.yaoyuan.jiscuss.entity.User;
|
||||
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||
import com.yaoyuan.jiscuss.entity.UserRole;
|
||||
import com.yaoyuan.jiscuss.entity.AuditLog;
|
||||
import com.yaoyuan.jiscuss.repository.DiscussionsRepository;
|
||||
import com.yaoyuan.jiscuss.repository.DiscussionsTagsRepository;
|
||||
import com.yaoyuan.jiscuss.repository.PostsRepository;
|
||||
import com.yaoyuan.jiscuss.repository.RoleRepository;
|
||||
import com.yaoyuan.jiscuss.repository.UpgradeLogRepository;
|
||||
import com.yaoyuan.jiscuss.repository.UserRoleRepository;
|
||||
import com.yaoyuan.jiscuss.repository.UsersRepository;
|
||||
import com.yaoyuan.jiscuss.repository.AuditLogRepository;
|
||||
import com.yaoyuan.jiscuss.service.AuditLogService;
|
||||
import org.springframework.boot.SpringBootVersion;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.MemoryMXBean;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author yaoyuan2.chu
|
||||
* 后台系统控制器
|
||||
*/
|
||||
@Controller
|
||||
public class AdminSystemController {
|
||||
public class AdminSystemController extends BaseController {
|
||||
|
||||
/**
|
||||
* 后台登录
|
||||
*/
|
||||
private final UsersRepository usersRepository;
|
||||
private final DiscussionsRepository discussionsRepository;
|
||||
private final PostsRepository postsRepository;
|
||||
private final DiscussionsTagsRepository discussionsTagsRepository;
|
||||
private final RoleRepository roleRepository;
|
||||
private final UserRoleRepository userRoleRepository;
|
||||
private final UpgradeLogRepository upgradeLogRepository;
|
||||
private final AuditLogRepository auditLogRepository;
|
||||
private final AuditLogService auditLogService;
|
||||
|
||||
/**
|
||||
* 后台退出
|
||||
*/
|
||||
@RequestMapping("/admin/logout")
|
||||
public String logout(@RequestParam(defaultValue = "discussion") String type, HttpServletRequest request, ModelMap map) {
|
||||
return "admin/logout";
|
||||
public AdminSystemController(
|
||||
UsersRepository usersRepository,
|
||||
DiscussionsRepository discussionsRepository,
|
||||
PostsRepository postsRepository,
|
||||
DiscussionsTagsRepository discussionsTagsRepository,
|
||||
RoleRepository roleRepository,
|
||||
UserRoleRepository userRoleRepository,
|
||||
UpgradeLogRepository upgradeLogRepository,
|
||||
AuditLogRepository auditLogRepository,
|
||||
AuditLogService auditLogService) {
|
||||
this.usersRepository = usersRepository;
|
||||
this.discussionsRepository = discussionsRepository;
|
||||
this.postsRepository = postsRepository;
|
||||
this.discussionsTagsRepository = discussionsTagsRepository;
|
||||
this.roleRepository = roleRepository;
|
||||
this.userRoleRepository = userRoleRepository;
|
||||
this.upgradeLogRepository = upgradeLogRepository;
|
||||
this.auditLogRepository = auditLogRepository;
|
||||
this.auditLogService = auditLogService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台设置管理
|
||||
*/
|
||||
@GetMapping("/admin/home")
|
||||
public String home(HttpServletRequest request, ModelMap map) {
|
||||
fillCommon(request, map, "home");
|
||||
|
||||
map.put("userCount", usersRepository.count());
|
||||
map.put("discussionCount", discussionsRepository.count());
|
||||
map.put("postCount", postsRepository.count());
|
||||
map.put("roleCount", roleRepository.count());
|
||||
|
||||
map.put("sys", buildSystemMetrics());
|
||||
map.put("deps", buildDependencyVersions());
|
||||
|
||||
/**
|
||||
* 后台页面
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping("/admin/home")
|
||||
public String home(@RequestParam(defaultValue = "discussion") String type, HttpServletRequest request, ModelMap map) {
|
||||
return "admin/home";
|
||||
}
|
||||
|
||||
@GetMapping("/admin/users")
|
||||
public String users(HttpServletRequest request, ModelMap map) {
|
||||
fillCommon(request, map, "users");
|
||||
|
||||
List<User> users = usersRepository.findAll(Sort.by(Sort.Direction.ASC, "id"));
|
||||
List<Role> roles = roleRepository.findAllByOrderByIdAsc();
|
||||
Map<Integer, Set<Integer>> userRoleIds = new HashMap<>();
|
||||
for (UserRole ur : userRoleRepository.findAll()) {
|
||||
userRoleIds.computeIfAbsent(ur.getUserId(), key -> new HashSet<>()).add(ur.getRoleId());
|
||||
}
|
||||
|
||||
map.put("users", users);
|
||||
map.put("roles", roles);
|
||||
map.put("userRoleIds", userRoleIds);
|
||||
return "admin/users";
|
||||
}
|
||||
|
||||
@PostMapping("/admin/users/{id}/roles")
|
||||
@Transactional
|
||||
public String updateUserRoles(HttpServletRequest request,
|
||||
@PathVariable("id") Integer id,
|
||||
@RequestParam(value = "roleIds", required = false) List<Integer> roleIds) {
|
||||
UserInfo currentAdmin = getUserInfo(request);
|
||||
userRoleRepository.deleteByUserId(id);
|
||||
if (roleIds != null) {
|
||||
Date now = new Date();
|
||||
Set<Integer> uniqueRoleIds = new HashSet<>(roleIds);
|
||||
Set<Integer> validEnabledRoleIds = roleRepository.findAllById(uniqueRoleIds).stream()
|
||||
.filter(role -> role.getEnabled() != null && role.getEnabled() == 1)
|
||||
.map(Role::getId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
for (Integer roleId : validEnabledRoleIds) {
|
||||
UserRole ur = new UserRole();
|
||||
ur.setUserId(id);
|
||||
ur.setRoleId(roleId);
|
||||
ur.setCreateTime(now);
|
||||
userRoleRepository.save(ur);
|
||||
}
|
||||
}
|
||||
auditLogService.log(currentAdmin, "update_user_roles", "user", id,
|
||||
"Updated user role assignments. Role count: " + (roleIds == null ? 0 : roleIds.size()));
|
||||
return "redirect:/admin/users";
|
||||
}
|
||||
|
||||
@PostMapping("/admin/roles")
|
||||
public String createRole(HttpServletRequest request,
|
||||
@RequestParam("code") String code,
|
||||
@RequestParam("name") String name,
|
||||
@RequestParam(value = "description", required = false) String description) {
|
||||
UserInfo currentAdmin = getUserInfo(request);
|
||||
String normalizedCode = code == null ? "" : code.trim().toUpperCase();
|
||||
if (normalizedCode.isEmpty()) {
|
||||
return "redirect:/admin/users";
|
||||
}
|
||||
|
||||
Role exists = roleRepository.findByCodeIgnoreCase(normalizedCode);
|
||||
if (exists == null) {
|
||||
Date now = new Date();
|
||||
Role role = new Role();
|
||||
role.setCode(normalizedCode);
|
||||
role.setName(name == null ? "" : name.trim());
|
||||
role.setDescription(description);
|
||||
role.setEnabled(1);
|
||||
role.setCreateTime(now);
|
||||
role.setUpdateTime(now);
|
||||
roleRepository.save(role);
|
||||
auditLogService.log(currentAdmin, "create_role", "role", role.getId(),
|
||||
"Created role: " + normalizedCode);
|
||||
} else {
|
||||
auditLogService.log(currentAdmin, "create_role_duplicate", "role", exists.getId(),
|
||||
"Attempted to create duplicate role: " + normalizedCode, "ignored");
|
||||
}
|
||||
return "redirect:/admin/users";
|
||||
}
|
||||
|
||||
@PostMapping("/admin/roles/{id}/toggle")
|
||||
public String toggleRole(HttpServletRequest request,
|
||||
@PathVariable("id") Integer id) {
|
||||
UserInfo currentAdmin = getUserInfo(request);
|
||||
Role role = roleRepository.findById(id).orElse(null);
|
||||
if (role != null) {
|
||||
int nextStatus = role.getEnabled() != null && role.getEnabled() == 1 ? 0 : 1;
|
||||
// Keep built-in ADMIN role enabled to avoid locking out management access.
|
||||
if ("ADMIN".equalsIgnoreCase(role.getCode()) && nextStatus == 0) {
|
||||
auditLogService.log(currentAdmin, "toggle_role_blocked", "role", id,
|
||||
"Attempted to disable built-in ADMIN role", "blocked");
|
||||
return "redirect:/admin/users";
|
||||
}
|
||||
role.setEnabled(nextStatus);
|
||||
role.setUpdateTime(new Date());
|
||||
roleRepository.save(role);
|
||||
auditLogService.log(currentAdmin, "toggle_role", "role", id,
|
||||
"Toggled role status to " + (nextStatus == 1 ? "enabled" : "disabled"));
|
||||
}
|
||||
return "redirect:/admin/users";
|
||||
}
|
||||
|
||||
@GetMapping("/admin/discussions")
|
||||
public String discussions(HttpServletRequest request, ModelMap map) {
|
||||
fillCommon(request, map, "discussions");
|
||||
List<Discussion> discussions = discussionsRepository.findAll(Sort.by(Sort.Direction.DESC, "createTime"));
|
||||
map.put("discussions", discussions);
|
||||
return "admin/discussions";
|
||||
}
|
||||
|
||||
@PostMapping("/admin/discussions/{id}/delete")
|
||||
@Transactional
|
||||
public String deleteDiscussion(HttpServletRequest request,
|
||||
@PathVariable("id") Integer id) {
|
||||
UserInfo currentAdmin = getUserInfo(request);
|
||||
Discussion discussion = discussionsRepository.findById(id).orElse(null);
|
||||
String title = discussion == null ? "unknown" : discussion.getTitle();
|
||||
discussionsTagsRepository.deleteByDiscussionId(id);
|
||||
postsRepository.deleteByDiscussionId(id);
|
||||
discussionsRepository.deleteById(id);
|
||||
auditLogService.log(currentAdmin, "delete_discussion", "discussion", id,
|
||||
"Deleted discussion: " + title);
|
||||
return "redirect:/admin/discussions";
|
||||
}
|
||||
|
||||
@GetMapping("/admin/posts")
|
||||
public String posts(HttpServletRequest request, ModelMap map) {
|
||||
fillCommon(request, map, "posts");
|
||||
List<Post> posts = postsRepository.findAll(Sort.by(Sort.Direction.DESC, "createTime"));
|
||||
map.put("posts", posts);
|
||||
|
||||
Set<Integer> userIds = posts.stream().map(Post::getCreateId).filter(v -> v != null).collect(Collectors.toSet());
|
||||
Set<Integer> discussionIds = posts.stream().map(Post::getDiscussionId).filter(v -> v != null).collect(Collectors.toSet());
|
||||
|
||||
Map<Integer, String> userNames = usersRepository.findAllById(userIds).stream()
|
||||
.collect(Collectors.toMap(User::getId, User::getUsername));
|
||||
Map<Integer, String> discussionNames = discussionsRepository.findAllById(discussionIds).stream()
|
||||
.collect(Collectors.toMap(Discussion::getId, Discussion::getTitle));
|
||||
|
||||
map.put("userNames", userNames);
|
||||
map.put("discussionNames", discussionNames);
|
||||
return "admin/posts";
|
||||
}
|
||||
|
||||
@PostMapping("/admin/posts/{id}/delete")
|
||||
public String deletePost(HttpServletRequest request,
|
||||
@PathVariable("id") Integer id) {
|
||||
UserInfo currentAdmin = getUserInfo(request);
|
||||
Post post = postsRepository.findById(id).orElse(null);
|
||||
String content = post == null ? "unknown" : (post.getContent() == null ? "" : post.getContent());
|
||||
if (content.length() > 50) {
|
||||
content = content.substring(0, 50) + "...";
|
||||
}
|
||||
postsRepository.deleteById(id);
|
||||
auditLogService.log(currentAdmin, "delete_post", "post", id,
|
||||
"Deleted post: " + content);
|
||||
return "redirect:/admin/posts";
|
||||
}
|
||||
|
||||
@GetMapping("/admin/upgrade-logs")
|
||||
public String upgradeLogs(HttpServletRequest request, ModelMap map) {
|
||||
fillCommon(request, map, "upgradeLogs");
|
||||
map.put("logs", upgradeLogRepository.findAllByOrderByCreateTimeDesc());
|
||||
return "admin/upgrade-logs";
|
||||
}
|
||||
|
||||
@PostMapping("/admin/upgrade-logs")
|
||||
public String createUpgradeLog(HttpServletRequest request,
|
||||
@RequestParam("version") String version,
|
||||
@RequestParam("title") String title,
|
||||
@RequestParam(value = "type", defaultValue = "feature") String type,
|
||||
@RequestParam("content") String content) {
|
||||
UserInfo user = getUserInfo(request);
|
||||
UpgradeLog log = new UpgradeLog();
|
||||
log.setVersion(version);
|
||||
log.setTitle(title);
|
||||
log.setType(type);
|
||||
log.setContent(content);
|
||||
log.setCreatedBy(user == null ? null : user.getId());
|
||||
log.setCreateTime(new Date());
|
||||
upgradeLogRepository.save(log);
|
||||
auditLogService.log(user, "create_upgrade_log", "upgrade_log", log.getId(),
|
||||
"Created upgrade log: " + version + " - " + title);
|
||||
return "redirect:/admin/upgrade-logs";
|
||||
}
|
||||
|
||||
@GetMapping("/admin/plugins")
|
||||
public String plugins(HttpServletRequest request, ModelMap map) {
|
||||
fillCommon(request, map, "plugins");
|
||||
return "admin/plugins";
|
||||
}
|
||||
|
||||
@GetMapping("/admin/audit-logs")
|
||||
public String auditLogs(HttpServletRequest request, ModelMap map) {
|
||||
fillCommon(request, map, "auditLogs");
|
||||
List<AuditLog> logs = auditLogRepository.findAllByOrderByCreateTimeDesc();
|
||||
map.put("logs", logs);
|
||||
return "admin/audit-logs";
|
||||
}
|
||||
|
||||
@GetMapping("/admin/themes")
|
||||
public String themes(HttpServletRequest request, ModelMap map) {
|
||||
fillCommon(request, map, "themes");
|
||||
return "admin/themes";
|
||||
}
|
||||
|
||||
private void fillCommon(HttpServletRequest request, ModelMap map, String active) {
|
||||
UserInfo user = getUserInfo(request);
|
||||
map.put("adminName", user == null ? "admin" : user.getUsername());
|
||||
map.put("active", active);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildSystemMetrics() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
Runtime rt = Runtime.getRuntime();
|
||||
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
|
||||
|
||||
long totalMb = bytesToMb(rt.totalMemory());
|
||||
long freeMb = bytesToMb(rt.freeMemory());
|
||||
long usedMb = totalMb - freeMb;
|
||||
long maxMb = bytesToMb(rt.maxMemory());
|
||||
long heapUsedMb = bytesToMb(memoryMXBean.getHeapMemoryUsage().getUsed());
|
||||
|
||||
result.put("totalMb", totalMb);
|
||||
result.put("freeMb", freeMb);
|
||||
result.put("usedMb", usedMb);
|
||||
result.put("maxMb", maxMb);
|
||||
result.put("heapUsedMb", heapUsedMb);
|
||||
result.put("uptimeMs", ManagementFactory.getRuntimeMXBean().getUptime());
|
||||
result.put("cpuLoad", readCpuLoadPercent());
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, String> buildDependencyVersions() {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("springBoot", SpringBootVersion.getVersion());
|
||||
result.put("java", System.getProperty("java.version"));
|
||||
result.put("spring", implVersion("org.springframework.core.SpringVersion"));
|
||||
result.put("hibernate", implVersion("org.hibernate.Version"));
|
||||
result.put("flyway", implVersion("org.flywaydb.core.Flyway"));
|
||||
result.put("druid", implVersion("com.alibaba.druid.pool.DruidDataSource"));
|
||||
result.put("ehcache", implVersion("org.ehcache.CacheManager"));
|
||||
result.put("springdoc", implVersion("org.springdoc.core.configuration.SpringDocConfiguration"));
|
||||
return result;
|
||||
}
|
||||
|
||||
private String implVersion(String className) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName(className);
|
||||
Package pkg = clazz.getPackage();
|
||||
String version = pkg == null ? null : pkg.getImplementationVersion();
|
||||
return version == null ? "unknown" : version;
|
||||
} catch (ClassNotFoundException e) {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
private String readCpuLoadPercent() {
|
||||
try {
|
||||
java.lang.management.OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
|
||||
if (osBean instanceof com.sun.management.OperatingSystemMXBean sunOsBean) {
|
||||
double value = sunOsBean.getCpuLoad();
|
||||
if (value >= 0) {
|
||||
return String.format("%.2f%%", value * 100);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return "N/A";
|
||||
}
|
||||
|
||||
private long bytesToMb(long bytes) {
|
||||
return bytes / 1024 / 1024;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,13 +21,17 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.servlet.view.RedirectView;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author yaoyuan2.chu
|
||||
@@ -142,6 +146,19 @@ public class UserSystemController extends BaseController {
|
||||
tagMap.put(tc.getDiscussionId(), tagCustomListTemp);
|
||||
}
|
||||
}
|
||||
|
||||
Set<Integer> userIds = new HashSet<>();
|
||||
for (Discussion dd : allDiscussions) {
|
||||
if (dd.getStartUserId() != null) {
|
||||
userIds.add(dd.getStartUserId());
|
||||
}
|
||||
if (dd.getLastUserId() != null) {
|
||||
userIds.add(dd.getLastUserId());
|
||||
}
|
||||
}
|
||||
Map<Integer, User> userMap = usersService.findAllById(userIds).stream()
|
||||
.collect(Collectors.toMap(User::getId, u -> u));
|
||||
|
||||
for (Discussion dd : allDiscussions) {
|
||||
DiscussionCustom newdd = new DiscussionCustom();
|
||||
BeanUtils.copyProperties(dd, newdd);
|
||||
@@ -154,13 +171,13 @@ public class UserSystemController extends BaseController {
|
||||
}
|
||||
newdd.setContent(newCon);
|
||||
if (null != newdd.getStartUserId()) {
|
||||
User startUser = usersService.findOne(newdd.getStartUserId());
|
||||
User startUser = userMap.get(newdd.getStartUserId());
|
||||
newdd.setAvatar(startUser != null && startUser.getAvatar() != null ? startUser.getAvatar() : "");
|
||||
newdd.setRealname(startUser != null && startUser.getRealname() != null ? startUser.getRealname() : "");
|
||||
newdd.setUsername(startUser != null && startUser.getUsername() != null ? startUser.getUsername() : "");
|
||||
}
|
||||
if (null != newdd.getLastUserId()) {
|
||||
User lastUser = usersService.findOne(newdd.getLastUserId());
|
||||
User lastUser = userMap.get(newdd.getLastUserId());
|
||||
newdd.setAvatarLast(lastUser != null && lastUser.getAvatar() != null ? lastUser.getAvatar() : "");
|
||||
newdd.setRealnameLast(lastUser != null && lastUser.getRealname() != null ? lastUser.getRealname() : "");
|
||||
newdd.setUsernameLast(lastUser != null && lastUser.getUsername() != null ? lastUser.getUsername() : "");
|
||||
@@ -209,6 +226,11 @@ public class UserSystemController extends BaseController {
|
||||
return "login";
|
||||
}
|
||||
|
||||
@GetMapping("/favicon.ico")
|
||||
public RedirectView favicon() {
|
||||
return new RedirectView("/static/assets/images/logo.png");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 注册提交
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.yaoyuan.jiscuss.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "audit_log")
|
||||
public class AuditLog implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
@Column(name = "admin_id")
|
||||
private Integer adminId;
|
||||
|
||||
@Column(name = "admin_name")
|
||||
private String adminName;
|
||||
|
||||
@Column(name = "action_type")
|
||||
private String actionType;
|
||||
|
||||
@Column(name = "target_type")
|
||||
private String targetType;
|
||||
|
||||
@Column(name = "target_id")
|
||||
private Integer targetId;
|
||||
|
||||
@Column(name = "description")
|
||||
private String description;
|
||||
|
||||
@Column(name = "status")
|
||||
private String status;
|
||||
|
||||
@Column(name = "create_time")
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.yaoyuan.jiscuss.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "rbac_role")
|
||||
public class Role implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
@Column(name = "code")
|
||||
private String code;
|
||||
|
||||
@Column(name = "name")
|
||||
private String name;
|
||||
|
||||
@Column(name = "description")
|
||||
private String description;
|
||||
|
||||
@Column(name = "enabled")
|
||||
private Integer enabled;
|
||||
|
||||
@Column(name = "create_time")
|
||||
private Date createTime;
|
||||
|
||||
@Column(name = "update_time")
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.yaoyuan.jiscuss.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "upgrade_log")
|
||||
public class UpgradeLog implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
@Column(name = "version")
|
||||
private String version;
|
||||
|
||||
@Column(name = "title")
|
||||
private String title;
|
||||
|
||||
@Column(name = "content")
|
||||
private String content;
|
||||
|
||||
@Column(name = "type")
|
||||
private String type;
|
||||
|
||||
@Column(name = "created_by")
|
||||
private Integer createdBy;
|
||||
|
||||
@Column(name = "create_time")
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.yaoyuan.jiscuss.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "rbac_user_role")
|
||||
public class UserRole implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
@Column(name = "user_id")
|
||||
private Integer userId;
|
||||
|
||||
@Column(name = "role_id")
|
||||
private Integer roleId;
|
||||
|
||||
@Column(name = "create_time")
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -69,6 +71,15 @@ public class GlobalExceptionHandler {
|
||||
.body(ApiResponse.error("没有权限访问该资源"));
|
||||
}
|
||||
|
||||
/** 404 mappings/resources should not be logged as system errors. */
|
||||
@ExceptionHandler({NoHandlerFoundException.class, NoResourceFoundException.class})
|
||||
public ResponseEntity<ApiResponse<Void>> handleNotFound(Exception ex) {
|
||||
log.warn("Not found: {}", ex.getMessage());
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.NOT_FOUND)
|
||||
.body(ApiResponse.error("资源不存在"));
|
||||
}
|
||||
|
||||
/** Catch-all for any unhandled exception — never expose stack traces to clients. */
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleGeneral(Exception ex) {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.yaoyuan.jiscuss.repository;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.AuditLog;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface AuditLogRepository extends JpaRepository<AuditLog, Integer> {
|
||||
List<AuditLog> findAllByOrderByCreateTimeDesc();
|
||||
|
||||
Page<AuditLog> findAllByOrderByCreateTimeDesc(Pageable pageable);
|
||||
|
||||
List<AuditLog> findByTargetTypeAndTargetIdOrderByCreateTimeDesc(String targetType, Integer targetId);
|
||||
|
||||
List<AuditLog> findByAdminIdOrderByCreateTimeDesc(Integer adminId);
|
||||
}
|
||||
@@ -7,4 +7,5 @@ import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface DiscussionsTagsRepository extends JpaRepository<DiscussionTag, Integer> {
|
||||
void deleteByDiscussionId(Integer discussionId);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import java.util.Map;
|
||||
@Repository
|
||||
public interface PostsRepository extends JpaRepository<Post, Integer> {
|
||||
|
||||
void deleteByDiscussionId(Integer discussionId);
|
||||
|
||||
/**
|
||||
* @param dId
|
||||
* @return
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.yaoyuan.jiscuss.repository;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.Role;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface RoleRepository extends JpaRepository<Role, Integer> {
|
||||
Role findByCode(String code);
|
||||
|
||||
Role findByCodeIgnoreCase(String code);
|
||||
|
||||
List<Role> findAllByOrderByIdAsc();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.yaoyuan.jiscuss.repository;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.UpgradeLog;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface UpgradeLogRepository extends JpaRepository<UpgradeLog, Integer> {
|
||||
List<UpgradeLog> findAllByOrderByCreateTimeDesc();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.yaoyuan.jiscuss.repository;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.UserRole;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface UserRoleRepository extends JpaRepository<UserRole, Integer> {
|
||||
List<UserRole> findByUserId(Integer userId);
|
||||
|
||||
void deleteByUserId(Integer userId);
|
||||
|
||||
@Query(value = "select r.code from rbac_user_role ur join rbac_role r on ur.role_id = r.id where ur.user_id = :userId and r.enabled = 1",
|
||||
nativeQuery = true)
|
||||
List<String> findRoleCodesByUserId(@Param("userId") Integer userId);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.yaoyuan.jiscuss.service;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.AuditLog;
|
||||
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||
import com.yaoyuan.jiscuss.repository.AuditLogRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Service for recording admin operations in audit log.
|
||||
*/
|
||||
@Service
|
||||
public class AuditLogService {
|
||||
|
||||
private final AuditLogRepository auditLogRepository;
|
||||
|
||||
public AuditLogService(AuditLogRepository auditLogRepository) {
|
||||
this.auditLogRepository = auditLogRepository;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void log(Integer adminId, String adminName, String actionType, String targetType,
|
||||
Integer targetId, String description, String status) {
|
||||
AuditLog log = new AuditLog();
|
||||
log.setAdminId(adminId);
|
||||
log.setAdminName(adminName);
|
||||
log.setActionType(actionType);
|
||||
log.setTargetType(targetType);
|
||||
log.setTargetId(targetId);
|
||||
log.setDescription(description);
|
||||
log.setStatus(status == null ? "success" : status);
|
||||
log.setCreateTime(new Date());
|
||||
auditLogRepository.save(log);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void log(Integer adminId, String adminName, String actionType, String targetType,
|
||||
Integer targetId, String description) {
|
||||
log(adminId, adminName, actionType, targetType, targetId, description, "success");
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void log(UserInfo user, String actionType, String targetType,
|
||||
Integer targetId, String description) {
|
||||
Integer userId = user == null ? null : user.getId();
|
||||
String userName = user == null ? "unknown" : user.getUsername();
|
||||
log(userId, userName, actionType, targetType, targetId, description, "success");
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void log(UserInfo user, String actionType, String targetType,
|
||||
Integer targetId, String description, String status) {
|
||||
Integer userId = user == null ? null : user.getId();
|
||||
String userName = user == null ? "unknown" : user.getUsername();
|
||||
log(userId, userName, actionType, targetType, targetId, description, status);
|
||||
}
|
||||
}
|
||||
@@ -24,5 +24,7 @@ public interface IUsersService {
|
||||
|
||||
User getByUsername(String username);
|
||||
|
||||
List<User> findAllById(Iterable<Integer> ids);
|
||||
|
||||
User update(User user, Integer id);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.yaoyuan.jiscuss.service.impl;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.User;
|
||||
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||
import com.yaoyuan.jiscuss.repository.UserRoleRepository;
|
||||
import com.yaoyuan.jiscuss.service.IUsersService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
@@ -29,6 +30,9 @@ public class UserDetailServiceImpl implements UserDetailsService {
|
||||
@Autowired
|
||||
private IUsersService userInfoService;
|
||||
|
||||
@Autowired
|
||||
private UserRoleRepository userRoleRepository;
|
||||
|
||||
@Override
|
||||
public UserInfo loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
User userInfo = userInfoService.getByUsername(username);
|
||||
@@ -36,11 +40,16 @@ public class UserDetailServiceImpl implements UserDetailsService {
|
||||
throw new UsernameNotFoundException("用户不存在: " + username);
|
||||
}
|
||||
|
||||
// Assign role based on user level: level >= 1 → ADMIN, else → USER
|
||||
String role = (userInfo.getLevel() != null && userInfo.getLevel() >= 1) ? "ADMIN" : "USER";
|
||||
|
||||
List<GrantedAuthority> authorities = new ArrayList<>();
|
||||
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
|
||||
List<String> roleCodes = userRoleRepository.findRoleCodesByUserId(userInfo.getId());
|
||||
if (roleCodes == null || roleCodes.isEmpty()) {
|
||||
// Compatibility fallback for legacy data before RBAC migration.
|
||||
String role = (userInfo.getLevel() != null && userInfo.getLevel() >= 1) ? "ADMIN" : "USER";
|
||||
roleCodes = List.of(role);
|
||||
}
|
||||
for (String roleCode : roleCodes) {
|
||||
authorities.add(new SimpleGrantedAuthority("ROLE_" + roleCode));
|
||||
}
|
||||
|
||||
return new UserInfo(
|
||||
authorities,
|
||||
|
||||
@@ -71,7 +71,7 @@ public class UsersServiceImpl implements IUsersService {
|
||||
@Cacheable(value = "user", key = "#id")
|
||||
@Override
|
||||
public User findOne(Integer id) {
|
||||
return usersRepository.getById(id);
|
||||
return usersRepository.findById(id).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,6 +140,12 @@ public class UsersServiceImpl implements IUsersService {
|
||||
return usersRepository.getByUsername(username);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Override
|
||||
public List<User> findAllById(Iterable<Integer> ids) {
|
||||
return usersRepository.findAllById(ids);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
-- V2.0.2: RBAC foundation + admin upgrade logs (Spring Boot 3 optimization)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS rbac_role
|
||||
(
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||
code VARCHAR(50) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description VARCHAR(255),
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
create_time DATETIME,
|
||||
update_time DATETIME
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS rbac_user_role
|
||||
(
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
role_id INTEGER NOT NULL,
|
||||
create_time DATETIME
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS upgrade_log
|
||||
(
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||
version VARCHAR(50) NOT NULL,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
content TEXT,
|
||||
type VARCHAR(50),
|
||||
created_by INTEGER,
|
||||
create_time DATETIME
|
||||
);
|
||||
|
||||
INSERT INTO rbac_role (code, name, description, enabled, create_time, update_time)
|
||||
SELECT 'ADMIN', '管理员', '后台管理与运维权限', 1, NOW(), NOW()
|
||||
WHERE NOT EXISTS (SELECT 1 FROM rbac_role WHERE code = 'ADMIN');
|
||||
|
||||
INSERT INTO rbac_role (code, name, description, enabled, create_time, update_time)
|
||||
SELECT 'USER', '普通用户', '论坛基础访问权限', 1, NOW(), NOW()
|
||||
WHERE NOT EXISTS (SELECT 1 FROM rbac_role WHERE code = 'USER');
|
||||
|
||||
INSERT INTO rbac_user_role (user_id, role_id, create_time)
|
||||
SELECT u.id, r.id, NOW()
|
||||
FROM user u
|
||||
JOIN rbac_role r ON r.code = 'USER'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM rbac_user_role ur WHERE ur.user_id = u.id AND ur.role_id = r.id
|
||||
);
|
||||
|
||||
INSERT INTO rbac_user_role (user_id, role_id, create_time)
|
||||
SELECT u.id, r.id, NOW()
|
||||
FROM user u
|
||||
JOIN rbac_role r ON r.code = 'ADMIN'
|
||||
WHERE u.level >= 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM rbac_user_role ur WHERE ur.user_id = u.id AND ur.role_id = r.id
|
||||
);
|
||||
|
||||
INSERT INTO upgrade_log (version, title, content, type, created_by, create_time)
|
||||
SELECT 'v3.5.13',
|
||||
'后台管理与RBAC架构初始化',
|
||||
'新增角色与用户角色映射;新增后台管理与运维面板;新增升级日志管理入口。',
|
||||
'feature',
|
||||
1,
|
||||
NOW()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM upgrade_log WHERE version = 'v3.5.13' AND title = '后台管理与RBAC架构初始化'
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
-- V2.0.3: Audit log for admin operations (Spring Boot 3 optimization)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit_log
|
||||
(
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||
admin_id INTEGER,
|
||||
admin_name VARCHAR(100),
|
||||
action_type VARCHAR(50) NOT NULL,
|
||||
target_type VARCHAR(50),
|
||||
target_id INTEGER,
|
||||
description VARCHAR(500),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'success',
|
||||
create_time DATETIME
|
||||
);
|
||||
|
||||
CREATE INDEX idx_audit_admin_time ON audit_log(admin_id, create_time DESC);
|
||||
CREATE INDEX idx_audit_target ON audit_log(target_type, target_id);
|
||||
@@ -0,0 +1,54 @@
|
||||
<#macro page title active adminName="admin">
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="_csrf" content="${_csrf.token}"/>
|
||||
<meta name="_csrf_header" content="${_csrf.headerName}"/>
|
||||
<title>${title}</title>
|
||||
<#include "admin-commjs.ftl"/>
|
||||
<style>
|
||||
body { background: #f6f7fb; }
|
||||
.admin-header { background: #1f2937; color: #fff; padding: 12px 18px; }
|
||||
.admin-grid { display: grid; grid-template-columns: 230px 1fr; gap: 16px; padding: 16px; }
|
||||
.admin-sidebar { background: #fff; border-radius: 8px; padding: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
|
||||
.admin-main { background: #fff; border-radius: 8px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
|
||||
.admin-sidebar a.item.active { background: #1d4ed8 !important; color: #fff !important; border-radius: 6px; }
|
||||
.metric-card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px; }
|
||||
.small-muted { color: #6b7280; font-size: 12px; }
|
||||
@media (max-width: 900px) {
|
||||
.admin-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-header">
|
||||
<div class="ui grid">
|
||||
<div class="ten wide column">Jiscuss 后台管理</div>
|
||||
<div class="six wide right aligned column">当前管理员: ${adminName} | <a style="color:#93c5fd" href="/swagger-ui.html" target="_blank">接口文档</a> | <a style="color:#93c5fd" href="/">前台首页</a></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-grid">
|
||||
<div class="admin-sidebar">
|
||||
<div class="ui vertical fluid menu">
|
||||
<a class="item <#if active=='home'>active</#if>" href="/admin/home">总览与运维</a>
|
||||
<a class="item <#if active=='users'>active</#if>" href="/admin/users">用户与角色</a>
|
||||
<a class="item <#if active=='discussions'>active</#if>" href="/admin/discussions">文章管理</a>
|
||||
<a class="item <#if active=='posts'>active</#if>" href="/admin/posts">评论管理</a>
|
||||
<a class="item <#if active=='upgradeLogs'>active</#if>" href="/admin/upgrade-logs">版本升级日志</a>
|
||||
<a class="item <#if active=='auditLogs'>active</#if>" href="/admin/audit-logs">操作审计日志</a>
|
||||
<div class="item small-muted">预留扩展</div>
|
||||
<a class="item <#if active=='plugins'>active</#if>" href="/admin/plugins">插件管理(预留)</a>
|
||||
<a class="item <#if active=='themes'>active</#if>" href="/admin/themes">主题管理(预留)</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-main">
|
||||
<#nested>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</#macro>
|
||||
@@ -0,0 +1,48 @@
|
||||
<#import "admin-shell.ftl" as shell>
|
||||
<@shell.page title="后台管理 - 操作审计日志" active=active adminName=adminName>
|
||||
<h2 class="ui header">操作审计日志</h2>
|
||||
<p class="small-muted">记录所有管理员的后台操作,包括角色管理、用户管理、内容管理等。</p>
|
||||
|
||||
<table class="ui striped celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>管理员</th>
|
||||
<th>操作类型</th>
|
||||
<th>目标类型</th>
|
||||
<th>目标ID</th>
|
||||
<th>描述</th>
|
||||
<th>状态</th>
|
||||
<th>操作时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<#list logs as l>
|
||||
<tr>
|
||||
<td>${l.id}</td>
|
||||
<td>${l.adminName!""}</td>
|
||||
<td><span class="ui label">${l.actionType!""}</span></td>
|
||||
<td>${l.targetType!""}</td>
|
||||
<td>${l.targetId!""}</td>
|
||||
<td>
|
||||
<div style="max-width:300px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" title="${l.description!""}">
|
||||
${l.description!""}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<#if l.status == "success">
|
||||
<span class="ui label green">成功</span>
|
||||
<#elseif l.status == "blocked">
|
||||
<span class="ui label orange">阻止</span>
|
||||
<#elseif l.status == "ignored">
|
||||
<span class="ui label yellow">忽略</span>
|
||||
<#else>
|
||||
<span class="ui label">${l.status!""}</span>
|
||||
</#if>
|
||||
</td>
|
||||
<td>${l.createTime?string('yyyy-MM-dd HH:mm:ss')!""}</td>
|
||||
</tr>
|
||||
</#list>
|
||||
</tbody>
|
||||
</table>
|
||||
</@shell.page>
|
||||
@@ -0,0 +1,32 @@
|
||||
<#import "admin-shell.ftl" as shell>
|
||||
<@shell.page title="后台管理 - 文章管理" active=active adminName=adminName>
|
||||
<h2 class="ui header">文章管理</h2>
|
||||
<table class="ui striped celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>标题</th>
|
||||
<th>创建人ID</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<#list discussions as d>
|
||||
<tr>
|
||||
<td>${d.id}</td>
|
||||
<td>${d.title!""}</td>
|
||||
<td>${d.createId!""}</td>
|
||||
<td>${d.createTime?string('yyyy-MM-dd HH:mm:ss')!""}</td>
|
||||
<td>
|
||||
<a class="ui tiny button" href="/getdiscussionsbyid?id=${d.id}" target="_blank">查看</a>
|
||||
<form style="display:inline" method="post" action="/admin/discussions/${d.id}/delete" onsubmit="return confirm('确认删除该文章及其评论吗?');">
|
||||
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
|
||||
<button class="ui tiny red button" type="submit">删除</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</#list>
|
||||
</tbody>
|
||||
</table>
|
||||
</@shell.page>
|
||||
@@ -1,338 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<title>后台管理首页</title>
|
||||
<head>
|
||||
<meta name="_csrf" content="${_csrf.token}"/>
|
||||
<meta name="_csrf_header" content="${_csrf.headerName}"/>
|
||||
<#import "admin-shell.ftl" as shell>
|
||||
<@shell.page title="后台管理 - 总览" active=active adminName=adminName>
|
||||
<h2 class="ui header">系统总览</h2>
|
||||
|
||||
<#--jquery-->
|
||||
<script crossorigin="anonymous" integrity="sha384-vk5WoKIaW/vJyUAd9n/wmopsmNhiy+L2Z+SBxGYnUkunIxVxAv/UtMOhba/xskxh"
|
||||
src="https://lib.baomitu.com/jquery/3.4.1/jquery.min.js"></script>
|
||||
|
||||
<#--semantic-ui-->
|
||||
<link rel="stylesheet" type="text/css" href="/static/semanticui/semantic.css">
|
||||
<script type="text/javascript" src="/static/semanticui/semantic.js"></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/static/semanticui/my.css">
|
||||
|
||||
<#--tinymce-->
|
||||
<script crossorigin="anonymous" integrity="sha384-CpsBIlOAWHuSRRN235sCBzEeKN6hLT6SpOGRkGadKpYj0gDP7+s3Q8pC38z8uGHH"
|
||||
src="https://lib.baomitu.com/tinymce/5.1.1/tinymce.min.js"></script>
|
||||
|
||||
|
||||
<#--layx-->
|
||||
<link href="/static/layx/layx.min.css?b14794a8a3baf3e8b58e" rel="stylesheet">
|
||||
|
||||
<script type="text/javascript" src="/static/layx/layx.min.js?b14794a8a3baf3e8b58e"></script>
|
||||
|
||||
<script type="text/javascript" charset="UTF-8" src="/static/js/comm/util.js"></script>
|
||||
|
||||
|
||||
<style type="text/css">
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding-top: 68px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
#sidebar .ui.menu > a.item {
|
||||
padding: 10px 20px;
|
||||
line-height: 20px;
|
||||
color: #337ab7;
|
||||
border-radius: 0 !important;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#sidebar .ui.menu > a.item.active {
|
||||
background-color: #337ab7;
|
||||
color: white;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
#sidebar .ui.menu > a.item:hover {
|
||||
background-color: #eee;
|
||||
color: #23527c;
|
||||
}
|
||||
|
||||
#content {
|
||||
padding-top: 56px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
#content h1 {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
#content .ui.dividing.header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ui.centered.small.circular.image {
|
||||
margin-top: 14px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.ui.borderless.menu {
|
||||
box-shadow: none;
|
||||
flex-wrap: wrap;
|
||||
border: none;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.ui.mobile.only.grid .ui.menu .ui.vertical.menu {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body id="root">
|
||||
<div class="ui tablet computer only padded grid">
|
||||
<div class="ui inverted borderless top fixed fluid menu">
|
||||
<a class="header item">Jiscuss后台管理</a>
|
||||
<div class="right menu">
|
||||
<div class="item">
|
||||
<div class="ui small input"><input placeholder="Search..." /></div>
|
||||
</div>
|
||||
<a class="item">常用管理</a> <a class="item">设置</a>
|
||||
<a class="item">用户</a> <a class="item">主题</a> <a class="item">注销</a>
|
||||
</div>
|
||||
<div class="ui four stackable cards">
|
||||
<div class="card"><div class="content"><div class="header">用户总数</div><div class="description">${userCount}</div></div></div>
|
||||
<div class="card"><div class="content"><div class="header">文章总数</div><div class="description">${discussionCount}</div></div></div>
|
||||
<div class="card"><div class="content"><div class="header">评论总数</div><div class="description">${postCount}</div></div></div>
|
||||
<div class="card"><div class="content"><div class="header">角色总数</div><div class="description">${roleCount}</div></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui mobile only padded grid">
|
||||
<div class="ui top fixed borderless fluid inverted menu">
|
||||
<a class="header item">Project Name</a>
|
||||
<div class="right menu">
|
||||
<div class="item">
|
||||
<button class="ui icon toggle basic inverted button">
|
||||
<i class="content icon"></i>
|
||||
</button>
|
||||
|
||||
<h3 class="ui dividing header">服务器与运行时</h3>
|
||||
<div class="ui two stackable cards">
|
||||
<div class="card">
|
||||
<div class="content">
|
||||
<div class="header">CPU / 内存</div>
|
||||
<div class="description">
|
||||
<p>CPU 负载: ${sys.cpuLoad}</p>
|
||||
<p>JVM 已用内存: ${sys.usedMb} MB / ${sys.totalMb} MB</p>
|
||||
<p>JVM 最大内存: ${sys.maxMb} MB</p>
|
||||
<p>Heap 已用: ${sys.heapUsedMb} MB</p>
|
||||
<p>应用运行时长: ${sys.uptimeMs} ms</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui vertical borderless inverted fluid menu">
|
||||
<a class="item">Dashboard</a> <a class="item">Settings</a>
|
||||
<a class="item">Profile</a> <a class="item">Help</a>
|
||||
<div class="ui fitted divider"></div>
|
||||
<div class="item">
|
||||
<div class="ui small input"><input placeholder="Search..." /></div>
|
||||
<div class="card">
|
||||
<div class="content">
|
||||
<div class="header">关键依赖版本</div>
|
||||
<div class="description">
|
||||
<p>Spring Boot: ${deps.springBoot}</p>
|
||||
<p>Java: ${deps.java}</p>
|
||||
<p>Spring: ${deps.spring}</p>
|
||||
<p>Hibernate: ${deps.hibernate}</p>
|
||||
<p>Flyway: ${deps.flyway}</p>
|
||||
<p>Druid: ${deps.druid}</p>
|
||||
<p>Ehcache: ${deps.ehcache}</p>
|
||||
<p>SpringDoc: ${deps.springdoc}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui padded grid">
|
||||
<div
|
||||
class="three wide tablet only three wide computer only column"
|
||||
id="sidebar"
|
||||
>
|
||||
<div class="ui vertical borderless fluid text menu">
|
||||
<a class="active item">常用管理</a> <a class="item">设置</a>
|
||||
<div class="ui hidden divider"></div>
|
||||
<a class="item">用户</a> <a class="item">主题</a>
|
||||
<div class="ui hidden divider"></div>
|
||||
<a class="item">Jiscuss文档</a>
|
||||
<a class="item" href="/">返回首页</a>
|
||||
</div>
|
||||
|
||||
<h3 class="ui dividing header">快捷入口</h3>
|
||||
<div class="ui buttons">
|
||||
<a class="ui primary button" href="/admin/users">用户与角色</a>
|
||||
<a class="ui button" href="/admin/discussions">文章管理</a>
|
||||
<a class="ui button" href="/admin/posts">评论管理</a>
|
||||
<a class="ui button" href="/swagger-ui.html" target="_blank">接口文档</a>
|
||||
</div>
|
||||
<div
|
||||
class="sixteen wide mobile thirteen wide tablet thirteen wide computer right floated column"
|
||||
id="content"
|
||||
>
|
||||
<div class="ui padded grid">
|
||||
<div class="row">
|
||||
<h1 class="ui huge dividing header">Dashboard</h1>
|
||||
</div>
|
||||
<div class="center aligned row">
|
||||
<div
|
||||
class="eight wide mobile four wide tablet four wide computer column"
|
||||
>
|
||||
<img
|
||||
class="ui centered small circular image"
|
||||
src="./static/images/wireframe/square-image.png"
|
||||
/>
|
||||
<div class="ui large basic label">Label</div>
|
||||
<p>Something else</p>
|
||||
</div>
|
||||
<div
|
||||
class="eight wide mobile four wide tablet four wide computer column"
|
||||
>
|
||||
<img
|
||||
class="ui centered small circular image"
|
||||
src="./static/images/wireframe/square-image.png"
|
||||
/>
|
||||
<div class="ui large basic label">Label</div>
|
||||
<p>Something else</p>
|
||||
</div>
|
||||
<div
|
||||
class="eight wide mobile four wide tablet four wide computer column"
|
||||
>
|
||||
<img
|
||||
class="ui centered small circular image"
|
||||
src="./static/images/wireframe/square-image.png"
|
||||
/>
|
||||
<div class="ui large basic label">Label</div>
|
||||
<p>Something else</p>
|
||||
</div>
|
||||
<div
|
||||
class="eight wide mobile four wide tablet four wide computer column"
|
||||
>
|
||||
<img
|
||||
class="ui centered small circular image"
|
||||
src="./static/images/wireframe/square-image.png"
|
||||
/>
|
||||
<div class="ui large basic label">Label</div>
|
||||
<p>Something else</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui hidden section divider"></div>
|
||||
<div class="row">
|
||||
<h1 class="ui huge dividing header">Section title</h1>
|
||||
</div>
|
||||
<div class="row">
|
||||
<table class="ui single line striped selectable unstackable table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Header</th>
|
||||
<th>Header</th>
|
||||
<th>Header</th>
|
||||
<th>Header</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1,001</td>
|
||||
<td>Lorem</td>
|
||||
<td>ipsum</td>
|
||||
<td>dolor</td>
|
||||
<td>sit</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1,002</td>
|
||||
<td>amet</td>
|
||||
<td>consectetur</td>
|
||||
<td>adipiscing</td>
|
||||
<td>elit</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1,003</td>
|
||||
<td>Integer</td>
|
||||
<td>nec</td>
|
||||
<td>odio</td>
|
||||
<td>Praesent</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1,003</td>
|
||||
<td>libero</td>
|
||||
<td>Sed</td>
|
||||
<td>cursus</td>
|
||||
<td>ante</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1,004</td>
|
||||
<td>dapibus</td>
|
||||
<td>diam</td>
|
||||
<td>Sed</td>
|
||||
<td>nisi</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1,005</td>
|
||||
<td>Nulla</td>
|
||||
<td>quis</td>
|
||||
<td>sem</td>
|
||||
<td>at</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1,006</td>
|
||||
<td>nibh</td>
|
||||
<td>elementum</td>
|
||||
<td>imperdiet</td>
|
||||
<td>Duis</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1,007</td>
|
||||
<td>sagittis</td>
|
||||
<td>ipsum</td>
|
||||
<td>Praesent</td>
|
||||
<td>mauris</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1,008</td>
|
||||
<td>Fusce</td>
|
||||
<td>nec</td>
|
||||
<td>tellus</td>
|
||||
<td>sed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1,009</td>
|
||||
<td>augue</td>
|
||||
<td>semper</td>
|
||||
<td>porta</td>
|
||||
<td>Mauris</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1,010</td>
|
||||
<td>massa</td>
|
||||
<td>Vestibulum</td>
|
||||
<td>lacinia</td>
|
||||
<td>arcu</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1,011</td>
|
||||
<td>eget</td>
|
||||
<td>nulla</td>
|
||||
<td>Class</td>
|
||||
<td>aptent</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1,012</td>
|
||||
<td>taciti</td>
|
||||
<td>sociosqu</td>
|
||||
<td>ad</td>
|
||||
<td>litora</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1,013</td>
|
||||
<td>torquent</td>
|
||||
<td>per</td>
|
||||
<td>conubia</td>
|
||||
<td>nostra</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1,014</td>
|
||||
<td>per</td>
|
||||
<td>inceptos</td>
|
||||
<td>himenaeos</td>
|
||||
<td>Curabitur</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1,015</td>
|
||||
<td>sodales</td>
|
||||
<td>ligula</td>
|
||||
<td>in</td>
|
||||
<td>libero</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$(".ui.toggle.button").click(function() {
|
||||
$(".mobile.only.grid .ui.vertical.menu").toggle(100);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</@shell.page>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<#import "admin-shell.ftl" as shell>
|
||||
<@shell.page title="后台管理 - 插件管理(预留)" active=active adminName=adminName>
|
||||
<h2 class="ui header">插件管理(预留)</h2>
|
||||
<div class="ui info message">
|
||||
<div class="header">预留扩展点</div>
|
||||
<p>后续可在此接入插件生命周期管理:安装、启停、配置、版本升级、依赖校验。</p>
|
||||
</div>
|
||||
</@shell.page>
|
||||
@@ -0,0 +1,33 @@
|
||||
<#import "admin-shell.ftl" as shell>
|
||||
<@shell.page title="后台管理 - 评论管理" active=active adminName=adminName>
|
||||
<h2 class="ui header">评论管理</h2>
|
||||
<table class="ui compact celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>文章</th>
|
||||
<th>作者</th>
|
||||
<th>内容</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<#list posts as p>
|
||||
<tr>
|
||||
<td>${p.id}</td>
|
||||
<td>${discussionNames[p.discussionId]!('#'+p.discussionId?c)}</td>
|
||||
<td>${userNames[p.createId]!('#'+p.createId?c)}</td>
|
||||
<td><div style="max-width:420px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${p.content!""}</div></td>
|
||||
<td>${p.createTime?string('yyyy-MM-dd HH:mm:ss')!""}</td>
|
||||
<td>
|
||||
<form style="display:inline" method="post" action="/admin/posts/${p.id}/delete" onsubmit="return confirm('确认删除该评论吗?');">
|
||||
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
|
||||
<button class="ui tiny red button" type="submit">删除</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</#list>
|
||||
</tbody>
|
||||
</table>
|
||||
</@shell.page>
|
||||
@@ -0,0 +1,8 @@
|
||||
<#import "admin-shell.ftl" as shell>
|
||||
<@shell.page title="后台管理 - 主题管理(预留)" active=active adminName=adminName>
|
||||
<h2 class="ui header">主题管理(预留)</h2>
|
||||
<div class="ui info message">
|
||||
<div class="header">预留扩展点</div>
|
||||
<p>后续可在此增加主题包管理、配色切换、模板覆盖与预览发布。</p>
|
||||
</div>
|
||||
</@shell.page>
|
||||
@@ -0,0 +1,65 @@
|
||||
<#import "admin-shell.ftl" as shell>
|
||||
<@shell.page title="后台管理 - 版本升级日志" active=active adminName=adminName>
|
||||
<h2 class="ui header">版本功能升级日志</h2>
|
||||
<p class="small-muted">记录每次升级新增与修复内容,便于发布追踪与回滚评估。</p>
|
||||
|
||||
<form class="ui form" method="post" action="/admin/upgrade-logs">
|
||||
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
|
||||
<div class="three fields">
|
||||
<div class="field">
|
||||
<label>版本号</label>
|
||||
<input name="version" placeholder="例如 v3.5.13" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>类型</label>
|
||||
<select class="ui dropdown" name="type">
|
||||
<option value="feature">feature</option>
|
||||
<option value="fix">fix</option>
|
||||
<option value="security">security</option>
|
||||
<option value="refactor">refactor</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>标题</label>
|
||||
<input name="title" placeholder="本次升级摘要" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>详细内容</label>
|
||||
<textarea name="content" rows="4" placeholder="描述新增、修复、兼容性说明等"></textarea>
|
||||
</div>
|
||||
<button class="ui primary button" type="submit">新增日志</button>
|
||||
</form>
|
||||
|
||||
<h3 class="ui dividing header">历史记录</h3>
|
||||
<table class="ui celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>版本</th>
|
||||
<th>类型</th>
|
||||
<th>标题</th>
|
||||
<th>内容</th>
|
||||
<th>创建人</th>
|
||||
<th>创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<#list logs as l>
|
||||
<tr>
|
||||
<td>${l.id}</td>
|
||||
<td>${l.version!""}</td>
|
||||
<td>${l.type!""}</td>
|
||||
<td>${l.title!""}</td>
|
||||
<td>${l.content!""}</td>
|
||||
<td>${l.createdBy!""}</td>
|
||||
<td>${l.createTime?string('yyyy-MM-dd HH:mm:ss')!""}</td>
|
||||
</tr>
|
||||
</#list>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
$('.ui.dropdown').dropdown();
|
||||
</script>
|
||||
</@shell.page>
|
||||
@@ -0,0 +1,82 @@
|
||||
<#import "admin-shell.ftl" as shell>
|
||||
<@shell.page title="后台管理 - 用户与角色" active=active adminName=adminName>
|
||||
<h2 class="ui header">用户与角色管理</h2>
|
||||
<p class="small-muted">简单 RBAC:用户可绑定多个角色,拥有 ADMIN 角色的用户可以进入后台管理。</p>
|
||||
|
||||
<h3 class="ui dividing header">角色管理</h3>
|
||||
<form class="ui form" method="post" action="/admin/roles">
|
||||
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
|
||||
<div class="three fields">
|
||||
<div class="field"><label>角色编码</label><input name="code" placeholder="如 ADMIN_AUDIT" required></div>
|
||||
<div class="field"><label>角色名称</label><input name="name" placeholder="如 审计管理员" required></div>
|
||||
<div class="field"><label>描述</label><input name="description" placeholder="角色用途说明"></div>
|
||||
</div>
|
||||
<button class="ui small primary button" type="submit">新增角色</button>
|
||||
</form>
|
||||
|
||||
<table class="ui celled compact table">
|
||||
<thead><tr><th>ID</th><th>编码</th><th>名称</th><th>状态</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
<#list roles as r>
|
||||
<tr>
|
||||
<td>${r.id}</td>
|
||||
<td>${r.code!""}</td>
|
||||
<td>${r.name!""}</td>
|
||||
<td><#if r.enabled?? && r.enabled==1>启用<#else>停用</#if></td>
|
||||
<td>
|
||||
<form style="display:inline" method="post" action="/admin/roles/${r.id}/toggle">
|
||||
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
|
||||
<button class="ui tiny button" type="submit">切换启停</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</#list>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3 class="ui dividing header">用户角色绑定</h3>
|
||||
<table class="ui celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>用户名</th>
|
||||
<th>邮箱</th>
|
||||
<th>角色绑定</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<#list users as u>
|
||||
<tr>
|
||||
<td>${u.id}</td>
|
||||
<td>${u.username!""}</td>
|
||||
<td>${u.email!""}</td>
|
||||
<td>
|
||||
<form class="ui form" method="post" action="/admin/users/${u.id}/roles">
|
||||
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
|
||||
<div class="inline fields">
|
||||
<#list roles as r>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="roleIds" value="${r.id}"
|
||||
<#if userRoleIds[u.id]?? && userRoleIds[u.id]?seq_contains(r.id)>checked</#if>>
|
||||
<label>${r.code}</label>
|
||||
</div>
|
||||
</div>
|
||||
</#list>
|
||||
</div>
|
||||
<button class="ui tiny primary button" type="submit">保存角色</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<a class="ui tiny button" href="/user?id=${u.id}" target="_blank">查看用户页</a>
|
||||
</td>
|
||||
</tr>
|
||||
</#list>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
$('.ui.checkbox').checkbox();
|
||||
</script>
|
||||
</@shell.page>
|
||||
@@ -1,4 +1,5 @@
|
||||
<#--jquery-->
|
||||
<link rel="icon" type="image/png" href="/static/assets/images/logo.png">
|
||||
<script crossorigin="anonymous" integrity="sha384-vk5WoKIaW/vJyUAd9n/wmopsmNhiy+L2Z+SBxGYnUkunIxVxAv/UtMOhba/xskxh"
|
||||
src="https://lib.baomitu.com/jquery/3.4.1/jquery.min.js"></script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user