diff --git a/docs/06-api-design.md b/docs/06-api-design.md index ca81383b..579b6269 100644 --- a/docs/06-api-design.md +++ b/docs/06-api-design.md @@ -337,9 +337,6 @@ Admin API 按最小权限拆分,不再统一要求 SUPER_ADMIN: | GET | `/api/v1/admin/settings/personal-namespace` | 读取「新账号自动建命名空间」策略 | | PUT | `/api/v1/admin/settings/personal-namespace` | 更新该策略(写审计日志) | | POST | `/api/v1/admin/settings/personal-namespace/backfill` | 为已有账号补建;`dryRun=true` 只返回计划,不写库 | -| GET | `/api/v1/admin/settings/default-namespaces` | 读取「新账号默认加入的命名空间」列表 | -| PUT | `/api/v1/admin/settings/default-namespaces` | 更新该列表(slug 必须存在且为 ACTIVE;写审计日志)| -| POST | `/api/v1/admin/settings/default-namespaces/backfill` | 把已有账号补加入这些命名空间;`dryRun=true` 只返回计划 | 详见 [`2026-08-13-personal-namespace-provisioning.md`](./2026-08-13-personal-namespace-provisioning.md)。 diff --git a/docs/2026-08-13-personal-namespace-provisioning.md b/docs/2026-08-13-personal-namespace-provisioning.md index f22d276e..c0948738 100644 --- a/docs/2026-08-13-personal-namespace-provisioning.md +++ b/docs/2026-08-13-personal-namespace-provisioning.md @@ -128,27 +128,6 @@ slug 模板渲染后按 `SlugValidator` 的规则归一化:转小写、 最初的实现里前两条是静默返回的,结果就是「什么都没发生,也查不出为什么」。 账号激活本身是低频事件,多两行日志的代价可以忽略。 -## 二·五、全员默认加入的命名空间 - -「自动建一个自己的命名空间」解决的是个人空间;另一个相邻问题是**组织级公共空间**。 - -部署方新建一个命名空间来代替内置的 `global` 时会发现它对所有人不可见—— -`listNamespaces` 只返回调用者是成员的命名空间,而「新账号自动入伙」这件事 -原本写死在 `GlobalNamespaceMembershipService` 里,只认 slug `global`。 - -所以把它一般化为 `DefaultNamespaceMembershipService`: - -- 设置项 `namespace.default-membership` 存一个 slug 列表,默认 `["global"]`, - 即改造前的行为 -- 保存时校验每个 slug 存在且为 ACTIVE,让拼错在保存那一刻就暴露, - 而不是变成某个人首次登录时的一条警告 -- 运行期遇到已被删除或改名的 slug 只记 WARN 并跳过—— - 一个不存在的命名空间不该让人登不上来 -- 同样配了预览 + 执行的补建,把存量账号一次性加进去 - -发布只要求「是该命名空间的成员」(任意角色),所以加入即可发布, -不需要额外授予角色。 - ## 三、配置 | 位置 | 项 | 默认 | diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/admin/AdminSystemSettingController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/admin/AdminSystemSettingController.java index 1118cc45..4b4735de 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/admin/AdminSystemSettingController.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/admin/AdminSystemSettingController.java @@ -4,15 +4,11 @@ import com.iflytek.skillhub.auth.rbac.PlatformPrincipal; import com.iflytek.skillhub.controller.BaseApiController; import com.iflytek.skillhub.dto.ApiResponse; import com.iflytek.skillhub.dto.ApiResponseFactory; -import com.iflytek.skillhub.dto.BackfillRequest; -import com.iflytek.skillhub.dto.DefaultNamespaceBackfillResponse; -import com.iflytek.skillhub.dto.DefaultNamespaceSettingsResponse; -import com.iflytek.skillhub.dto.DefaultNamespaceSettingsUpdateRequest; +import com.iflytek.skillhub.dto.PersonalNamespaceBackfillRequest; import com.iflytek.skillhub.dto.PersonalNamespaceBackfillResponse; import com.iflytek.skillhub.dto.PersonalNamespaceSettingsResponse; import com.iflytek.skillhub.dto.PersonalNamespaceSettingsUpdateRequest; import com.iflytek.skillhub.service.AuditRequestContext; -import com.iflytek.skillhub.service.DefaultNamespaceSettingsAppService; import com.iflytek.skillhub.service.PersonalNamespaceSettingsAppService; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; @@ -33,14 +29,11 @@ import org.springframework.web.bind.annotation.RestController; public class AdminSystemSettingController extends BaseApiController { private final PersonalNamespaceSettingsAppService personalNamespaceSettingsAppService; - private final DefaultNamespaceSettingsAppService defaultNamespaceSettingsAppService; public AdminSystemSettingController(PersonalNamespaceSettingsAppService personalNamespaceSettingsAppService, - DefaultNamespaceSettingsAppService defaultNamespaceSettingsAppService, ApiResponseFactory responseFactory) { super(responseFactory); this.personalNamespaceSettingsAppService = personalNamespaceSettingsAppService; - this.defaultNamespaceSettingsAppService = defaultNamespaceSettingsAppService; } @GetMapping("/personal-namespace") @@ -66,41 +59,10 @@ public class AdminSystemSettingController extends BaseApiController { @PostMapping("/personal-namespace/backfill") @PreAuthorize("hasRole('SUPER_ADMIN')") public ApiResponse backfillPersonalNamespaces( - @Valid @RequestBody BackfillRequest request, + @Valid @RequestBody PersonalNamespaceBackfillRequest request, @AuthenticationPrincipal PlatformPrincipal principal, HttpServletRequest httpRequest) { return ok("response.success", personalNamespaceSettingsAppService.backfill( request, principal.userId(), AuditRequestContext.from(httpRequest))); } - - @GetMapping("/default-namespaces") - @PreAuthorize("hasRole('SUPER_ADMIN')") - public ApiResponse getDefaultNamespaces() { - return ok("response.success.read", defaultNamespaceSettingsAppService.get()); - } - - @PutMapping("/default-namespaces") - @PreAuthorize("hasRole('SUPER_ADMIN')") - public ApiResponse updateDefaultNamespaces( - @Valid @RequestBody DefaultNamespaceSettingsUpdateRequest request, - @AuthenticationPrincipal PlatformPrincipal principal, - HttpServletRequest httpRequest) { - return ok("response.success.updated", defaultNamespaceSettingsAppService.update( - request, principal.userId(), AuditRequestContext.from(httpRequest))); - } - - /** - * Enrols existing accounts in the configured default namespaces, for when one is added after - * people have already signed up. Send {@code dryRun} to see the plan first. - */ - @PostMapping("/default-namespaces/backfill") - @PreAuthorize("hasRole('SUPER_ADMIN')") - public ApiResponse backfillDefaultNamespaces( - @Valid @RequestBody BackfillRequest request, - @AuthenticationPrincipal PlatformPrincipal principal, - HttpServletRequest httpRequest) { - return ok("response.success", defaultNamespaceSettingsAppService.backfill( - Boolean.TRUE.equals(request.dryRun()), principal.userId(), - AuditRequestContext.from(httpRequest))); - } } diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/BackfillRequest.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/BackfillRequest.java deleted file mode 100644 index 353dd928..00000000 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/BackfillRequest.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.iflytek.skillhub.dto; - -import jakarta.validation.constraints.NotNull; - -/** - * Shared body for the admin backfill endpoints. - * - * @param dryRun when true, report what would happen without writing anything - */ -public record BackfillRequest(@NotNull Boolean dryRun) {} diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/DefaultNamespaceBackfillResponse.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/DefaultNamespaceBackfillResponse.java deleted file mode 100644 index fe185636..00000000 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/DefaultNamespaceBackfillResponse.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.iflytek.skillhub.dto; - -import java.util.List; - -/** - * @param truncated the run stopped at its per-run account cap; re-run to continue - * @param entries only the accounts that were enrolled, or would be - */ -public record DefaultNamespaceBackfillResponse( - boolean dryRun, - int scannedAccounts, - int alreadyEnrolled, - int systemAccountsSkipped, - boolean truncated, - List entries) { - - public record Entry(String userId, String displayName, List slugs) {} -} diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/DefaultNamespaceSettingsResponse.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/DefaultNamespaceSettingsResponse.java deleted file mode 100644 index f276cc65..00000000 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/DefaultNamespaceSettingsResponse.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.iflytek.skillhub.dto; - -import java.util.List; - -/** - * @param slugs namespaces every newly activated account is enrolled in - */ -public record DefaultNamespaceSettingsResponse(List slugs) {} diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/DefaultNamespaceSettingsUpdateRequest.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/DefaultNamespaceSettingsUpdateRequest.java deleted file mode 100644 index a7aca30c..00000000 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/DefaultNamespaceSettingsUpdateRequest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.iflytek.skillhub.dto; - -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; - -import java.util.List; - -/** - * @param slugs may be empty, which means new accounts are enrolled nowhere - */ -public record DefaultNamespaceSettingsUpdateRequest( - @NotNull @Size(max = 20) List<@Size(max = 64) String> slugs -) {} diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/PersonalNamespaceBackfillRequest.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/PersonalNamespaceBackfillRequest.java new file mode 100644 index 00000000..04df3456 --- /dev/null +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/PersonalNamespaceBackfillRequest.java @@ -0,0 +1,8 @@ +package com.iflytek.skillhub.dto; + +import jakarta.validation.constraints.NotNull; + +/** + * @param dryRun when true, report the accounts that would get a namespace without creating any + */ +public record PersonalNamespaceBackfillRequest(@NotNull Boolean dryRun) {} diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/DefaultNamespaceSettingsAppService.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/DefaultNamespaceSettingsAppService.java deleted file mode 100644 index a35ed25a..00000000 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/DefaultNamespaceSettingsAppService.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.iflytek.skillhub.service; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.iflytek.skillhub.domain.audit.AuditLogService; -import com.iflytek.skillhub.domain.namespace.DefaultNamespaceBackfillReport; -import com.iflytek.skillhub.domain.namespace.DefaultNamespaceMembershipService; -import com.iflytek.skillhub.domain.namespace.DefaultNamespaceSettings; -import com.iflytek.skillhub.dto.DefaultNamespaceBackfillResponse; -import com.iflytek.skillhub.dto.DefaultNamespaceSettingsResponse; -import com.iflytek.skillhub.dto.DefaultNamespaceSettingsUpdateRequest; -import com.iflytek.skillhub.observability.RequestIdAccessor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * Exposes the "namespaces every new account joins" policy to the admin console. - */ -@Service -public class DefaultNamespaceSettingsAppService { - - private static final String AUDIT_TARGET_TYPE = "SYSTEM_SETTING"; - private static final String AUDIT_ACTION_UPDATE = "SYSTEM_SETTING_DEFAULT_NAMESPACES_UPDATE"; - private static final String AUDIT_ACTION_BACKFILL = "SYSTEM_SETTING_DEFAULT_NAMESPACES_BACKFILL"; - - private final DefaultNamespaceMembershipService defaultNamespaceMembershipService; - private final AuditLogService auditLogService; - private final RequestIdAccessor requestIdAccessor; - private final ObjectMapper objectMapper; - - public DefaultNamespaceSettingsAppService( - DefaultNamespaceMembershipService defaultNamespaceMembershipService, - AuditLogService auditLogService, - RequestIdAccessor requestIdAccessor, - ObjectMapper objectMapper) { - this.defaultNamespaceMembershipService = defaultNamespaceMembershipService; - this.auditLogService = auditLogService; - this.requestIdAccessor = requestIdAccessor; - this.objectMapper = objectMapper; - } - - @Transactional(readOnly = true) - public DefaultNamespaceSettingsResponse get() { - return new DefaultNamespaceSettingsResponse( - defaultNamespaceMembershipService.currentSettings().slugs()); - } - - @Transactional - public DefaultNamespaceSettingsResponse update(DefaultNamespaceSettingsUpdateRequest request, - String actorUserId, - AuditRequestContext auditContext) { - DefaultNamespaceSettings previous = defaultNamespaceMembershipService.currentSettings(); - DefaultNamespaceSettings updated = defaultNamespaceMembershipService.updateSettings( - new DefaultNamespaceSettings(request.slugs()), actorUserId); - - Map detail = new LinkedHashMap<>(); - detail.put("before", previous.slugs()); - detail.put("after", updated.slugs()); - record(actorUserId, auditContext, AUDIT_ACTION_UPDATE, detail); - return new DefaultNamespaceSettingsResponse(updated.slugs()); - } - - /** - * A dry run writes nothing and is not audited; an applied run records who it enrolled. - */ - public DefaultNamespaceBackfillResponse backfill(boolean dryRun, - String actorUserId, - AuditRequestContext auditContext) { - DefaultNamespaceBackfillReport report = defaultNamespaceMembershipService.backfill(dryRun); - - if (!dryRun) { - Map detail = new LinkedHashMap<>(); - detail.put("scannedAccounts", report.scannedAccounts()); - detail.put("alreadyEnrolled", report.alreadyEnrolled()); - detail.put("truncated", report.truncated()); - detail.put("enrolled", report.entries().stream() - .map(entry -> Map.of("userId", entry.userId(), "slugs", entry.slugs())) - .toList()); - record(actorUserId, auditContext, AUDIT_ACTION_BACKFILL, detail); - } - - return new DefaultNamespaceBackfillResponse( - report.dryRun(), - report.scannedAccounts(), - report.alreadyEnrolled(), - report.systemAccountsSkipped(), - report.truncated(), - report.entries().stream() - .map(entry -> new DefaultNamespaceBackfillResponse.Entry( - entry.userId(), entry.displayName(), entry.slugs())) - .toList()); - } - - private void record(String actorUserId, - AuditRequestContext auditContext, - String action, - Map detail) { - auditLogService.record( - actorUserId, - action, - AUDIT_TARGET_TYPE, - null, - requestIdAccessor.current(), - auditContext != null ? auditContext.clientIp() : null, - auditContext != null ? auditContext.userAgent() : null, - toJson(detail)); - } - - private String toJson(Map detail) { - try { - return objectMapper.writeValueAsString(detail); - } catch (Exception e) { - return null; - } - } -} diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/PersonalNamespaceSettingsAppService.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/PersonalNamespaceSettingsAppService.java index e47a03ac..7f3868e9 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/PersonalNamespaceSettingsAppService.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/PersonalNamespaceSettingsAppService.java @@ -6,7 +6,7 @@ import com.iflytek.skillhub.domain.namespace.PersonalNamespaceBackfillEntry; import com.iflytek.skillhub.domain.namespace.PersonalNamespaceBackfillReport; import com.iflytek.skillhub.domain.namespace.PersonalNamespaceProvisioningService; import com.iflytek.skillhub.domain.namespace.PersonalNamespaceSettings; -import com.iflytek.skillhub.dto.BackfillRequest; +import com.iflytek.skillhub.dto.PersonalNamespaceBackfillRequest; import com.iflytek.skillhub.dto.PersonalNamespaceBackfillResponse; import com.iflytek.skillhub.dto.PersonalNamespaceSettingsResponse; import com.iflytek.skillhub.dto.PersonalNamespaceSettingsUpdateRequest; @@ -73,7 +73,7 @@ public class PersonalNamespaceSettingsAppService { * Runs the backfill over existing accounts. A dry run writes nothing and is not audited; an * applied run records what it created. */ - public PersonalNamespaceBackfillResponse backfill(BackfillRequest request, + public PersonalNamespaceBackfillResponse backfill(PersonalNamespaceBackfillRequest request, String actorUserId, AuditRequestContext auditContext) { boolean dryRun = Boolean.TRUE.equals(request.dryRun()); diff --git a/server/skillhub-app/src/main/resources/application.yml b/server/skillhub-app/src/main/resources/application.yml index 3744d179..e7abdd12 100644 --- a/server/skillhub-app/src/main/resources/application.yml +++ b/server/skillhub-app/src/main/resources/application.yml @@ -125,11 +125,6 @@ skillhub: # the admin console instead; their defaults live in PersonalNamespaceProvisioningProperties. personal-provisioning: enabled: ${SKILLHUB_NAMESPACE_PERSONAL_PROVISIONING_ENABLED:false} - # Namespaces every newly activated account is enrolled in. The built-in global namespace is - # the historical behaviour; an administrator can change the list in the admin console, and the - # stored choice then wins over this file. - default-membership: - slugs: ${SKILLHUB_NAMESPACE_DEFAULT_MEMBERSHIP_SLUGS:global} public: base-url: ${SKILLHUB_PUBLIC_BASE_URL:} access-policy: diff --git a/server/skillhub-app/src/main/resources/messages.properties b/server/skillhub-app/src/main/resources/messages.properties index 6dcf494a..8791e6be 100644 --- a/server/skillhub-app/src/main/resources/messages.properties +++ b/server/skillhub-app/src/main/resources/messages.properties @@ -76,8 +76,6 @@ error.namespace.membership.required=Namespace membership required error.namespace.global.members.platformAdmin.required=Only platform user administrators can list global namespace members error.namespace.admin.required=Namespace owner or admin role required error.namespace.owner.required=Namespace owner role required -error.namespace.defaultMembership.unknownSlug=Namespace ''{0}'' does not exist -error.namespace.defaultMembership.inactiveSlug=Namespace ''{0}'' is not active error.namespace.create.platformAdminRequired=Only SKILL_ADMIN or SUPER_ADMIN can create namespaces error.namespace.delete.hasDependencies=Namespace cannot be deleted while it still contains skills or governance records error.namespace.member.owner.assignDirect=Cannot assign OWNER role directly diff --git a/server/skillhub-app/src/main/resources/messages_zh.properties b/server/skillhub-app/src/main/resources/messages_zh.properties index 91297887..0e1b3fc3 100644 --- a/server/skillhub-app/src/main/resources/messages_zh.properties +++ b/server/skillhub-app/src/main/resources/messages_zh.properties @@ -76,8 +76,6 @@ error.namespace.membership.required=需要先加入该命名空间 error.namespace.global.members.platformAdmin.required=只有平台用户管理员可以查看 global 命名空间成员 error.namespace.admin.required=需要命名空间管理员或所有者权限 error.namespace.owner.required=需要命名空间所有者权限 -error.namespace.defaultMembership.unknownSlug=命名空间 ''{0}'' 不存在 -error.namespace.defaultMembership.inactiveSlug=命名空间 ''{0}'' 不是启用状态 error.namespace.create.platformAdminRequired=只有 SKILL_ADMIN 或 SUPER_ADMIN 可以创建命名空间 error.namespace.delete.hasDependencies=命名空间下仍有技能或治理记录,暂时不能删除 error.namespace.member.owner.assignDirect=不能直接分配 OWNER 角色 diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/PersonalNamespaceSettingsAppServiceTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/PersonalNamespaceSettingsAppServiceTest.java index fa8a2faa..eedc1495 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/PersonalNamespaceSettingsAppServiceTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/PersonalNamespaceSettingsAppServiceTest.java @@ -6,7 +6,7 @@ import com.iflytek.skillhub.domain.namespace.PersonalNamespaceBackfillEntry; import com.iflytek.skillhub.domain.namespace.PersonalNamespaceBackfillReport; import com.iflytek.skillhub.domain.namespace.PersonalNamespaceProvisioningService; import com.iflytek.skillhub.domain.namespace.PersonalNamespaceSettings; -import com.iflytek.skillhub.dto.BackfillRequest; +import com.iflytek.skillhub.dto.PersonalNamespaceBackfillRequest; import com.iflytek.skillhub.dto.PersonalNamespaceBackfillResponse; import com.iflytek.skillhub.dto.PersonalNamespaceSettingsResponse; import com.iflytek.skillhub.dto.PersonalNamespaceSettingsUpdateRequest; @@ -120,7 +120,7 @@ class PersonalNamespaceSettingsAppServiceTest { PersonalNamespaceBackfillEntry.Outcome.PLANNED)))); PersonalNamespaceBackfillResponse response = service.backfill( - new BackfillRequest(true), "usr_admin", null); + new PersonalNamespaceBackfillRequest(true), "usr_admin", null); assertThat(response.dryRun()).isTrue(); assertThat(response.entries()).singleElement() @@ -137,7 +137,7 @@ class PersonalNamespaceSettingsAppServiceTest { new PersonalNamespaceBackfillEntry("usr_2", "admin", null, PersonalNamespaceBackfillEntry.Outcome.NO_SLUG)))); - service.backfill(new BackfillRequest(false), "usr_admin", + service.backfill(new PersonalNamespaceBackfillRequest(false), "usr_admin", new AuditRequestContext("10.0.0.1", "curl/8")); ArgumentCaptor detailCaptor = ArgumentCaptor.forClass(String.class); diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/identity/IdentityBindingService.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/identity/IdentityBindingService.java index 2c4d80bf..8e922431 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/identity/IdentityBindingService.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/identity/IdentityBindingService.java @@ -7,7 +7,7 @@ import com.iflytek.skillhub.auth.rbac.PlatformRoleDefaults; import com.iflytek.skillhub.auth.repository.IdentityBindingRepository; import com.iflytek.skillhub.auth.repository.UserRoleBindingRepository; import com.iflytek.skillhub.domain.event.UserActivatedEvent; -import com.iflytek.skillhub.domain.namespace.DefaultNamespaceMembershipService; +import com.iflytek.skillhub.domain.namespace.GlobalNamespaceMembershipService; import com.iflytek.skillhub.domain.user.UserAccount; import com.iflytek.skillhub.domain.user.UserAccountRepository; import com.iflytek.skillhub.domain.user.UserStatus; @@ -28,18 +28,18 @@ public class IdentityBindingService { private final IdentityBindingRepository bindingRepo; private final UserAccountRepository userRepo; private final UserRoleBindingRepository roleBindingRepo; - private final DefaultNamespaceMembershipService defaultNamespaceMembershipService; + private final GlobalNamespaceMembershipService globalNamespaceMembershipService; private final ApplicationEventPublisher eventPublisher; public IdentityBindingService(IdentityBindingRepository bindingRepo, UserAccountRepository userRepo, UserRoleBindingRepository roleBindingRepo, - DefaultNamespaceMembershipService defaultNamespaceMembershipService, + GlobalNamespaceMembershipService globalNamespaceMembershipService, ApplicationEventPublisher eventPublisher) { this.bindingRepo = bindingRepo; this.userRepo = userRepo; this.roleBindingRepo = roleBindingRepo; - this.defaultNamespaceMembershipService = defaultNamespaceMembershipService; + this.globalNamespaceMembershipService = globalNamespaceMembershipService; this.eventPublisher = eventPublisher; } @@ -69,7 +69,7 @@ public class IdentityBindingService { user.setStatus(initialStatus); user = userRepo.save(user); if (initialStatus == UserStatus.ACTIVE) { - defaultNamespaceMembershipService.ensureMember(user.getId()); + globalNamespaceMembershipService.ensureMember(user.getId()); eventPublisher.publishEvent( new UserActivatedEvent(user.getId(), claims.providerLogin(), claims.email())); } diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalAuthService.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalAuthService.java index ad0bc5c5..5d0dad22 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalAuthService.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalAuthService.java @@ -5,7 +5,7 @@ import com.iflytek.skillhub.auth.rbac.PlatformPrincipal; import com.iflytek.skillhub.auth.rbac.PlatformRoleDefaults; import com.iflytek.skillhub.auth.repository.UserRoleBindingRepository; import com.iflytek.skillhub.domain.event.UserActivatedEvent; -import com.iflytek.skillhub.domain.namespace.DefaultNamespaceMembershipService; +import com.iflytek.skillhub.domain.namespace.GlobalNamespaceMembershipService; import com.iflytek.skillhub.domain.user.UserAccount; import com.iflytek.skillhub.domain.user.UserAccountRepository; import com.iflytek.skillhub.domain.user.UserStatus; @@ -42,7 +42,7 @@ public class LocalAuthService { private final LocalCredentialRepository credentialRepository; private final UserAccountRepository userAccountRepository; private final UserRoleBindingRepository userRoleBindingRepository; - private final DefaultNamespaceMembershipService defaultNamespaceMembershipService; + private final GlobalNamespaceMembershipService globalNamespaceMembershipService; private final PasswordPolicyValidator passwordPolicyValidator; private final PasswordEncoder passwordEncoder; private final Clock clock; @@ -51,7 +51,7 @@ public class LocalAuthService { public LocalAuthService(LocalCredentialRepository credentialRepository, UserAccountRepository userAccountRepository, UserRoleBindingRepository userRoleBindingRepository, - DefaultNamespaceMembershipService defaultNamespaceMembershipService, + GlobalNamespaceMembershipService globalNamespaceMembershipService, PasswordPolicyValidator passwordPolicyValidator, PasswordEncoder passwordEncoder, Clock clock, @@ -59,7 +59,7 @@ public class LocalAuthService { this.credentialRepository = credentialRepository; this.userAccountRepository = userAccountRepository; this.userRoleBindingRepository = userRoleBindingRepository; - this.defaultNamespaceMembershipService = defaultNamespaceMembershipService; + this.globalNamespaceMembershipService = globalNamespaceMembershipService; this.passwordPolicyValidator = passwordPolicyValidator; this.passwordEncoder = passwordEncoder; this.clock = clock; @@ -104,7 +104,7 @@ public class LocalAuthService { normalizedUsername, passwordEncoder.encode(password) )); - defaultNamespaceMembershipService.ensureMember(user.getId()); + globalNamespaceMembershipService.ensureMember(user.getId()); eventPublisher.publishEvent(new UserActivatedEvent(user.getId(), normalizedUsername, normalizedEmail)); return buildPrincipal(user); diff --git a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/identity/IdentityBindingServiceTest.java b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/identity/IdentityBindingServiceTest.java index b8499161..7c9281c3 100644 --- a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/identity/IdentityBindingServiceTest.java +++ b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/identity/IdentityBindingServiceTest.java @@ -17,7 +17,7 @@ import com.iflytek.skillhub.auth.rbac.PlatformPrincipal; import com.iflytek.skillhub.auth.repository.IdentityBindingRepository; import com.iflytek.skillhub.auth.repository.UserRoleBindingRepository; import com.iflytek.skillhub.domain.event.UserActivatedEvent; -import com.iflytek.skillhub.domain.namespace.DefaultNamespaceMembershipService; +import com.iflytek.skillhub.domain.namespace.GlobalNamespaceMembershipService; import com.iflytek.skillhub.domain.user.UserAccount; import com.iflytek.skillhub.domain.user.UserAccountRepository; import com.iflytek.skillhub.domain.user.UserStatus; @@ -46,7 +46,7 @@ class IdentityBindingServiceTest { private UserRoleBindingRepository roleBindingRepo; @Mock - private DefaultNamespaceMembershipService defaultNamespaceMembershipService; + private GlobalNamespaceMembershipService globalNamespaceMembershipService; @Mock private ApplicationEventPublisher eventPublisher; @@ -56,7 +56,7 @@ class IdentityBindingServiceTest { @BeforeEach void setUp() { service = new IdentityBindingService(bindingRepo, userRepo, roleBindingRepo, - defaultNamespaceMembershipService, eventPublisher); + globalNamespaceMembershipService, eventPublisher); } @Test @@ -77,7 +77,7 @@ class IdentityBindingServiceTest { ArgumentCaptor userCaptor = ArgumentCaptor.forClass(UserAccount.class); verify(userRepo).save(userCaptor.capture()); - verify(defaultNamespaceMembershipService).ensureMember(userCaptor.getValue().getId()); + verify(globalNamespaceMembershipService).ensureMember(userCaptor.getValue().getId()); verify(bindingRepo).save(any(IdentityBinding.class)); assertThat(principal.displayName()).isEqualTo("alice"); assertThat(principal.oauthProvider()).isEqualTo("github"); @@ -137,7 +137,7 @@ class IdentityBindingServiceTest { assertThatThrownBy(() -> service.bindOrCreate(claims, UserStatus.PENDING)) .isInstanceOf(AccountPendingException.class); - verify(defaultNamespaceMembershipService, never()).ensureMember(any()); + verify(globalNamespaceMembershipService, never()).ensureMember(any()); } @Test diff --git a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/local/LocalAuthServiceTest.java b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/local/LocalAuthServiceTest.java index 96a1dd32..f11b9116 100644 --- a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/local/LocalAuthServiceTest.java +++ b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/local/LocalAuthServiceTest.java @@ -14,7 +14,7 @@ import com.iflytek.skillhub.auth.entity.Role; import com.iflytek.skillhub.auth.entity.UserRoleBinding; import com.iflytek.skillhub.auth.repository.UserRoleBindingRepository; import com.iflytek.skillhub.domain.event.UserActivatedEvent; -import com.iflytek.skillhub.domain.namespace.DefaultNamespaceMembershipService; +import com.iflytek.skillhub.domain.namespace.GlobalNamespaceMembershipService; import com.iflytek.skillhub.domain.user.UserAccount; import com.iflytek.skillhub.domain.user.UserAccountRepository; import com.iflytek.skillhub.domain.user.UserStatus; @@ -48,7 +48,7 @@ class LocalAuthServiceTest { private UserRoleBindingRepository userRoleBindingRepository; @Mock - private DefaultNamespaceMembershipService defaultNamespaceMembershipService; + private GlobalNamespaceMembershipService globalNamespaceMembershipService; @Mock private PasswordEncoder passwordEncoder; @@ -64,7 +64,7 @@ class LocalAuthServiceTest { credentialRepository, userAccountRepository, userRoleBindingRepository, - defaultNamespaceMembershipService, + globalNamespaceMembershipService, new PasswordPolicyValidator(), passwordEncoder, CLOCK, @@ -89,7 +89,7 @@ class LocalAuthServiceTest { assertThat(principal.email()).isEqualTo("alice@example.com"); assertThat(principal.platformRoles()).containsExactly("USER"); verify(credentialRepository).save(any(LocalCredential.class)); - verify(defaultNamespaceMembershipService).ensureMember(userCaptor.getValue().getId()); + verify(globalNamespaceMembershipService).ensureMember(userCaptor.getValue().getId()); } @Test diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/DefaultNamespaceBackfillEntry.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/DefaultNamespaceBackfillEntry.java deleted file mode 100644 index b2b88339..00000000 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/DefaultNamespaceBackfillEntry.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.iflytek.skillhub.domain.namespace; - -import java.util.List; - -/** - * One account a default-namespace backfill enrolled, or would enrol. - * - * @param slugs the default namespaces the account is not yet a member of - */ -public record DefaultNamespaceBackfillEntry(String userId, String displayName, List slugs) { - - public DefaultNamespaceBackfillEntry { - slugs = slugs == null ? List.of() : List.copyOf(slugs); - } -} diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/DefaultNamespaceBackfillReport.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/DefaultNamespaceBackfillReport.java deleted file mode 100644 index ce231141..00000000 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/DefaultNamespaceBackfillReport.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.iflytek.skillhub.domain.namespace; - -import java.util.List; - -/** - * Outcome of enrolling existing accounts in the configured default namespaces. - * - * @param truncated the run stopped at its per-run account cap; re-run to continue - * @param entries only the accounts that were enrolled, or would be - */ -public record DefaultNamespaceBackfillReport( - boolean dryRun, - int scannedAccounts, - int alreadyEnrolled, - int systemAccountsSkipped, - boolean truncated, - List entries) { -} diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/DefaultNamespaceMembershipService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/DefaultNamespaceMembershipService.java deleted file mode 100644 index 608a8082..00000000 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/DefaultNamespaceMembershipService.java +++ /dev/null @@ -1,178 +0,0 @@ -package com.iflytek.skillhub.domain.namespace; - -import com.iflytek.skillhub.domain.setting.SystemSettingService; -import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException; -import com.iflytek.skillhub.domain.user.UserAccount; -import com.iflytek.skillhub.domain.user.UserAccountRepository; -import com.iflytek.skillhub.domain.user.UserStatus; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Sort; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -/** - * Enrolls newly active users in the namespaces the operator has designated as defaults. - * - *

