mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-10 03:27:54 +00:00
Complete phase 3 and 4 backend workflows
This commit is contained in:
parent
76808ca794
commit
6a9e0845d4
29 changed files with 878 additions and 41 deletions
|
|
@ -0,0 +1,100 @@
|
|||
package com.iflytek.skillhub.bootstrap;
|
||||
|
||||
import com.iflytek.skillhub.auth.entity.Role;
|
||||
import com.iflytek.skillhub.auth.entity.UserRoleBinding;
|
||||
import com.iflytek.skillhub.auth.local.LocalCredential;
|
||||
import com.iflytek.skillhub.auth.local.LocalCredentialRepository;
|
||||
import com.iflytek.skillhub.auth.repository.RoleRepository;
|
||||
import com.iflytek.skillhub.auth.repository.UserRoleBindingRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMember;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.user.UserAccount;
|
||||
import com.iflytek.skillhub.domain.user.UserAccountRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Seeds default admin account for Docker one-click startup.
|
||||
* Idempotent: skips if admin credential already exists.
|
||||
*/
|
||||
@Component
|
||||
@Profile("docker")
|
||||
public class DockerSeedDataRunner implements ApplicationRunner {
|
||||
|
||||
private static final String ADMIN_USER_ID = "docker-admin";
|
||||
private static final String ADMIN_USERNAME = "admin";
|
||||
private static final String ADMIN_PASSWORD = "Admin@2026";
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DockerSeedDataRunner.class);
|
||||
|
||||
private final UserAccountRepository userAccountRepository;
|
||||
private final LocalCredentialRepository localCredentialRepository;
|
||||
private final RoleRepository roleRepository;
|
||||
private final UserRoleBindingRepository userRoleBindingRepository;
|
||||
private final NamespaceRepository namespaceRepository;
|
||||
private final NamespaceMemberRepository namespaceMemberRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public DockerSeedDataRunner(UserAccountRepository userAccountRepository,
|
||||
LocalCredentialRepository localCredentialRepository,
|
||||
RoleRepository roleRepository,
|
||||
UserRoleBindingRepository userRoleBindingRepository,
|
||||
NamespaceRepository namespaceRepository,
|
||||
NamespaceMemberRepository namespaceMemberRepository,
|
||||
PasswordEncoder passwordEncoder) {
|
||||
this.userAccountRepository = userAccountRepository;
|
||||
this.localCredentialRepository = localCredentialRepository;
|
||||
this.roleRepository = roleRepository;
|
||||
this.userRoleBindingRepository = userRoleBindingRepository;
|
||||
this.namespaceRepository = namespaceRepository;
|
||||
this.namespaceMemberRepository = namespaceMemberRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void run(ApplicationArguments args) {
|
||||
if (localCredentialRepository.existsByUsernameIgnoreCase(ADMIN_USERNAME)) {
|
||||
log.info("Docker seed data already exists, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Create admin user account
|
||||
UserAccount admin = userAccountRepository.findById(ADMIN_USER_ID)
|
||||
.orElseGet(() -> userAccountRepository.save(
|
||||
new UserAccount(ADMIN_USER_ID, "Admin", "admin@skillhub.dev", null)
|
||||
));
|
||||
|
||||
// 2. Create local credential (username/password)
|
||||
localCredentialRepository.save(
|
||||
new LocalCredential(admin.getId(), ADMIN_USERNAME, passwordEncoder.encode(ADMIN_PASSWORD))
|
||||
);
|
||||
|
||||
// 3. Assign SUPER_ADMIN role
|
||||
Role superAdmin = roleRepository.findByCode("SUPER_ADMIN")
|
||||
.orElseThrow(() -> new IllegalStateException("Missing built-in role: SUPER_ADMIN"));
|
||||
boolean hasRole = userRoleBindingRepository.findByUserId(admin.getId()).stream()
|
||||
.anyMatch(b -> b.getRole().getCode().equals("SUPER_ADMIN"));
|
||||
if (!hasRole) {
|
||||
userRoleBindingRepository.save(new UserRoleBinding(admin.getId(), superAdmin));
|
||||
}
|
||||
|
||||
// 4. Ensure global namespace + membership
|
||||
Namespace globalNs = namespaceRepository.findBySlug("global")
|
||||
.orElseThrow(() -> new IllegalStateException("Missing built-in global namespace"));
|
||||
if (namespaceMemberRepository.findByNamespaceIdAndUserId(globalNs.getId(), admin.getId()).isEmpty()) {
|
||||
namespaceMemberRepository.save(new NamespaceMember(globalNs.getId(), admin.getId(), NamespaceRole.OWNER));
|
||||
}
|
||||
|
||||
log.info("Docker seed data initialized — admin account: {} / {}", ADMIN_USERNAME, ADMIN_PASSWORD);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +1,130 @@
|
|||
package com.iflytek.skillhub.compat;
|
||||
|
||||
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
||||
import com.iflytek.skillhub.compat.dto.ClawHubPublishResponse;
|
||||
import com.iflytek.skillhub.controller.support.ZipPackageExtractor;
|
||||
import com.iflytek.skillhub.domain.audit.AuditLogService;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.skill.SkillVisibility;
|
||||
import com.iflytek.skillhub.domain.skill.service.SkillPublishService;
|
||||
import com.iflytek.skillhub.domain.skill.service.SkillQueryService;
|
||||
import com.iflytek.skillhub.compat.dto.ClawHubResolveResponse;
|
||||
import com.iflytek.skillhub.compat.dto.ClawHubSearchResponse;
|
||||
import com.iflytek.skillhub.compat.dto.ClawHubSkillItem;
|
||||
import com.iflytek.skillhub.compat.dto.ClawHubWhoamiResponse;
|
||||
import com.iflytek.skillhub.service.SkillSearchAppService;
|
||||
import java.io.IOException;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.slf4j.MDC;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/compat/v1")
|
||||
public class ClawHubCompatController {
|
||||
|
||||
private final CanonicalSlugMapper mapper;
|
||||
private final SkillSearchAppService skillSearchAppService;
|
||||
private final SkillQueryService skillQueryService;
|
||||
private final SkillPublishService skillPublishService;
|
||||
private final ZipPackageExtractor zipPackageExtractor;
|
||||
private final AuditLogService auditLogService;
|
||||
|
||||
public ClawHubCompatController(CanonicalSlugMapper mapper) {
|
||||
public ClawHubCompatController(CanonicalSlugMapper mapper,
|
||||
SkillSearchAppService skillSearchAppService,
|
||||
SkillQueryService skillQueryService,
|
||||
SkillPublishService skillPublishService,
|
||||
ZipPackageExtractor zipPackageExtractor,
|
||||
AuditLogService auditLogService) {
|
||||
this.mapper = mapper;
|
||||
this.skillSearchAppService = skillSearchAppService;
|
||||
this.skillQueryService = skillQueryService;
|
||||
this.skillPublishService = skillPublishService;
|
||||
this.zipPackageExtractor = zipPackageExtractor;
|
||||
this.auditLogService = auditLogService;
|
||||
}
|
||||
|
||||
@GetMapping("/search")
|
||||
public ClawHubSearchResponse search(@RequestParam String q) {
|
||||
// Return empty results for now (placeholder)
|
||||
return new ClawHubSearchResponse(List.of());
|
||||
public ClawHubSearchResponse search(@RequestParam String q,
|
||||
@RequestAttribute(value = "userId", required = false) String userId,
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles) {
|
||||
var result = skillSearchAppService.search(q, null, "relevance", 0, 20, userId, userNsRoles != null ? userNsRoles : Map.of());
|
||||
return new ClawHubSearchResponse(result.items().stream()
|
||||
.map(item -> new ClawHubSkillItem(
|
||||
mapper.toCanonical(item.namespace(), item.slug()),
|
||||
item.summary(),
|
||||
item.latestVersion(),
|
||||
item.starCount()
|
||||
))
|
||||
.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/resolve/{canonicalSlug}")
|
||||
public ClawHubResolveResponse resolve(
|
||||
@PathVariable String canonicalSlug,
|
||||
@RequestParam(defaultValue = "latest") String version) {
|
||||
@RequestParam(defaultValue = "latest") String version,
|
||||
@RequestAttribute(value = "userId", required = false) String userId,
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles) {
|
||||
SkillCoordinate coord = mapper.fromCanonical(canonicalSlug);
|
||||
var resolved = skillQueryService.resolveVersion(
|
||||
coord.namespace(),
|
||||
coord.slug(),
|
||||
"latest".equals(version) ? null : version,
|
||||
"latest".equals(version) ? "latest" : null,
|
||||
null,
|
||||
userId,
|
||||
userNsRoles != null ? userNsRoles : Map.of()
|
||||
);
|
||||
return new ClawHubResolveResponse(
|
||||
canonicalSlug,
|
||||
version,
|
||||
"/api/v1/skills/" + coord.namespace() + "/" + coord.slug() + "/download"
|
||||
resolved.version(),
|
||||
resolved.downloadUrl()
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/download/{canonicalSlug}")
|
||||
public ResponseEntity<Void> download(@PathVariable String canonicalSlug,
|
||||
@RequestParam(defaultValue = "latest") String version) {
|
||||
SkillCoordinate coord = mapper.fromCanonical(canonicalSlug);
|
||||
String location = "latest".equals(version)
|
||||
? "/api/v1/skills/" + coord.namespace() + "/" + coord.slug() + "/download"
|
||||
: "/api/v1/skills/" + coord.namespace() + "/" + coord.slug() + "/versions/" + version + "/download";
|
||||
return ResponseEntity.status(HttpStatus.FOUND)
|
||||
.header(HttpHeaders.LOCATION, location)
|
||||
.build();
|
||||
}
|
||||
|
||||
@PostMapping("/publish")
|
||||
public ClawHubPublishResponse publish(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("namespace") String namespace,
|
||||
@RequestAttribute("userId") String userId,
|
||||
jakarta.servlet.http.HttpServletRequest request) throws IOException {
|
||||
var result = skillPublishService.publishFromEntries(
|
||||
namespace,
|
||||
zipPackageExtractor.extract(file),
|
||||
userId,
|
||||
SkillVisibility.PUBLIC
|
||||
);
|
||||
auditLogService.record(
|
||||
userId,
|
||||
"COMPAT_PUBLISH",
|
||||
"SKILL_VERSION",
|
||||
result.version().getId(),
|
||||
MDC.get("requestId"),
|
||||
request.getRemoteAddr(),
|
||||
request.getHeader("User-Agent"),
|
||||
"{\"namespace\":\"" + namespace + "\"}"
|
||||
);
|
||||
return new ClawHubPublishResponse(
|
||||
mapper.toCanonical(namespace, result.slug()),
|
||||
result.version().getVersion(),
|
||||
result.version().getStatus().name()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,11 +49,23 @@ public class AccountMergeController extends BaseApiController {
|
|||
if (principal == null) {
|
||||
throw new UnauthorizedException("error.auth.required");
|
||||
}
|
||||
accountMergeService.verifyAndComplete(
|
||||
accountMergeService.verify(
|
||||
principal.userId(),
|
||||
request.mergeRequestId(),
|
||||
request.verificationToken()
|
||||
);
|
||||
return ok("response.success.updated", new MessageResponse("Account merge verified"));
|
||||
}
|
||||
|
||||
@PostMapping("/confirm")
|
||||
public ApiResponse<MessageResponse> confirm(@AuthenticationPrincipal PlatformPrincipal principal,
|
||||
@Valid @RequestBody ConfirmMergeRequest request) {
|
||||
if (principal == null) {
|
||||
throw new UnauthorizedException("error.auth.required");
|
||||
}
|
||||
accountMergeService.confirm(principal.userId(), request.mergeRequestId());
|
||||
return ok("response.success.updated", new MessageResponse("Account merge completed"));
|
||||
}
|
||||
|
||||
public record ConfirmMergeRequest(@jakarta.validation.constraints.NotNull Long mergeRequestId) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@ import com.iflytek.skillhub.domain.skill.validation.ValidationResult;
|
|||
import com.iflytek.skillhub.dto.ApiResponse;
|
||||
import com.iflytek.skillhub.dto.ApiResponseFactory;
|
||||
import com.iflytek.skillhub.dto.CliWhoamiResponse;
|
||||
import com.iflytek.skillhub.dto.ResolveVersionResponse;
|
||||
import com.iflytek.skillhub.dto.SkillCheckResponse;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.skill.service.SkillQueryService;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import com.iflytek.skillhub.exception.UnauthorizedException;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
|
@ -24,11 +27,14 @@ import java.util.zip.ZipInputStream;
|
|||
public class CliController extends BaseApiController {
|
||||
|
||||
private final SkillPackageValidator skillPackageValidator;
|
||||
private final SkillQueryService skillQueryService;
|
||||
|
||||
public CliController(ApiResponseFactory responseFactory,
|
||||
SkillPackageValidator skillPackageValidator) {
|
||||
SkillPackageValidator skillPackageValidator,
|
||||
SkillQueryService skillQueryService) {
|
||||
super(responseFactory);
|
||||
this.skillPackageValidator = skillPackageValidator;
|
||||
this.skillQueryService = skillQueryService;
|
||||
}
|
||||
|
||||
@GetMapping("/whoami")
|
||||
|
|
@ -55,6 +61,35 @@ public class CliController extends BaseApiController {
|
|||
return ok("response.success.validated", response);
|
||||
}
|
||||
|
||||
@GetMapping("/resolve/{namespace}/{slug}")
|
||||
public ApiResponse<ResolveVersionResponse> resolve(@PathVariable String namespace,
|
||||
@PathVariable String slug,
|
||||
@RequestParam(required = false) String version,
|
||||
@RequestParam(required = false) String tag,
|
||||
@RequestParam(required = false) String hash,
|
||||
@RequestAttribute(value = "userId", required = false) String userId,
|
||||
@RequestAttribute(value = "userNsRoles", required = false) java.util.Map<Long, NamespaceRole> userNsRoles) {
|
||||
SkillQueryService.ResolvedVersionDTO resolved = skillQueryService.resolveVersion(
|
||||
namespace,
|
||||
slug,
|
||||
version,
|
||||
tag,
|
||||
hash,
|
||||
userId,
|
||||
userNsRoles != null ? userNsRoles : java.util.Map.of()
|
||||
);
|
||||
return ok("response.success.read", new ResolveVersionResponse(
|
||||
resolved.skillId(),
|
||||
resolved.namespace(),
|
||||
resolved.slug(),
|
||||
resolved.version(),
|
||||
resolved.versionId(),
|
||||
resolved.fingerprint(),
|
||||
resolved.matched(),
|
||||
resolved.downloadUrl()
|
||||
));
|
||||
}
|
||||
|
||||
private List<PackageEntry> extractZipEntries(MultipartFile file) throws IOException {
|
||||
List<PackageEntry> entries = new ArrayList<>();
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@ package com.iflytek.skillhub.controller;
|
|||
|
||||
import com.iflytek.skillhub.auth.device.DeviceAuthService;
|
||||
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
||||
import com.iflytek.skillhub.domain.audit.AuditLogService;
|
||||
import com.iflytek.skillhub.dto.ApiResponse;
|
||||
import com.iflytek.skillhub.dto.ApiResponseFactory;
|
||||
import com.iflytek.skillhub.dto.MessageResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
|
@ -16,18 +19,33 @@ import org.springframework.web.bind.annotation.RestController;
|
|||
public class DeviceAuthWebController extends BaseApiController {
|
||||
|
||||
private final DeviceAuthService deviceAuthService;
|
||||
private final AuditLogService auditLogService;
|
||||
|
||||
public DeviceAuthWebController(ApiResponseFactory responseFactory, DeviceAuthService deviceAuthService) {
|
||||
public DeviceAuthWebController(ApiResponseFactory responseFactory,
|
||||
DeviceAuthService deviceAuthService,
|
||||
AuditLogService auditLogService) {
|
||||
super(responseFactory);
|
||||
this.deviceAuthService = deviceAuthService;
|
||||
this.auditLogService = auditLogService;
|
||||
}
|
||||
|
||||
@PostMapping("/authorize")
|
||||
public ApiResponse<MessageResponse> authorizeDevice(
|
||||
@RequestBody AuthorizeRequest request,
|
||||
@AuthenticationPrincipal PlatformPrincipal principal
|
||||
@AuthenticationPrincipal PlatformPrincipal principal,
|
||||
HttpServletRequest httpRequest
|
||||
) {
|
||||
deviceAuthService.authorizeDeviceCode(request.userCode(), principal.userId());
|
||||
auditLogService.record(
|
||||
principal.userId(),
|
||||
"DEVICE_AUTHORIZE",
|
||||
"DEVICE_CODE",
|
||||
null,
|
||||
MDC.get("requestId"),
|
||||
httpRequest.getRemoteAddr(),
|
||||
httpRequest.getHeader("User-Agent"),
|
||||
"{\"userCode\":\"" + request.userCode() + "\"}"
|
||||
);
|
||||
return ok("response.success.updated", new MessageResponse("Device authorized successfully"));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.iflytek.skillhub.controller.admin;
|
||||
|
||||
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
||||
import com.iflytek.skillhub.controller.BaseApiController;
|
||||
import com.iflytek.skillhub.dto.AdminUserMutationResponse;
|
||||
import com.iflytek.skillhub.dto.AdminUserRoleUpdateRequest;
|
||||
|
|
@ -8,39 +9,42 @@ import com.iflytek.skillhub.dto.AdminUserSummaryResponse;
|
|||
import com.iflytek.skillhub.dto.ApiResponse;
|
||||
import com.iflytek.skillhub.dto.ApiResponseFactory;
|
||||
import com.iflytek.skillhub.dto.PageResponse;
|
||||
import com.iflytek.skillhub.service.AdminUserManagementService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/admin/users")
|
||||
public class UserManagementController extends BaseApiController {
|
||||
|
||||
public UserManagementController(ApiResponseFactory responseFactory) {
|
||||
private final AdminUserManagementService adminUserManagementService;
|
||||
|
||||
public UserManagementController(ApiResponseFactory responseFactory,
|
||||
AdminUserManagementService adminUserManagementService) {
|
||||
super(responseFactory);
|
||||
this.adminUserManagementService = adminUserManagementService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAnyRole('USER_ADMIN', 'SUPER_ADMIN')")
|
||||
public ApiResponse<PageResponse<AdminUserSummaryResponse>> listUsers(
|
||||
@RequestParam(required = false) String search,
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size) {
|
||||
List<AdminUserSummaryResponse> users = List.of(
|
||||
new AdminUserSummaryResponse("user-1", "alice", "USER", "ACTIVE"),
|
||||
new AdminUserSummaryResponse("user-2", "bob", "USER", "ACTIVE")
|
||||
);
|
||||
return ok("response.success.read", PageResponse.from(new PageImpl<>(users)));
|
||||
return ok("response.success.read", adminUserManagementService.listUsers(search, status, page, size));
|
||||
}
|
||||
|
||||
@PutMapping("/{userId}/role")
|
||||
@PreAuthorize("hasAnyRole('USER_ADMIN', 'SUPER_ADMIN')")
|
||||
public ApiResponse<AdminUserMutationResponse> updateUserRole(
|
||||
@PathVariable String userId,
|
||||
@Valid @RequestBody AdminUserRoleUpdateRequest request) {
|
||||
return ok("response.success.updated", new AdminUserMutationResponse(userId, request.role(), null));
|
||||
@Valid @RequestBody AdminUserRoleUpdateRequest request,
|
||||
@AuthenticationPrincipal PlatformPrincipal principal) {
|
||||
AdminUserSummaryResponse user = adminUserManagementService.updateUserRole(userId, request.role(), principal);
|
||||
return ok("response.success.updated", new AdminUserMutationResponse(user.userId(), request.role(), user.status()));
|
||||
}
|
||||
|
||||
@PutMapping("/{userId}/status")
|
||||
|
|
@ -48,6 +52,28 @@ public class UserManagementController extends BaseApiController {
|
|||
public ApiResponse<AdminUserMutationResponse> updateUserStatus(
|
||||
@PathVariable String userId,
|
||||
@Valid @RequestBody AdminUserStatusUpdateRequest request) {
|
||||
return ok("response.success.updated", new AdminUserMutationResponse(userId, null, request.status()));
|
||||
AdminUserSummaryResponse user = adminUserManagementService.updateUserStatus(userId, request.status());
|
||||
return ok("response.success.updated", new AdminUserMutationResponse(user.userId(), null, user.status()));
|
||||
}
|
||||
|
||||
@PostMapping("/{userId}/approve")
|
||||
@PreAuthorize("hasAnyRole('USER_ADMIN', 'SUPER_ADMIN')")
|
||||
public ApiResponse<AdminUserMutationResponse> approveUser(@PathVariable String userId) {
|
||||
AdminUserSummaryResponse user = adminUserManagementService.approveUser(userId);
|
||||
return ok("response.success.updated", new AdminUserMutationResponse(user.userId(), null, user.status()));
|
||||
}
|
||||
|
||||
@PostMapping("/{userId}/disable")
|
||||
@PreAuthorize("hasAnyRole('USER_ADMIN', 'SUPER_ADMIN')")
|
||||
public ApiResponse<AdminUserMutationResponse> disableUser(@PathVariable String userId) {
|
||||
AdminUserSummaryResponse user = adminUserManagementService.disableUser(userId);
|
||||
return ok("response.success.updated", new AdminUserMutationResponse(user.userId(), null, user.status()));
|
||||
}
|
||||
|
||||
@PostMapping("/{userId}/enable")
|
||||
@PreAuthorize("hasAnyRole('USER_ADMIN', 'SUPER_ADMIN')")
|
||||
public ApiResponse<AdminUserMutationResponse> enableUser(@PathVariable String userId) {
|
||||
AdminUserSummaryResponse user = adminUserManagementService.enableUser(userId);
|
||||
return ok("response.success.updated", new AdminUserMutationResponse(user.userId(), null, user.status()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.iflytek.skillhub.controller.cli;
|
|||
|
||||
import com.iflytek.skillhub.controller.BaseApiController;
|
||||
import com.iflytek.skillhub.controller.support.ZipPackageExtractor;
|
||||
import com.iflytek.skillhub.domain.audit.AuditLogService;
|
||||
import com.iflytek.skillhub.domain.skill.SkillVisibility;
|
||||
import com.iflytek.skillhub.domain.skill.service.SkillPublishService;
|
||||
import com.iflytek.skillhub.domain.skill.validation.PackageEntry;
|
||||
|
|
@ -10,6 +11,8 @@ import com.iflytek.skillhub.dto.ApiResponseFactory;
|
|||
import com.iflytek.skillhub.dto.PublishResponse;
|
||||
import com.iflytek.skillhub.metrics.SkillHubMetrics;
|
||||
import com.iflytek.skillhub.ratelimit.RateLimit;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
|
|
@ -23,15 +26,18 @@ public class CliPublishController extends BaseApiController {
|
|||
private final SkillPublishService skillPublishService;
|
||||
private final ZipPackageExtractor zipPackageExtractor;
|
||||
private final SkillHubMetrics skillHubMetrics;
|
||||
private final AuditLogService auditLogService;
|
||||
|
||||
public CliPublishController(SkillPublishService skillPublishService,
|
||||
ZipPackageExtractor zipPackageExtractor,
|
||||
ApiResponseFactory responseFactory,
|
||||
SkillHubMetrics skillHubMetrics) {
|
||||
SkillHubMetrics skillHubMetrics,
|
||||
AuditLogService auditLogService) {
|
||||
super(responseFactory);
|
||||
this.skillPublishService = skillPublishService;
|
||||
this.zipPackageExtractor = zipPackageExtractor;
|
||||
this.skillHubMetrics = skillHubMetrics;
|
||||
this.auditLogService = auditLogService;
|
||||
}
|
||||
|
||||
@PostMapping("/publish")
|
||||
|
|
@ -40,7 +46,8 @@ public class CliPublishController extends BaseApiController {
|
|||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("namespace") String namespace,
|
||||
@RequestParam("visibility") String visibility,
|
||||
@RequestAttribute("userId") String userId) throws IOException {
|
||||
@RequestAttribute("userId") String userId,
|
||||
HttpServletRequest request) throws IOException {
|
||||
|
||||
SkillVisibility skillVisibility = SkillVisibility.valueOf(visibility.toUpperCase());
|
||||
|
||||
|
|
@ -63,6 +70,16 @@ public class CliPublishController extends BaseApiController {
|
|||
publishResult.version().getTotalSize()
|
||||
);
|
||||
skillHubMetrics.incrementSkillPublish(namespace, publishResult.version().getStatus().name());
|
||||
auditLogService.record(
|
||||
userId,
|
||||
"CLI_PUBLISH",
|
||||
"SKILL_VERSION",
|
||||
publishResult.version().getId(),
|
||||
MDC.get("requestId"),
|
||||
request.getRemoteAddr(),
|
||||
request.getHeader("User-Agent"),
|
||||
"{\"namespace\":\"" + namespace + "\"}"
|
||||
);
|
||||
|
||||
return ok("response.success.published", response);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,4 +34,14 @@ public class MeController extends BaseApiController {
|
|||
|
||||
return ok("response.success.read", mySkillAppService.listMySkills(principal.userId()));
|
||||
}
|
||||
|
||||
@GetMapping("/stars")
|
||||
public ApiResponse<List<SkillSummaryResponse>> listMyStars(
|
||||
@AuthenticationPrincipal PlatformPrincipal principal) {
|
||||
if (principal == null) {
|
||||
throw new UnauthorizedException("error.auth.required");
|
||||
}
|
||||
|
||||
return ok("response.success.read", mySkillAppService.listMyStars(principal.userId()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.iflytek.skillhub.controller.portal;
|
|||
|
||||
import com.iflytek.skillhub.auth.rbac.RbacService;
|
||||
import com.iflytek.skillhub.controller.BaseApiController;
|
||||
import com.iflytek.skillhub.domain.audit.AuditLogService;
|
||||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
|
|
@ -18,12 +19,14 @@ import com.iflytek.skillhub.domain.skill.SkillVersionRepository;
|
|||
import com.iflytek.skillhub.domain.user.UserAccount;
|
||||
import com.iflytek.skillhub.domain.user.UserAccountRepository;
|
||||
import com.iflytek.skillhub.dto.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.slf4j.MDC;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/promotions")
|
||||
|
|
@ -36,6 +39,7 @@ public class PromotionController extends BaseApiController {
|
|||
private final NamespaceRepository namespaceRepository;
|
||||
private final UserAccountRepository userAccountRepository;
|
||||
private final RbacService rbacService;
|
||||
private final AuditLogService auditLogService;
|
||||
|
||||
public PromotionController(PromotionService promotionService,
|
||||
PromotionRequestRepository promotionRequestRepository,
|
||||
|
|
@ -44,6 +48,7 @@ public class PromotionController extends BaseApiController {
|
|||
NamespaceRepository namespaceRepository,
|
||||
UserAccountRepository userAccountRepository,
|
||||
RbacService rbacService,
|
||||
AuditLogService auditLogService,
|
||||
ApiResponseFactory responseFactory) {
|
||||
super(responseFactory);
|
||||
this.promotionService = promotionService;
|
||||
|
|
@ -53,18 +58,22 @@ public class PromotionController extends BaseApiController {
|
|||
this.namespaceRepository = namespaceRepository;
|
||||
this.userAccountRepository = userAccountRepository;
|
||||
this.rbacService = rbacService;
|
||||
this.auditLogService = auditLogService;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ApiResponse<PromotionResponseDto> submitPromotion(
|
||||
@RequestBody PromotionRequestDto request,
|
||||
@RequestAttribute("userId") String userId,
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles) {
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
|
||||
HttpServletRequest httpRequest) {
|
||||
PromotionRequest promotion = promotionService.submitPromotion(
|
||||
request.sourceSkillId(), request.sourceVersionId(),
|
||||
request.targetNamespaceId(), userId,
|
||||
userNsRoles != null ? userNsRoles : Map.of(),
|
||||
rbacService.getUserRoleCodes(userId));
|
||||
recordAudit("PROMOTION_SUBMIT", userId, promotion.getId(), httpRequest,
|
||||
"{\"sourceSkillId\":" + request.sourceSkillId() + ",\"sourceVersionId\":" + request.sourceVersionId() + "}");
|
||||
return ok("response.success.created", toResponse(promotion));
|
||||
}
|
||||
|
||||
|
|
@ -72,10 +81,12 @@ public class PromotionController extends BaseApiController {
|
|||
public ApiResponse<PromotionResponseDto> approvePromotion(
|
||||
@PathVariable Long id,
|
||||
@RequestBody(required = false) PromotionActionRequest request,
|
||||
@RequestAttribute("userId") String userId) {
|
||||
@RequestAttribute("userId") String userId,
|
||||
HttpServletRequest httpRequest) {
|
||||
String comment = request != null ? request.comment() : null;
|
||||
Set<String> platformRoles = rbacService.getUserRoleCodes(userId);
|
||||
PromotionRequest promotion = promotionService.approvePromotion(id, userId, comment, platformRoles);
|
||||
recordAudit("PROMOTION_APPROVE", userId, promotion.getId(), httpRequest, detailWithComment(comment));
|
||||
return ok("response.success.updated", toResponse(promotion));
|
||||
}
|
||||
|
||||
|
|
@ -83,13 +94,31 @@ public class PromotionController extends BaseApiController {
|
|||
public ApiResponse<PromotionResponseDto> rejectPromotion(
|
||||
@PathVariable Long id,
|
||||
@RequestBody(required = false) PromotionActionRequest request,
|
||||
@RequestAttribute("userId") String userId) {
|
||||
@RequestAttribute("userId") String userId,
|
||||
HttpServletRequest httpRequest) {
|
||||
String comment = request != null ? request.comment() : null;
|
||||
Set<String> platformRoles = rbacService.getUserRoleCodes(userId);
|
||||
PromotionRequest promotion = promotionService.rejectPromotion(id, userId, comment, platformRoles);
|
||||
recordAudit("PROMOTION_REJECT", userId, promotion.getId(), httpRequest, detailWithComment(comment));
|
||||
return ok("response.success.updated", toResponse(promotion));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ApiResponse<PageResponse<PromotionResponseDto>> listPromotions(
|
||||
@RequestParam(defaultValue = "PENDING") String status,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size,
|
||||
@RequestAttribute("userId") String userId) {
|
||||
Set<String> platformRoles = rbacService.getUserRoleCodes(userId);
|
||||
boolean hasAdminRole = platformRoles.contains("SKILL_ADMIN") || platformRoles.contains("SUPER_ADMIN");
|
||||
if (!hasAdminRole) {
|
||||
throw new DomainForbiddenException("promotion.no_permission");
|
||||
}
|
||||
ReviewTaskStatus reviewStatus = ReviewTaskStatus.valueOf(status.toUpperCase());
|
||||
Page<PromotionRequest> requests = promotionRequestRepository.findByStatus(reviewStatus, PageRequest.of(page, size));
|
||||
return ok("response.success.read", PageResponse.from(requests.map(this::toResponse)));
|
||||
}
|
||||
|
||||
@GetMapping("/pending")
|
||||
public ApiResponse<PageResponse<PromotionResponseDto>> listPendingPromotions(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
|
|
@ -152,4 +181,28 @@ public class PromotionController extends BaseApiController {
|
|||
req.getReviewedAt()
|
||||
);
|
||||
}
|
||||
|
||||
private void recordAudit(String action,
|
||||
String userId,
|
||||
Long targetId,
|
||||
HttpServletRequest httpRequest,
|
||||
String detailJson) {
|
||||
auditLogService.record(
|
||||
userId,
|
||||
action,
|
||||
"PROMOTION_REQUEST",
|
||||
targetId,
|
||||
MDC.get("requestId"),
|
||||
httpRequest.getRemoteAddr(),
|
||||
httpRequest.getHeader("User-Agent"),
|
||||
detailJson
|
||||
);
|
||||
}
|
||||
|
||||
private String detailWithComment(String comment) {
|
||||
if (comment == null || comment.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return "{\"comment\":\"" + comment.replace("\"", "\\\"") + "\"}";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.iflytek.skillhub.controller.portal;
|
|||
|
||||
import com.iflytek.skillhub.auth.rbac.RbacService;
|
||||
import com.iflytek.skillhub.controller.BaseApiController;
|
||||
import com.iflytek.skillhub.domain.audit.AuditLogService;
|
||||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
|
|
@ -18,12 +19,15 @@ import com.iflytek.skillhub.domain.skill.SkillVersionRepository;
|
|||
import com.iflytek.skillhub.domain.user.UserAccount;
|
||||
import com.iflytek.skillhub.domain.user.UserAccountRepository;
|
||||
import com.iflytek.skillhub.dto.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.slf4j.MDC;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/reviews")
|
||||
|
|
@ -36,6 +40,7 @@ public class ReviewController extends BaseApiController {
|
|||
private final NamespaceRepository namespaceRepository;
|
||||
private final UserAccountRepository userAccountRepository;
|
||||
private final RbacService rbacService;
|
||||
private final AuditLogService auditLogService;
|
||||
|
||||
public ReviewController(ReviewService reviewService,
|
||||
ReviewTaskRepository reviewTaskRepository,
|
||||
|
|
@ -44,6 +49,7 @@ public class ReviewController extends BaseApiController {
|
|||
NamespaceRepository namespaceRepository,
|
||||
UserAccountRepository userAccountRepository,
|
||||
RbacService rbacService,
|
||||
AuditLogService auditLogService,
|
||||
ApiResponseFactory responseFactory) {
|
||||
super(responseFactory);
|
||||
this.reviewService = reviewService;
|
||||
|
|
@ -53,19 +59,22 @@ public class ReviewController extends BaseApiController {
|
|||
this.namespaceRepository = namespaceRepository;
|
||||
this.userAccountRepository = userAccountRepository;
|
||||
this.rbacService = rbacService;
|
||||
this.auditLogService = auditLogService;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ApiResponse<ReviewTaskResponse> submitReview(
|
||||
@RequestBody ReviewTaskRequest request,
|
||||
@RequestAttribute("userId") String userId,
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles) {
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
|
||||
HttpServletRequest httpRequest) {
|
||||
ReviewTask task = reviewService.submitReview(
|
||||
request.skillVersionId(),
|
||||
userId,
|
||||
userNsRoles != null ? userNsRoles : Map.of(),
|
||||
rbacService.getUserRoleCodes(userId)
|
||||
);
|
||||
recordAudit("REVIEW_SUBMIT", userId, task.getId(), httpRequest, "{\"skillVersionId\":" + request.skillVersionId() + "}");
|
||||
return ok("response.success.created", toResponse(task));
|
||||
}
|
||||
|
||||
|
|
@ -74,11 +83,13 @@ public class ReviewController extends BaseApiController {
|
|||
@PathVariable Long id,
|
||||
@RequestBody(required = false) ReviewActionRequest request,
|
||||
@RequestAttribute("userId") String userId,
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles) {
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
|
||||
HttpServletRequest httpRequest) {
|
||||
String comment = request != null ? request.comment() : null;
|
||||
Set<String> platformRoles = rbacService.getUserRoleCodes(userId);
|
||||
ReviewTask task = reviewService.approveReview(id, userId, comment,
|
||||
userNsRoles != null ? userNsRoles : Map.of(), platformRoles);
|
||||
recordAudit("REVIEW_APPROVE", userId, task.getId(), httpRequest, detailWithComment(comment));
|
||||
return ok("response.success.updated", toResponse(task));
|
||||
}
|
||||
|
||||
|
|
@ -87,23 +98,59 @@ public class ReviewController extends BaseApiController {
|
|||
@PathVariable Long id,
|
||||
@RequestBody(required = false) ReviewActionRequest request,
|
||||
@RequestAttribute("userId") String userId,
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles) {
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
|
||||
HttpServletRequest httpRequest) {
|
||||
String comment = request != null ? request.comment() : null;
|
||||
Set<String> platformRoles = rbacService.getUserRoleCodes(userId);
|
||||
ReviewTask task = reviewService.rejectReview(id, userId, comment,
|
||||
userNsRoles != null ? userNsRoles : Map.of(), platformRoles);
|
||||
recordAudit("REVIEW_REJECT", userId, task.getId(), httpRequest, detailWithComment(comment));
|
||||
return ok("response.success.updated", toResponse(task));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/withdraw")
|
||||
public ApiResponse<Void> withdrawReview(
|
||||
@PathVariable Long id,
|
||||
@RequestAttribute("userId") String userId) {
|
||||
@RequestAttribute("userId") String userId,
|
||||
HttpServletRequest httpRequest) {
|
||||
ReviewTask task = reviewTaskRepository.findById(id).orElseThrow();
|
||||
reviewService.withdrawReview(task.getSkillVersionId(), userId);
|
||||
recordAudit("REVIEW_WITHDRAW", userId, id, httpRequest, "{\"skillVersionId\":" + task.getSkillVersionId() + "}");
|
||||
return ok("response.success.updated", null);
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ApiResponse<PageResponse<ReviewTaskResponse>> listReviews(
|
||||
@RequestParam String status,
|
||||
@RequestParam(required = false) Long namespaceId,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size,
|
||||
@RequestAttribute("userId") String userId,
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles) {
|
||||
ReviewTaskStatus reviewStatus = ReviewTaskStatus.valueOf(status.toUpperCase());
|
||||
Map<Long, NamespaceRole> namespaceRoles = userNsRoles != null ? userNsRoles : Map.of();
|
||||
|
||||
Page<ReviewTask> tasks;
|
||||
if (namespaceId != null) {
|
||||
Namespace namespace = namespaceRepository.findById(namespaceId)
|
||||
.orElseThrow(() -> new DomainNotFoundException("namespace.not_found", namespaceId));
|
||||
ReviewTask probe = new ReviewTask(0L, namespaceId, userId);
|
||||
if (!reviewService.canReviewNamespace(probe, userId, namespace.getType(), namespaceRoles, rbacService.getUserRoleCodes(userId))) {
|
||||
throw new DomainForbiddenException("review.no_permission");
|
||||
}
|
||||
tasks = reviewTaskRepository.findByNamespaceIdAndStatus(namespaceId, reviewStatus, PageRequest.of(page, size));
|
||||
} else {
|
||||
tasks = reviewTaskRepository.findByStatus(reviewStatus, PageRequest.of(page, size));
|
||||
}
|
||||
|
||||
java.util.List<ReviewTaskResponse> visibleItems = tasks.getContent().stream()
|
||||
.filter(task -> canViewReview(task, userId, namespaceRoles))
|
||||
.map(this::toResponse)
|
||||
.toList();
|
||||
Page<ReviewTaskResponse> responsePage = new PageImpl<>(visibleItems, tasks.getPageable(), visibleItems.size());
|
||||
return ok("response.success.read", PageResponse.from(responsePage));
|
||||
}
|
||||
|
||||
@GetMapping("/pending")
|
||||
public ApiResponse<PageResponse<ReviewTaskResponse>> listPendingReviews(
|
||||
@RequestParam Long namespaceId,
|
||||
|
|
@ -180,4 +227,34 @@ public class ReviewController extends BaseApiController {
|
|||
task.getReviewedAt()
|
||||
);
|
||||
}
|
||||
|
||||
private boolean canViewReview(ReviewTask task, String userId, Map<Long, NamespaceRole> namespaceRoles) {
|
||||
Namespace namespace = namespaceRepository.findById(task.getNamespaceId())
|
||||
.orElseThrow(() -> new DomainNotFoundException("namespace.not_found", task.getNamespaceId()));
|
||||
return reviewService.canViewReview(task, userId, namespace.getType(), namespaceRoles, rbacService.getUserRoleCodes(userId));
|
||||
}
|
||||
|
||||
private void recordAudit(String action,
|
||||
String userId,
|
||||
Long targetId,
|
||||
HttpServletRequest httpRequest,
|
||||
String detailJson) {
|
||||
auditLogService.record(
|
||||
userId,
|
||||
action,
|
||||
"REVIEW_TASK",
|
||||
targetId,
|
||||
MDC.get("requestId"),
|
||||
httpRequest.getRemoteAddr(),
|
||||
httpRequest.getHeader("User-Agent"),
|
||||
detailJson
|
||||
);
|
||||
}
|
||||
|
||||
private String detailWithComment(String comment) {
|
||||
if (comment == null || comment.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return "{\"comment\":\"" + comment.replace("\"", "\\\"") + "\"}";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,9 @@ public class SkillController extends BaseApiController {
|
|||
detail.status(),
|
||||
detail.downloadCount(),
|
||||
detail.starCount(),
|
||||
detail.ratingAvg(),
|
||||
detail.ratingCount(),
|
||||
detail.hidden(),
|
||||
detail.latestVersion(),
|
||||
namespace
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
package com.iflytek.skillhub.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public record AdminUserSummaryResponse(
|
||||
String userId,
|
||||
String username,
|
||||
String role,
|
||||
String status
|
||||
String email,
|
||||
List<String> platformRoles,
|
||||
String status,
|
||||
LocalDateTime createdAt
|
||||
) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.iflytek.skillhub.dto;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public record SkillDetailResponse(
|
||||
Long id,
|
||||
String slug,
|
||||
|
|
@ -9,6 +11,9 @@ public record SkillDetailResponse(
|
|||
String status,
|
||||
Long downloadCount,
|
||||
Integer starCount,
|
||||
BigDecimal ratingAvg,
|
||||
Integer ratingCount,
|
||||
boolean hidden,
|
||||
String latestVersion,
|
||||
String namespace
|
||||
) {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,140 @@
|
|||
package com.iflytek.skillhub.service;
|
||||
|
||||
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
||||
import com.iflytek.skillhub.auth.entity.Role;
|
||||
import com.iflytek.skillhub.auth.entity.UserRoleBinding;
|
||||
import com.iflytek.skillhub.auth.repository.RoleRepository;
|
||||
import com.iflytek.skillhub.auth.repository.UserRoleBindingRepository;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainNotFoundException;
|
||||
import com.iflytek.skillhub.domain.user.UserAccount;
|
||||
import com.iflytek.skillhub.domain.user.UserAccountRepository;
|
||||
import com.iflytek.skillhub.domain.user.UserStatus;
|
||||
import com.iflytek.skillhub.dto.AdminUserSummaryResponse;
|
||||
import com.iflytek.skillhub.dto.PageResponse;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class AdminUserManagementService {
|
||||
|
||||
private final UserAccountRepository userAccountRepository;
|
||||
private final UserRoleBindingRepository userRoleBindingRepository;
|
||||
private final RoleRepository roleRepository;
|
||||
|
||||
public AdminUserManagementService(UserAccountRepository userAccountRepository,
|
||||
UserRoleBindingRepository userRoleBindingRepository,
|
||||
RoleRepository roleRepository) {
|
||||
this.userAccountRepository = userAccountRepository;
|
||||
this.userRoleBindingRepository = userRoleBindingRepository;
|
||||
this.roleRepository = roleRepository;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResponse<AdminUserSummaryResponse> listUsers(String keyword, String status, int page, int size) {
|
||||
UserStatus userStatus = parseStatus(status);
|
||||
Page<UserAccount> users = userAccountRepository.search(normalize(keyword), userStatus, PageRequest.of(page, size));
|
||||
List<AdminUserSummaryResponse> items = users.getContent().stream()
|
||||
.map(this::toSummary)
|
||||
.toList();
|
||||
return PageResponse.from(new PageImpl<>(items, users.getPageable(), users.getTotalElements()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AdminUserSummaryResponse updateUserRole(String userId, String roleCode, PlatformPrincipal principal) {
|
||||
UserAccount user = loadUser(userId);
|
||||
if (principal != null
|
||||
&& !principal.platformRoles().contains("SUPER_ADMIN")
|
||||
&& "SUPER_ADMIN".equalsIgnoreCase(roleCode)) {
|
||||
throw new DomainForbiddenException("error.admin.role.assign_super_admin_forbidden");
|
||||
}
|
||||
Role role = roleRepository.findByCode(roleCode)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.role.notFound", roleCode));
|
||||
|
||||
List<UserRoleBinding> existing = userRoleBindingRepository.findByUserId(userId);
|
||||
boolean alreadyAssigned = existing.stream().anyMatch(binding -> binding.getRole().getCode().equals(roleCode));
|
||||
if (!alreadyAssigned) {
|
||||
userRoleBindingRepository.save(new UserRoleBinding(userId, role));
|
||||
}
|
||||
return toSummary(user);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AdminUserSummaryResponse approveUser(String userId) {
|
||||
UserAccount user = loadUser(userId);
|
||||
user.setStatus(UserStatus.ACTIVE);
|
||||
return toSummary(userAccountRepository.save(user));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AdminUserSummaryResponse updateUserStatus(String userId, String status) {
|
||||
UserAccount user = loadUser(userId);
|
||||
user.setStatus(parseRequiredStatus(status));
|
||||
return toSummary(userAccountRepository.save(user));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AdminUserSummaryResponse disableUser(String userId) {
|
||||
UserAccount user = loadUser(userId);
|
||||
user.setStatus(UserStatus.DISABLED);
|
||||
return toSummary(userAccountRepository.save(user));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AdminUserSummaryResponse enableUser(String userId) {
|
||||
UserAccount user = loadUser(userId);
|
||||
user.setStatus(UserStatus.ACTIVE);
|
||||
return toSummary(userAccountRepository.save(user));
|
||||
}
|
||||
|
||||
private UserAccount loadUser(String userId) {
|
||||
return userAccountRepository.findById(userId)
|
||||
.orElseThrow(() -> new DomainNotFoundException("error.user.notFound", userId));
|
||||
}
|
||||
|
||||
private AdminUserSummaryResponse toSummary(UserAccount user) {
|
||||
Set<String> roles = new LinkedHashSet<>();
|
||||
userRoleBindingRepository.findByUserId(user.getId()).stream()
|
||||
.map(binding -> binding.getRole().getCode())
|
||||
.sorted(Comparator.naturalOrder())
|
||||
.forEach(roles::add);
|
||||
return new AdminUserSummaryResponse(
|
||||
user.getId(),
|
||||
user.getDisplayName(),
|
||||
user.getEmail(),
|
||||
List.copyOf(roles),
|
||||
user.getStatus().name(),
|
||||
user.getCreatedAt()
|
||||
);
|
||||
}
|
||||
|
||||
private String normalize(String keyword) {
|
||||
if (keyword == null || keyword.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return keyword.trim();
|
||||
}
|
||||
|
||||
private UserStatus parseStatus(String status) {
|
||||
if (status == null || status.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return parseRequiredStatus(status);
|
||||
}
|
||||
|
||||
private UserStatus parseRequiredStatus(String status) {
|
||||
try {
|
||||
return UserStatus.valueOf(status.trim().toUpperCase());
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new DomainBadRequestException("error.user.status.invalid", status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,8 +5,10 @@ import com.iflytek.skillhub.domain.skill.Skill;
|
|||
import com.iflytek.skillhub.domain.skill.SkillRepository;
|
||||
import com.iflytek.skillhub.domain.skill.SkillVersion;
|
||||
import com.iflytek.skillhub.domain.skill.SkillVersionRepository;
|
||||
import com.iflytek.skillhub.domain.social.SkillStarRepository;
|
||||
import com.iflytek.skillhub.dto.SkillSummaryResponse;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
|
@ -21,14 +23,17 @@ public class MySkillAppService {
|
|||
private final SkillRepository skillRepository;
|
||||
private final NamespaceRepository namespaceRepository;
|
||||
private final SkillVersionRepository skillVersionRepository;
|
||||
private final SkillStarRepository skillStarRepository;
|
||||
|
||||
public MySkillAppService(
|
||||
SkillRepository skillRepository,
|
||||
NamespaceRepository namespaceRepository,
|
||||
SkillVersionRepository skillVersionRepository) {
|
||||
SkillVersionRepository skillVersionRepository,
|
||||
SkillStarRepository skillStarRepository) {
|
||||
this.skillRepository = skillRepository;
|
||||
this.namespaceRepository = namespaceRepository;
|
||||
this.skillVersionRepository = skillVersionRepository;
|
||||
this.skillStarRepository = skillStarRepository;
|
||||
}
|
||||
|
||||
public List<SkillSummaryResponse> listMySkills(String userId) {
|
||||
|
|
@ -62,6 +67,50 @@ public class MySkillAppService {
|
|||
.toList();
|
||||
}
|
||||
|
||||
public List<SkillSummaryResponse> listMyStars(String userId) {
|
||||
List<com.iflytek.skillhub.domain.social.SkillStar> stars = skillStarRepository.findByUserId(
|
||||
userId,
|
||||
PageRequest.of(0, 200)
|
||||
).getContent();
|
||||
|
||||
List<Long> skillIds = stars.stream()
|
||||
.map(com.iflytek.skillhub.domain.social.SkillStar::getSkillId)
|
||||
.distinct()
|
||||
.toList();
|
||||
Map<Long, Skill> skillsById = skillIds.isEmpty()
|
||||
? Map.of()
|
||||
: skillRepository.findByIdIn(skillIds).stream()
|
||||
.collect(Collectors.toMap(Skill::getId, Function.identity()));
|
||||
|
||||
List<Long> latestVersionIds = skillsById.values().stream()
|
||||
.map(Skill::getLatestVersionId)
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
Map<Long, SkillVersion> versionsById = latestVersionIds.isEmpty()
|
||||
? Map.of()
|
||||
: skillVersionRepository.findByIdIn(latestVersionIds).stream()
|
||||
.collect(Collectors.toMap(SkillVersion::getId, Function.identity()));
|
||||
|
||||
List<Long> namespaceIds = skillsById.values().stream()
|
||||
.map(Skill::getNamespaceId)
|
||||
.distinct()
|
||||
.toList();
|
||||
Map<Long, String> namespaceSlugsById = namespaceIds.isEmpty()
|
||||
? Map.of()
|
||||
: namespaceRepository.findByIdIn(namespaceIds).stream()
|
||||
.collect(Collectors.toMap(
|
||||
com.iflytek.skillhub.domain.namespace.Namespace::getId,
|
||||
com.iflytek.skillhub.domain.namespace.Namespace::getSlug));
|
||||
|
||||
return stars.stream()
|
||||
.sorted(Comparator.comparing(com.iflytek.skillhub.domain.social.SkillStar::getCreatedAt).reversed())
|
||||
.map(star -> skillsById.get(star.getSkillId()))
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.map(skill -> toSummaryResponse(skill, versionsById, namespaceSlugsById))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private SkillSummaryResponse toSummaryResponse(
|
||||
Skill skill,
|
||||
Map<Long, SkillVersion> versionsById,
|
||||
|
|
|
|||
|
|
@ -100,5 +100,10 @@ error.auth.merge.pendingExists=A pending merge request already exists for this s
|
|||
error.auth.merge.localCredentialConflict=Both accounts already have local credentials
|
||||
error.auth.merge.requestNotFound=Merge request not found
|
||||
error.auth.merge.requestNotPending=Merge request is not pending
|
||||
error.auth.merge.requestNotVerified=Merge request is not verified
|
||||
error.auth.merge.tokenExpired=Merge verification token has expired
|
||||
error.auth.merge.invalidToken=Invalid merge verification token
|
||||
error.admin.role.assign_super_admin_forbidden=Only SUPER_ADMIN can assign the SUPER_ADMIN role
|
||||
error.role.notFound=Role not found: {0}
|
||||
error.user.notFound=User not found: {0}
|
||||
error.user.status.invalid=Invalid user status: {0}
|
||||
|
|
|
|||
|
|
@ -100,5 +100,10 @@ error.auth.merge.pendingExists=该待合并账号已有进行中的合并请求
|
|||
error.auth.merge.localCredentialConflict=两个账号都已启用本地密码登录,无法自动合并
|
||||
error.auth.merge.requestNotFound=未找到合并请求
|
||||
error.auth.merge.requestNotPending=该合并请求不处于待验证状态
|
||||
error.auth.merge.requestNotVerified=该合并请求尚未完成验证
|
||||
error.auth.merge.tokenExpired=合并验证 token 已过期
|
||||
error.auth.merge.invalidToken=合并验证 token 无效
|
||||
error.admin.role.assign_super_admin_forbidden=只有 SUPER_ADMIN 才能分配 SUPER_ADMIN 角色
|
||||
error.role.notFound=角色不存在:{0}
|
||||
error.user.notFound=用户不存在:{0}
|
||||
error.user.status.invalid=非法的用户状态:{0}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ package com.iflytek.skillhub.compat;
|
|||
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository;
|
||||
import com.iflytek.skillhub.auth.device.DeviceAuthService;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.skill.service.SkillQueryService;
|
||||
import com.iflytek.skillhub.service.SkillSearchAppService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
|
|
@ -14,8 +17,12 @@ import org.springframework.test.context.ActiveProfiles;
|
|||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
|
|
@ -35,8 +42,17 @@ class ClawHubCompatControllerTest {
|
|||
@MockBean
|
||||
private DeviceAuthService deviceAuthService;
|
||||
|
||||
@MockBean
|
||||
private SkillSearchAppService skillSearchAppService;
|
||||
|
||||
@MockBean
|
||||
private SkillQueryService skillQueryService;
|
||||
|
||||
@Test
|
||||
void search_returns_200() throws Exception {
|
||||
given(skillSearchAppService.search("test", null, "relevance", 0, 20, null, Map.of()))
|
||||
.willReturn(new SkillSearchAppService.SearchResponse(List.of(), 0, 0, 20));
|
||||
|
||||
mockMvc.perform(get("/api/compat/v1/search")
|
||||
.param("q", "test"))
|
||||
.andExpect(status().isOk())
|
||||
|
|
@ -46,6 +62,25 @@ class ClawHubCompatControllerTest {
|
|||
|
||||
@Test
|
||||
void resolve_returns_correct_downloadUrl() throws Exception {
|
||||
given(skillQueryService.resolveVersion(
|
||||
eq("global"),
|
||||
eq("my-skill"),
|
||||
isNull(),
|
||||
eq("latest"),
|
||||
isNull(),
|
||||
isNull(),
|
||||
eq(Map.<Long, NamespaceRole>of())))
|
||||
.willReturn(new SkillQueryService.ResolvedVersionDTO(
|
||||
1L,
|
||||
"global",
|
||||
"my-skill",
|
||||
"latest",
|
||||
1L,
|
||||
"sha256:test",
|
||||
true,
|
||||
"/api/v1/skills/global/my-skill/download"
|
||||
));
|
||||
|
||||
mockMvc.perform(get("/api/compat/v1/resolve/my-skill"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.canonicalSlug").value("my-skill"))
|
||||
|
|
@ -55,6 +90,25 @@ class ClawHubCompatControllerTest {
|
|||
|
||||
@Test
|
||||
void resolve_with_namespace_returns_correct_downloadUrl() throws Exception {
|
||||
given(skillQueryService.resolveVersion(
|
||||
eq("team-ai"),
|
||||
eq("my-skill"),
|
||||
isNull(),
|
||||
eq("latest"),
|
||||
isNull(),
|
||||
isNull(),
|
||||
eq(Map.<Long, NamespaceRole>of())))
|
||||
.willReturn(new SkillQueryService.ResolvedVersionDTO(
|
||||
1L,
|
||||
"team-ai",
|
||||
"my-skill",
|
||||
"latest",
|
||||
1L,
|
||||
"sha256:test",
|
||||
true,
|
||||
"/api/v1/skills/team-ai/my-skill/download"
|
||||
));
|
||||
|
||||
mockMvc.perform(get("/api/compat/v1/resolve/team-ai--my-skill"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.canonicalSlug").value("team-ai--my-skill"))
|
||||
|
|
@ -64,6 +118,25 @@ class ClawHubCompatControllerTest {
|
|||
|
||||
@Test
|
||||
void resolve_with_version_returns_specified_version() throws Exception {
|
||||
given(skillQueryService.resolveVersion(
|
||||
eq("global"),
|
||||
eq("my-skill"),
|
||||
eq("1.0.0"),
|
||||
isNull(),
|
||||
isNull(),
|
||||
isNull(),
|
||||
eq(Map.<Long, NamespaceRole>of())))
|
||||
.willReturn(new SkillQueryService.ResolvedVersionDTO(
|
||||
1L,
|
||||
"global",
|
||||
"my-skill",
|
||||
"1.0.0",
|
||||
2L,
|
||||
"sha256:test",
|
||||
true,
|
||||
"/api/v1/skills/global/my-skill/download"
|
||||
));
|
||||
|
||||
mockMvc.perform(get("/api/compat/v1/resolve/my-skill")
|
||||
.param("version", "1.0.0"))
|
||||
.andExpect(status().isOk())
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.iflytek.skillhub.controller;
|
||||
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
|
|
@ -73,6 +74,27 @@ class AccountMergeControllerTest {
|
|||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.message").value("Account merge verified"));
|
||||
|
||||
verify(accountMergeService).verify("usr_primary", 1L, "merge-token");
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirm_returnsSuccessMessage() throws Exception {
|
||||
PlatformPrincipal principal = new PlatformPrincipal("usr_primary", "primary", "p@example.com", "", "local", Set.of());
|
||||
var auth = new UsernamePasswordAuthenticationToken(principal, null, List.of(new SimpleGrantedAuthority("ROLE_SUPER_ADMIN")));
|
||||
|
||||
mockMvc.perform(post("/api/v1/account/merge/confirm")
|
||||
.with(authentication(auth))
|
||||
.with(csrf())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"mergeRequestId":1}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.message").value("Account merge completed"));
|
||||
|
||||
verify(accountMergeService).confirm("usr_primary", 1L);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import com.iflytek.skillhub.TestRedisConfig;
|
|||
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
||||
import com.iflytek.skillhub.auth.device.DeviceAuthService;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository;
|
||||
import com.iflytek.skillhub.dto.AdminUserSummaryResponse;
|
||||
import com.iflytek.skillhub.dto.PageResponse;
|
||||
import com.iflytek.skillhub.service.AdminUserManagementService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
|
|
@ -17,7 +20,9 @@ import org.springframework.test.web.servlet.MockMvc;
|
|||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
|
|
@ -41,6 +46,9 @@ class UserManagementControllerTest {
|
|||
@MockBean
|
||||
private DeviceAuthService deviceAuthService;
|
||||
|
||||
@MockBean
|
||||
private AdminUserManagementService adminUserManagementService;
|
||||
|
||||
@Test
|
||||
void listUsers_unauthenticated_returns401() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/admin/users"))
|
||||
|
|
@ -49,6 +57,17 @@ class UserManagementControllerTest {
|
|||
|
||||
@Test
|
||||
void listUsers_withUserAdminRole_returns200() throws Exception {
|
||||
given(adminUserManagementService.listUsers(null, null, 0, 20))
|
||||
.willReturn(new PageResponse<>(
|
||||
List.of(
|
||||
new AdminUserSummaryResponse("user-1", "alice", "alice@example.com", List.of("USER"), "ACTIVE", LocalDateTime.parse("2026-03-12T12:00:00")),
|
||||
new AdminUserSummaryResponse("user-2", "bob", "bob@example.com", List.of("USER_ADMIN"), "PENDING", LocalDateTime.parse("2026-03-12T13:00:00"))
|
||||
),
|
||||
2,
|
||||
0,
|
||||
20
|
||||
));
|
||||
|
||||
PlatformPrincipal principal = new PlatformPrincipal(
|
||||
"user-42", "admin", "admin@example.com", "", "github", Set.of("USER_ADMIN")
|
||||
);
|
||||
|
|
@ -65,6 +84,14 @@ class UserManagementControllerTest {
|
|||
|
||||
@Test
|
||||
void listUsers_withSuperAdminRole_returns200() throws Exception {
|
||||
given(adminUserManagementService.listUsers(null, null, 0, 20))
|
||||
.willReturn(new PageResponse<>(
|
||||
List.of(new AdminUserSummaryResponse("user-99", "superadmin", "super@example.com", List.of("SUPER_ADMIN"), "ACTIVE", LocalDateTime.parse("2026-03-12T14:00:00"))),
|
||||
1,
|
||||
0,
|
||||
20
|
||||
));
|
||||
|
||||
PlatformPrincipal principal = new PlatformPrincipal(
|
||||
"user-99", "superadmin", "super@example.com", "", "github", Set.of("SUPER_ADMIN")
|
||||
);
|
||||
|
|
@ -79,6 +106,9 @@ class UserManagementControllerTest {
|
|||
|
||||
@Test
|
||||
void updateUserRole_withUserAdminRole_returns200() throws Exception {
|
||||
given(adminUserManagementService.updateUserRole(org.mockito.ArgumentMatchers.eq("user-123"), org.mockito.ArgumentMatchers.eq("USER_ADMIN"), org.mockito.ArgumentMatchers.any()))
|
||||
.willReturn(new AdminUserSummaryResponse("user-123", "target", "target@example.com", List.of("USER_ADMIN"), "ACTIVE", LocalDateTime.parse("2026-03-12T15:00:00")));
|
||||
|
||||
PlatformPrincipal principal = new PlatformPrincipal(
|
||||
"user-42", "admin", "admin@example.com", "", "github", Set.of("USER_ADMIN")
|
||||
);
|
||||
|
|
@ -86,7 +116,7 @@ class UserManagementControllerTest {
|
|||
principal, null, List.of(new SimpleGrantedAuthority("ROLE_USER_ADMIN"))
|
||||
);
|
||||
|
||||
String requestBody = "{\"role\":\"MODERATOR\"}";
|
||||
String requestBody = "{\"role\":\"USER_ADMIN\"}";
|
||||
|
||||
mockMvc.perform(put("/api/v1/admin/users/user-123/role")
|
||||
.with(authentication(auth))
|
||||
|
|
@ -96,11 +126,14 @@ class UserManagementControllerTest {
|
|||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.userId").value("user-123"))
|
||||
.andExpect(jsonPath("$.data.role").value("MODERATOR"));
|
||||
.andExpect(jsonPath("$.data.role").value("USER_ADMIN"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateUserStatus_withUserAdminRole_returns200() throws Exception {
|
||||
given(adminUserManagementService.updateUserStatus("user-123", "DISABLED"))
|
||||
.willReturn(new AdminUserSummaryResponse("user-123", "target", "target@example.com", List.of("USER"), "DISABLED", LocalDateTime.parse("2026-03-12T16:00:00")));
|
||||
|
||||
PlatformPrincipal principal = new PlatformPrincipal(
|
||||
"user-42", "admin", "admin@example.com", "", "github", Set.of("USER_ADMIN")
|
||||
);
|
||||
|
|
@ -108,7 +141,7 @@ class UserManagementControllerTest {
|
|||
principal, null, List.of(new SimpleGrantedAuthority("ROLE_USER_ADMIN"))
|
||||
);
|
||||
|
||||
String requestBody = "{\"status\":\"BANNED\"}";
|
||||
String requestBody = "{\"status\":\"DISABLED\"}";
|
||||
|
||||
mockMvc.perform(put("/api/v1/admin/users/user-123/status")
|
||||
.with(authentication(auth))
|
||||
|
|
@ -118,6 +151,6 @@ class UserManagementControllerTest {
|
|||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.userId").value("user-123"))
|
||||
.andExpect(jsonPath("$.data.status").value("BANNED"));
|
||||
.andExpect(jsonPath("$.data.status").value("DISABLED"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,8 @@ public class SecurityConfig {
|
|||
"/swagger-ui/**",
|
||||
"/.well-known/**",
|
||||
"/api/compat/v1/search",
|
||||
"/api/compat/v1/resolve/**"
|
||||
"/api/compat/v1/resolve/**",
|
||||
"/api/compat/v1/download/**"
|
||||
).permitAll()
|
||||
.requestMatchers(HttpMethod.GET,
|
||||
"/api/v1/skills",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import java.time.LocalDateTime;
|
|||
public class AccountMergeRequest {
|
||||
|
||||
public static final String STATUS_PENDING = "PENDING";
|
||||
public static final String STATUS_VERIFIED = "VERIFIED";
|
||||
public static final String STATUS_COMPLETED = "COMPLETED";
|
||||
public static final String STATUS_CANCELLED = "CANCELLED";
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ public class AccountMergeService {
|
|||
}
|
||||
|
||||
@Transactional
|
||||
public void verifyAndComplete(String primaryUserId, Long mergeRequestId, String verificationToken) {
|
||||
public void verify(String primaryUserId, Long mergeRequestId, String verificationToken) {
|
||||
AccountMergeRequest request = mergeRequestRepository.findByIdAndPrimaryUserId(mergeRequestId, primaryUserId)
|
||||
.orElseThrow(() -> new AuthFlowException(HttpStatus.NOT_FOUND, "error.auth.merge.requestNotFound"));
|
||||
if (!AccountMergeRequest.STATUS_PENDING.equals(request.getStatus())) {
|
||||
|
|
@ -113,6 +113,23 @@ public class AccountMergeService {
|
|||
throw new AuthFlowException(HttpStatus.UNAUTHORIZED, "error.auth.merge.invalidToken");
|
||||
}
|
||||
|
||||
loadActiveUser(primaryUserId);
|
||||
UserAccount secondaryUser = userAccountRepository.findById(request.getSecondaryUserId())
|
||||
.orElseThrow(() -> new AuthFlowException(HttpStatus.NOT_FOUND, "error.auth.merge.secondaryNotFound"));
|
||||
validateMergePair(loadActiveUser(primaryUserId), secondaryUser);
|
||||
|
||||
request.setStatus(AccountMergeRequest.STATUS_VERIFIED);
|
||||
mergeRequestRepository.save(request);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void confirm(String primaryUserId, Long mergeRequestId) {
|
||||
AccountMergeRequest request = mergeRequestRepository.findByIdAndPrimaryUserId(mergeRequestId, primaryUserId)
|
||||
.orElseThrow(() -> new AuthFlowException(HttpStatus.NOT_FOUND, "error.auth.merge.requestNotFound"));
|
||||
if (!AccountMergeRequest.STATUS_VERIFIED.equals(request.getStatus())) {
|
||||
throw new AuthFlowException(HttpStatus.BAD_REQUEST, "error.auth.merge.requestNotVerified");
|
||||
}
|
||||
|
||||
UserAccount primaryUser = loadActiveUser(primaryUserId);
|
||||
UserAccount secondaryUser = userAccountRepository.findById(request.getSecondaryUserId())
|
||||
.orElseThrow(() -> new AuthFlowException(HttpStatus.NOT_FOUND, "error.auth.merge.secondaryNotFound"));
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.iflytek.skillhub.auth.repository;
|
||||
|
||||
import com.iflytek.skillhub.auth.entity.UserRoleBinding;
|
||||
import java.util.Collection;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.util.List;
|
||||
|
|
@ -8,4 +9,6 @@ import java.util.List;
|
|||
@Repository
|
||||
public interface UserRoleBindingRepository extends JpaRepository<UserRoleBinding, Long> {
|
||||
List<UserRoleBinding> findByUserId(String userId);
|
||||
List<UserRoleBinding> findByUserIdIn(Collection<String> userIds);
|
||||
long deleteByUserId(String userId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ public interface ReviewTaskRepository {
|
|||
ReviewTask save(ReviewTask reviewTask);
|
||||
Optional<ReviewTask> findById(Long id);
|
||||
Optional<ReviewTask> findBySkillVersionIdAndStatus(Long skillVersionId, ReviewTaskStatus status);
|
||||
Page<ReviewTask> findByStatus(ReviewTaskStatus status, Pageable pageable);
|
||||
Page<ReviewTask> findByNamespaceIdAndStatus(Long namespaceId, ReviewTaskStatus status, Pageable pageable);
|
||||
Page<ReviewTask> findBySubmittedByAndStatus(String submittedBy, ReviewTaskStatus status, Pageable pageable);
|
||||
void delete(ReviewTask reviewTask);
|
||||
|
|
|
|||
|
|
@ -61,6 +61,9 @@ public class SkillQueryService {
|
|||
String status,
|
||||
Long downloadCount,
|
||||
Integer starCount,
|
||||
java.math.BigDecimal ratingAvg,
|
||||
Integer ratingCount,
|
||||
boolean hidden,
|
||||
String latestVersion,
|
||||
Long namespaceId
|
||||
) {}
|
||||
|
|
@ -120,6 +123,9 @@ public class SkillQueryService {
|
|||
skill.getStatus().name(),
|
||||
skill.getDownloadCount(),
|
||||
skill.getStarCount(),
|
||||
skill.getRatingAvg(),
|
||||
skill.getRatingCount(),
|
||||
skill.isHidden(),
|
||||
latestVersion,
|
||||
skill.getNamespaceId()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
package com.iflytek.skillhub.domain.user;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface UserAccountRepository {
|
||||
Optional<UserAccount> findById(String id);
|
||||
List<UserAccount> findByIdIn(List<String> ids);
|
||||
Optional<UserAccount> findByEmailIgnoreCase(String email);
|
||||
Page<UserAccount> search(String keyword, UserStatus status, Pageable pageable);
|
||||
UserAccount save(UserAccount user);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ public interface ReviewTaskJpaRepository extends JpaRepository<ReviewTask, Long>
|
|||
|
||||
Optional<ReviewTask> findBySkillVersionIdAndStatus(Long skillVersionId, ReviewTaskStatus status);
|
||||
|
||||
Page<ReviewTask> findByStatus(ReviewTaskStatus status, Pageable pageable);
|
||||
|
||||
Page<ReviewTask> findByNamespaceIdAndStatus(Long namespaceId, ReviewTaskStatus status, Pageable pageable);
|
||||
|
||||
Page<ReviewTask> findBySubmittedByAndStatus(String submittedBy, ReviewTaskStatus status, Pageable pageable);
|
||||
|
|
|
|||
|
|
@ -2,10 +2,31 @@ package com.iflytek.skillhub.infra.jpa;
|
|||
|
||||
import com.iflytek.skillhub.domain.user.UserAccount;
|
||||
import com.iflytek.skillhub.domain.user.UserAccountRepository;
|
||||
import com.iflytek.skillhub.domain.user.UserStatus;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
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;
|
||||
|
||||
@Repository
|
||||
public interface UserAccountJpaRepository
|
||||
extends JpaRepository<UserAccount, String>, UserAccountRepository {
|
||||
|
||||
@Override
|
||||
@Query("""
|
||||
SELECT u
|
||||
FROM UserAccount u
|
||||
WHERE (:status IS NULL OR u.status = :status)
|
||||
AND (
|
||||
:keyword IS NULL
|
||||
OR lower(u.displayName) LIKE lower(concat('%', :keyword, '%'))
|
||||
OR lower(coalesce(u.email, '')) LIKE lower(concat('%', :keyword, '%'))
|
||||
OR lower(u.id) LIKE lower(concat('%', :keyword, '%'))
|
||||
)
|
||||
""")
|
||||
Page<UserAccount> search(@Param("keyword") String keyword,
|
||||
@Param("status") UserStatus status,
|
||||
Pageable pageable);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue