461 lines
18 KiB
Java
461 lines
18 KiB
Java
package com.par.api.controller;
|
|
|
|
import com.par.core.dto.AccountDTO;
|
|
import com.par.core.dto.ApiResponse;
|
|
import com.par.core.dto.SiteConfigDTO;
|
|
import com.par.core.entity.Account;
|
|
import com.par.core.entity.SiteConfig;
|
|
import com.par.core.enums.ConfigStatus;
|
|
import com.par.core.enums.TrustLevel;
|
|
import com.par.core.mapper.AccountMapper;
|
|
import com.par.core.mapper.ConfigReviewMapper;
|
|
import com.par.core.mapper.SiteConfigMapper;
|
|
import com.par.core.mapper.SiteMapper;
|
|
import com.par.core.entity.Psk;
|
|
import com.par.core.service.AccountService;
|
|
import com.par.core.service.ConfigService;
|
|
import com.par.core.service.PskService;
|
|
import com.par.core.storage.CachedStorageService;
|
|
import com.par.common.util.HmacUtil;
|
|
import jakarta.servlet.http.HttpServletRequest;
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.time.LocalDateTime;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* 管理员控制器
|
|
* 所有接口需鉴权 + 管理员权限
|
|
*/
|
|
@Slf4j
|
|
@RestController
|
|
@RequestMapping("/api/v1/admin")
|
|
@RequiredArgsConstructor
|
|
public class AdminController {
|
|
|
|
private final SiteConfigMapper siteConfigMapper;
|
|
private final ConfigReviewMapper configReviewMapper;
|
|
private final AccountMapper accountMapper;
|
|
private final SiteMapper siteMapper;
|
|
private final AccountService accountService;
|
|
private final PskService pskService;
|
|
private final ConfigService configService;
|
|
private final CachedStorageService storageService;
|
|
private final com.par.core.service.SiteService siteService;
|
|
|
|
/**
|
|
* 获取所有用户列表
|
|
*/
|
|
@GetMapping("/accounts")
|
|
public ApiResponse<List<AccountDTO>> listAccounts(HttpServletRequest request) {
|
|
checkAdmin(request);
|
|
List<Account> accounts = accountMapper.selectList(null);
|
|
List<AccountDTO> dtos = accounts.stream()
|
|
.map(accountService::toDTO)
|
|
.toList();
|
|
return ApiResponse.success(dtos);
|
|
}
|
|
|
|
/**
|
|
* 审核配置
|
|
*/
|
|
@PostMapping("/reviews/{reviewId}/approve")
|
|
public ApiResponse<Void> approveConfig(
|
|
@PathVariable Long reviewId,
|
|
@RequestParam(value = "comment", required = false) String comment,
|
|
HttpServletRequest request) {
|
|
|
|
checkAdmin(request);
|
|
|
|
// Phase 1: 简化实现,直接更新配置状态
|
|
SiteConfig config = siteConfigMapper.selectById(reviewId);
|
|
if (config == null) {
|
|
return ApiResponse.error(404, "Config not found");
|
|
}
|
|
|
|
Long reviewerId = (Long) request.getAttribute("accountId");
|
|
|
|
// 自动分配版本号:取该站点已审批的最大版本号 + 1
|
|
var latestApproved = siteConfigMapper.selectList(
|
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SiteConfig>()
|
|
.eq(SiteConfig::getSiteId, config.getSiteId())
|
|
.eq(SiteConfig::getStatus, ConfigStatus.APPROVED)
|
|
.orderByDesc(SiteConfig::getVersion));
|
|
int nextVersion = 1;
|
|
if (!latestApproved.isEmpty()) {
|
|
try { nextVersion = Integer.parseInt(latestApproved.get(0).getVersion()) + 1; }
|
|
catch (NumberFormatException ignored) {}
|
|
}
|
|
config.setVersion(String.valueOf(nextVersion));
|
|
|
|
// 文件从 pending 移到 approved 路径
|
|
String approvedPath = config.getStoragePath().replace("/pending/", "/approved/");
|
|
String content = storageService.read(config.getStoragePath());
|
|
if (content != null) {
|
|
storageService.store(approvedPath, content);
|
|
config.setStoragePath(approvedPath);
|
|
}
|
|
|
|
// Groovy 脚本也移到 approved
|
|
if (config.getScriptStoragePath() != null && !config.getScriptStoragePath().isBlank()) {
|
|
String approvedScriptPath = config.getScriptStoragePath().replace("/pending/", "/approved/");
|
|
String script = storageService.read(config.getScriptStoragePath());
|
|
if (script != null) {
|
|
storageService.store(approvedScriptPath, script);
|
|
config.setScriptStoragePath(approvedScriptPath);
|
|
}
|
|
}
|
|
|
|
config.setStatus(ConfigStatus.APPROVED);
|
|
config.setReviewerId(reviewerId);
|
|
config.setReviewedAt(LocalDateTime.now());
|
|
config.setReviewComment(comment);
|
|
|
|
// 更新 is_latest 标记
|
|
siteConfigMapper.clearLatestFlag(config.getSiteId());
|
|
config.setIsLatest(true);
|
|
|
|
siteConfigMapper.updateById(config);
|
|
|
|
// 更新 sites 表的 current_config_id
|
|
var site = siteMapper.selectBySiteId(config.getSiteId());
|
|
if (site != null) {
|
|
site.setCurrentConfigId(config.getId());
|
|
site.setSchemaVersion(config.getSchemaVersion());
|
|
siteMapper.updateById(site);
|
|
}
|
|
|
|
log.info("Config approved: id={}, reviewer={}", reviewId, reviewerId);
|
|
return ApiResponse.success();
|
|
}
|
|
|
|
/**
|
|
* 拒绝配置
|
|
*/
|
|
@PostMapping("/reviews/{reviewId}/reject")
|
|
public ApiResponse<Void> rejectConfig(
|
|
@PathVariable Long reviewId,
|
|
@RequestParam(value = "comment", required = false, defaultValue = "") String comment,
|
|
HttpServletRequest request) {
|
|
|
|
checkAdmin(request);
|
|
|
|
SiteConfig config = siteConfigMapper.selectById(reviewId);
|
|
if (config == null) {
|
|
return ApiResponse.error(404, "Config not found");
|
|
}
|
|
|
|
Long reviewerId = (Long) request.getAttribute("accountId");
|
|
|
|
// 删除 pending 文件
|
|
storageService.delete(config.getStoragePath());
|
|
|
|
config.setStoragePath(null);
|
|
config.setStatus(ConfigStatus.REJECTED);
|
|
config.setReviewerId(reviewerId);
|
|
config.setReviewedAt(LocalDateTime.now());
|
|
config.setReviewComment(comment);
|
|
|
|
siteConfigMapper.updateById(config);
|
|
|
|
log.info("Config rejected: id={}, reviewer={}", reviewId, reviewerId);
|
|
return ApiResponse.success();
|
|
}
|
|
|
|
/**
|
|
* 获取指定站点的待审核配置列表
|
|
*/
|
|
@GetMapping("/configs/pending/{siteId}")
|
|
public ApiResponse<List<SiteConfigDTO>> listPendingConfigsBySite(
|
|
@PathVariable String siteId, HttpServletRequest request) {
|
|
checkAdmin(request);
|
|
List<SiteConfig> configs = siteConfigMapper.selectList(
|
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SiteConfig>()
|
|
.eq(SiteConfig::getSiteId, siteId)
|
|
.eq(SiteConfig::getStatus, ConfigStatus.PENDING)
|
|
.eq(SiteConfig::getDeleted, 0));
|
|
return ApiResponse.success(configs.stream().map(configService::toDTO).toList());
|
|
}
|
|
|
|
/**
|
|
* 查看指定版本配置内容
|
|
*/
|
|
@GetMapping("/configs/{configId}/content")
|
|
public ApiResponse<Map<String, Object>> getConfigContent(
|
|
@PathVariable Long configId, HttpServletRequest request) {
|
|
checkAdmin(request);
|
|
SiteConfig config = siteConfigMapper.selectById(configId);
|
|
if (config == null) return ApiResponse.error(404, "Config not found");
|
|
String content = null;
|
|
try { content = storageService.read(config.getStoragePath()); } catch (Exception e) {
|
|
return ApiResponse.error(500, "读取配置失败: " + e.getMessage());
|
|
}
|
|
return ApiResponse.success(Map.of("id", config.getId(), "content", content != null ? content : ""));
|
|
}
|
|
|
|
/**
|
|
* 编辑待审核配置内容
|
|
*/
|
|
@PutMapping("/configs/{configId}/content")
|
|
public ApiResponse<SiteConfigDTO> updateConfigContent(
|
|
@PathVariable Long configId,
|
|
@RequestBody Map<String, String> payload,
|
|
HttpServletRequest request) {
|
|
checkAdmin(request);
|
|
SiteConfig config = siteConfigMapper.selectById(configId);
|
|
if (config == null) return ApiResponse.error(404, "Config not found");
|
|
if (config.getStatus() != ConfigStatus.PENDING) {
|
|
return ApiResponse.error(400, "只能编辑待审核的配置");
|
|
}
|
|
|
|
String content = payload.get("content");
|
|
if (content == null || content.isBlank()) {
|
|
return ApiResponse.error(400, "content is required");
|
|
}
|
|
|
|
// 更新存储文件
|
|
storageService.store(config.getStoragePath(), content);
|
|
|
|
// 更新 hash 和文件大小
|
|
config.setConfigHash(HmacUtil.sha256(content));
|
|
config.setFileSize((long) content.getBytes(StandardCharsets.UTF_8).length);
|
|
siteConfigMapper.updateById(config);
|
|
|
|
log.info("Config content updated: id={}, siteId={}", configId, config.getSiteId());
|
|
return ApiResponse.success(configService.toDTO(config));
|
|
}
|
|
|
|
/**
|
|
* 查看指定配置的 Groovy 脚本
|
|
*/
|
|
@GetMapping("/configs/{configId}/script")
|
|
public ApiResponse<Map<String, Object>> getConfigScript(
|
|
@PathVariable Long configId, HttpServletRequest request) {
|
|
checkAdmin(request);
|
|
SiteConfig config = siteConfigMapper.selectById(configId);
|
|
if (config == null) return ApiResponse.error(404, "Config not found");
|
|
String script = null;
|
|
if (config.getScriptStoragePath() != null) {
|
|
try { script = storageService.read(config.getScriptStoragePath()); } catch (Exception ignored) {}
|
|
}
|
|
return ApiResponse.success(Map.of("id", config.getId(), "script", script != null ? script : ""));
|
|
}
|
|
|
|
/**
|
|
* 编辑待审核配置的 Groovy 脚本
|
|
*/
|
|
@PutMapping("/configs/{configId}/script")
|
|
public ApiResponse<SiteConfigDTO> updateConfigScript(
|
|
@PathVariable Long configId,
|
|
@RequestBody Map<String, String> payload,
|
|
HttpServletRequest request) {
|
|
checkAdmin(request);
|
|
SiteConfig config = siteConfigMapper.selectById(configId);
|
|
if (config == null) return ApiResponse.error(404, "Config not found");
|
|
if (config.getStatus() != ConfigStatus.PENDING) {
|
|
return ApiResponse.error(400, "只能编辑待审核的配置");
|
|
}
|
|
|
|
String script = payload.get("script");
|
|
if (script == null) return ApiResponse.error(400, "script is required");
|
|
|
|
String scriptPath = config.getScriptStoragePath();
|
|
if (scriptPath == null || scriptPath.isBlank()) {
|
|
scriptPath = String.format("configs/%s/pending/%s.groovy", config.getSiteId(), config.getConfigHash().substring(0, 8));
|
|
config.setScriptStoragePath(scriptPath);
|
|
}
|
|
storageService.store(scriptPath, script);
|
|
siteConfigMapper.updateById(config);
|
|
|
|
log.info("Config script updated: id={}, siteId={}", configId, config.getSiteId());
|
|
return ApiResponse.success(configService.toDTO(config));
|
|
}
|
|
|
|
/**
|
|
* 设置用户信任等级(禁止修改管理员、禁止提升为管理员)
|
|
*/
|
|
@PostMapping("/accounts/{accountId}/trust-level")
|
|
public ApiResponse<Void> setTrustLevel(
|
|
@PathVariable Long accountId,
|
|
@RequestParam("level") String level,
|
|
HttpServletRequest request) {
|
|
|
|
checkAdmin(request);
|
|
|
|
var target = accountMapper.selectById(accountId);
|
|
if (target == null) {
|
|
return ApiResponse.error(404, "Account not found");
|
|
}
|
|
|
|
// 禁止修改管理员
|
|
if (target.getTrustLevel() == TrustLevel.ADMIN) {
|
|
return ApiResponse.error(403, "不能修改管理员账户的信任等级");
|
|
}
|
|
|
|
// 禁止将用户设置为管理员
|
|
TrustLevel newLevel = TrustLevel.fromValue(level);
|
|
if (newLevel == TrustLevel.ADMIN) {
|
|
return ApiResponse.error(403, "不能通过此接口提升为管理员,请直接操作数据库");
|
|
}
|
|
|
|
target.setTrustLevel(newLevel);
|
|
accountMapper.updateById(target);
|
|
|
|
log.info("Trust level updated: accountId={}, level={}", accountId, level);
|
|
return ApiResponse.success();
|
|
}
|
|
|
|
/**
|
|
* 绑定额外邮箱
|
|
*/
|
|
@PostMapping("/accounts/{accountId}/emails")
|
|
public ApiResponse<Void> bindEmail(
|
|
@PathVariable Long accountId,
|
|
@RequestBody Map<String, String> payload,
|
|
HttpServletRequest request) {
|
|
checkAdmin(request);
|
|
String email = payload.get("email");
|
|
if (email == null || email.isBlank()) {
|
|
return ApiResponse.error(400, "email is required");
|
|
}
|
|
accountService.bindEmail(accountId, email);
|
|
log.info("Email bound to account: accountId={}, email={}", accountId, email);
|
|
return ApiResponse.success();
|
|
}
|
|
|
|
/**
|
|
* 解绑邮箱
|
|
*/
|
|
@DeleteMapping("/accounts/{accountId}/emails/{emailId}")
|
|
public ApiResponse<Void> unbindEmail(
|
|
@PathVariable Long accountId,
|
|
@PathVariable Long emailId,
|
|
HttpServletRequest request) {
|
|
checkAdmin(request);
|
|
accountService.unbindEmail(emailId);
|
|
log.info("Email unbound: accountId={}, emailId={}", accountId, emailId);
|
|
return ApiResponse.success();
|
|
}
|
|
|
|
/**
|
|
* 获取账户的绑定邮箱列表
|
|
*/
|
|
@GetMapping("/accounts/{accountId}/emails")
|
|
public ApiResponse<List<String>> listEmails(
|
|
@PathVariable Long accountId,
|
|
HttpServletRequest request) {
|
|
checkAdmin(request);
|
|
return ApiResponse.success(accountService.listEmails(accountId));
|
|
}
|
|
|
|
// ===================== PSK 管理 =====================
|
|
|
|
@GetMapping("/psks")
|
|
public ApiResponse<List<Psk>> listPsks(HttpServletRequest request) {
|
|
checkAdmin(request);
|
|
return ApiResponse.success(pskService.listAll());
|
|
}
|
|
|
|
@PostMapping("/psks")
|
|
public ApiResponse<Psk> addPsk(@RequestBody Map<String, String> body, HttpServletRequest request) {
|
|
checkAdmin(request);
|
|
String version = body.get("version");
|
|
String secret = body.get("secret");
|
|
String description = body.get("description");
|
|
if (version == null || secret == null) {
|
|
return ApiResponse.error(400, "version and secret are required");
|
|
}
|
|
return ApiResponse.success(pskService.add(version, secret, description));
|
|
}
|
|
|
|
@PostMapping("/psks/{id}/toggle")
|
|
public ApiResponse<Void> togglePsk(@PathVariable Long id, @RequestParam("active") boolean active,
|
|
HttpServletRequest request) {
|
|
checkAdmin(request);
|
|
pskService.toggle(id, active);
|
|
return ApiResponse.success();
|
|
}
|
|
|
|
// ===================== 站点审核 =====================
|
|
|
|
@GetMapping("/sites")
|
|
public ApiResponse<List<com.par.core.dto.SiteDTO>> listAllSites(HttpServletRequest request) {
|
|
checkAdmin(request);
|
|
return ApiResponse.success(siteService.listAllSites().stream()
|
|
.map(siteService::toDTO).toList());
|
|
}
|
|
|
|
@GetMapping("/sites/pending")
|
|
public ApiResponse<List<com.par.core.dto.SiteChangeDTO>> listPendingChanges(HttpServletRequest request) {
|
|
checkAdmin(request);
|
|
return ApiResponse.success(siteService.listPendingChanges());
|
|
}
|
|
|
|
@PostMapping("/changes/{id}/approve")
|
|
public ApiResponse<Void> approveChange(@PathVariable Long id,
|
|
@RequestBody(required = false) Map<String, Object> body,
|
|
HttpServletRequest request) {
|
|
checkAdmin(request);
|
|
Long reviewerId = (Long) request.getAttribute("accountId");
|
|
@SuppressWarnings("unchecked")
|
|
List<String> names = body != null ? (List<String>) body.get("names") : null;
|
|
@SuppressWarnings("unchecked")
|
|
List<String> urls = body != null ? (List<String>) body.get("urls") : null;
|
|
String siteType = body != null ? (String) body.get("siteType") : null;
|
|
String status = body != null ? (String) body.get("status") : null;
|
|
String notes = body != null ? (String) body.get("notes") : null;
|
|
siteService.approveChange(id, reviewerId, names, urls, siteType, status, notes);
|
|
log.info("Change approved: id={}, reviewer={}", id, reviewerId);
|
|
return ApiResponse.success();
|
|
}
|
|
|
|
@PostMapping("/changes/{id}/reject")
|
|
public ApiResponse<Void> rejectChange(@PathVariable Long id, HttpServletRequest request) {
|
|
checkAdmin(request);
|
|
Long reviewerId = (Long) request.getAttribute("accountId");
|
|
siteService.rejectChange(id, reviewerId);
|
|
log.info("Change rejected: id={}", id);
|
|
return ApiResponse.success();
|
|
}
|
|
|
|
@GetMapping("/sites/{siteId}/changes")
|
|
public ApiResponse<List<com.par.core.dto.SiteChangeDTO>> listSiteChanges(
|
|
@PathVariable("siteId") String siteId, HttpServletRequest request) {
|
|
checkAdmin(request);
|
|
return ApiResponse.success(siteService.listChangesBySite(siteId));
|
|
}
|
|
|
|
@GetMapping("/sites/search")
|
|
public ApiResponse<List<com.par.core.dto.SiteDTO>> searchSites(
|
|
@RequestParam("q") String q, HttpServletRequest request) {
|
|
checkAdmin(request);
|
|
return ApiResponse.success(siteService.searchSites(q));
|
|
}
|
|
|
|
@PostMapping("/changes/{id}/link")
|
|
public ApiResponse<Void> linkChangeToSite(
|
|
@PathVariable Long id,
|
|
@RequestBody Map<String, String> payload,
|
|
HttpServletRequest request) {
|
|
checkAdmin(request);
|
|
String targetSiteId = payload.get("siteId");
|
|
if (targetSiteId == null || targetSiteId.isBlank()) {
|
|
return ApiResponse.error(400, "siteId is required");
|
|
}
|
|
siteService.linkChangeToSite(id, targetSiteId);
|
|
log.info("Change linked: changeId={}, siteId={}", id, targetSiteId);
|
|
return ApiResponse.success();
|
|
}
|
|
|
|
private void checkAdmin(HttpServletRequest request) {
|
|
TrustLevel level = (TrustLevel) request.getAttribute("trustLevel");
|
|
if (level == null || level != TrustLevel.ADMIN) {
|
|
throw new IllegalArgumentException("Admin permission required");
|
|
}
|
|
}
|
|
}
|