fix(admin): 补齐 /api/admin 方法级鉴权并加兜底过滤器

ShopKey/SkipPriceAsin/QueryAsin/ShopManage/ProductCategory 五个 controller
此前无任何 require* 鉴权,SecurityConfig 全局 permitAll 且 nginx 无 ACL,
公网 18080 上可匿名读写店铺密钥等敏感数据。统一补 requireAdminOrInternal;
其中 ShopManage/SkipPrice/QueryAsin 原先信任调用方传入 operatorId/superAdmin,
改为从已认证 principal 推导,消除传 superAdmin=true 越权扩大数据范围的问题。

新增 AdminApiGuardFilter 对 /api/admin/** 兜底要求有效身份(JWT 或内部令牌),
防止后续新增端点遗漏鉴权再裸奔;支持 admin-guard-enabled/exempt-prefixes 配置。
This commit is contained in:
2026-09-04 10:21:12 +08:00
parent 1474f69c73
commit e5909c9977
7 changed files with 436 additions and 116 deletions
@@ -0,0 +1,101 @@
package com.nanri.aiimage.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.common.api.ApiResponse;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
/**
* /api/admin/** 兜底鉴权过滤器。
*
* <p>SecurityConfig 全局 permitAll 且无路径级拦截,历史上有多个 /api/admin 控制器遗漏方法级
* require*(公网 18080 直连可达即匿名裸奔)。本过滤器作为第二道网:凡进入 /api/admin 的请求
* 必须具备"有效身份"Java JWT,或可信内部令牌 X-Internal-Token + operatorId,复用
* AdminAuthSupport),否则直接返回与全局异常一致的 401 响应体(HTTP 200 + {success:false,code})。
*
* <p>只保证"已认证",管理员/菜单级校验仍由各控制器 require* 负责,避免拦截普通登录用户本可
* 访问的 /api/admin 接口(如权限菜单列表仅 requireUser)。
*
* <p>豁免项:OPTIONS 预检;以及 aiimage.security.admin-guard-exempt-prefixes 配置的前缀
* (逗号分隔,用于内部自动化/设备回调等确需匿名可达的端点)。紧急回退:
* aiimage.security.admin-guard-enabled=false 关闭本过滤器。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class AdminApiGuardFilter extends OncePerRequestFilter {
private static final String ADMIN_API_PREFIX = "/api/admin";
private final AdminAuthSupport adminAuthSupport;
private final ObjectMapper objectMapper;
@Value("${aiimage.security.admin-guard-enabled:true}")
private boolean enabled;
@Value("${aiimage.security.admin-guard-exempt-prefixes:}")
private String exemptPrefixes;
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
if (!enabled) {
return true;
}
String uri = request.getRequestURI();
if (!(uri.equals(ADMIN_API_PREFIX) || uri.startsWith(ADMIN_API_PREFIX + "/"))) {
return true;
}
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
for (String prefix : exemptSet()) {
if (uri.startsWith(prefix)) {
return true;
}
}
return false;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
adminAuthSupport.requireUserOrInternal(request);
} catch (BusinessException ex) {
ApiResponse<Void> body = ex.getCode() == null
? ApiResponse.fail(ex.getMessage())
: ApiResponse.fail(ex.getCode(), ex.getMessage());
log.warn("[admin-guard] {} {} rejected: {}", request.getMethod(), request.getRequestURI(), ex.getMessage());
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(body));
return;
}
chain.doFilter(request, response);
}
private Set<String> exemptSet() {
if (exemptPrefixes == null || exemptPrefixes.isBlank()) {
return Collections.emptySet();
}
return Arrays.stream(exemptPrefixes.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toSet());
}
}
@@ -2,12 +2,14 @@ package com.nanri.aiimage.modules.productcategory.controller;
import com.nanri.aiimage.common.api.ApiResponse;
import com.nanri.aiimage.common.util.DownloadHeaderUtil;
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
import com.nanri.aiimage.modules.productcategory.model.dto.ProductCategorySaveRequest;
import com.nanri.aiimage.modules.productcategory.model.vo.ProductCategoryItemVo;
import com.nanri.aiimage.modules.productcategory.model.vo.ProductCategoryListVo;
import com.nanri.aiimage.modules.productcategory.service.ProductCategoryService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
@@ -35,34 +37,45 @@ public class ProductCategoryController {
private static final DateTimeFormatter EXPORT_FILENAME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
private final ProductCategoryService productCategoryService;
private final AdminAuthSupport adminAuthSupport;
@GetMapping("/product-categories")
@Operation(summary = "查询商品类目树")
public ApiResponse<ProductCategoryListVo> list(@RequestParam(value = "keyword", required = false) String keyword) {
public ApiResponse<ProductCategoryListVo> list(
HttpServletRequest request,
@RequestParam(value = "keyword", required = false) String keyword) {
adminAuthSupport.requireAdminOrInternal(request);
return ApiResponse.success(productCategoryService.list(keyword));
}
@GetMapping("/product-categories/children")
@Operation(summary = "分页查询指定父级下的商品类目")
public ApiResponse<ProductCategoryListVo> children(
HttpServletRequest request,
@RequestParam(value = "parentId", required = false) Long parentId,
@RequestParam(value = "page", defaultValue = "1") Long page,
@RequestParam(value = "pageSize", defaultValue = "20") Long pageSize) {
adminAuthSupport.requireAdminOrInternal(request);
return ApiResponse.success(productCategoryService.children(parentId, page, pageSize));
}
@GetMapping("/product-categories/search")
@Operation(summary = "分页搜索商品类目")
public ApiResponse<ProductCategoryListVo> search(
HttpServletRequest request,
@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "page", defaultValue = "1") Long page,
@RequestParam(value = "pageSize", defaultValue = "20") Long pageSize) {
adminAuthSupport.requireAdminOrInternal(request);
return ApiResponse.success(productCategoryService.search(keyword, page, pageSize));
}
@GetMapping("/product-categories/export")
@Operation(summary = "导出商品类目")
public ResponseEntity<byte[]> export(@RequestParam(value = "keyword", required = false) String keyword) {
public ResponseEntity<byte[]> export(
HttpServletRequest request,
@RequestParam(value = "keyword", required = false) String keyword) {
adminAuthSupport.requireAdminOrInternal(request);
byte[] bytes = productCategoryService.export(keyword);
String filename = "product-categories-" + LocalDateTime.now().format(EXPORT_FILENAME_FORMATTER) + ".xlsx";
return ResponseEntity.ok()
@@ -74,20 +87,29 @@ public class ProductCategoryController {
@PostMapping("/product-category")
@Operation(summary = "新增商品类目")
public ApiResponse<ProductCategoryItemVo> create(@Valid @RequestBody ProductCategorySaveRequest request) {
return ApiResponse.success("创建成功", productCategoryService.create(request));
public ApiResponse<ProductCategoryItemVo> create(
HttpServletRequest request,
@Valid @RequestBody ProductCategorySaveRequest body) {
adminAuthSupport.requireAdminOrInternal(request);
return ApiResponse.success("创建成功", productCategoryService.create(body));
}
@PutMapping("/product-category/{id}")
@Operation(summary = "更新商品类目")
public ApiResponse<ProductCategoryItemVo> update(@PathVariable Long id,
@Valid @RequestBody ProductCategorySaveRequest request) {
return ApiResponse.success("保存成功", productCategoryService.update(id, request));
public ApiResponse<ProductCategoryItemVo> update(
HttpServletRequest request,
@PathVariable Long id,
@Valid @RequestBody ProductCategorySaveRequest body) {
adminAuthSupport.requireAdminOrInternal(request);
return ApiResponse.success("保存成功", productCategoryService.update(id, body));
}
@DeleteMapping("/product-category/{id}")
@Operation(summary = "删除商品类目")
public ApiResponse<Void> delete(@PathVariable Long id) {
public ApiResponse<Void> delete(
HttpServletRequest request,
@PathVariable Long id) {
adminAuthSupport.requireAdminOrInternal(request);
productCategoryService.delete(id);
return ApiResponse.success("删除成功", null);
}
@@ -2,6 +2,8 @@ package com.nanri.aiimage.modules.shopkey.controller;
import com.nanri.aiimage.common.api.ApiResponse;
import com.nanri.aiimage.common.util.DownloadHeaderUtil;
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
import com.nanri.aiimage.modules.shopkey.model.dto.QueryAsinCountryUpdateRequest;
import com.nanri.aiimage.modules.shopkey.model.dto.QueryAsinCreateRequest;
import com.nanri.aiimage.modules.shopkey.model.vo.QueryAsinImportProgressVo;
@@ -12,6 +14,7 @@ import com.nanri.aiimage.modules.shopkey.service.QueryAsinService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
@@ -40,18 +43,24 @@ public class QueryAsinController {
private static final DateTimeFormatter EXPORT_FILENAME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
private final QueryAsinService queryAsinService;
private final AdminAuthSupport adminAuthSupport;
/** 解析当前操作者(JWT 或内部令牌通道),权限与数据范围一律以该身份为准。 */
private AdminUserEntity requireOperator(HttpServletRequest request) {
return adminAuthSupport.requireAdminOrInternal(request);
}
@GetMapping
@Operation(summary = "分页查询 ASIN")
public ApiResponse<QueryAsinPageVo> page(
HttpServletRequest request,
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Long page,
@Parameter(description = "每页数量") @RequestParam(defaultValue = "15") Long pageSize,
@Parameter(description = "分组ID") @RequestParam(required = false) Long groupId,
@Parameter(description = "店铺名") @RequestParam(required = false) String shopName,
@Parameter(description = "商品 ASIN") @RequestParam(required = false) String asin,
@Parameter(description = "国家代码(DE/UK/FR/IT/ES)") @RequestParam(required = false) String country,
@Parameter(description = "当前操作人用户ID") @RequestParam(required = false) Long operatorId,
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
@Parameter(description = "国家代码(DE/UK/FR/IT/ES)") @RequestParam(required = false) String country) {
AdminUserEntity operator = requireOperator(request);
return ApiResponse.success(queryAsinService.page(
page,
pageSize,
@@ -59,20 +68,21 @@ public class QueryAsinController {
shopName,
asin,
country,
operatorId,
Boolean.TRUE.equals(superAdmin)));
operator.getId(),
isSuper(operator)));
}
@GetMapping("/export")
@Operation(summary = "导出查询 ASIN")
public ResponseEntity<byte[]> export(
HttpServletRequest request,
@Parameter(description = "分组ID") @RequestParam(required = false) Long groupId,
@Parameter(description = "店铺名") @RequestParam(required = false) String shopName,
@Parameter(description = "商品 ASIN") @RequestParam(required = false) String asin,
@Parameter(description = "国家代码(DE/UK/FR/IT/ES)") @RequestParam(required = false) String country,
@Parameter(description = "当前操作人用户ID") @RequestParam(required = false) Long operatorId,
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
byte[] bytes = queryAsinService.export(groupId, shopName, asin, country, operatorId, Boolean.TRUE.equals(superAdmin));
@Parameter(description = "国家代码(DE/UK/FR/IT/ES)") @RequestParam(required = false) String country) {
AdminUserEntity operator = requireOperator(request);
byte[] bytes = queryAsinService.export(groupId, shopName, asin, country,
operator.getId(), isSuper(operator));
String filename = "query-asin-" + LocalDateTime.now().format(EXPORT_FILENAME_FORMATTER) + ".xlsx";
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, DownloadHeaderUtil.contentDisposition(filename))
@@ -84,71 +94,81 @@ public class QueryAsinController {
@PostMapping
@Operation(summary = "新增或覆盖查询 ASIN")
public ApiResponse<QueryAsinItemVo> create(
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin,
@Valid @RequestBody QueryAsinCreateRequest request) {
HttpServletRequest request,
@Valid @RequestBody QueryAsinCreateRequest body) {
AdminUserEntity operator = requireOperator(request);
return ApiResponse.success("保存成功",
queryAsinService.createOrUpdate(request, operatorId, Boolean.TRUE.equals(superAdmin)));
queryAsinService.createOrUpdate(body, operator.getId(), isSuper(operator)));
}
@PostMapping("/import")
@Operation(summary = "导入添加查询 ASIN")
public ApiResponse<QueryAsinImportStartVo> importExcel(
HttpServletRequest request,
@Parameter(description = "xlsx/xls 文件", required = true) @RequestParam("file") MultipartFile file,
@Parameter(description = "Excel 未提供分组时使用的分组ID") @RequestParam(required = false) Long groupId,
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
@Parameter(description = "Excel 未提供分组时使用的分组ID") @RequestParam(required = false) Long groupId) {
AdminUserEntity operator = requireOperator(request);
return ApiResponse.success("开始导入",
queryAsinService.startImport(file, groupId, operatorId, Boolean.TRUE.equals(superAdmin)));
queryAsinService.startImport(file, groupId, operator.getId(), isSuper(operator)));
}
@GetMapping("/import/{importId}")
@Operation(summary = "查询导入添加进度")
public ApiResponse<QueryAsinImportProgressVo> importProgress(@PathVariable String importId) {
public ApiResponse<QueryAsinImportProgressVo> importProgress(
HttpServletRequest request,
@Parameter(description = "导入任务 ID", required = true) @PathVariable String importId) {
requireOperator(request);
return ApiResponse.success(queryAsinService.getImportProgress(importId));
}
@PostMapping("/delete-import")
@Operation(summary = "导入删除查询 ASIN")
public ApiResponse<QueryAsinImportStartVo> deleteImportExcel(
HttpServletRequest request,
@Parameter(description = "xlsx/xls 文件", required = true) @RequestParam("file") MultipartFile file,
@Parameter(description = "Excel 未提供分组时使用的分组ID") @RequestParam(required = false) Long groupId,
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
@Parameter(description = "Excel 未提供分组时使用的分组ID") @RequestParam(required = false) Long groupId) {
AdminUserEntity operator = requireOperator(request);
return ApiResponse.success("开始删除",
queryAsinService.startDeleteImport(file, groupId, operatorId, Boolean.TRUE.equals(superAdmin)));
queryAsinService.startDeleteImport(file, groupId, operator.getId(), isSuper(operator)));
}
@GetMapping("/delete-import/{importId}")
@Operation(summary = "查询导入删除进度")
public ApiResponse<QueryAsinImportProgressVo> deleteImportProgress(@PathVariable String importId) {
public ApiResponse<QueryAsinImportProgressVo> deleteImportProgress(
HttpServletRequest request,
@Parameter(description = "导入删除任务 ID", required = true) @PathVariable String importId) {
requireOperator(request);
return ApiResponse.success(queryAsinService.getDeleteImportProgress(importId));
}
@PutMapping("/{id}/countries/{country}")
@Operation(summary = "编辑指定国家的查询 ASIN")
public ApiResponse<QueryAsinItemVo> updateCountry(
HttpServletRequest request,
@Parameter(description = "主键ID", required = true) @PathVariable Long id,
@Parameter(description = "国家编码", required = true) @PathVariable String country,
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin,
@Valid @RequestBody QueryAsinCountryUpdateRequest request) {
@Valid @RequestBody QueryAsinCountryUpdateRequest body) {
AdminUserEntity operator = requireOperator(request);
return ApiResponse.success("保存成功", queryAsinService.updateCountry(
id,
country,
request.getAsin(),
operatorId,
Boolean.TRUE.equals(superAdmin)));
body.getAsin(),
operator.getId(),
isSuper(operator)));
}
@DeleteMapping("/{id}/countries/{country}")
@Operation(summary = "删除指定国家的查询 ASIN")
public ApiResponse<Void> deleteCountry(
HttpServletRequest request,
@Parameter(description = "主键ID", required = true) @PathVariable Long id,
@Parameter(description = "国家编码", required = true) @PathVariable String country,
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
queryAsinService.deleteCountry(id, country, operatorId, Boolean.TRUE.equals(superAdmin));
@Parameter(description = "国家编码", required = true) @PathVariable String country) {
AdminUserEntity operator = requireOperator(request);
queryAsinService.deleteCountry(id, country, operator.getId(), isSuper(operator));
return ApiResponse.success("删除成功", null);
}
private boolean isSuper(AdminUserEntity operator) {
return "super_admin".equals(adminAuthSupport.currentRole(operator));
}
}
@@ -1,6 +1,7 @@
package com.nanri.aiimage.modules.shopkey.controller;
import com.nanri.aiimage.common.api.ApiResponse;
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
import com.nanri.aiimage.modules.shopkey.model.dto.ShopKeyCreateRequest;
import com.nanri.aiimage.modules.shopkey.model.dto.ShopKeyUpdateRequest;
import com.nanri.aiimage.modules.shopkey.model.vo.ShopKeyItemVo;
@@ -12,6 +13,7 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
@@ -31,6 +33,7 @@ import org.springframework.web.bind.annotation.RestController;
public class ShopKeyController {
private final ShopKeyService shopKeyService;
private final AdminAuthSupport adminAuthSupport;
@GetMapping
@Operation(summary = "分页查询店铺密钥", description = "分页查询店铺密钥列表。")
@@ -38,8 +41,10 @@ public class ShopKeyController {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = ShopKeyPageVo.class)))
})
public ApiResponse<ShopKeyPageVo> page(
HttpServletRequest request,
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Long page,
@Parameter(description = "每页数量") @RequestParam(defaultValue = "15") Long pageSize) {
adminAuthSupport.requireAdminOrInternal(request);
return ApiResponse.success(shopKeyService.page(page, pageSize));
}
@@ -49,8 +54,11 @@ public class ShopKeyController {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "创建成功", content = @Content(schema = @Schema(implementation = ShopKeyItemVo.class))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "参数不合法")
})
public ApiResponse<ShopKeyItemVo> create(@Valid @RequestBody ShopKeyCreateRequest request) {
return ApiResponse.success("创建成功", shopKeyService.create(request));
public ApiResponse<ShopKeyItemVo> create(
HttpServletRequest request,
@Valid @RequestBody ShopKeyCreateRequest body) {
adminAuthSupport.requireAdminOrInternal(request);
return ApiResponse.success("创建成功", shopKeyService.create(body));
}
@PutMapping("/{id}")
@@ -61,9 +69,11 @@ public class ShopKeyController {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "记录不存在")
})
public ApiResponse<ShopKeyItemVo> update(
HttpServletRequest request,
@Parameter(description = "主键ID", required = true) @PathVariable Long id,
@Valid @RequestBody ShopKeyUpdateRequest request) {
return ApiResponse.success("更新成功", shopKeyService.update(id, request));
@Valid @RequestBody ShopKeyUpdateRequest body) {
adminAuthSupport.requireAdminOrInternal(request);
return ApiResponse.success("更新成功", shopKeyService.update(id, body));
}
@DeleteMapping("/{id}")
@@ -72,7 +82,10 @@ public class ShopKeyController {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "删除成功"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "记录不存在")
})
public ApiResponse<Void> delete(@Parameter(description = "主键ID", required = true) @PathVariable Long id) {
public ApiResponse<Void> delete(
HttpServletRequest request,
@Parameter(description = "主键ID", required = true) @PathVariable Long id) {
adminAuthSupport.requireAdminOrInternal(request);
shopKeyService.delete(id);
return ApiResponse.success("删除成功", null);
}
@@ -1,6 +1,8 @@
package com.nanri.aiimage.modules.shopkey.controller;
import com.nanri.aiimage.common.api.ApiResponse;
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
import com.nanri.aiimage.modules.shopkey.model.dto.ShopManageCreateRequest;
import com.nanri.aiimage.modules.shopkey.model.dto.ShopManageGroupCreateRequest;
import com.nanri.aiimage.modules.shopkey.model.dto.ShopManageGroupUpdateRequest;
@@ -17,6 +19,7 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
@@ -37,11 +40,18 @@ import org.springframework.web.bind.annotation.RestController;
@Tag(name = "店铺管理", description = "维护店铺信息,支持查询、新建、修改、删除")
public class ShopManageController {
@Value("${aiimage.security.internal-token:}")
private String internalToken;
private final ShopManageService shopManageService;
private final ShopManageGroupService shopManageGroupService;
private final AdminAuthSupport adminAuthSupport;
/** 解析当前操作者(JWT 或内部令牌通道),数据范围一律以该身份为准,不信任调用方传入的 operatorId/superAdmin。 */
private AdminUserEntity requireOperator(HttpServletRequest request) {
return adminAuthSupport.requireAdminOrInternal(request);
}
private boolean isSuper(AdminUserEntity operator) {
return "super_admin".equals(adminAuthSupport.currentRole(operator));
}
@GetMapping
@Operation(summary = "分页查询店铺", description = "支持按分组、店铺名、当前操作者可见范围筛选")
@@ -52,92 +62,92 @@ public class ShopManageController {
content = @Content(schema = @Schema(implementation = ShopManagePageVo.class)))
})
public ApiResponse<ShopManagePageVo> page(
HttpServletRequest request,
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Long page,
@Parameter(description = "每页数量") @RequestParam(defaultValue = "15") Long pageSize,
@Parameter(description = "分组 ID") @RequestParam(required = false) Long groupId,
@Parameter(description = "店铺名称") @RequestParam(required = false) String shopName,
@Parameter(description = "当前操作人用户ID") @RequestParam(required = false) Long operatorId,
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
@Parameter(description = "店铺名称") @RequestParam(required = false) String shopName) {
AdminUserEntity operator = requireOperator(request);
return ApiResponse.success(shopManageService.page(
page,
pageSize,
groupId,
shopName,
operatorId,
Boolean.TRUE.equals(superAdmin)));
operator.getId(),
isSuper(operator)));
}
@PostMapping
@Operation(summary = "新增店铺", description = "创建店铺记录,并写入创建人")
public ApiResponse<ShopManageItemVo> create(
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin,
@Valid @RequestBody ShopManageCreateRequest request) {
HttpServletRequest request,
@Valid @RequestBody ShopManageCreateRequest body) {
AdminUserEntity operator = requireOperator(request);
return ApiResponse.success("创建成功",
shopManageService.create(request, operatorId, Boolean.TRUE.equals(superAdmin)));
shopManageService.create(body, operator.getId(), isSuper(operator)));
}
@PutMapping("/{id}")
@Operation(summary = "更新店铺", description = "按 ID 更新店铺")
public ApiResponse<ShopManageItemVo> update(
HttpServletRequest request,
@Parameter(description = "主键 ID", required = true) @PathVariable Long id,
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin,
@Valid @RequestBody ShopManageUpdateRequest request) {
@Valid @RequestBody ShopManageUpdateRequest body) {
AdminUserEntity operator = requireOperator(request);
return ApiResponse.success("更新成功",
shopManageService.update(id, operatorId, Boolean.TRUE.equals(superAdmin), request));
shopManageService.update(id, operator.getId(), isSuper(operator), body));
}
@DeleteMapping("/{id}")
@Operation(summary = "删除店铺", description = "按 ID 删除店铺")
public ApiResponse<Void> delete(
@Parameter(description = "主键 ID", required = true) @PathVariable Long id,
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
shopManageService.delete(id, operatorId, Boolean.TRUE.equals(superAdmin));
HttpServletRequest request,
@Parameter(description = "主键 ID", required = true) @PathVariable Long id) {
AdminUserEntity operator = requireOperator(request);
shopManageService.delete(id, operator.getId(), isSuper(operator));
return ApiResponse.success("删除成功", null);
}
@GetMapping("/groups")
@Operation(summary = "查询分组列表", description = "普通管理员返回当前可访问的分组,超级管理员返回全部")
public ApiResponse<java.util.List<ShopManageGroupItemVo>> listGroups(
@Parameter(description = "创建人用户ID") @RequestParam(required = false) Long createdById,
@Parameter(description = "当前操作人用户ID") @RequestParam(required = false) Long operatorId,
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
if (Boolean.TRUE.equals(superAdmin)) {
HttpServletRequest request,
@Parameter(description = "创建人用户ID") @RequestParam(required = false) Long createdById) {
AdminUserEntity operator = requireOperator(request);
if (isSuper(operator)) {
return ApiResponse.success(shopManageGroupService.list(createdById));
}
return ApiResponse.success(shopManageGroupService.listAccessible(operatorId, false));
return ApiResponse.success(shopManageGroupService.listAccessible(operator.getId(), false));
}
@PostMapping("/groups")
@Operation(summary = "新增分组", description = "创建分组并写入创建人")
public ApiResponse<ShopManageGroupItemVo> createGroup(
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin,
@Valid @RequestBody ShopManageGroupCreateRequest request) {
HttpServletRequest request,
@Valid @RequestBody ShopManageGroupCreateRequest body) {
AdminUserEntity operator = requireOperator(request);
return ApiResponse.success("创建成功",
shopManageGroupService.create(request, operatorId, Boolean.TRUE.equals(superAdmin)));
shopManageGroupService.create(body, operator.getId(), isSuper(operator)));
}
@PutMapping("/groups/{id}")
@Operation(summary = "更新分组", description = "普通管理员只能修改自己可访问的分组")
public ApiResponse<ShopManageGroupItemVo> updateGroup(
HttpServletRequest request,
@Parameter(description = "主键 ID", required = true) @PathVariable Long id,
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin,
@Valid @RequestBody ShopManageGroupUpdateRequest request) {
@Valid @RequestBody ShopManageGroupUpdateRequest body) {
AdminUserEntity operator = requireOperator(request);
return ApiResponse.success("更新成功",
shopManageGroupService.update(id, operatorId, Boolean.TRUE.equals(superAdmin), request));
shopManageGroupService.update(id, operator.getId(), isSuper(operator), body));
}
@DeleteMapping("/groups/{id}")
@Operation(summary = "删除分组", description = "普通管理员只能删除自己可访问的分组")
public ApiResponse<Void> deleteGroup(
@Parameter(description = "主键 ID", required = true) @PathVariable Long id,
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
shopManageGroupService.delete(id, operatorId, Boolean.TRUE.equals(superAdmin));
HttpServletRequest request,
@Parameter(description = "主键 ID", required = true) @PathVariable Long id) {
AdminUserEntity operator = requireOperator(request);
shopManageGroupService.delete(id, operator.getId(), isSuper(operator));
return ApiResponse.success("删除成功", null);
}
@@ -151,4 +161,17 @@ public class ShopManageController {
}
return ApiResponse.success(shopManageService.getCredentialByShopName(shopName));
}
@GetMapping("/{id}/credential")
@Operation(summary = "查看店铺明文凭据(管理端)", description = "管理员在后台查看店铺明文凭据;受管理员身份保护(建议后续补充归属/角色细粒度校验)")
public ApiResponse<ShopManageCredentialVo> adminCredential(
HttpServletRequest request,
@Parameter(description = "店铺主键 ID") @PathVariable Long id,
@Parameter(description = "店铺名") @RequestParam("shop_name") String shopName) {
requireOperator(request);
return ApiResponse.success(shopManageService.getCredentialByShopName(shopName));
}
@Value("${aiimage.security.internal-token:}")
private String internalToken;
}
@@ -2,6 +2,8 @@ package com.nanri.aiimage.modules.shopkey.controller;
import com.nanri.aiimage.common.api.ApiResponse;
import com.nanri.aiimage.common.util.DownloadHeaderUtil;
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
import com.nanri.aiimage.modules.shopkey.model.dto.SkipPriceAsinCountryUpdateRequest;
import com.nanri.aiimage.modules.shopkey.model.dto.SkipPriceAsinCreateRequest;
import com.nanri.aiimage.modules.shopkey.model.vo.QueryAsinImportProgressVo;
@@ -12,6 +14,7 @@ import com.nanri.aiimage.modules.shopkey.service.SkipPriceAsinService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
@@ -31,7 +34,6 @@ import org.springframework.web.multipart.MultipartFile;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.math.BigDecimal;
@RestController
@RequiredArgsConstructor
@@ -42,10 +44,17 @@ public class SkipPriceAsinController {
private static final DateTimeFormatter EXPORT_FILENAME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
private final SkipPriceAsinService skipPriceAsinService;
private final AdminAuthSupport adminAuthSupport;
/** 解析当前操作者(JWT 或内部令牌通道),权限与数据范围一律以该身份为准。 */
private AdminUserEntity requireOperator(HttpServletRequest request) {
return adminAuthSupport.requireAdminOrInternal(request);
}
@GetMapping
@Operation(summary = "分页查询跳过跟价 ASIN", description = "支持按分组、店铺名、ASIN 和当前操作人可见范围筛选。")
public ApiResponse<SkipPriceAsinPageVo> page(
HttpServletRequest request,
@Parameter(description = "页码") @RequestParam(name = "page", defaultValue = "1") Long page,
@Parameter(description = "每页数量") @RequestParam(name = "page_size", defaultValue = "15") Long pageSize,
@Parameter(description = "分组 ID") @RequestParam(name = "group_id", required = false) Long groupId,
@@ -53,9 +62,8 @@ public class SkipPriceAsinController {
@Parameter(description = "商品 ASIN") @RequestParam(name = "asin", required = false) String asin,
@Parameter(description = "国家代码(DE/UK/FR/IT/ES)") @RequestParam(name = "country", required = false) String country,
@Parameter(description = "最低价下限") @RequestParam(name = "minimum_price_from", required = false) BigDecimal minimumPriceFrom,
@Parameter(description = "最低价上限") @RequestParam(name = "minimum_price_to", required = false) BigDecimal minimumPriceTo,
@Parameter(description = "当前操作人用户 ID") @RequestParam(name = "operator_id", required = false) Long operatorId,
@Parameter(description = "是否超级管理员") @RequestParam(name = "super_admin", defaultValue = "false") Boolean superAdmin) {
@Parameter(description = "最低价上限") @RequestParam(name = "minimum_price_to", required = false) BigDecimal minimumPriceTo) {
AdminUserEntity operator = requireOperator(request);
return ApiResponse.success(skipPriceAsinService.page(
page,
pageSize,
@@ -65,23 +73,23 @@ public class SkipPriceAsinController {
country,
minimumPriceFrom,
minimumPriceTo,
operatorId,
Boolean.TRUE.equals(superAdmin)));
operator.getId(),
isSuper(operator)));
}
@GetMapping("/export")
@Operation(summary = "导出跳过跟价 ASIN", description = "按当前查询条件导出 Excel。")
public ResponseEntity<byte[]> export(
HttpServletRequest request,
@Parameter(description = "分组 ID") @RequestParam(name = "group_id", required = false) Long groupId,
@Parameter(description = "店铺名称") @RequestParam(name = "shop_name", required = false) String shopName,
@Parameter(description = "商品 ASIN") @RequestParam(name = "asin", required = false) String asin,
@Parameter(description = "国家代码(DE/UK/FR/IT/ES)") @RequestParam(name = "country", required = false) String country,
@Parameter(description = "最低价下限") @RequestParam(name = "minimum_price_from", required = false) BigDecimal minimumPriceFrom,
@Parameter(description = "最低价上限") @RequestParam(name = "minimum_price_to", required = false) BigDecimal minimumPriceTo,
@Parameter(description = "当前操作人用户 ID") @RequestParam(name = "operator_id", required = false) Long operatorId,
@Parameter(description = "是否超级管理员") @RequestParam(name = "super_admin", defaultValue = "false") Boolean superAdmin) {
@Parameter(description = "最低价上限") @RequestParam(name = "minimum_price_to", required = false) BigDecimal minimumPriceTo) {
AdminUserEntity operator = requireOperator(request);
byte[] bytes = skipPriceAsinService.export(groupId, shopName, asin, country, minimumPriceFrom, minimumPriceTo,
operatorId, Boolean.TRUE.equals(superAdmin));
operator.getId(), isSuper(operator));
String filename = "skip-price-asin-" + LocalDateTime.now().format(EXPORT_FILENAME_FORMATTER) + ".xlsx";
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, DownloadHeaderUtil.contentDisposition(filename))
@@ -93,74 +101,82 @@ public class SkipPriceAsinController {
@PostMapping
@Operation(summary = "新增跳过跟价 ASIN", description = "新增一条跳过跟价 ASIN 记录。")
public ApiResponse<SkipPriceAsinItemVo> create(
@Parameter(description = "当前操作人用户 ID") @RequestParam(name = "operator_id") Long operatorId,
@Parameter(description = "是否超级管理员") @RequestParam(name = "super_admin", defaultValue = "false") Boolean superAdmin,
@Valid @RequestBody SkipPriceAsinCreateRequest request) {
HttpServletRequest request,
@Valid @RequestBody SkipPriceAsinCreateRequest body) {
AdminUserEntity operator = requireOperator(request);
return ApiResponse.success("保存成功",
skipPriceAsinService.create(request, operatorId, Boolean.TRUE.equals(superAdmin)));
skipPriceAsinService.create(body, operator.getId(), isSuper(operator)));
}
@PostMapping("/import")
@Operation(summary = "导入跳过跟价 ASIN", description = "上传 xlsx 或 xls 文件,异步导入新增数据。")
public ApiResponse<QueryAsinImportStartVo> importExcel(
HttpServletRequest request,
@Parameter(description = "xlsx 或 xls 文件", required = true) @RequestParam("file") MultipartFile file,
@Parameter(description = "分组 ID", required = true) @RequestParam(name = "group_id") Long groupId,
@Parameter(description = "当前操作人用户 ID") @RequestParam(name = "operator_id") Long operatorId,
@Parameter(description = "是否超级管理员") @RequestParam(name = "super_admin", defaultValue = "false") Boolean superAdmin) {
@Parameter(description = "分组 ID", required = true) @RequestParam(name = "group_id") Long groupId) {
AdminUserEntity operator = requireOperator(request);
return ApiResponse.success("开始导入",
skipPriceAsinService.startImport(file, groupId, operatorId, Boolean.TRUE.equals(superAdmin)));
skipPriceAsinService.startImport(file, groupId, operator.getId(), isSuper(operator)));
}
@GetMapping("/import/{importId}")
@Operation(summary = "查询导入进度")
public ApiResponse<QueryAsinImportProgressVo> importProgress(
HttpServletRequest request,
@Parameter(description = "导入任务 ID", required = true) @PathVariable String importId) {
requireOperator(request);
return ApiResponse.success(skipPriceAsinService.getImportProgress(importId));
}
@PostMapping("/delete-import")
@Operation(summary = "导入删除跳过跟价 ASIN", description = "上传 xlsx 或 xls 文件,异步按文件内容删除数据。")
public ApiResponse<QueryAsinImportStartVo> deleteImportExcel(
HttpServletRequest request,
@Parameter(description = "xlsx 或 xls 文件", required = true) @RequestParam("file") MultipartFile file,
@Parameter(description = "分组 ID", required = true) @RequestParam(name = "group_id") Long groupId,
@Parameter(description = "当前操作人用户 ID") @RequestParam(name = "operator_id") Long operatorId,
@Parameter(description = "是否超级管理员") @RequestParam(name = "super_admin", defaultValue = "false") Boolean superAdmin) {
@Parameter(description = "分组 ID", required = true) @RequestParam(name = "group_id") Long groupId) {
AdminUserEntity operator = requireOperator(request);
return ApiResponse.success("开始删除",
skipPriceAsinService.startDeleteImport(file, groupId, operatorId, Boolean.TRUE.equals(superAdmin)));
skipPriceAsinService.startDeleteImport(file, groupId, operator.getId(), isSuper(operator)));
}
@GetMapping("/delete-import/{importId}")
@Operation(summary = "查询导入删除进度")
public ApiResponse<QueryAsinImportProgressVo> deleteImportProgress(
HttpServletRequest request,
@Parameter(description = "导入删除任务 ID", required = true) @PathVariable String importId) {
requireOperator(request);
return ApiResponse.success(skipPriceAsinService.getDeleteImportProgress(importId));
}
@PutMapping("/{id}/countries/{country}")
@Operation(summary = "更新指定国家的跳过跟价 ASIN")
public ApiResponse<SkipPriceAsinItemVo> updateCountry(
HttpServletRequest request,
@Parameter(description = "主键 ID", required = true) @PathVariable Long id,
@Parameter(description = "国家代码", required = true) @PathVariable String country,
@Parameter(description = "当前操作人用户 ID") @RequestParam(name = "operator_id") Long operatorId,
@Parameter(description = "是否超级管理员") @RequestParam(name = "super_admin", defaultValue = "false") Boolean superAdmin,
@Valid @RequestBody SkipPriceAsinCountryUpdateRequest request) {
@Valid @RequestBody SkipPriceAsinCountryUpdateRequest body) {
AdminUserEntity operator = requireOperator(request);
return ApiResponse.success("保存成功", skipPriceAsinService.updateCountry(
id,
country,
request.getAsin(),
request.getMinimumPrice(),
operatorId,
Boolean.TRUE.equals(superAdmin)));
body.getAsin(),
body.getMinimumPrice(),
operator.getId(),
isSuper(operator)));
}
@DeleteMapping("/{id}/countries/{country}")
@Operation(summary = "删除指定国家的跳过跟价 ASIN")
public ApiResponse<Void> deleteCountry(
HttpServletRequest request,
@Parameter(description = "主键 ID", required = true) @PathVariable Long id,
@Parameter(description = "国家代码", required = true) @PathVariable String country,
@Parameter(description = "当前操作人用户 ID") @RequestParam(name = "operator_id") Long operatorId,
@Parameter(description = "是否超级管理员") @RequestParam(name = "super_admin", defaultValue = "false") Boolean superAdmin) {
skipPriceAsinService.deleteCountry(id, country, operatorId, Boolean.TRUE.equals(superAdmin));
@Parameter(description = "国家代码", required = true) @PathVariable String country) {
AdminUserEntity operator = requireOperator(request);
skipPriceAsinService.deleteCountry(id, country, operator.getId(), isSuper(operator));
return ApiResponse.success("删除成功", null);
}
private boolean isSuper(AdminUserEntity operator) {
return "super_admin".equals(adminAuthSupport.currentRole(operator));
}
}
@@ -0,0 +1,125 @@
package com.nanri.aiimage.config;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* /api/admin 兜底鉴权过滤器单测:非 admin 路径放行、OPTIONS/豁免前缀放行、
* 匿名拒绝返回与全局一致的 401 体、紧急开关可整体关闭。
*/
class AdminApiGuardFilterTest {
private final ObjectMapper objectMapper = new ObjectMapper();
private AdminApiGuardFilter newFilter(AdminAuthSupport adminAuthSupport, boolean enabled, String exemptPrefixes) {
AdminApiGuardFilter filter = new AdminApiGuardFilter(adminAuthSupport, objectMapper);
ReflectionTestUtils.setField(filter, "enabled", enabled);
ReflectionTestUtils.setField(filter, "exemptPrefixes", exemptPrefixes);
return filter;
}
@Test
void nonAdminApiPathIsNotGuarded() throws Exception {
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
AdminApiGuardFilter filter = newFilter(authSupport, true, "");
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/admin.html");
MockHttpServletResponse response = new MockHttpServletResponse();
MockFilterChain chain = new MockFilterChain();
filter.doFilter(request, response, chain);
verify(authSupport, never()).requireUserOrInternal(any());
assertThat(chain.getRequest()).isNotNull();
}
@Test
void validIdentityPassesThrough() throws Exception {
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
when(authSupport.requireUserOrInternal(any())).thenReturn(new AdminUserEntity());
AdminApiGuardFilter filter = newFilter(authSupport, true, "");
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/admin/users");
MockHttpServletResponse response = new MockHttpServletResponse();
MockFilterChain chain = new MockFilterChain();
filter.doFilter(request, response, chain);
assertThat(chain.getRequest()).isNotNull();
assertThat(response.getContentAsString()).isEmpty();
}
@Test
void anonymousAdminApiRequestRejectedWith401Body() throws Exception {
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
when(authSupport.requireUserOrInternal(any()))
.thenThrow(new BusinessException(401, "未登录"));
AdminApiGuardFilter filter = newFilter(authSupport, true, "");
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/admin/shop-keys");
MockHttpServletResponse response = new MockHttpServletResponse();
MockFilterChain chain = new MockFilterChain();
filter.doFilter(request, response, chain);
JsonNode body = objectMapper.readTree(response.getContentAsString());
assertThat(body.path("success").asBoolean()).isFalse();
assertThat(body.path("code").asInt()).isEqualTo(401);
assertThat(chain.getRequest()).isNull();
}
@Test
void optionsPreflightIsExempt() throws Exception {
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
AdminApiGuardFilter filter = newFilter(authSupport, true, "");
MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/api/admin/shop-keys");
MockHttpServletResponse response = new MockHttpServletResponse();
MockFilterChain chain = new MockFilterChain();
filter.doFilter(request, response, chain);
verify(authSupport, never()).requireUserOrInternal(any());
assertThat(chain.getRequest()).isNotNull();
}
@Test
void exemptPrefixPassesThrough() throws Exception {
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
AdminApiGuardFilter filter = newFilter(authSupport, true, "/api/admin/shop-credential-checks");
MockHttpServletRequest request =
new MockHttpServletRequest("GET", "/api/admin/shop-credential-checks/poll");
MockHttpServletResponse response = new MockHttpServletResponse();
MockFilterChain chain = new MockFilterChain();
filter.doFilter(request, response, chain);
verify(authSupport, never()).requireUserOrInternal(any());
assertThat(chain.getRequest()).isNotNull();
}
@Test
void disabledGuardPassesEverything() throws Exception {
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
AdminApiGuardFilter filter = newFilter(authSupport, false, "");
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/admin/shop-keys");
MockHttpServletResponse response = new MockHttpServletResponse();
MockFilterChain chain = new MockFilterChain();
filter.doFilter(request, response, chain);
verify(authSupport, never()).requireUserOrInternal(any());
assertThat(chain.getRequest()).isNotNull();
}
}