diff --git a/.github/workflows/publish-images.yml b/.github/workflows/publish-images.yml index f4575c38..74655dc6 100644 --- a/.github/workflows/publish-images.yml +++ b/.github/workflows/publish-images.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - feature/project-init tags: - "v*.*.*" workflow_dispatch: @@ -59,7 +60,7 @@ jobs: images: ${{ matrix.image }} tags: | type=raw,value=edge,enable={{is_default_branch}} - type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-') }} + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} type=ref,event=tag type=sha,format=short,prefix=sha- type=semver,pattern={{version}} diff --git a/README.md b/README.md index 893025a1..a4ed20c5 100644 --- a/README.md +++ b/README.md @@ -37,66 +37,6 @@ firewall, with the same polish you'd expect from a public registry. - Docker & Docker Compose -### One-Command Runtime - -Published runtime images are built by GitHub Actions and pushed to GHCR. -This is the recommended path for anyone who wants a ready-to-use local -environment without building the backend or frontend on their machine. -Published images target both `linux/amd64` and `linux/arm64`. - -Start the latest runtime from `main`: - -```bash -curl -fsSL https://raw.githubusercontent.com/iflytek/skillhub/main/scripts/runtime.sh | sh -s -- up -``` - -Start the first beta release explicitly: - -```bash -curl -fsSL https://raw.githubusercontent.com/iflytek/skillhub/main/scripts/runtime.sh | \ - sh -s -- up --version v0.1.0-beta.1 -``` - -Then open: - -- Web UI: `http://localhost` -- Backend API: `http://localhost:8080` - -The script downloads the runtime files into `${TMPDIR:-/tmp}/skillhub-runtime` -by default and starts Docker Compose from there, so it does not pollute -your current working directory. - -If you want a persistent location instead of the temp directory: - -```bash -curl -fsSL https://raw.githubusercontent.com/iflytek/skillhub/main/scripts/runtime.sh | \ - SKILLHUB_HOME=$HOME/.skillhub-runtime sh -s -- up --version v0.1.0-beta.1 -``` - -Other useful commands: - -- `curl -fsSL https://raw.githubusercontent.com/iflytek/skillhub/main/scripts/runtime.sh | sh -s -- down` -- `curl -fsSL https://raw.githubusercontent.com/iflytek/skillhub/main/scripts/runtime.sh | sh -s -- clean` -- `curl -fsSL https://raw.githubusercontent.com/iflytek/skillhub/main/scripts/runtime.sh | sh -s -- ps` -- `curl -fsSL https://raw.githubusercontent.com/iflytek/skillhub/main/scripts/runtime.sh | sh -s -- logs` - -The runtime stack uses its own Compose project name, so it does not -collide with containers from `make dev-all`. - -Use `clean` if you also want to remove the downloaded runtime files from -`${TMPDIR:-/tmp}/skillhub-runtime`. - -The runtime uses the existing `local,docker` profile combination so it -is immediately usable with the same mock-auth flow as local development. -Available seeded users: - -- `local-user` -- `local-admin` - -Pass `X-Mock-User-Id` to the backend when you need an authenticated -session without configuring GitHub OAuth. If the GHCR package remains -private, run `docker login ghcr.io` before `docker compose up -d`. - ### Local Development ```bash @@ -129,6 +69,57 @@ make dev-all-reset Run `make help` to see all available commands. +### Container Runtime + +Published runtime images are built by GitHub Actions and pushed to GHCR. +This is the supported path for anyone who wants a ready-to-use local +environment without building the backend or frontend on their machine. +Published images target both `linux/amd64` and `linux/arm64`. + +1. Copy the runtime environment template. +2. Pick an image tag. +3. Start the stack with Docker Compose. + +```bash +cp .env.release.example .env.release +``` + +Recommended image tags: + +- `SKILLHUB_VERSION=edge` for the latest `main` build +- `SKILLHUB_VERSION=vX.Y.Z` for a fixed release + +Start the runtime: + +```bash +docker compose --env-file .env.release -f compose.release.yml up -d +``` + +Then open: + +- Web UI: `http://localhost` +- Backend API: `http://localhost:8080` + +Stop it with: + +```bash +docker compose --env-file .env.release -f compose.release.yml down +``` + +The runtime stack uses its own Compose project name, so it does not +collide with containers from `make dev-all`. + +The runtime uses the existing `local,docker` profile combination so it +is immediately usable with the same mock-auth flow as local development. +Available seeded users: + +- `local-user` +- `local-admin` + +Pass `X-Mock-User-Id` to the backend when you need an authenticated +session without configuring GitHub OAuth. If the GHCR package remains +private, run `docker login ghcr.io` before `docker compose up -d`. + ## Architecture ``` diff --git a/scripts/runtime.sh b/scripts/runtime.sh deleted file mode 100644 index 1d04b4d5..00000000 --- a/scripts/runtime.sh +++ /dev/null @@ -1,174 +0,0 @@ -#!/bin/sh - -set -eu - -COMMAND="up" -if [ "$#" -gt 0 ] && [ "${1#-}" = "$1" ]; then - COMMAND="$1" - shift -fi - -SKILLHUB_REF="${SKILLHUB_REF:-main}" -SKILLHUB_HOME_DEFAULT="${TMPDIR:-/tmp}/skillhub-runtime" -SKILLHUB_HOME="${SKILLHUB_HOME:-$SKILLHUB_HOME_DEFAULT}" -SKILLHUB_VERSION_VALUE="${SKILLHUB_VERSION:-}" -SKILLHUB_SERVER_IMAGE_VALUE="${SKILLHUB_SERVER_IMAGE:-}" -SKILLHUB_WEB_IMAGE_VALUE="${SKILLHUB_WEB_IMAGE:-}" - -while [ "$#" -gt 0 ]; do - case "$1" in - --version) - [ "$#" -ge 2 ] || { echo "Missing value for --version" >&2; exit 1; } - SKILLHUB_VERSION_VALUE="$2" - shift 2 - ;; - --home) - [ "$#" -ge 2 ] || { echo "Missing value for --home" >&2; exit 1; } - SKILLHUB_HOME="$2" - shift 2 - ;; - --ref) - [ "$#" -ge 2 ] || { echo "Missing value for --ref" >&2; exit 1; } - SKILLHUB_REF="$2" - shift 2 - ;; - --server-image) - [ "$#" -ge 2 ] || { echo "Missing value for --server-image" >&2; exit 1; } - SKILLHUB_SERVER_IMAGE_VALUE="$2" - shift 2 - ;; - --web-image) - [ "$#" -ge 2 ] || { echo "Missing value for --web-image" >&2; exit 1; } - SKILLHUB_WEB_IMAGE_VALUE="$2" - shift 2 - ;; - --help|-h) - cat < Use a specific image tag, for example v0.1.0 - --home Store runtime files in a specific directory - --ref Download runtime files from a specific Git ref - --server-image Override backend image repository - --web-image Override frontend image repository -EOF - exit 0 - ;; - *) - echo "Unsupported argument: $1" >&2 - exit 1 - ;; - esac -done - -SKILLHUB_RAW_BASE="${SKILLHUB_RAW_BASE:-https://raw.githubusercontent.com/iflytek/skillhub/$SKILLHUB_REF}" -COMPOSE_FILE="$SKILLHUB_HOME/compose.release.yml" -ENV_EXAMPLE_FILE="$SKILLHUB_HOME/.env.release.example" -ENV_FILE="$SKILLHUB_HOME/.env.release" - -find_compose() { - if docker compose version >/dev/null 2>&1; then - echo "docker compose" - return 0 - fi - - if command -v docker-compose >/dev/null 2>&1; then - echo "docker-compose" - return 0 - fi - - echo "Docker Compose is required." >&2 - exit 1 -} - -download_file() { - src="$1" - dest="$2" - tmp="$dest.tmp" - curl -fsSL "$src" -o "$tmp" - mv "$tmp" "$dest" -} - -set_env_value() { - key="$1" - value="$2" - - if [ ! -f "$ENV_FILE" ]; then - return 0 - fi - - tmp="$ENV_FILE.tmp" - if grep -q "^$key=" "$ENV_FILE"; then - sed "s|^$key=.*|$key=$value|" "$ENV_FILE" >"$tmp" - else - cat "$ENV_FILE" >"$tmp" - printf '%s=%s\n' "$key" "$value" >>"$tmp" - fi - mv "$tmp" "$ENV_FILE" -} - -prepare_runtime_files() { - mkdir -p "$SKILLHUB_HOME" - download_file "$SKILLHUB_RAW_BASE/compose.release.yml" "$COMPOSE_FILE" - download_file "$SKILLHUB_RAW_BASE/.env.release.example" "$ENV_EXAMPLE_FILE" - - if [ ! -f "$ENV_FILE" ]; then - cp "$ENV_EXAMPLE_FILE" "$ENV_FILE" - fi - - if [ -n "$SKILLHUB_VERSION_VALUE" ]; then - set_env_value "SKILLHUB_VERSION" "$SKILLHUB_VERSION_VALUE" - fi - - if [ -n "$SKILLHUB_SERVER_IMAGE_VALUE" ]; then - set_env_value "SKILLHUB_SERVER_IMAGE" "$SKILLHUB_SERVER_IMAGE_VALUE" - fi - - if [ -n "$SKILLHUB_WEB_IMAGE_VALUE" ]; then - set_env_value "SKILLHUB_WEB_IMAGE" "$SKILLHUB_WEB_IMAGE_VALUE" - fi -} - -run_compose() { - compose_cmd="$(find_compose)" - # shellcheck disable=SC2086 - $compose_cmd --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@" -} - -prepare_runtime_files - -case "$COMMAND" in - up) - run_compose up -d - cat <&2 - echo "Usage: sh runtime.sh [up|down|clean|ps|logs|pull] [options]" >&2 - exit 1 - ;; -esac diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/DomainBeanConfig.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/DomainBeanConfig.java index 92705731..668a3b44 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/DomainBeanConfig.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/DomainBeanConfig.java @@ -15,15 +15,8 @@ public class DomainBeanConfig { } @Bean - public SkillPackageValidator skillPackageValidator(SkillMetadataParser skillMetadataParser, - SkillPublishProperties skillPublishProperties) { - return new SkillPackageValidator( - skillMetadataParser, - skillPublishProperties.getMaxFileCount(), - skillPublishProperties.getMaxSingleFileSize(), - skillPublishProperties.getMaxPackageSize(), - skillPublishProperties.getAllowedFileExtensions() - ); + public SkillPackageValidator skillPackageValidator(SkillMetadataParser skillMetadataParser) { + return new SkillPackageValidator(skillMetadataParser); } @Bean diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/SkillPublishProperties.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/SkillPublishProperties.java deleted file mode 100644 index 7d5fadd7..00000000 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/SkillPublishProperties.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.iflytek.skillhub.config; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -import java.util.LinkedHashSet; -import java.util.Set; - -@Component -@ConfigurationProperties(prefix = "skillhub.publish") -public class SkillPublishProperties { - - private int maxFileCount = 100; - private long maxSingleFileSize = 1024 * 1024; - private long maxPackageSize = 100 * 1024 * 1024; - private Set allowedFileExtensions = new LinkedHashSet<>(Set.of( - ".md", ".txt", ".json", ".yaml", ".yml", - ".js", ".ts", ".py", ".sh", - ".png", ".jpg", ".svg" - )); - - public int getMaxFileCount() { - return maxFileCount; - } - - public void setMaxFileCount(int maxFileCount) { - this.maxFileCount = maxFileCount; - } - - public long getMaxSingleFileSize() { - return maxSingleFileSize; - } - - public void setMaxSingleFileSize(long maxSingleFileSize) { - this.maxSingleFileSize = maxSingleFileSize; - } - - public long getMaxPackageSize() { - return maxPackageSize; - } - - public void setMaxPackageSize(long maxPackageSize) { - this.maxPackageSize = maxPackageSize; - } - - public Set getAllowedFileExtensions() { - return allowedFileExtensions; - } - - public void setAllowedFileExtensions(Set allowedFileExtensions) { - this.allowedFileExtensions = new LinkedHashSet<>(allowedFileExtensions); - } -} diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/cli/CliPublishController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/cli/CliPublishController.java index c27ee079..76bae215 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/cli/CliPublishController.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/cli/CliPublishController.java @@ -1,7 +1,6 @@ package com.iflytek.skillhub.controller.cli; import com.iflytek.skillhub.controller.BaseApiController; -import com.iflytek.skillhub.controller.support.ZipPackageExtractor; import com.iflytek.skillhub.domain.skill.SkillVisibility; import com.iflytek.skillhub.domain.skill.service.SkillPublishService; import com.iflytek.skillhub.domain.skill.validation.PackageEntry; @@ -13,21 +12,21 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; +import java.util.ArrayList; import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; @RestController @RequestMapping("/api/v1/cli") public class CliPublishController extends BaseApiController { private final SkillPublishService skillPublishService; - private final ZipPackageExtractor zipPackageExtractor; public CliPublishController(SkillPublishService skillPublishService, - ZipPackageExtractor zipPackageExtractor, ApiResponseFactory responseFactory) { super(responseFactory); this.skillPublishService = skillPublishService; - this.zipPackageExtractor = zipPackageExtractor; } @PostMapping("/publish") @@ -40,7 +39,7 @@ public class CliPublishController extends BaseApiController { SkillVisibility skillVisibility = SkillVisibility.valueOf(visibility.toUpperCase()); - List entries = zipPackageExtractor.extract(file); + List entries = extractZipEntries(file); SkillPublishService.PublishResult publishResult = skillPublishService.publishFromEntries( namespace, @@ -61,4 +60,35 @@ public class CliPublishController extends BaseApiController { return ok("response.success.published", response); } + + private List extractZipEntries(MultipartFile file) throws IOException { + List entries = new ArrayList<>(); + + try (ZipInputStream zis = new ZipInputStream(file.getInputStream())) { + ZipEntry zipEntry; + while ((zipEntry = zis.getNextEntry()) != null) { + if (!zipEntry.isDirectory()) { + byte[] content = zis.readAllBytes(); + entries.add(new PackageEntry( + zipEntry.getName(), + content, + content.length, + determineContentType(zipEntry.getName()) + )); + } + zis.closeEntry(); + } + } + + return entries; + } + + private String determineContentType(String filename) { + if (filename.endsWith(".py")) return "text/x-python"; + if (filename.endsWith(".json")) return "application/json"; + if (filename.endsWith(".yaml") || filename.endsWith(".yml")) return "application/x-yaml"; + if (filename.endsWith(".txt")) return "text/plain"; + if (filename.endsWith(".md")) return "text/markdown"; + return "application/octet-stream"; + } } diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/PromotionController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/PromotionController.java index eb074de5..d65bf9f9 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/PromotionController.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/PromotionController.java @@ -4,13 +4,10 @@ import com.iflytek.skillhub.auth.rbac.RbacService; import com.iflytek.skillhub.controller.BaseApiController; import com.iflytek.skillhub.domain.namespace.Namespace; import com.iflytek.skillhub.domain.namespace.NamespaceRepository; -import com.iflytek.skillhub.domain.namespace.NamespaceRole; import com.iflytek.skillhub.domain.review.PromotionRequest; import com.iflytek.skillhub.domain.review.PromotionRequestRepository; import com.iflytek.skillhub.domain.review.PromotionService; import com.iflytek.skillhub.domain.review.ReviewTaskStatus; -import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException; -import com.iflytek.skillhub.domain.shared.exception.DomainNotFoundException; import com.iflytek.skillhub.domain.skill.Skill; import com.iflytek.skillhub.domain.skill.SkillRepository; import com.iflytek.skillhub.domain.skill.SkillVersion; @@ -22,7 +19,6 @@ 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; @RestController @@ -58,13 +54,10 @@ public class PromotionController extends BaseApiController { @PostMapping public ApiResponse submitPromotion( @RequestBody PromotionRequestDto request, - @RequestAttribute("userId") String userId, - @RequestAttribute(value = "userNsRoles", required = false) Map userNsRoles) { + @RequestAttribute("userId") String userId) { PromotionRequest promotion = promotionService.submitPromotion( request.sourceSkillId(), request.sourceVersionId(), - request.targetNamespaceId(), userId, - userNsRoles != null ? userNsRoles : Map.of(), - rbacService.getUserRoleCodes(userId)); + request.targetNamespaceId(), userId); return ok("response.success.created", toResponse(promotion)); } @@ -98,7 +91,7 @@ public class PromotionController extends BaseApiController { Set platformRoles = rbacService.getUserRoleCodes(userId); boolean hasAdminRole = platformRoles.contains("SKILL_ADMIN") || platformRoles.contains("SUPER_ADMIN"); if (!hasAdminRole) { - throw new DomainForbiddenException("promotion.no_permission"); + return ok("response.success.read", PageResponse.from(Page.empty())); } Page requests = promotionRequestRepository.findByStatus( ReviewTaskStatus.PENDING, PageRequest.of(page, size)); @@ -106,25 +99,16 @@ public class PromotionController extends BaseApiController { } @GetMapping("/{id}") - public ApiResponse getPromotionDetail(@PathVariable Long id, - @RequestAttribute("userId") String userId) { - PromotionRequest promotion = promotionRequestRepository.findById(id) - .orElseThrow(() -> new DomainNotFoundException("promotion.not_found", id)); - if (!promotionService.canViewPromotion(promotion, userId, rbacService.getUserRoleCodes(userId))) { - throw new DomainForbiddenException("promotion.no_permission"); - } + public ApiResponse getPromotionDetail(@PathVariable Long id) { + PromotionRequest promotion = promotionRequestRepository.findById(id).orElseThrow(); return ok("response.success.read", toResponse(promotion)); } private PromotionResponseDto toResponse(PromotionRequest req) { - Skill sourceSkill = skillRepository.findById(req.getSourceSkillId()) - .orElseThrow(() -> new DomainNotFoundException("skill.not_found", req.getSourceSkillId())); - SkillVersion sourceVersion = skillVersionRepository.findById(req.getSourceVersionId()) - .orElseThrow(() -> new DomainNotFoundException("skill_version.not_found", req.getSourceVersionId())); - Namespace sourceNs = namespaceRepository.findById(sourceSkill.getNamespaceId()) - .orElseThrow(() -> new DomainNotFoundException("namespace.not_found", sourceSkill.getNamespaceId())); - Namespace targetNs = namespaceRepository.findById(req.getTargetNamespaceId()) - .orElseThrow(() -> new DomainNotFoundException("namespace.not_found", req.getTargetNamespaceId())); + Skill sourceSkill = skillRepository.findById(req.getSourceSkillId()).orElseThrow(); + SkillVersion sourceVersion = skillVersionRepository.findById(req.getSourceVersionId()).orElseThrow(); + Namespace sourceNs = namespaceRepository.findById(sourceSkill.getNamespaceId()).orElseThrow(); + Namespace targetNs = namespaceRepository.findById(req.getTargetNamespaceId()).orElseThrow(); String submittedByName = userAccountRepository.findById(req.getSubmittedBy()) .map(UserAccount::getDisplayName).orElse(null); diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/ReviewController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/ReviewController.java index 36429752..781962a0 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/ReviewController.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/ReviewController.java @@ -9,8 +9,6 @@ import com.iflytek.skillhub.domain.review.ReviewService; import com.iflytek.skillhub.domain.review.ReviewTask; import com.iflytek.skillhub.domain.review.ReviewTaskRepository; import com.iflytek.skillhub.domain.review.ReviewTaskStatus; -import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException; -import com.iflytek.skillhub.domain.shared.exception.DomainNotFoundException; import com.iflytek.skillhub.domain.skill.Skill; import com.iflytek.skillhub.domain.skill.SkillRepository; import com.iflytek.skillhub.domain.skill.SkillVersion; @@ -58,14 +56,11 @@ public class ReviewController extends BaseApiController { @PostMapping public ApiResponse submitReview( @RequestBody ReviewTaskRequest request, - @RequestAttribute("userId") String userId, - @RequestAttribute(value = "userNsRoles", required = false) Map userNsRoles) { - ReviewTask task = reviewService.submitReview( - request.skillVersionId(), - userId, - userNsRoles != null ? userNsRoles : Map.of(), - rbacService.getUserRoleCodes(userId) - ); + @RequestAttribute("userId") String userId) { + SkillVersion sv = skillVersionRepository.findById(request.skillVersionId()) + .orElseThrow(); + Skill skill = skillRepository.findById(sv.getSkillId()).orElseThrow(); + ReviewTask task = reviewService.submitReview(request.skillVersionId(), skill.getNamespaceId(), userId); return ok("response.success.created", toResponse(task)); } @@ -109,15 +104,7 @@ public class ReviewController extends BaseApiController { @RequestParam Long namespaceId, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size, - @RequestAttribute("userId") String userId, - @RequestAttribute(value = "userNsRoles", required = false) Map userNsRoles) { - Namespace namespace = namespaceRepository.findById(namespaceId) - .orElseThrow(() -> new DomainNotFoundException("namespace.not_found", namespaceId)); - ReviewTask probe = new ReviewTask(0L, namespaceId, "probe"); - if (!reviewService.canReviewNamespace(probe, userId, namespace.getType(), - userNsRoles != null ? userNsRoles : Map.of(), rbacService.getUserRoleCodes(userId))) { - throw new DomainForbiddenException("review.no_permission"); - } + @RequestAttribute("userId") String userId) { Page tasks = reviewTaskRepository.findByNamespaceIdAndStatus( namespaceId, ReviewTaskStatus.PENDING, PageRequest.of(page, size)); return ok("response.success.read", PageResponse.from(tasks.map(this::toResponse))); @@ -134,27 +121,15 @@ public class ReviewController extends BaseApiController { } @GetMapping("/{id}") - public ApiResponse getReviewDetail(@PathVariable Long id, - @RequestAttribute("userId") String userId, - @RequestAttribute(value = "userNsRoles", required = false) Map userNsRoles) { - ReviewTask task = reviewTaskRepository.findById(id) - .orElseThrow(() -> new DomainNotFoundException("review_task.not_found", id)); - Namespace namespace = namespaceRepository.findById(task.getNamespaceId()) - .orElseThrow(() -> new DomainNotFoundException("namespace.not_found", task.getNamespaceId())); - if (!reviewService.canViewReview(task, userId, namespace.getType(), - userNsRoles != null ? userNsRoles : Map.of(), rbacService.getUserRoleCodes(userId))) { - throw new DomainForbiddenException("review.no_permission"); - } + public ApiResponse getReviewDetail(@PathVariable Long id) { + ReviewTask task = reviewTaskRepository.findById(id).orElseThrow(); return ok("response.success.read", toResponse(task)); } private ReviewTaskResponse toResponse(ReviewTask task) { - SkillVersion sv = skillVersionRepository.findById(task.getSkillVersionId()) - .orElseThrow(() -> new DomainNotFoundException("skill_version.not_found", task.getSkillVersionId())); - Skill skill = skillRepository.findById(sv.getSkillId()) - .orElseThrow(() -> new DomainNotFoundException("skill.not_found", sv.getSkillId())); - Namespace ns = namespaceRepository.findById(skill.getNamespaceId()) - .orElseThrow(() -> new DomainNotFoundException("namespace.not_found", skill.getNamespaceId())); + SkillVersion sv = skillVersionRepository.findById(task.getSkillVersionId()).orElseThrow(); + Skill skill = skillRepository.findById(sv.getSkillId()).orElseThrow(); + Namespace ns = namespaceRepository.findById(skill.getNamespaceId()).orElseThrow(); String submittedByName = userAccountRepository.findById(task.getSubmittedBy()) .map(UserAccount::getDisplayName).orElse(null); diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillController.java index 429c2457..2fba90df 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillController.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillController.java @@ -75,16 +75,10 @@ public class SkillController extends BaseApiController { @PathVariable String namespace, @PathVariable String slug, @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "20") int size, - @RequestAttribute(value = "userId", required = false) String userId, - @RequestAttribute(value = "userNsRoles", required = false) Map userNsRoles) { + @RequestParam(defaultValue = "20") int size) { Page versions = skillQueryService.listVersions( - namespace, - slug, - userId, - userNsRoles != null ? userNsRoles : Map.of(), - PageRequest.of(page, size)); + namespace, slug, PageRequest.of(page, size)); PageResponse response = PageResponse.from(versions.map(v -> new SkillVersionResponse( v.getId(), diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillPublishController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillPublishController.java index 7e927815..e984e31e 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillPublishController.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillPublishController.java @@ -1,7 +1,6 @@ package com.iflytek.skillhub.controller.portal; import com.iflytek.skillhub.controller.BaseApiController; -import com.iflytek.skillhub.controller.support.ZipPackageExtractor; import com.iflytek.skillhub.domain.skill.SkillVisibility; import com.iflytek.skillhub.domain.skill.service.SkillPublishService; import com.iflytek.skillhub.domain.skill.validation.PackageEntry; @@ -13,21 +12,21 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; +import java.util.ArrayList; import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; @RestController @RequestMapping("/api/v1/skills") public class SkillPublishController extends BaseApiController { private final SkillPublishService skillPublishService; - private final ZipPackageExtractor zipPackageExtractor; public SkillPublishController(SkillPublishService skillPublishService, - ZipPackageExtractor zipPackageExtractor, ApiResponseFactory responseFactory) { super(responseFactory); this.skillPublishService = skillPublishService; - this.zipPackageExtractor = zipPackageExtractor; } @PostMapping("/{namespace}/publish") @@ -40,7 +39,7 @@ public class SkillPublishController extends BaseApiController { SkillVisibility skillVisibility = SkillVisibility.valueOf(visibility.toUpperCase()); - List entries = zipPackageExtractor.extract(file); + List entries = extractZipEntries(file); SkillPublishService.PublishResult publishResult = skillPublishService.publishFromEntries( namespace, @@ -61,4 +60,35 @@ public class SkillPublishController extends BaseApiController { return ok("response.success.published", response); } + + private List extractZipEntries(MultipartFile file) throws IOException { + List entries = new ArrayList<>(); + + try (ZipInputStream zis = new ZipInputStream(file.getInputStream())) { + ZipEntry zipEntry; + while ((zipEntry = zis.getNextEntry()) != null) { + if (!zipEntry.isDirectory()) { + byte[] content = zis.readAllBytes(); + entries.add(new PackageEntry( + zipEntry.getName(), + content, + content.length, + determineContentType(zipEntry.getName()) + )); + } + zis.closeEntry(); + } + } + + return entries; + } + + private String determineContentType(String filename) { + if (filename.endsWith(".py")) return "text/x-python"; + if (filename.endsWith(".json")) return "application/json"; + if (filename.endsWith(".yaml") || filename.endsWith(".yml")) return "application/x-yaml"; + if (filename.endsWith(".txt")) return "text/plain"; + if (filename.endsWith(".md")) return "text/markdown"; + return "application/octet-stream"; + } } diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillTagController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillTagController.java index eab43d3f..1882dac8 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillTagController.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillTagController.java @@ -1,7 +1,6 @@ package com.iflytek.skillhub.controller.portal; import com.iflytek.skillhub.controller.BaseApiController; -import com.iflytek.skillhub.domain.namespace.NamespaceRole; import com.iflytek.skillhub.domain.skill.SkillTag; import com.iflytek.skillhub.domain.skill.service.SkillTagService; import com.iflytek.skillhub.dto.ApiResponse; @@ -13,7 +12,6 @@ import jakarta.validation.Valid; import org.springframework.web.bind.annotation.*; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; @RestController @@ -31,16 +29,9 @@ public class SkillTagController extends BaseApiController { @GetMapping public ApiResponse> listTags( @PathVariable String namespace, - @PathVariable String slug, - @RequestAttribute(value = "userId", required = false) String userId, - @RequestAttribute(value = "userNsRoles", required = false) Map userNsRoles) { + @PathVariable String slug) { - List tags = skillTagService.listTags( - namespace, - slug, - userId, - userNsRoles != null ? userNsRoles : Map.of() - ); + List tags = skillTagService.listTags(namespace, slug); List response = tags.stream() .map(TagResponse::from) diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/support/ZipPackageExtractor.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/support/ZipPackageExtractor.java deleted file mode 100644 index f9791115..00000000 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/support/ZipPackageExtractor.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.iflytek.skillhub.controller.support; - -import com.iflytek.skillhub.config.SkillPublishProperties; -import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException; -import com.iflytek.skillhub.domain.skill.validation.PackageEntry; -import org.springframework.stereotype.Component; -import org.springframework.web.multipart.MultipartFile; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.file.InvalidPathException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -@Component -public class ZipPackageExtractor { - - private static final int BUFFER_SIZE = 8192; - - private final SkillPublishProperties properties; - - public ZipPackageExtractor(SkillPublishProperties properties) { - this.properties = properties; - } - - public List extract(MultipartFile file) throws IOException { - List entries = new ArrayList<>(); - Set seenPaths = new HashSet<>(); - long totalSize = 0L; - - try (ZipInputStream zis = new ZipInputStream(file.getInputStream())) { - ZipEntry zipEntry; - while ((zipEntry = zis.getNextEntry()) != null) { - if (zipEntry.isDirectory()) { - zis.closeEntry(); - continue; - } - - if (entries.size() >= properties.getMaxFileCount()) { - throw new DomainBadRequestException("error.skill.publish.package.invalid", - "Too many files: max " + properties.getMaxFileCount()); - } - - String normalizedPath = normalizeEntryPath(zipEntry.getName()); - if (!seenPaths.add(normalizedPath)) { - throw new DomainBadRequestException("error.skill.publish.package.invalid", - "Duplicate package path: " + normalizedPath); - } - - byte[] content = readEntry(zis, normalizedPath); - totalSize += content.length; - if (totalSize > properties.getMaxPackageSize()) { - throw new DomainBadRequestException("error.skill.publish.package.invalid", - "Package too large: max " + properties.getMaxPackageSize() + " bytes"); - } - - entries.add(new PackageEntry( - normalizedPath, - content, - content.length, - determineContentType(normalizedPath) - )); - zis.closeEntry(); - } - } - - return entries; - } - - private byte[] readEntry(ZipInputStream zis, String path) throws IOException { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - byte[] buffer = new byte[BUFFER_SIZE]; - int read; - long fileSize = 0L; - while ((read = zis.read(buffer)) != -1) { - fileSize += read; - if (fileSize > properties.getMaxSingleFileSize()) { - throw new DomainBadRequestException("error.skill.publish.package.invalid", - "File too large: " + path + " (max " + properties.getMaxSingleFileSize() + " bytes)"); - } - outputStream.write(buffer, 0, read); - } - return outputStream.toByteArray(); - } - - private String normalizeEntryPath(String path) { - if (path == null || path.isBlank()) { - throw new DomainBadRequestException("error.skill.publish.package.invalid", "Package entry path is blank"); - } - if (path.contains("\\")) { - throw new DomainBadRequestException("error.skill.publish.package.invalid", - "Package entry must use '/' separators: " + path); - } - - try { - Path normalized = Path.of(path).normalize(); - String normalizedPath = normalized.toString().replace('\\', '/'); - if (normalized.isAbsolute() - || normalizedPath.isBlank() - || normalizedPath.startsWith("../") - || normalizedPath.equals("..") - || path.startsWith("/") - || path.contains("//")) { - throw new DomainBadRequestException("error.skill.publish.package.invalid", - "Unsafe package path: " + path); - } - return normalizedPath; - } catch (InvalidPathException ex) { - throw new DomainBadRequestException("error.skill.publish.package.invalid", - "Invalid package path: " + path); - } - } - - private String determineContentType(String filename) { - if (filename.endsWith(".py")) return "text/x-python"; - if (filename.endsWith(".json")) return "application/json"; - if (filename.endsWith(".yaml") || filename.endsWith(".yml")) return "application/x-yaml"; - if (filename.endsWith(".txt")) return "text/plain"; - if (filename.endsWith(".md")) return "text/markdown"; - return "application/octet-stream"; - } -} diff --git a/server/skillhub-app/src/main/resources/application.yml b/server/skillhub-app/src/main/resources/application.yml index 5ece3012..a9e25284 100644 --- a/server/skillhub-app/src/main/resources/application.yml +++ b/server/skillhub-app/src/main/resources/application.yml @@ -53,15 +53,13 @@ skillhub: access-policy: mode: OPEN storage: - provider: local + type: local local: base-path: ${STORAGE_BASE_PATH:/tmp/skillhub-storage} search: engine: postgres rebuild-on-startup: false publish: - max-file-count: 100 - max-single-file-size: 1048576 # 1MB max-package-size: 104857600 # 100MB allowed-file-extensions: .py,.json,.yaml,.yml,.txt,.md,.sh diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java index 84fa1f23..a8c94865 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java @@ -75,21 +75,7 @@ public class SecurityConfig { "/api/compat/v1/search", "/api/compat/v1/resolve/**" ).permitAll() - .requestMatchers(HttpMethod.GET, - "/api/v1/skills", - "/api/v1/skills/*/*", - "/api/v1/skills/*/*/versions", - "/api/v1/skills/*/*/versions/*", - "/api/v1/skills/*/*/versions/*/files", - "/api/v1/skills/*/*/versions/*/file", - "/api/v1/skills/*/*/resolve", - "/api/v1/skills/*/*/download", - "/api/v1/skills/*/*/versions/*/download", - "/api/v1/skills/*/*/tags", - "/api/v1/skills/*/*/tags/*/files", - "/api/v1/skills/*/*/tags/*/file", - "/api/v1/skills/*/*/tags/*/download" - ).permitAll() + .requestMatchers(HttpMethod.GET, "/api/v1/skills", "/api/v1/skills/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/namespaces", "/api/v1/namespaces/*").permitAll() .requestMatchers("/api/v1/admin/**").hasAnyRole("SUPER_ADMIN", "SKILL_ADMIN", "USER_ADMIN", "AUDITOR") .anyRequest().authenticated() diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/device/DeviceAuthService.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/device/DeviceAuthService.java index 2e3db564..8007f29b 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/device/DeviceAuthService.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/device/DeviceAuthService.java @@ -5,10 +5,7 @@ import com.iflytek.skillhub.auth.token.ApiTokenService; import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.SessionCallback; -import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Service; import java.security.SecureRandom; @@ -79,48 +76,24 @@ public class DeviceAuthService { } public DeviceTokenResponse pollToken(String deviceCode) { - String key = DEVICE_CODE_PREFIX + deviceCode; - DeviceCodeData consumed = redisTemplate.execute(new SessionCallback<>() { - @Override - public DeviceCodeData execute(RedisOperations operations) { - while (true) { - operations.watch(key); - DeviceCodeData data = readDeviceCodeData(operations.opsForValue(), deviceCode); + DeviceCodeData data = readDeviceCodeData(deviceCode); - if (data == null) { - operations.unwatch(); - throw new DomainBadRequestException("error.deviceAuth.deviceCode.invalid"); - } - - switch (data.getStatus()) { - case PENDING -> { - operations.unwatch(); - return null; - } - case USED -> { - operations.unwatch(); - throw new DomainBadRequestException("error.deviceAuth.deviceCode.used"); - } - case AUTHORIZED -> { - data.setStatus(DeviceCodeStatus.USED); - operations.multi(); - operations.opsForValue().set(key, data, 1, TimeUnit.MINUTES); - if (operations.exec() != null) { - return data; - } - } - } - } - } - }); - - if (consumed == null) { - return DeviceTokenResponse.pending(); + if (data == null) { + throw new DomainBadRequestException("error.deviceAuth.deviceCode.invalid"); } - String token = apiTokenService.createToken( - consumed.getUserId(), "device-auth", "[]").rawToken(); - return DeviceTokenResponse.success(token); + return switch (data.getStatus()) { + case PENDING -> DeviceTokenResponse.pending(); + case AUTHORIZED -> { + data.setStatus(DeviceCodeStatus.USED); + redisTemplate.opsForValue().set( + DEVICE_CODE_PREFIX + deviceCode, data, 1, TimeUnit.MINUTES); + String token = apiTokenService.createToken( + data.getUserId(), "device-auth", "[]").rawToken(); + yield DeviceTokenResponse.success(token); + } + case USED -> throw new DomainBadRequestException("error.deviceAuth.deviceCode.used"); + }; } private String generateRandomDeviceCode() { @@ -139,11 +112,7 @@ public class DeviceAuthService { } private DeviceCodeData readDeviceCodeData(String deviceCode) { - return readDeviceCodeData(redisTemplate.opsForValue(), deviceCode); - } - - private DeviceCodeData readDeviceCodeData(ValueOperations valueOperations, String deviceCode) { - Object raw = valueOperations.get(DEVICE_CODE_PREFIX + deviceCode); + Object raw = redisTemplate.opsForValue().get(DEVICE_CODE_PREFIX + deviceCode); if (raw == null) { return null; } diff --git a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/device/DeviceAuthServiceTest.java b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/device/DeviceAuthServiceTest.java index e3118d65..a8ada47d 100644 --- a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/device/DeviceAuthServiceTest.java +++ b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/device/DeviceAuthServiceTest.java @@ -9,8 +9,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.redis.core.RedisOperations; -import org.springframework.data.redis.core.SessionCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; @@ -31,8 +29,6 @@ class DeviceAuthServiceTest { @Mock private ValueOperations valueOperations; - @Mock - private RedisOperations redisOperations; @Mock private ApiTokenService apiTokenService; @@ -41,10 +37,7 @@ class DeviceAuthServiceTest { @BeforeEach void setUp() { - lenient().when(redisTemplate.opsForValue()).thenReturn(valueOperations); - lenient().when(redisOperations.opsForValue()).thenReturn(valueOperations); - lenient().when(redisTemplate.execute(any(SessionCallback.class))) - .thenAnswer(invocation -> invocation.>getArgument(0).execute(redisOperations)); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); service = new DeviceAuthService(redisTemplate, apiTokenService, "https://skillhub.example.com/device", new ObjectMapper()); } @@ -144,7 +137,6 @@ class DeviceAuthServiceTest { redisValue.put("status", "AUTHORIZED"); redisValue.put("userId", "42"); when(valueOperations.get("device:code:device123")).thenReturn(redisValue); - when(redisOperations.exec()).thenReturn(java.util.List.of("OK")); when(apiTokenService.createToken("42", "device-auth", "[]")) .thenReturn(new ApiTokenService.TokenCreateResult("sk_device_token", null)); @@ -153,17 +145,13 @@ class DeviceAuthServiceTest { assertThat(response.error()).isNull(); assertThat(response.accessToken()).isEqualTo("sk_device_token"); assertThat(response.tokenType()).isEqualTo("Bearer"); - verify(redisOperations).watch("device:code:device123"); - verify(redisOperations).multi(); verify(valueOperations).set(eq("device:code:device123"), any(DeviceCodeData.class), eq(1L), eq(TimeUnit.MINUTES)); - verify(redisOperations).exec(); } @Test void pollToken_returns_access_token_when_authorized() { DeviceCodeData data = new DeviceCodeData("device123", "ABCD-1234", DeviceCodeStatus.AUTHORIZED, "42"); when(valueOperations.get("device:code:device123")).thenReturn(data); - when(redisOperations.exec()).thenReturn(java.util.List.of("OK")); when(apiTokenService.createToken("42", "device-auth", "[]")) .thenReturn(new ApiTokenService.TokenCreateResult("sk_device_token", null)); @@ -172,9 +160,6 @@ class DeviceAuthServiceTest { assertThat(response.error()).isNull(); assertThat(response.accessToken()).isEqualTo("sk_device_token"); assertThat(response.tokenType()).isEqualTo("Bearer"); - verify(redisOperations).watch("device:code:device123"); - verify(redisOperations).multi(); verify(valueOperations).set(eq("device:code:device123"), eq(data), eq(1L), eq(TimeUnit.MINUTES)); - verify(redisOperations).exec(); } } diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionService.java index 40aff667..006ba1bf 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionService.java @@ -3,7 +3,6 @@ package com.iflytek.skillhub.domain.review; import com.iflytek.skillhub.domain.event.SkillPublishedEvent; import com.iflytek.skillhub.domain.namespace.Namespace; import com.iflytek.skillhub.domain.namespace.NamespaceRepository; -import com.iflytek.skillhub.domain.namespace.NamespaceRole; import com.iflytek.skillhub.domain.namespace.NamespaceType; import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException; import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException; @@ -52,9 +51,7 @@ public class PromotionService { @Transactional public PromotionRequest submitPromotion(Long sourceSkillId, Long sourceVersionId, - Long targetNamespaceId, String userId, - java.util.Map userNamespaceRoles, - Set platformRoles) { + Long targetNamespaceId, String userId) { Skill sourceSkill = skillRepository.findById(sourceSkillId) .orElseThrow(() -> new DomainNotFoundException("skill.not_found", sourceSkillId)); @@ -69,10 +66,6 @@ public class PromotionService { throw new DomainBadRequestException("promotion.version_not_published", sourceVersionId); } - if (!permissionChecker.canSubmitPromotion(sourceSkill, userId, userNamespaceRoles, platformRoles)) { - throw new DomainForbiddenException("promotion.submit.no_permission"); - } - Namespace targetNamespace = namespaceRepository.findById(targetNamespaceId) .orElseThrow(() -> new DomainNotFoundException("namespace.not_found", targetNamespaceId)); @@ -194,8 +187,4 @@ public class PromotionService { request.setReviewedAt(Instant.now()); return request; } - - public boolean canViewPromotion(PromotionRequest request, String userId, Set platformRoles) { - return permissionChecker.canViewPromotion(request, userId, platformRoles); - } } diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewPermissionChecker.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewPermissionChecker.java index e2f09e8b..259372c9 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewPermissionChecker.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewPermissionChecker.java @@ -2,7 +2,6 @@ package com.iflytek.skillhub.domain.review; import com.iflytek.skillhub.domain.namespace.NamespaceRole; import com.iflytek.skillhub.domain.namespace.NamespaceType; -import com.iflytek.skillhub.domain.skill.Skill; import org.springframework.stereotype.Component; import java.util.Map; @@ -31,52 +30,21 @@ public class ReviewPermissionChecker { return false; } - return canReviewNamespace(task.getNamespaceId(), namespaceType, userNamespaceRoles, platformRoles); - } - - public boolean canSubmitForReview(Skill skill, - String userId, - Map userNamespaceRoles, - Set platformRoles) { - if (skill.getOwnerId().equals(userId)) { - return true; - } - - if (platformRoles.contains("SKILL_ADMIN") - || platformRoles.contains("SUPER_ADMIN")) { - return true; - } - - NamespaceRole role = userNamespaceRoles.get(skill.getNamespaceId()); - return role == NamespaceRole.ADMIN || role == NamespaceRole.OWNER; - } - - public boolean canViewReview(ReviewTask task, - String userId, - NamespaceType namespaceType, - Map userNamespaceRoles, - Set platformRoles) { - if (task.getSubmittedBy().equals(userId)) { - return true; - } - return canReview(task, userId, namespaceType, userNamespaceRoles, platformRoles); - } - - public boolean canReviewNamespace(Long namespaceId, - NamespaceType namespaceType, - Map userNamespaceRoles, - Set platformRoles) { if (platformRoles.contains("SKILL_ADMIN") || platformRoles.contains("SUPER_ADMIN")) { return true; } + // Global namespace: only SKILL_ADMIN or SUPER_ADMIN if (namespaceType == NamespaceType.GLOBAL) { return false; } - NamespaceRole role = userNamespaceRoles.get(namespaceId); - return role == NamespaceRole.ADMIN || role == NamespaceRole.OWNER; + // Team namespace: namespace ADMIN or OWNER + NamespaceRole role = userNamespaceRoles.get( + task.getNamespaceId()); + return role == NamespaceRole.ADMIN + || role == NamespaceRole.OWNER; } /** @@ -93,20 +61,4 @@ public class ReviewPermissionChecker { return platformRoles.contains("SKILL_ADMIN") || platformRoles.contains("SUPER_ADMIN"); } - - public boolean canSubmitPromotion(Skill skill, - String userId, - Map userNamespaceRoles, - Set platformRoles) { - return canSubmitForReview(skill, userId, userNamespaceRoles, platformRoles); - } - - public boolean canViewPromotion(PromotionRequest request, - String userId, - Set platformRoles) { - if (request.getSubmittedBy().equals(userId)) { - return true; - } - return canReviewPromotion(request, userId, platformRoles); - } } diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewService.java index 9207b8cb..a9392024 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewService.java @@ -52,18 +52,9 @@ public class ReviewService { } @Transactional - public ReviewTask submitReview(Long skillVersionId, - String userId, - Map userNamespaceRoles, - Set platformRoles) { + public ReviewTask submitReview(Long skillVersionId, Long namespaceId, String userId) { SkillVersion skillVersion = skillVersionRepository.findById(skillVersionId) .orElseThrow(() -> new DomainNotFoundException("skill_version.not_found", skillVersionId)); - Skill skill = skillRepository.findById(skillVersion.getSkillId()) - .orElseThrow(() -> new DomainNotFoundException("skill.not_found", skillVersion.getSkillId())); - - if (!permissionChecker.canSubmitForReview(skill, userId, userNamespaceRoles, platformRoles)) { - throw new DomainForbiddenException("review.submit.no_permission"); - } if (skillVersion.getStatus() != SkillVersionStatus.DRAFT) { throw new DomainBadRequestException("review.submit.not_draft", skillVersionId); @@ -72,7 +63,7 @@ public class ReviewService { skillVersion.setStatus(SkillVersionStatus.PENDING_REVIEW); skillVersionRepository.save(skillVersion); - ReviewTask task = new ReviewTask(skillVersionId, skill.getNamespaceId(), userId); + ReviewTask task = new ReviewTask(skillVersionId, namespaceId, userId); try { return reviewTaskRepository.save(task); } catch (DataIntegrityViolationException e) { @@ -182,20 +173,4 @@ public class ReviewService { skillVersion.setStatus(SkillVersionStatus.DRAFT); skillVersionRepository.save(skillVersion); } - - public boolean canReviewNamespace(ReviewTask task, - String userId, - com.iflytek.skillhub.domain.namespace.NamespaceType namespaceType, - Map userNamespaceRoles, - Set platformRoles) { - return permissionChecker.canReviewNamespace(task.getNamespaceId(), namespaceType, userNamespaceRoles, platformRoles); - } - - public boolean canViewReview(ReviewTask task, - String userId, - com.iflytek.skillhub.domain.namespace.NamespaceType namespaceType, - Map userNamespaceRoles, - Set platformRoles) { - return permissionChecker.canViewReview(task, userId, namespaceType, userNamespaceRoles, platformRoles); - } } diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java index 580f9c22..009e8103 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java @@ -1,6 +1,7 @@ package com.iflytek.skillhub.domain.skill.service; import com.fasterxml.jackson.databind.ObjectMapper; +import com.iflytek.skillhub.domain.event.SkillPublishedEvent; import com.iflytek.skillhub.domain.namespace.Namespace; import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository; import com.iflytek.skillhub.domain.namespace.NamespaceRepository; @@ -16,6 +17,7 @@ import com.iflytek.skillhub.domain.skill.validation.PrePublishValidator; import com.iflytek.skillhub.domain.skill.validation.SkillPackageValidator; import com.iflytek.skillhub.domain.skill.validation.ValidationResult; import com.iflytek.skillhub.storage.ObjectStorageService; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -48,6 +50,7 @@ public class SkillPublishService { private final SkillPackageValidator skillPackageValidator; private final SkillMetadataParser skillMetadataParser; private final PrePublishValidator prePublishValidator; + private final ApplicationEventPublisher eventPublisher; private final ObjectMapper objectMapper; private final ReviewTaskRepository reviewTaskRepository; @@ -61,6 +64,7 @@ public class SkillPublishService { SkillPackageValidator skillPackageValidator, SkillMetadataParser skillMetadataParser, PrePublishValidator prePublishValidator, + ApplicationEventPublisher eventPublisher, ObjectMapper objectMapper, ReviewTaskRepository reviewTaskRepository) { this.namespaceRepository = namespaceRepository; @@ -72,6 +76,7 @@ public class SkillPublishService { this.skillPackageValidator = skillPackageValidator; this.skillMetadataParser = skillMetadataParser; this.prePublishValidator = prePublishValidator; + this.eventPublisher = eventPublisher; this.objectMapper = objectMapper; this.reviewTaskRepository = reviewTaskRepository; } @@ -212,13 +217,17 @@ public class SkillPublishService { ReviewTask reviewTask = new ReviewTask(version.getId(), namespace.getId(), publisherId); reviewTaskRepository.save(reviewTask); - // 12. Update skill metadata without moving the published pointer + // 12. Update skill + skill.setLatestVersionId(version.getId()); skill.setDisplayName(metadata.name()); skill.setSummary(metadata.description()); skill.setUpdatedBy(publisherId); skillRepository.save(skill); - // 13. Return identifiers for the pending review version + // 13. Publish SkillPublishedEvent + eventPublisher.publishEvent(new SkillPublishedEvent(skill.getId(), version.getId(), publisherId)); + + // 14. Return published identifiers return new PublishResult(skill.getId(), skill.getSlug(), version); } diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java index 0b127f28..997b2982 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java @@ -230,13 +230,8 @@ public class SkillQueryService { return objectStorageService.getObject(file.getStorageKey()); } - public Page listVersions(String namespaceSlug, - String skillSlug, - String currentUserId, - Map userNsRoles, - Pageable pageable) { + public Page listVersions(String namespaceSlug, String skillSlug, Pageable pageable) { Skill skill = findSkill(namespaceSlug, skillSlug); - assertPublishedAccessible(skill, currentUserId, userNsRoles); List publishedVersions = skillVersionRepository.findBySkillIdAndStatus( skill.getId(), SkillVersionStatus.PUBLISHED); diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillTagService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillTagService.java index a65ad7d0..39ca41f9 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillTagService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillTagService.java @@ -22,33 +22,24 @@ public class SkillTagService { private final SkillRepository skillRepository; private final SkillVersionRepository skillVersionRepository; private final SkillTagRepository skillTagRepository; - private final VisibilityChecker visibilityChecker; public SkillTagService( NamespaceRepository namespaceRepository, NamespaceMemberRepository namespaceMemberRepository, SkillRepository skillRepository, SkillVersionRepository skillVersionRepository, - SkillTagRepository skillTagRepository, - VisibilityChecker visibilityChecker) { + SkillTagRepository skillTagRepository) { this.namespaceRepository = namespaceRepository; this.namespaceMemberRepository = namespaceMemberRepository; this.skillRepository = skillRepository; this.skillVersionRepository = skillVersionRepository; this.skillTagRepository = skillTagRepository; - this.visibilityChecker = visibilityChecker; } - public List listTags(String namespaceSlug, - String skillSlug, - String currentUserId, - java.util.Map userNamespaceRoles) { + public List listTags(String namespaceSlug, String skillSlug) { Namespace namespace = findNamespace(namespaceSlug); Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug) .orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug)); - if (!visibilityChecker.canAccess(skill, currentUserId, userNamespaceRoles)) { - throw new DomainForbiddenException("error.skill.access.denied", skillSlug); - } List tags = new java.util.ArrayList<>(skillTagRepository.findBySkillId(skill.getId())); if (skill.getLatestVersionId() != null) { diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/SkillPackageValidator.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/SkillPackageValidator.java index 4f11b546..57977dc6 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/SkillPackageValidator.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/SkillPackageValidator.java @@ -3,64 +3,32 @@ package com.iflytek.skillhub.domain.skill.validation; import com.iflytek.skillhub.domain.shared.exception.LocalizedDomainException; import com.iflytek.skillhub.domain.skill.metadata.SkillMetadataParser; -import java.nio.file.InvalidPathException; -import java.nio.file.Path; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; import java.util.Set; public class SkillPackageValidator { + private static final int MAX_FILE_COUNT = 100; + private static final long MAX_SINGLE_FILE_SIZE = 1024 * 1024; // 1MB + private static final long MAX_TOTAL_PACKAGE_SIZE = 10 * 1024 * 1024; // 10MB private static final String SKILL_MD_PATH = "SKILL.md"; - private static final Set DEFAULT_ALLOWED_EXTENSIONS = Set.of( + private static final Set ALLOWED_EXTENSIONS = Set.of( ".md", ".txt", ".json", ".yaml", ".yml", ".js", ".ts", ".py", ".sh", ".png", ".jpg", ".svg" ); private final SkillMetadataParser metadataParser; - private final int maxFileCount; - private final long maxSingleFileSize; - private final long maxTotalPackageSize; - private final Set allowedExtensions; public SkillPackageValidator(SkillMetadataParser metadataParser) { - this(metadataParser, 100, 1024 * 1024, 10 * 1024 * 1024, DEFAULT_ALLOWED_EXTENSIONS); - } - - public SkillPackageValidator(SkillMetadataParser metadataParser, - int maxFileCount, - long maxSingleFileSize, - long maxTotalPackageSize, - Set allowedExtensions) { this.metadataParser = metadataParser; - this.maxFileCount = maxFileCount; - this.maxSingleFileSize = maxSingleFileSize; - this.maxTotalPackageSize = maxTotalPackageSize; - this.allowedExtensions = allowedExtensions.stream() - .map(String::toLowerCase) - .collect(java.util.stream.Collectors.toUnmodifiableSet()); } public ValidationResult validate(List entries) { List errors = new ArrayList<>(); - Set seenPaths = new HashSet<>(); - // 1. Check file count - if (entries.size() > maxFileCount) { - errors.add("Too many files: " + entries.size() + " (max: " + maxFileCount + ")"); - } - - // 2. Validate paths and duplicates - for (PackageEntry entry : entries) { - String normalizedPath = validateAndNormalizePath(entry.path(), errors); - if (normalizedPath != null && !seenPaths.add(normalizedPath)) { - errors.add("Duplicate file path: " + normalizedPath); - } - } - - // 3. Check SKILL.md exists at root + // 1. Check SKILL.md exists at root PackageEntry skillMd = entries.stream() .filter(e -> e.path().equals(SKILL_MD_PATH)) .findFirst() @@ -71,7 +39,7 @@ public class SkillPackageValidator { return ValidationResult.fail(errors); } - // 4. Validate frontmatter + // 2. Validate frontmatter try { String content = new String(skillMd.content()); metadataParser.parse(content); @@ -82,60 +50,34 @@ public class SkillPackageValidator { errors.add("Invalid SKILL.md frontmatter: " + detail); } - // 5. Check file extensions + // 3. Check file count + if (entries.size() > MAX_FILE_COUNT) { + errors.add("Too many files: " + entries.size() + " (max: " + MAX_FILE_COUNT + ")"); + } + + // 4. Check file extensions for (PackageEntry entry : entries) { - String path = entry.path().toLowerCase(); - boolean hasAllowedExtension = allowedExtensions.stream().anyMatch(path::endsWith); + String path = entry.path(); + boolean hasAllowedExtension = ALLOWED_EXTENSIONS.stream() + .anyMatch(path::endsWith); if (!hasAllowedExtension) { errors.add("Disallowed file extension: " + path); } } - // 6. Check single file size + // 5. Check single file size for (PackageEntry entry : entries) { - if (entry.size() > maxSingleFileSize) { - errors.add("File too large: " + entry.path() + " (" + entry.size() + " bytes, max: " + maxSingleFileSize + ")"); + if (entry.size() > MAX_SINGLE_FILE_SIZE) { + errors.add("File too large: " + entry.path() + " (" + entry.size() + " bytes, max: " + MAX_SINGLE_FILE_SIZE + ")"); } } - // 7. Check total package size + // 6. Check total package size long totalSize = entries.stream().mapToLong(PackageEntry::size).sum(); - if (totalSize > maxTotalPackageSize) { - errors.add("Package too large: " + totalSize + " bytes (max: " + maxTotalPackageSize + ")"); + if (totalSize > MAX_TOTAL_PACKAGE_SIZE) { + errors.add("Package too large: " + totalSize + " bytes (max: " + MAX_TOTAL_PACKAGE_SIZE + ")"); } return errors.isEmpty() ? ValidationResult.pass() : ValidationResult.fail(errors); } - - private String validateAndNormalizePath(String path, List errors) { - if (path == null || path.isBlank()) { - errors.add("Package entry path must not be blank"); - return null; - } - if (path.contains("\\")) { - errors.add("Package entry must use '/' separators: " + path); - return null; - } - if (path.startsWith("/") || path.contains("//")) { - errors.add("Unsafe file path: " + path); - return null; - } - - try { - Path normalized = Path.of(path).normalize(); - String normalizedPath = normalized.toString().replace('\\', '/'); - if (normalized.isAbsolute() - || normalizedPath.isBlank() - || normalizedPath.equals(".") - || normalizedPath.equals("..") - || normalizedPath.startsWith("../")) { - errors.add("Unsafe file path: " + path); - return null; - } - return normalizedPath; - } catch (InvalidPathException ex) { - errors.add("Invalid file path: " + path); - return null; - } - } } diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/PromotionServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/PromotionServiceTest.java index 71677aa3..b73e9e52 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/PromotionServiceTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/PromotionServiceTest.java @@ -2,7 +2,6 @@ package com.iflytek.skillhub.domain.review; import com.iflytek.skillhub.domain.event.SkillPublishedEvent; import com.iflytek.skillhub.domain.namespace.Namespace; -import com.iflytek.skillhub.domain.namespace.NamespaceRole; import com.iflytek.skillhub.domain.namespace.NamespaceRepository; import com.iflytek.skillhub.domain.namespace.NamespaceType; import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException; @@ -21,6 +20,7 @@ import jakarta.persistence.EntityManager; import java.util.*; +import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -123,7 +123,6 @@ class PromotionServiceTest { when(skillRepository.findById(SOURCE_SKILL_ID)).thenReturn(Optional.of(sourceSkill)); when(skillVersionRepository.findById(SOURCE_VERSION_ID)).thenReturn(Optional.of(sourceVersion)); - when(permissionChecker.canSubmitPromotion(eq(sourceSkill), eq(USER_ID), anyMap(), anySet())).thenReturn(true); when(namespaceRepository.findById(TARGET_NAMESPACE_ID)).thenReturn(Optional.of(globalNs)); when(promotionRequestRepository.findBySourceVersionIdAndStatus(SOURCE_VERSION_ID, ReviewTaskStatus.PENDING)) .thenReturn(Optional.empty()); @@ -135,8 +134,7 @@ class PromotionServiceTest { }); PromotionRequest result = promotionService.submitPromotion( - SOURCE_SKILL_ID, SOURCE_VERSION_ID, TARGET_NAMESPACE_ID, USER_ID, - Map.of(5L, NamespaceRole.OWNER), Set.of()); + SOURCE_SKILL_ID, SOURCE_VERSION_ID, TARGET_NAMESPACE_ID, USER_ID); assertNotNull(result); assertEquals(SOURCE_SKILL_ID, result.getSourceSkillId()); @@ -151,7 +149,7 @@ class PromotionServiceTest { when(skillRepository.findById(SOURCE_SKILL_ID)).thenReturn(Optional.empty()); assertThrows(DomainNotFoundException.class, - () -> promotionService.submitPromotion(SOURCE_SKILL_ID, SOURCE_VERSION_ID, TARGET_NAMESPACE_ID, USER_ID, Map.of(), Set.of())); + () -> promotionService.submitPromotion(SOURCE_SKILL_ID, SOURCE_VERSION_ID, TARGET_NAMESPACE_ID, USER_ID)); } @Test @@ -160,7 +158,7 @@ class PromotionServiceTest { when(skillVersionRepository.findById(SOURCE_VERSION_ID)).thenReturn(Optional.empty()); assertThrows(DomainNotFoundException.class, - () -> promotionService.submitPromotion(SOURCE_SKILL_ID, SOURCE_VERSION_ID, TARGET_NAMESPACE_ID, USER_ID, Map.of(), Set.of())); + () -> promotionService.submitPromotion(SOURCE_SKILL_ID, SOURCE_VERSION_ID, TARGET_NAMESPACE_ID, USER_ID)); } @Test @@ -173,7 +171,7 @@ class PromotionServiceTest { when(skillVersionRepository.findById(SOURCE_VERSION_ID)).thenReturn(Optional.of(sv)); assertThrows(DomainBadRequestException.class, - () -> promotionService.submitPromotion(SOURCE_SKILL_ID, SOURCE_VERSION_ID, TARGET_NAMESPACE_ID, USER_ID, Map.of(), Set.of())); + () -> promotionService.submitPromotion(SOURCE_SKILL_ID, SOURCE_VERSION_ID, TARGET_NAMESPACE_ID, USER_ID)); } @Test @@ -186,42 +184,39 @@ class PromotionServiceTest { when(skillVersionRepository.findById(SOURCE_VERSION_ID)).thenReturn(Optional.of(sv)); assertThrows(DomainBadRequestException.class, - () -> promotionService.submitPromotion(SOURCE_SKILL_ID, SOURCE_VERSION_ID, TARGET_NAMESPACE_ID, USER_ID, Map.of(), Set.of())); + () -> promotionService.submitPromotion(SOURCE_SKILL_ID, SOURCE_VERSION_ID, TARGET_NAMESPACE_ID, USER_ID)); } @Test void shouldThrowWhenTargetNamespaceNotFound() { when(skillRepository.findById(SOURCE_SKILL_ID)).thenReturn(Optional.of(createSourceSkill())); when(skillVersionRepository.findById(SOURCE_VERSION_ID)).thenReturn(Optional.of(createPublishedVersion())); - when(permissionChecker.canSubmitPromotion(any(), eq(USER_ID), anyMap(), anySet())).thenReturn(true); when(namespaceRepository.findById(TARGET_NAMESPACE_ID)).thenReturn(Optional.empty()); assertThrows(DomainNotFoundException.class, - () -> promotionService.submitPromotion(SOURCE_SKILL_ID, SOURCE_VERSION_ID, TARGET_NAMESPACE_ID, USER_ID, Map.of(), Set.of())); + () -> promotionService.submitPromotion(SOURCE_SKILL_ID, SOURCE_VERSION_ID, TARGET_NAMESPACE_ID, USER_ID)); } @Test void shouldThrowWhenTargetNamespaceNotGlobal() { when(skillRepository.findById(SOURCE_SKILL_ID)).thenReturn(Optional.of(createSourceSkill())); when(skillVersionRepository.findById(SOURCE_VERSION_ID)).thenReturn(Optional.of(createPublishedVersion())); - when(permissionChecker.canSubmitPromotion(any(), eq(USER_ID), anyMap(), anySet())).thenReturn(true); when(namespaceRepository.findById(TARGET_NAMESPACE_ID)).thenReturn(Optional.of(createTeamNamespace())); assertThrows(DomainBadRequestException.class, - () -> promotionService.submitPromotion(SOURCE_SKILL_ID, SOURCE_VERSION_ID, TARGET_NAMESPACE_ID, USER_ID, Map.of(), Set.of())); + () -> promotionService.submitPromotion(SOURCE_SKILL_ID, SOURCE_VERSION_ID, TARGET_NAMESPACE_ID, USER_ID)); } @Test void shouldThrowWhenDuplicatePendingExists() { when(skillRepository.findById(SOURCE_SKILL_ID)).thenReturn(Optional.of(createSourceSkill())); when(skillVersionRepository.findById(SOURCE_VERSION_ID)).thenReturn(Optional.of(createPublishedVersion())); - when(permissionChecker.canSubmitPromotion(any(), eq(USER_ID), anyMap(), anySet())).thenReturn(true); when(namespaceRepository.findById(TARGET_NAMESPACE_ID)).thenReturn(Optional.of(createGlobalNamespace())); when(promotionRequestRepository.findBySourceVersionIdAndStatus(SOURCE_VERSION_ID, ReviewTaskStatus.PENDING)) .thenReturn(Optional.of(createPendingPromotion())); assertThrows(DomainBadRequestException.class, - () -> promotionService.submitPromotion(SOURCE_SKILL_ID, SOURCE_VERSION_ID, TARGET_NAMESPACE_ID, USER_ID, Map.of(), Set.of())); + () -> promotionService.submitPromotion(SOURCE_SKILL_ID, SOURCE_VERSION_ID, TARGET_NAMESPACE_ID, USER_ID)); } } diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/ReviewServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/ReviewServiceTest.java index 786f6af2..42bbad07 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/ReviewServiceTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/ReviewServiceTest.java @@ -106,14 +106,11 @@ class ReviewServiceTest { @Test void shouldSubmitReviewSuccessfully() { SkillVersion sv = createDraftSkillVersion(); - Skill skill = createSkill(); when(skillVersionRepository.findById(SKILL_VERSION_ID)).thenReturn(Optional.of(sv)); - when(skillRepository.findById(SKILL_ID)).thenReturn(Optional.of(skill)); - when(permissionChecker.canSubmitForReview(eq(skill), eq(USER_ID), anyMap(), anySet())).thenReturn(true); ReviewTask savedTask = createPendingReviewTask(); when(reviewTaskRepository.save(any(ReviewTask.class))).thenReturn(savedTask); - ReviewTask result = reviewService.submitReview(SKILL_VERSION_ID, USER_ID, Map.of(), Set.of()); + ReviewTask result = reviewService.submitReview(SKILL_VERSION_ID, NAMESPACE_ID, USER_ID); assertNotNull(result); assertEquals(SkillVersionStatus.PENDING_REVIEW, sv.getStatus()); @@ -126,33 +123,27 @@ class ReviewServiceTest { when(skillVersionRepository.findById(SKILL_VERSION_ID)).thenReturn(Optional.empty()); assertThrows(DomainNotFoundException.class, - () -> reviewService.submitReview(SKILL_VERSION_ID, USER_ID, Map.of(), Set.of())); + () -> reviewService.submitReview(SKILL_VERSION_ID, NAMESPACE_ID, USER_ID)); } @Test void shouldThrowWhenStatusNotDraft() { SkillVersion sv = createPendingReviewSkillVersion(); - Skill skill = createSkill(); when(skillVersionRepository.findById(SKILL_VERSION_ID)).thenReturn(Optional.of(sv)); - when(skillRepository.findById(SKILL_ID)).thenReturn(Optional.of(skill)); - when(permissionChecker.canSubmitForReview(eq(skill), eq(USER_ID), anyMap(), anySet())).thenReturn(true); assertThrows(DomainBadRequestException.class, - () -> reviewService.submitReview(SKILL_VERSION_ID, USER_ID, Map.of(), Set.of())); + () -> reviewService.submitReview(SKILL_VERSION_ID, NAMESPACE_ID, USER_ID)); } @Test void shouldThrowOnDuplicateSubmission() { SkillVersion sv = createDraftSkillVersion(); - Skill skill = createSkill(); when(skillVersionRepository.findById(SKILL_VERSION_ID)).thenReturn(Optional.of(sv)); - when(skillRepository.findById(SKILL_ID)).thenReturn(Optional.of(skill)); - when(permissionChecker.canSubmitForReview(eq(skill), eq(USER_ID), anyMap(), anySet())).thenReturn(true); when(reviewTaskRepository.save(any(ReviewTask.class))) .thenThrow(new DataIntegrityViolationException("duplicate")); assertThrows(DomainBadRequestException.class, - () -> reviewService.submitReview(SKILL_VERSION_ID, USER_ID, Map.of(), Set.of())); + () -> reviewService.submitReview(SKILL_VERSION_ID, NAMESPACE_ID, USER_ID)); } } diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java index 9d6a8781..7f87af7b 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java @@ -1,6 +1,7 @@ package com.iflytek.skillhub.domain.skill.service; import com.fasterxml.jackson.databind.ObjectMapper; +import com.iflytek.skillhub.domain.event.SkillPublishedEvent; import com.iflytek.skillhub.domain.namespace.Namespace; import com.iflytek.skillhub.domain.namespace.NamespaceMember; import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository; @@ -22,6 +23,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; import java.lang.reflect.Field; import java.util.List; @@ -54,6 +56,8 @@ class SkillPublishServiceTest { @Mock private PrePublishValidator prePublishValidator; @Mock + private ApplicationEventPublisher eventPublisher; + @Mock private ReviewTaskRepository reviewTaskRepository; private SkillPublishService service; @@ -72,6 +76,7 @@ class SkillPublishServiceTest { skillPackageValidator, skillMetadataParser, prePublishValidator, + eventPublisher, objectMapper, reviewTaskRepository ); @@ -121,6 +126,7 @@ class SkillPublishServiceTest { assertEquals(1L, result.skillId()); assertEquals("test-skill", result.slug()); assertEquals("1.0.0", result.version().getVersion()); + verify(eventPublisher).publishEvent(any(SkillPublishedEvent.class)); verify(skillFileRepository).saveAll(anyList()); verify(objectStorageService, atLeastOnce()).putObject(anyString(), any(), anyLong(), anyString()); verify(reviewTaskRepository).save(any(ReviewTask.class)); diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillTagServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillTagServiceTest.java index 223b24dc..85b5f317 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillTagServiceTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillTagServiceTest.java @@ -35,8 +35,6 @@ class SkillTagServiceTest { private SkillVersionRepository skillVersionRepository; @Mock private SkillTagRepository skillTagRepository; - @Mock - private VisibilityChecker visibilityChecker; private SkillTagService service; @@ -47,8 +45,7 @@ class SkillTagServiceTest { namespaceMemberRepository, skillRepository, skillVersionRepository, - skillTagRepository, - visibilityChecker + skillTagRepository ); } @@ -177,10 +174,9 @@ class SkillTagServiceTest { when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); when(skillTagRepository.findBySkillId(1L)).thenReturn(List.of(tag1, tag2)); - when(visibilityChecker.canAccess(eq(skill), isNull(), eq(java.util.Map.of()))).thenReturn(true); // Act - List result = service.listTags(namespaceSlug, skillSlug, null, java.util.Map.of()); + List result = service.listTags(namespaceSlug, skillSlug); // Assert assertEquals(2, result.size()); diff --git a/server/skillhub-storage/src/main/java/com/iflytek/skillhub/storage/LocalFileStorageService.java b/server/skillhub-storage/src/main/java/com/iflytek/skillhub/storage/LocalFileStorageService.java index f322cc10..5fd9ee8d 100644 --- a/server/skillhub-storage/src/main/java/com/iflytek/skillhub/storage/LocalFileStorageService.java +++ b/server/skillhub-storage/src/main/java/com/iflytek/skillhub/storage/LocalFileStorageService.java @@ -15,7 +15,7 @@ public class LocalFileStorageService implements ObjectStorageService { private final Path basePath; public LocalFileStorageService(StorageProperties properties) { - this.basePath = Paths.get(properties.getLocal().getBasePath()).toAbsolutePath().normalize(); + this.basePath = Paths.get(properties.getLocal().getBasePath()); } @Override @@ -58,11 +58,5 @@ public class LocalFileStorageService implements ObjectStorageService { } catch (IOException e) { throw new UncheckedIOException("Failed to get metadata: " + key, e); } } - private Path resolve(String key) { - Path resolved = basePath.resolve(key).normalize(); - if (!resolved.startsWith(basePath)) { - throw new IllegalArgumentException("Resolved path escapes storage base path: " + key); - } - return resolved; - } + private Path resolve(String key) { return basePath.resolve(key); } }