This used to be hard-wired to the built-in {@code global} namespace. Deployments that stand up - * their own organisation-wide namespace found it invisible to everyone, because the namespace - * listing only returns namespaces the caller belongs to and nothing ever added them. - */ -@Service -public class DefaultNamespaceMembershipService { - - public static final String SETTING_KEY = "namespace.default-membership"; - - private static final int MAX_BACKFILL_ACCOUNTS = 5000; - private static final int BACKFILL_PAGE_SIZE = 200; - - private static final Logger log = LoggerFactory.getLogger(DefaultNamespaceMembershipService.class); - - private final SystemSettingService systemSettingService; - private final DefaultNamespaceProperties defaults; - private final NamespaceRepository namespaceRepository; - private final NamespaceMemberRepository namespaceMemberRepository; - private final UserAccountRepository userAccountRepository; - - public DefaultNamespaceMembershipService(SystemSettingService systemSettingService, - DefaultNamespaceProperties defaults, - NamespaceRepository namespaceRepository, - NamespaceMemberRepository namespaceMemberRepository, - UserAccountRepository userAccountRepository) { - this.systemSettingService = systemSettingService; - this.defaults = defaults; - this.namespaceRepository = namespaceRepository; - this.namespaceMemberRepository = namespaceMemberRepository; - this.userAccountRepository = userAccountRepository; - } - - public DefaultNamespaceSettings currentSettings() { - return systemSettingService.get(SETTING_KEY, DefaultNamespaceSettings.class, defaults.toSettings()); - } - - /** - * Stores the operator's choice after checking every slug resolves to a live namespace, so a - * typo surfaces here rather than as a warning on somebody's first login. - */ - @Transactional - public DefaultNamespaceSettings updateSettings(DefaultNamespaceSettings settings, String actorUserId) { - Set normalized = new LinkedHashSet<>(); - for (String slug : settings.slugs()) { - String trimmed = slug == null ? "" : slug.trim(); - if (trimmed.isEmpty()) { - continue; - } - Namespace namespace = namespaceRepository.findBySlug(trimmed) - .orElseThrow(() -> new DomainBadRequestException( - "error.namespace.defaultMembership.unknownSlug", trimmed)); - if (namespace.getStatus() != NamespaceStatus.ACTIVE) { - throw new DomainBadRequestException( - "error.namespace.defaultMembership.inactiveSlug", trimmed); - } - normalized.add(trimmed); - } - return systemSettingService.put( - SETTING_KEY, new DefaultNamespaceSettings(List.copyOf(normalized)), actorUserId); - } - - /** - * Adds {@code userId} to every configured default namespace it is not already in. - * - *

A slug that no longer resolves is logged and skipped: a namespace that was renamed or - * deleted must not cost somebody their registration. - */ - @Transactional - public void ensureMember(String userId) { - for (String slug : currentSettings().slugs()) { - Optional namespace = namespaceRepository.findBySlug(slug); - if (namespace.isEmpty()) { - log.warn("Default namespace '{}' does not exist; skipping enrolment for user {}", slug, userId); - continue; - } - join(namespace.get(), userId); - } - } - - /** - * Enrolls existing accounts in the configured defaults, for when an operator adds a namespace - * after people have already signed up. - * - *

Not {@code @Transactional}: each account is enrolled on its own, so one failure does not - * discard the rest of the run. - */ - public DefaultNamespaceBackfillReport backfill(boolean dryRun) { - List targets = new ArrayList<>(); - for (String slug : currentSettings().slugs()) { - namespaceRepository.findBySlug(slug).ifPresentOrElse( - targets::add, - () -> log.warn("Default namespace '{}' does not exist; excluded from backfill", slug)); - } - - List entries = new ArrayList<>(); - int scanned = 0; - int alreadyMember = 0; - int systemAccounts = 0; - boolean truncated = false; - - for (int page = 0; !truncated && !targets.isEmpty(); page++) { - Page batch = userAccountRepository.findByStatus(UserStatus.ACTIVE, - PageRequest.of(page, BACKFILL_PAGE_SIZE, Sort.by("id"))); - if (batch.isEmpty()) { - break; - } - for (UserAccount user : batch) { - if (scanned >= MAX_BACKFILL_ACCOUNTS) { - truncated = true; - break; - } - scanned++; - if (user.isSystemAccount()) { - systemAccounts++; - continue; - } - List missing = targets.stream() - .filter(namespace -> namespaceMemberRepository - .findByNamespaceIdAndUserId(namespace.getId(), user.getId()).isEmpty()) - .map(Namespace::getSlug) - .toList(); - if (missing.isEmpty()) { - alreadyMember++; - continue; - } - if (!dryRun) { - targets.stream() - .filter(namespace -> missing.contains(namespace.getSlug())) - .forEach(namespace -> join(namespace, user.getId())); - } - entries.add(new DefaultNamespaceBackfillEntry(user.getId(), user.getDisplayName(), missing)); - } - if (!batch.hasNext()) { - break; - } - } - - log.info("Default namespace backfill ({}): scanned {}, already enrolled {}, acted on {}{}", - dryRun ? "dry run" : "applied", scanned, alreadyMember, entries.size(), - truncated ? ", stopped at the per-run cap" : ""); - return new DefaultNamespaceBackfillReport( - dryRun, scanned, alreadyMember, systemAccounts, truncated, List.copyOf(entries)); - } - - /** - * Idempotent. Called from {@link #ensureMember} inside the caller's transaction, and from the - * backfill outside one, where each save commits on its own. - */ - private void join(Namespace namespace, String userId) { - namespaceMemberRepository.findByNamespaceIdAndUserId(namespace.getId(), userId) - .orElseGet(() -> namespaceMemberRepository.save( - new NamespaceMember(namespace.getId(), userId, NamespaceRole.MEMBER))); - } -} diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/DefaultNamespaceProperties.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/DefaultNamespaceProperties.java deleted file mode 100644 index 4cead465..00000000 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/DefaultNamespaceProperties.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.iflytek.skillhub.domain.namespace; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.List; - -/** - * Deployment default for {@link DefaultNamespaceSettings}, used until an administrator saves a - * choice in the admin console. - * - *

Defaults to the built-in global namespace, which is what every deployment did before this was - * configurable. - */ -@Component -@ConfigurationProperties(prefix = "skillhub.namespace.default-membership") -public class DefaultNamespaceProperties { - - private List slugs = new ArrayList<>(List.of("global")); - - public List getSlugs() { - return slugs; - } - - public void setSlugs(List slugs) { - this.slugs = slugs; - } - - public DefaultNamespaceSettings toSettings() { - return new DefaultNamespaceSettings(slugs); - } -} diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/DefaultNamespaceSettings.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/DefaultNamespaceSettings.java deleted file mode 100644 index f4ac928d..00000000 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/DefaultNamespaceSettings.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.iflytek.skillhub.domain.namespace; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -import java.util.List; - -/** - * The namespaces every newly activated account is enrolled in, as member. - * - *

A deployment that outgrows the built-in {@code global} namespace — say it wants an - * organisation-wide space of its own — needs to say so somewhere, because a namespace nobody is a - * member of is invisible: the namespace listing only returns namespaces the caller belongs to. - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public record DefaultNamespaceSettings(List slugs) { - - public DefaultNamespaceSettings { - slugs = slugs == null ? List.of() : List.copyOf(slugs); - } -} diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/GlobalNamespaceMembershipService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/GlobalNamespaceMembershipService.java new file mode 100644 index 00000000..1ef7d247 --- /dev/null +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/GlobalNamespaceMembershipService.java @@ -0,0 +1,33 @@ +package com.iflytek.skillhub.domain.namespace; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Ensures newly active users belong to the built-in global namespace. + */ +@Service +public class GlobalNamespaceMembershipService { + + private static final String GLOBAL_NAMESPACE_SLUG = "global"; + + private final NamespaceRepository namespaceRepository; + private final NamespaceMemberRepository namespaceMemberRepository; + + public GlobalNamespaceMembershipService(NamespaceRepository namespaceRepository, + NamespaceMemberRepository namespaceMemberRepository) { + this.namespaceRepository = namespaceRepository; + this.namespaceMemberRepository = namespaceMemberRepository; + } + + @Transactional + public void ensureMember(String userId) { + Namespace globalNamespace = namespaceRepository.findBySlug(GLOBAL_NAMESPACE_SLUG) + .orElseThrow(() -> new IllegalStateException("Missing built-in global namespace")); + + namespaceMemberRepository.findByNamespaceIdAndUserId(globalNamespace.getId(), userId) + .orElseGet(() -> namespaceMemberRepository.save( + new NamespaceMember(globalNamespace.getId(), userId, NamespaceRole.MEMBER) + )); + } +} diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/namespace/DefaultNamespaceMembershipServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/namespace/DefaultNamespaceMembershipServiceTest.java deleted file mode 100644 index 72881028..00000000 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/namespace/DefaultNamespaceMembershipServiceTest.java +++ /dev/null @@ -1,208 +0,0 @@ -package com.iflytek.skillhub.domain.namespace; - -import com.iflytek.skillhub.domain.setting.SystemSettingService; -import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException; -import com.iflytek.skillhub.domain.user.UserAccount; -import com.iflytek.skillhub.domain.user.UserAccountRepository; -import com.iflytek.skillhub.domain.user.UserStatus; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -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.domain.PageImpl; - -import java.lang.reflect.Field; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class DefaultNamespaceMembershipServiceTest { - - @Mock - private SystemSettingService systemSettingService; - - @Mock - private NamespaceRepository namespaceRepository; - - @Mock - private NamespaceMemberRepository namespaceMemberRepository; - - @Mock - private UserAccountRepository userAccountRepository; - - private DefaultNamespaceMembershipService service; - - @BeforeEach - void setUp() { - service = new DefaultNamespaceMembershipService( - systemSettingService, - new DefaultNamespaceProperties(), - namespaceRepository, - namespaceMemberRepository, - userAccountRepository); - } - - private Namespace namespace(long id, String slug) { - Namespace namespace = new Namespace(slug, slug, "usr_owner"); - try { - Field field = Namespace.class.getDeclaredField("id"); - field.setAccessible(true); - field.set(namespace, id); - } catch (ReflectiveOperationException e) { - throw new IllegalStateException(e); - } - return namespace; - } - - private void configured(String... slugs) { - when(systemSettingService.get(eq(DefaultNamespaceMembershipService.SETTING_KEY), - eq(DefaultNamespaceSettings.class), any())) - .thenReturn(new DefaultNamespaceSettings(List.of(slugs))); - } - - @Test - void defaultsToTheBuiltInGlobalNamespace() { - assertEquals(List.of("global"), new DefaultNamespaceProperties().toSettings().slugs()); - } - - @Test - void ensureMemberJoinsEveryConfiguredNamespace() { - configured("global", "musee"); - when(namespaceRepository.findBySlug("global")).thenReturn(Optional.of(namespace(1L, "global"))); - when(namespaceRepository.findBySlug("musee")).thenReturn(Optional.of(namespace(2L, "musee"))); - when(namespaceMemberRepository.findByNamespaceIdAndUserId(any(), eq("usr_1"))) - .thenReturn(Optional.empty()); - - service.ensureMember("usr_1"); - - ArgumentCaptor captor = ArgumentCaptor.forClass(NamespaceMember.class); - verify(namespaceMemberRepository, org.mockito.Mockito.times(2)).save(captor.capture()); - assertEquals(List.of(1L, 2L), captor.getAllValues().stream().map(NamespaceMember::getNamespaceId).toList()); - assertTrue(captor.getAllValues().stream().allMatch(m -> m.getRole() == NamespaceRole.MEMBER)); - } - - @Test - void ensureMemberSkipsASlugThatNoLongerResolves() { - configured("global", "deleted-one"); - when(namespaceRepository.findBySlug("global")).thenReturn(Optional.of(namespace(1L, "global"))); - when(namespaceRepository.findBySlug("deleted-one")).thenReturn(Optional.empty()); - when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, "usr_1")).thenReturn(Optional.empty()); - - service.ensureMember("usr_1"); - - verify(namespaceMemberRepository, org.mockito.Mockito.times(1)).save(any(NamespaceMember.class)); - } - - @Test - void ensureMemberIsIdempotent() { - configured("global"); - when(namespaceRepository.findBySlug("global")).thenReturn(Optional.of(namespace(1L, "global"))); - when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, "usr_1")) - .thenReturn(Optional.of(new NamespaceMember(1L, "usr_1", NamespaceRole.MEMBER))); - - service.ensureMember("usr_1"); - - verify(namespaceMemberRepository, never()).save(any(NamespaceMember.class)); - } - - @Test - void updateSettingsRejectsASlugThatDoesNotExist() { - when(namespaceRepository.findBySlug("typo")).thenReturn(Optional.empty()); - - assertThrows(DomainBadRequestException.class, () -> - service.updateSettings(new DefaultNamespaceSettings(List.of("typo")), "usr_admin")); - verify(systemSettingService, never()).put(any(), any(), any()); - } - - @Test - void updateSettingsRejectsANamespaceThatIsNotActive() { - Namespace archived = namespace(3L, "old"); - archived.setStatus(NamespaceStatus.ARCHIVED); - when(namespaceRepository.findBySlug("old")).thenReturn(Optional.of(archived)); - - assertThrows(DomainBadRequestException.class, () -> - service.updateSettings(new DefaultNamespaceSettings(List.of("old")), "usr_admin")); - } - - @Test - void updateSettingsTrimsBlanksAndDropsDuplicates() { - when(namespaceRepository.findBySlug("global")).thenReturn(Optional.of(namespace(1L, "global"))); - when(systemSettingService.put(any(), any(), any())).thenAnswer(i -> i.getArgument(1)); - - service.updateSettings(new DefaultNamespaceSettings(List.of(" global ", "", "global")), "usr_admin"); - - ArgumentCaptor captor = - ArgumentCaptor.forClass(DefaultNamespaceSettings.class); - verify(systemSettingService).put(eq(DefaultNamespaceMembershipService.SETTING_KEY), - captor.capture(), eq("usr_admin")); - assertEquals(List.of("global"), captor.getValue().slugs()); - } - - @Test - void backfillDryRunListsAccountsMissingMembershipWithoutWriting() { - configured("musee"); - when(namespaceRepository.findBySlug("musee")).thenReturn(Optional.of(namespace(2L, "musee"))); - when(userAccountRepository.findByStatus(eq(UserStatus.ACTIVE), any())) - .thenReturn(new PageImpl<>(List.of(new UserAccount("usr_1", "alice", null, null)))); - when(namespaceMemberRepository.findByNamespaceIdAndUserId(2L, "usr_1")).thenReturn(Optional.empty()); - - DefaultNamespaceBackfillReport report = service.backfill(true); - - assertEquals(1, report.entries().size()); - assertEquals(List.of("musee"), report.entries().getFirst().slugs()); - verify(namespaceMemberRepository, never()).save(any(NamespaceMember.class)); - } - - @Test - void backfillEnrollsAccountsThatAreMissing() { - configured("musee"); - when(namespaceRepository.findBySlug("musee")).thenReturn(Optional.of(namespace(2L, "musee"))); - when(userAccountRepository.findByStatus(eq(UserStatus.ACTIVE), any())) - .thenReturn(new PageImpl<>(List.of(new UserAccount("usr_1", "alice", null, null)))); - when(namespaceMemberRepository.findByNamespaceIdAndUserId(2L, "usr_1")).thenReturn(Optional.empty()); - - service.backfill(false); - - verify(namespaceMemberRepository).save(any(NamespaceMember.class)); - } - - @Test - void backfillCountsAccountsThatAreAlreadyEnrolled() { - configured("musee"); - when(namespaceRepository.findBySlug("musee")).thenReturn(Optional.of(namespace(2L, "musee"))); - when(userAccountRepository.findByStatus(eq(UserStatus.ACTIVE), any())) - .thenReturn(new PageImpl<>(List.of(new UserAccount("usr_1", "alice", null, null)))); - when(namespaceMemberRepository.findByNamespaceIdAndUserId(2L, "usr_1")) - .thenReturn(Optional.of(new NamespaceMember(2L, "usr_1", NamespaceRole.MEMBER))); - - DefaultNamespaceBackfillReport report = service.backfill(false); - - assertEquals(1, report.alreadyEnrolled()); - assertTrue(report.entries().isEmpty()); - } - - @Test - void backfillLeavesSystemAccountsAlone() { - configured("musee"); - when(namespaceRepository.findBySlug("musee")).thenReturn(Optional.of(namespace(2L, "musee"))); - when(userAccountRepository.findByStatus(eq(UserStatus.ACTIVE), any())) - .thenReturn(new PageImpl<>(List.of( - UserAccount.systemAccount("builtin-skill-publisher", "Built-in", null, null)))); - - DefaultNamespaceBackfillReport report = service.backfill(false); - - assertEquals(1, report.systemAccountsSkipped()); - assertTrue(report.entries().isEmpty()); - } -} diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/namespace/GlobalNamespaceMembershipServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/namespace/GlobalNamespaceMembershipServiceTest.java new file mode 100644 index 00000000..c4e211bb --- /dev/null +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/namespace/GlobalNamespaceMembershipServiceTest.java @@ -0,0 +1,71 @@ +package com.iflytek.skillhub.domain.namespace; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import java.lang.reflect.Field; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class GlobalNamespaceMembershipServiceTest { + + @Mock + private NamespaceRepository namespaceRepository; + + @Mock + private NamespaceMemberRepository namespaceMemberRepository; + + private GlobalNamespaceMembershipService service; + + @BeforeEach + void setUp() { + service = new GlobalNamespaceMembershipService(namespaceRepository, namespaceMemberRepository); + } + + @Test + void ensureMember_createsGlobalMembershipWhenMissing() throws Exception { + Namespace global = new Namespace("global", "Global", "system"); + setNamespaceId(global, 1L); + + when(namespaceRepository.findBySlug("global")).thenReturn(Optional.of(global)); + when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, "usr_1")).thenReturn(Optional.empty()); + + service.ensureMember("usr_1"); + + ArgumentCaptor memberCaptor = ArgumentCaptor.forClass(NamespaceMember.class); + verify(namespaceMemberRepository).save(memberCaptor.capture()); + assertThat(memberCaptor.getValue().getNamespaceId()).isEqualTo(1L); + assertThat(memberCaptor.getValue().getUserId()).isEqualTo("usr_1"); + assertThat(memberCaptor.getValue().getRole()).isEqualTo(NamespaceRole.MEMBER); + } + + @Test + void ensureMember_keepsExistingGlobalMembership() throws Exception { + Namespace global = new Namespace("global", "Global", "system"); + setNamespaceId(global, 1L); + NamespaceMember existing = new NamespaceMember(1L, "usr_1", NamespaceRole.ADMIN); + + when(namespaceRepository.findBySlug("global")).thenReturn(Optional.of(global)); + when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, "usr_1")).thenReturn(Optional.of(existing)); + + service.ensureMember("usr_1"); + + verify(namespaceMemberRepository, never()).save(any()); + } + + private void setNamespaceId(Namespace namespace, Long id) throws Exception { + Field field = Namespace.class.getDeclaredField("id"); + field.setAccessible(true); + field.set(namespace, id); + } +} diff --git a/web/src/api/client.ts b/web/src/api/client.ts index db3ea600..906947e3 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -50,8 +50,6 @@ import type { PersonalNamespaceSettings, PersonalNamespaceSettingsInput, PersonalNamespaceBackfillResult, - DefaultNamespaceSettings, - DefaultNamespaceBackfillResult, } from './types' import { ApiError } from '@/shared/lib/api-error' import i18n from '@/i18n/config' @@ -1477,29 +1475,6 @@ export const adminApi = { }, ) }, - - async getDefaultNamespaces(): Promise { - return fetchJson('/api/v1/admin/settings/default-namespaces') - }, - - async updateDefaultNamespaces(slugs: string[]): Promise { - return fetchJson('/api/v1/admin/settings/default-namespaces', { - method: 'PUT', - headers: getCsrfHeaders({ 'Content-Type': 'application/json' }), - body: JSON.stringify({ slugs }), - }) - }, - - async backfillDefaultNamespaces(dryRun: boolean): Promise { - return fetchJson( - '/api/v1/admin/settings/default-namespaces/backfill', - { - method: 'POST', - headers: getCsrfHeaders({ 'Content-Type': 'application/json' }), - body: JSON.stringify({ dryRun }), - }, - ) - }, } export const notificationApi = { diff --git a/web/src/api/generated/schema.d.ts b/web/src/api/generated/schema.d.ts index 573bf962..1bfb84d3 100644 --- a/web/src/api/generated/schema.d.ts +++ b/web/src/api/generated/schema.d.ts @@ -388,22 +388,6 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/admin/settings/default-namespaces": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations["getDefaultNamespaces"]; - put: operations["updateDefaultNamespaces"]; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; "/api/v1/admin/namespaces/{slug}/members/{userId}/role": { parameters: { query?: never; @@ -1620,22 +1604,6 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/admin/settings/default-namespaces/backfill": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post: operations["backfillDefaultNamespaces"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; "/api/v1/admin/search/rebuild": { parameters: { query?: never; @@ -3794,21 +3762,6 @@ export interface components { displayNameTemplate?: string; supportedPlaceholders?: string[]; }; - DefaultNamespaceSettingsUpdateRequest: { - slugs: string[]; - }; - ApiResponseDefaultNamespaceSettingsResponse: { - /** Format: int32 */ - code?: number; - msg?: string; - data?: components["schemas"]["DefaultNamespaceSettingsResponse"]; - /** Format: date-time */ - timestamp?: string; - requestId?: string; - }; - DefaultNamespaceSettingsResponse: { - slugs?: string[]; - }; AdminLabelUpdateRequest: { /** @enum {string} */ type: "RECOMMENDED" | "PRIVILEGED"; @@ -4232,7 +4185,7 @@ export interface components { comment?: string; disposition?: string; }; - BackfillRequest: { + PersonalNamespaceBackfillRequest: { dryRun: boolean; }; ApiResponsePersonalNamespaceBackfillResponse: { @@ -4261,26 +4214,6 @@ export interface components { truncated?: boolean; entries?: components["schemas"]["Entry"][]; }; - ApiResponseDefaultNamespaceBackfillResponse: { - /** Format: int32 */ - code?: number; - msg?: string; - data?: components["schemas"]["DefaultNamespaceBackfillResponse"]; - /** Format: date-time */ - timestamp?: string; - requestId?: string; - }; - DefaultNamespaceBackfillResponse: { - dryRun?: boolean; - /** Format: int32 */ - scannedAccounts?: number; - /** Format: int32 */ - alreadyEnrolled?: number; - /** Format: int32 */ - systemAccountsSkipped?: number; - truncated?: boolean; - entries?: components["schemas"]["Entry"][]; - }; ProfileReviewRejectRequest: { comment: string; }; @@ -6584,50 +6517,6 @@ export interface operations { }; }; }; - getDefaultNamespaces: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "*/*": components["schemas"]["ApiResponseDefaultNamespaceSettingsResponse"]; - }; - }; - }; - }; - updateDefaultNamespaces: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["DefaultNamespaceSettingsUpdateRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "*/*": components["schemas"]["ApiResponseDefaultNamespaceSettingsResponse"]; - }; - }; - }; - }; updateMemberRole_2: { parameters: { query?: never; @@ -8791,7 +8680,7 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["BackfillRequest"]; + "application/json": components["schemas"]["PersonalNamespaceBackfillRequest"]; }; }; responses: { @@ -8806,30 +8695,6 @@ export interface operations { }; }; }; - backfillDefaultNamespaces: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["BackfillRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "*/*": components["schemas"]["ApiResponseDefaultNamespaceBackfillResponse"]; - }; - }; - }; - }; rebuildAll: { parameters: { query?: never; diff --git a/web/src/api/types.ts b/web/src/api/types.ts index 590ad720..9733263a 100644 --- a/web/src/api/types.ts +++ b/web/src/api/types.ts @@ -612,22 +612,3 @@ export interface PersonalNamespaceBackfillResult { truncated: boolean entries: PersonalNamespaceBackfillEntry[] } - -export interface DefaultNamespaceSettings { - slugs: string[] -} - -export interface DefaultNamespaceBackfillEntry { - userId: string - displayName: string | null - slugs: string[] -} - -export interface DefaultNamespaceBackfillResult { - dryRun: boolean - scannedAccounts: number - alreadyEnrolled: number - systemAccountsSkipped: number - truncated: boolean - entries: DefaultNamespaceBackfillEntry[] -} diff --git a/web/src/features/admin/use-default-namespaces.ts b/web/src/features/admin/use-default-namespaces.ts deleted file mode 100644 index 9345bd44..00000000 --- a/web/src/features/admin/use-default-namespaces.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { adminApi } from '@/api/client' -import type { DefaultNamespaceBackfillResult, DefaultNamespaceSettings } from '@/api/types' - -const QUERY_KEY = ['admin', 'settings', 'default-namespaces'] - -export function useDefaultNamespaces() { - return useQuery({ - queryKey: QUERY_KEY, - queryFn: () => adminApi.getDefaultNamespaces(), - }) -} - -export function useUpdateDefaultNamespaces() { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: (slugs: string[]) => adminApi.updateDefaultNamespaces(slugs), - onSuccess: (settings) => { - queryClient.setQueryData(QUERY_KEY, settings) - }, - }) -} - -export function useBackfillDefaultNamespaces() { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: (dryRun: boolean) => adminApi.backfillDefaultNamespaces(dryRun), - onSuccess: (result) => { - if (!result.dryRun) { - queryClient.invalidateQueries({ queryKey: ['admin', 'namespaces'] }) - } - }, - }) -} diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index df498205..8f5d9cf9 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1687,17 +1687,6 @@ }, "backfillDoneTitle": "Backfill finished", "backfillDoneDescription": "Created {{count}} namespaces.", - "backfillErrorTitle": "Backfill failed", - "defaultsTitle": "Namespaces every account joins", - "defaultsDescription": "A namespace nobody belongs to is invisible: the namespace list only shows namespaces you are a member of. List the ones every newly activated account should be enrolled in.", - "defaultsLabel": "Namespace slugs", - "defaultsHint": "Comma separated. Each must be an existing, active namespace.", - "defaultsSaveDescription": "Accounts activated from now on join these namespaces.", - "defaultsBackfillHint": "Preview first — the enrol button stays disabled until you do.", - "defaultsBackfillApplyAction": "Enrol {{count}} accounts", - "defaultsBackfillSummary": "Scanned {{scanned}} accounts · {{already}} already enrolled · {{acted}} to enrol", - "defaultsBackfillNothingToDo": "Every account is already enrolled.", - "defaultsBackfillDoneDescription": "Enrolled {{count}} accounts.", - "defaultsColumnSlugs": "Will join" + "backfillErrorTitle": "Backfill failed" } } diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index ff86398f..74bb82ad 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -1687,17 +1687,6 @@ }, "backfillDoneTitle": "补建完成", "backfillDoneDescription": "已创建 {{count}} 个命名空间。", - "backfillErrorTitle": "补建失败", - "defaultsTitle": "全员默认加入的命名空间", - "defaultsDescription": "没有成员的命名空间是看不见的——命名空间列表只显示你是成员的那些。在这里列出每个新激活的账号都应该加入的命名空间。", - "defaultsLabel": "命名空间标识", - "defaultsHint": "用逗号分隔,每个都必须是已存在且启用的命名空间。", - "defaultsSaveDescription": "此后激活的账号会自动加入这些命名空间。", - "defaultsBackfillHint": "请先预览——未预览前加入按钮不可用。", - "defaultsBackfillApplyAction": "加入 {{count}} 个账号", - "defaultsBackfillSummary": "扫描 {{scanned}} 个账号 · {{already}} 个已加入 · {{acted}} 个待加入", - "defaultsBackfillNothingToDo": "所有账号都已经加入了。", - "defaultsBackfillDoneDescription": "已为 {{count}} 个账号加入。", - "defaultsColumnSlugs": "将加入" + "backfillErrorTitle": "补建失败" } } diff --git a/web/src/pages/admin/settings.test.tsx b/web/src/pages/admin/settings.test.tsx index 1f959c57..b20639a5 100644 --- a/web/src/pages/admin/settings.test.tsx +++ b/web/src/pages/admin/settings.test.tsx @@ -4,7 +4,6 @@ import { cleanup, render, screen, waitFor } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const usePersonalNamespaceSettingsMock = vi.fn() -const useDefaultNamespacesMock = vi.fn() vi.mock('react-i18next', async () => { const actual = await vi.importActual('react-i18next') @@ -30,12 +29,6 @@ vi.mock('@/features/admin/use-personal-namespace-settings', () => ({ useBackfillPersonalNamespaces: () => ({ mutateAsync: vi.fn(), isPending: false }), })) -vi.mock('@/features/admin/use-default-namespaces', () => ({ - useDefaultNamespaces: () => useDefaultNamespacesMock(), - useUpdateDefaultNamespaces: () => ({ mutateAsync: vi.fn(), isPending: false }), - useBackfillDefaultNamespaces: () => ({ mutateAsync: vi.fn(), isPending: false }), -})) - import { AdminSettingsPage, previewSlug, renderTemplate } from './settings' describe('previewSlug', () => { @@ -71,10 +64,6 @@ describe('AdminSettingsPage', () => { }, isLoading: false, }) - useDefaultNamespacesMock.mockReturnValue({ - data: { slugs: ['global', 'musee'] }, - isLoading: false, - }) }) afterEach(() => { @@ -121,23 +110,13 @@ describe('AdminSettingsPage', () => { render() expect(await screen.findByText('adminSettings.backfillTitle')).toBeDefined() - // One preview button per backfill: default namespaces, and personal namespaces. - expect(screen.getAllByRole('button', { name: 'adminSettings.backfillPreviewAction' })).toHaveLength(2) + expect(screen.getByRole('button', { name: 'adminSettings.backfillPreviewAction' })).toBeDefined() }) it('keeps the apply button disabled until a preview has been run', async () => { render() - const apply = await screen.findByRole('button', { name: /adminSettings.backfillApplyAction/ }) + const apply = await screen.findByRole('button', { name: /backfillApplyAction/ }) expect((apply as HTMLButtonElement).disabled).toBe(true) }) - - it('shows the configured default namespaces the server returned', async () => { - render() - - const input = (await screen.findByLabelText('adminSettings.defaultsLabel')) as HTMLInputElement - await waitFor(() => { - expect(input.value).toBe('global, musee') - }) - }) }) diff --git a/web/src/pages/admin/settings.tsx b/web/src/pages/admin/settings.tsx index 64bc8181..3036b3b2 100644 --- a/web/src/pages/admin/settings.tsx +++ b/web/src/pages/admin/settings.tsx @@ -14,21 +14,12 @@ import { TableHeader, TableRow, } from '@/shared/ui/table' -import type { - DefaultNamespaceBackfillResult, - PersonalNamespaceBackfillResult, - PersonalNamespaceSettingsInput, -} from '@/api/types' +import type { PersonalNamespaceBackfillResult, PersonalNamespaceSettingsInput } from '@/api/types' import { useBackfillPersonalNamespaces, usePersonalNamespaceSettings, useUpdatePersonalNamespaceSettings, } from '@/features/admin/use-personal-namespace-settings' -import { - useBackfillDefaultNamespaces, - useDefaultNamespaces, - useUpdateDefaultNamespaces, -} from '@/features/admin/use-default-namespaces' /** * Sample account used for the live template preview. @@ -64,20 +55,6 @@ export function AdminSettingsPage() { const backfillMutation = useBackfillPersonalNamespaces() const [backfill, setBackfill] = useState(null) - const { data: defaults, isLoading: defaultsLoading } = useDefaultNamespaces() - const updateDefaultsMutation = useUpdateDefaultNamespaces() - const defaultsBackfillMutation = useBackfillDefaultNamespaces() - // Null until loaded, for the same reason as `form` below. - const [defaultSlugs, setDefaultSlugs] = useState(null) - const [defaultsBackfill, setDefaultsBackfill] = useState(null) - - useEffect(() => { - if (!defaults) { - return - } - setDefaultSlugs((current) => current ?? defaults.slugs.join(', ')) - }, [defaults]) - // Null until the server answers. The form must not mount before then: Radix's // Select keeps a hidden native setDefaultSlugs(event.target.value)} - /> -

{t('adminSettings.defaultsHint')}

- -
- -
- -
- - -
-

{t('adminSettings.defaultsBackfillHint')}

- - {defaultsBackfill ? ( -
-

- {t('adminSettings.defaultsBackfillSummary', { - scanned: defaultsBackfill.scannedAccounts, - already: defaultsBackfill.alreadyEnrolled, - acted: defaultsBackfill.entries.length, - })} -

- {defaultsBackfill.truncated ? ( -

- {t('adminSettings.backfillTruncated')} -

- ) : null} - {defaultsBackfill.entries.length === 0 ? ( -

- {t('adminSettings.defaultsBackfillNothingToDo')} -

- ) : ( -
- - - - {t('adminSettings.backfillColumnUser')} - {t('adminSettings.defaultsColumnSlugs')} - - - - {defaultsBackfill.entries.map((entry) => ( - - {entry.displayName || entry.userId} - {entry.slugs.join(', ')} - - ))} - -
-
- )} -
- ) : null} - - )} - -

{t('adminSettings.backfillTitle')}