Przeglądaj źródła

增加角色、机构管理,优化菜单管理

zhouhao 2 lat temu
rodzic
commit
4fcda29b7a
32 zmienionych plików z 2069 dodań i 391 usunięć
  1. 7 0
      jetlinks-manager/authentication-manager/pom.xml
  2. 42 0
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/configuration/CustomAuthenticationConfiguration.java
  3. 23 0
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/configuration/MenuProperties.java
  4. 138 0
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/BaseDimensionProvider.java
  5. 21 0
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/OrgDimensionType.java
  6. 61 0
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/OrganizationDimensionProvider.java
  7. 42 0
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/RoleDimensionProvider.java
  8. 124 0
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/MenuBindEntity.java
  9. 3 0
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/MenuButtonInfo.java
  10. 67 20
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/MenuEntity.java
  11. 142 6
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/MenuView.java
  12. 84 0
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/OrganizationEntity.java
  13. 46 0
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/OrganizationInfo.java
  14. 66 0
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/RoleEntity.java
  15. 20 0
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/RoleInfo.java
  16. 0 36
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/SystemConfigEntity.java
  17. 26 0
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/enums/RoleState.java
  18. 88 2
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/AuthorizationSettingDetailService.java
  19. 165 3
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/DefaultMenuService.java
  20. 0 34
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/DimensionInitService.java
  21. 57 0
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/MenuGrantService.java
  22. 95 0
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/OrganizationService.java
  23. 77 0
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/RoleService.java
  24. 35 6
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/request/MenuGrantRequest.java
  25. 29 0
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/request/request/SaveUserDetailRequest.java
  26. 46 0
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/request/request/SaveUserRequest.java
  27. 85 0
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/utils/DimensionUserBindUtils.java
  28. 226 113
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/MenuController.java
  29. 50 121
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/OrganizationController.java
  30. 45 0
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/RoleController.java
  31. 0 50
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/SystemConfigController.java
  32. 159 0
      jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/WebFluxUserController.java

+ 7 - 0
jetlinks-manager/authentication-manager/pom.xml

@@ -63,6 +63,13 @@
             <scope>provided</scope>
         </dependency>
 
+        <dependency>
+            <groupId>org.jetlinks.community</groupId>
+            <artifactId>common-component</artifactId>
+            <version>${project.version}</version>
+            <scope>compile</scope>
+        </dependency>
+
     </dependencies>
 
 </project>

+ 42 - 0
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/configuration/CustomAuthenticationConfiguration.java

@@ -0,0 +1,42 @@
+package org.jetlinks.community.auth.configuration;
+
+import com.github.benmanes.caffeine.cache.Caffeine;
+import org.hswebframework.web.authorization.token.UserTokenManager;
+import org.hswebframework.web.authorization.token.redis.RedisUserTokenManager;
+import org.hswebframework.web.authorization.token.redis.SimpleUserToken;
+import org.jetlinks.community.auth.web.WebFluxUserController;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.data.redis.core.ReactiveRedisOperations;
+
+import java.time.Duration;
+
+@Configuration(proxyBeanMethods = false)
+@EnableConfigurationProperties({MenuProperties.class})
+public class CustomAuthenticationConfiguration {
+
+    @Bean
+    @Primary
+    public WebFluxUserController webFluxUserController() {
+        return new WebFluxUserController();
+    }
+
+    @Bean
+    @ConfigurationProperties(prefix = "hsweb.user-token")
+    public UserTokenManager userTokenManager(ReactiveRedisOperations<Object, Object> template,
+                                             ApplicationEventPublisher eventPublisher) {
+        RedisUserTokenManager userTokenManager = new RedisUserTokenManager(template);
+        userTokenManager.setLocalCache(Caffeine
+                                           .newBuilder()
+                                           .expireAfterAccess(Duration.ofMinutes(10))
+                                           .expireAfterWrite(Duration.ofHours(2))
+                                           .<String, SimpleUserToken>build()
+                                           .asMap());
+        userTokenManager.setEventPublisher(eventPublisher);
+        return userTokenManager;
+    }
+}

+ 23 - 0
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/configuration/MenuProperties.java

@@ -0,0 +1,23 @@
+package org.jetlinks.community.auth.configuration;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.web.authorization.Authentication;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+@Getter
+@Setter
+@ConfigurationProperties(prefix = "menu")
+public class MenuProperties {
+
+    private Set<String> allowAllMenusUsers = new HashSet<>(Collections.singletonList("admin"));
+
+    public boolean isAllowAllMenu(Authentication auth) {
+        return allowAllMenusUsers.contains(auth.getUser().getUsername());
+//            || auth.hasPermission(allowPermission);
+    }
+}

+ 138 - 0
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/BaseDimensionProvider.java

@@ -0,0 +1,138 @@
+package org.jetlinks.community.auth.dimension;
+
+import lombok.AllArgsConstructor;
+import org.apache.commons.collections4.CollectionUtils;
+import org.hswebframework.ezorm.rdb.mapping.ReactiveQuery;
+import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
+import org.hswebframework.web.api.crud.entity.GenericEntity;
+import org.hswebframework.web.authorization.Dimension;
+import org.hswebframework.web.authorization.DimensionProvider;
+import org.hswebframework.web.authorization.DimensionType;
+import org.hswebframework.web.crud.events.EntityDeletedEvent;
+import org.hswebframework.web.crud.events.EntityModifyEvent;
+import org.hswebframework.web.crud.events.EntitySavedEvent;
+import org.hswebframework.web.system.authorization.api.entity.DimensionUserEntity;
+import org.hswebframework.web.system.authorization.api.event.ClearUserAuthorizationCacheEvent;
+import org.hswebframework.web.system.authorization.defaults.service.DefaultDimensionUserService;
+import org.hswebframework.web.system.authorization.defaults.service.terms.DimensionTerm;
+import org.reactivestreams.Publisher;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.event.EventListener;
+import org.springframework.util.StringUtils;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@AllArgsConstructor
+public abstract class BaseDimensionProvider<T extends GenericEntity<String>> implements DimensionProvider {
+
+    protected final ReactiveRepository<T, String> repository;
+
+    protected final ApplicationEventPublisher eventPublisher;
+
+    protected final DefaultDimensionUserService dimensionUserService;
+
+    protected abstract DimensionType getDimensionType();
+
+    protected abstract Mono<Dimension> convertToDimension(T entity);
+
+    protected ReactiveQuery<T> createQuery() {
+        return repository.createQuery();
+    }
+
+    @Override
+    public Flux<? extends Dimension> getDimensionByUserId(String s) {
+        return DimensionTerm
+            .inject(createQuery(), "id", getDimensionType().getId(), Collections.singletonList(s))
+            .fetch()
+            .as(this::convertToDimension)
+            ;
+    }
+
+    @Override
+    public Mono<? extends Dimension> getDimensionById(DimensionType dimensionType, String s) {
+        if (!dimensionType.isSameType(getDimensionType())) {
+            return Mono.empty();
+        }
+        return repository
+            .findById(s)
+            .as(this::convertToDimension)
+            .singleOrEmpty();
+    }
+
+    @Override
+    public Flux<? extends Dimension> getDimensionsById(DimensionType type, Collection<String> idList) {
+        if (!type.isSameType(getDimensionType())) {
+            return Flux.empty();
+        }
+        return repository
+            .findById(idList)
+            .as(this::convertToDimension);
+    }
+
+    protected Flux<? extends Dimension> convertToDimension(Publisher<T> source) {
+        return Flux.from(source).flatMap(this::convertToDimension);
+    }
+
+    @Override
+    public Flux<String> getUserIdByDimensionId(String s) {
+        return dimensionUserService
+            .createQuery()
+            .where(DimensionUserEntity::getDimensionId, s)
+            .and(DimensionUserEntity::getDimensionTypeId, getDimensionType().getId())
+            .fetch()
+            .map(DimensionUserEntity::getUserId);
+    }
+
+    @Override
+    public Flux<? extends DimensionType> getAllType() {
+        return Flux.just(getDimensionType());
+    }
+
+    @EventListener
+    public void handleEvent(EntityDeletedEvent<T> event) {
+        event.async(
+            clearUserAuthenticationCache(event.getEntity())
+        );
+    }
+
+    @EventListener
+    public void handleEvent(EntitySavedEvent<T> event) {
+        event.async(
+            clearUserAuthenticationCache(event.getEntity())
+        );
+    }
+
+    @EventListener
+    public void handleEvent(EntityModifyEvent<T> event) {
+        event.async(
+            clearUserAuthenticationCache(event.getAfter())
+        );
+    }
+
+    private Mono<Void> clearUserAuthenticationCache(Collection<T> roles) {
+        List<String> idList = roles
+            .stream()
+            .map(GenericEntity::getId)
+            .filter(StringUtils::hasText)
+            .collect(Collectors.toList());
+        if (CollectionUtils.isEmpty(idList)) {
+            return Mono.empty();
+        }
+        return dimensionUserService
+            .createQuery()
+            .where()
+            .in(DimensionUserEntity::getDimensionId, idList)
+            .and(DimensionUserEntity::getDimensionTypeId, getDimensionType().getId())
+            .fetch()
+            .map(DimensionUserEntity::getUserId)
+            .collectList()
+            .filter(CollectionUtils::isNotEmpty)
+            .flatMap(users->ClearUserAuthorizationCacheEvent.of(users).publish(eventPublisher));
+    }
+
+}

+ 21 - 0
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/OrgDimensionType.java

@@ -0,0 +1,21 @@
+package org.jetlinks.community.auth.dimension;
+
+import lombok.AllArgsConstructor;
+import lombok.Generated;
+import lombok.Getter;
+import org.hswebframework.web.authorization.DimensionType;
+
+/**
+ * @author wangzheng
+ * @since 1.0
+ */
+@AllArgsConstructor
+@Getter
+@Generated
+public enum OrgDimensionType implements DimensionType {
+    org("org","机构");
+
+    private final String id;
+    private final String name;
+
+}

+ 61 - 0
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/OrganizationDimensionProvider.java

@@ -0,0 +1,61 @@
+package org.jetlinks.community.auth.dimension;
+
+import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
+import org.hswebframework.web.authorization.Dimension;
+import org.hswebframework.web.authorization.DimensionType;
+import org.hswebframework.web.system.authorization.defaults.service.DefaultDimensionUserService;
+import org.hswebframework.web.system.authorization.defaults.service.terms.DimensionTerm;
+import org.jetlinks.community.auth.entity.OrganizationEntity;
+import org.jetlinks.community.auth.entity.OrganizationEntity;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Component;
+import org.springframework.util.ObjectUtils;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+@Component
+public class OrganizationDimensionProvider extends BaseDimensionProvider<OrganizationEntity> {
+
+    public OrganizationDimensionProvider(ReactiveRepository<OrganizationEntity, String> repository,
+                                         DefaultDimensionUserService dimensionUserService,
+                                         ApplicationEventPublisher eventPublisher) {
+        super(repository, eventPublisher, dimensionUserService);
+    }
+
+    @Override
+    protected DimensionType getDimensionType() {
+        return OrgDimensionType.org;
+    }
+
+    @Override
+    protected Mono<Dimension> convertToDimension(OrganizationEntity entity) {
+        return Mono.just(entity.toDimension(true));
+    }
+
+    @Override
+    public Flux<? extends Dimension> getDimensionByUserId(String s) {
+        Map<String, Dimension> dimensions = new LinkedHashMap<>();
+
+        return DimensionTerm
+            .inject(createQuery(), "id", getDimensionType().getId(), Collections.singletonList(s))
+            .fetch()
+            //直接关联的部门
+            .doOnNext(org -> dimensions.put(org.getId(), org.toDimension(true)))
+            .concatMap(e -> ObjectUtils.isEmpty(e.getPath())
+                ? Mono.just(e)
+                : createQuery()
+                .where()
+                //使用path快速查询
+                .like$("path", e.getPath())
+                .fetch()
+                //子级部门
+                .doOnNext(org -> dimensions.putIfAbsent(org.getId(), org.toDimension(false)))
+            )
+            .thenMany(Flux.defer(() -> Flux.fromIterable(dimensions.values())));
+    }
+
+}

+ 42 - 0
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/RoleDimensionProvider.java

@@ -0,0 +1,42 @@
+package org.jetlinks.community.auth.dimension;
+
+import org.hswebframework.ezorm.rdb.mapping.ReactiveQuery;
+import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
+import org.hswebframework.web.authorization.DefaultDimensionType;
+import org.hswebframework.web.authorization.Dimension;
+import org.hswebframework.web.authorization.DimensionType;
+import org.hswebframework.web.system.authorization.defaults.service.DefaultDimensionUserService;
+import org.jetlinks.community.auth.entity.RoleEntity;
+import org.jetlinks.community.auth.enums.RoleState;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+@Component
+public class RoleDimensionProvider extends BaseDimensionProvider<RoleEntity> {
+
+    public RoleDimensionProvider(ReactiveRepository<RoleEntity, String> repository,
+                                 DefaultDimensionUserService dimensionUserService,
+                                 ApplicationEventPublisher eventPublisher) {
+        super(repository, eventPublisher, dimensionUserService);
+    }
+
+    @Override
+    protected DimensionType getDimensionType() {
+        return DefaultDimensionType.role;
+    }
+
+    @Override
+    protected Mono<Dimension> convertToDimension(RoleEntity entity) {
+
+        return Mono.just(entity.toDimension());
+    }
+
+    @Override
+    protected ReactiveQuery<RoleEntity> createQuery() {
+        return super
+            .createQuery()
+            .and(RoleEntity::getState, RoleState.enabled.getValue());
+    }
+
+}

+ 124 - 0
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/MenuBindEntity.java

@@ -0,0 +1,124 @@
+package org.jetlinks.community.auth.entity;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.commons.collections4.CollectionUtils;
+import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
+import org.hswebframework.ezorm.rdb.mapping.annotation.Comment;
+import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue;
+import org.hswebframework.ezorm.rdb.mapping.annotation.JsonCodec;
+import org.hswebframework.web.api.crud.entity.GenericEntity;
+import org.hswebframework.web.utils.DigestUtils;
+import org.springframework.util.ObjectUtils;
+
+import javax.persistence.Column;
+import javax.persistence.Index;
+import javax.persistence.Table;
+import javax.validation.constraints.NotBlank;
+import java.sql.JDBCType;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Getter
+@Setter
+@Table(name = "s_menu_bind", indexes = {
+    @Index(name = "idx_menu_bind_dim_key", columnList = "target_key")
+})
+@Comment("菜单绑定信息表")
+public class MenuBindEntity extends GenericEntity<String> {
+
+    @Schema(description = "绑定维度类型,比如role,user")
+    @Column(nullable = false, length = 32, updatable = false)
+    @NotBlank
+    private String targetType;
+
+    @Schema(description = "绑定维度ID")
+    @Column(nullable = false, length = 64, updatable = false)
+    @NotBlank
+    private String targetId;
+
+    @Schema(description = "绑定key", hidden = true)
+    @Column(nullable = false, length = 64, updatable = false)
+    @NotBlank
+    private String targetKey;
+
+    @Schema(description = "菜单ID")
+    @Column(nullable = false, length = 64, updatable = false)
+    @NotBlank
+    private String menuId;
+
+    @Schema(description = "其他配置")
+    @Column
+    @ColumnType(jdbcType = JDBCType.LONGVARCHAR, javaType = String.class)
+    @JsonCodec
+    private Map<String, Object> options;
+
+    @Schema(description = "分配的按钮")
+    @Column
+    @ColumnType(jdbcType = JDBCType.LONGVARCHAR, javaType = String.class)
+    @JsonCodec
+    private List<MenuView.ButtonView> buttons;
+
+    @Schema(description = "冲突时是否合并")
+    @Column
+    @DefaultValue("true")
+    private Boolean merge;
+
+    @Schema(description = "冲突时合并优先级")
+    @Column
+    @DefaultValue("10")
+    private Integer priority;
+
+    @Override
+    public String getId() {
+        if (ObjectUtils.isEmpty(super.getId())) {
+            generateId();
+        }
+        return super.getId();
+    }
+
+    public void generateId() {
+        generateTargetKey();
+        setId(DigestUtils.md5Hex(String.join("|", targetKey, menuId)));
+    }
+
+    public void generateTargetKey() {
+        setTargetKey(generateTargetKey(targetType, targetId));
+    }
+
+    public static String generateTargetKey(String dimensionType, String dimensionId) {
+        return DigestUtils.md5Hex(String.join("|", dimensionType, dimensionId));
+    }
+
+    public MenuBindEntity withTarget(String targetType, String targetId) {
+        this.targetId = targetId;
+        this.targetType = targetType;
+        generateTargetKey();
+        return this;
+    }
+
+    public MenuBindEntity withMerge(Boolean merge, Integer priority) {
+        this.merge = merge;
+        this.priority = priority;
+        return this;
+    }
+
+
+    public static MenuBindEntity of(MenuView view) {
+        MenuBindEntity entity = new MenuBindEntity();
+        entity.setMenuId(view.getId());
+        entity.setOptions(view.getOptions());
+
+        if (CollectionUtils.isNotEmpty(view.getButtons())) {
+            //只保存已经授权的按钮
+            entity.setButtons(view.getButtons()
+                                  .stream()
+                                  .filter(MenuView.ButtonView::isGranted)
+                                  .collect(Collectors.toList()));
+        }
+
+        return entity;
+    }
+}

+ 3 - 0
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/MenuButtonInfo.java

@@ -20,6 +20,9 @@ public class MenuButtonInfo implements Serializable {
     @Schema(description = "按钮名称")
     private String name;
 
+    @Schema(description = "说明")
+    private String description;
+
     @Schema(description = "权限信息")
     private List<PermissionInfo> permissions;
 

+ 67 - 20
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/MenuEntity.java

@@ -1,16 +1,16 @@
 package org.jetlinks.community.auth.entity;
 
-import io.swagger.v3.oas.annotations.Hidden;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Getter;
 import lombok.Setter;
 import org.apache.commons.collections4.CollectionUtils;
-import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
-import org.hswebframework.ezorm.rdb.mapping.annotation.Comment;
-import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue;
-import org.hswebframework.ezorm.rdb.mapping.annotation.JsonCodec;
+import org.hibernate.validator.constraints.Length;
+import org.hswebframework.ezorm.rdb.mapping.annotation.*;
 import org.hswebframework.web.api.crud.entity.GenericTreeSortSupportEntity;
-import org.jetlinks.community.auth.web.request.AuthorizationSettingDetail;
+import org.hswebframework.web.api.crud.entity.RecordCreationEntity;
+import org.hswebframework.web.crud.annotation.EnableEntityEvent;
+import org.hswebframework.web.crud.generator.Generators;
+import org.hswebframework.web.validator.CreateGroup;
 
 import javax.persistence.Column;
 import javax.persistence.Index;
@@ -32,44 +32,56 @@ import java.util.stream.Collectors;
 @Table(name = "s_menu", indexes = {
     @Index(name = "idx_menu_path", columnList = "path")
 })
+@Comment("菜单信息表")
+@EnableEntityEvent
 public class MenuEntity
-    extends GenericTreeSortSupportEntity<String> {
+    extends GenericTreeSortSupportEntity<String> implements RecordCreationEntity {
+
+    /**
+     * 在多应用集成运行时使用此字段来区分菜单属于哪个系统
+     * 具体标识由各应用前端进行定义
+     */
+    @Schema(description = "菜单所有者")
+    @Column(length = 64)
+    private String owner;
 
     @Schema(description = "名称")
-    @Comment("菜单名称")
-    @Column(length = 32)
+    @Column(length = 32, nullable = false)
+    @Length(max = 32, min = 1, groups = CreateGroup.class)
     private String name;
 
-    @Comment("描述")
+    @Schema(description = "编码")
+    @Column(length = 32)
+    @Length(max = 32, groups = CreateGroup.class)
+    private String code;
+
+    @Schema(description = "所属应用")
+    @Column(length = 64)
+    @Length(max = 64, groups = CreateGroup.class)
+    private String application;
+
     @Column
     @ColumnType(jdbcType = JDBCType.CLOB)
     @Schema(description = "描述")
     private String describe;
 
-    @Hidden
-    @Deprecated
-    @Comment("权限表达式")
-    @Column(name = "permission_expression", length = 256)
-    private String permissionExpression;
-
-    @Comment("菜单对应的url")
     @Column(length = 512)
     @Schema(description = "URL,路由")
+    @Length(max = 512, groups = CreateGroup.class)
     private String url;
 
-    @Comment("图标")
     @Column(length = 256)
     @Schema(description = "图标")
+    @Length(max = 256, groups = CreateGroup.class)
     private String icon;
 
-    @Comment("状态")
     @Column
     @ColumnType(jdbcType = JDBCType.SMALLINT)
     @Schema(description = "状态,0为禁用,1为启用")
     @DefaultValue("1")
     private Byte status;
 
-    @Schema(description = "默认权限信息")
+    @Schema(description = "绑定权限信息")
     @Column
     @JsonCodec
     @ColumnType(jdbcType = JDBCType.LONGVARCHAR, javaType = String.class)
@@ -91,6 +103,25 @@ public class MenuEntity
     @Schema(description = "子菜单")
     private List<MenuEntity> children;
 
+    @Column(name = "creator_id", updatable = false)
+    @Schema(
+        description = "创建者ID(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String creatorId;
+
+    @Column(name = "create_time", updatable = false)
+    @DefaultValue(generator = Generators.CURRENT_TIME)
+    @Schema(
+        description = "创建时间(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private Long createTime;
+
+    public boolean isSupportDataAccess() {
+        return false;
+    }
+
     public MenuEntity copy(Predicate<MenuButtonInfo> buttonPredicate) {
         MenuEntity entity = this.copyTo(new MenuEntity());
 
@@ -140,4 +171,20 @@ public class MenuEntity
             .filter(button -> Objects.equals(button.getId(), id))
             .findAny();
     }
+
+    /**
+     * 构建应用的菜单信息
+     * 清除菜单ID,用于新增
+     *
+     * @param appId 应用ID
+     * @param owner 所属系统
+     * @return 菜单
+     */
+    public MenuEntity ofApp(String appId,
+                            String owner) {
+        setId(null);
+        setParentId(null);
+        setOwner(owner);
+        return this;
+    }
 }

+ 142 - 6
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/MenuView.java

@@ -5,19 +5,29 @@ import lombok.AllArgsConstructor;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.Setter;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.collections4.MapUtils;
 import org.hswebframework.web.api.crud.entity.GenericTreeSortSupportEntity;
 import org.hswebframework.web.bean.FastBeanCopier;
 
-import java.util.List;
-import java.util.Map;
+import java.io.Serializable;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 @Getter
 @Setter
 public class MenuView extends GenericTreeSortSupportEntity<String> {
 
+    @Schema(description = "菜单所有者")
+    private String owner;
+
     @Schema(description = "菜单名称")
     private String name;
 
+    @Schema(description = "编码")
+    private String code;
+
     @Schema(description = "图标")
     private String icon;
 
@@ -36,11 +46,81 @@ public class MenuView extends GenericTreeSortSupportEntity<String> {
     @Schema(description = "子节点")
     private List<MenuView> children;
 
+    @Schema(description = "创建时间")
+    private Long createTime;
+
+    @Schema(description = "数据权限说明")
+    private String accessDescription;
+
+    @Schema(description = "是否已授权")
+    private boolean granted;
+
+    public MenuView withGranted(MenuView granted) {
+        if (granted == null) {
+            return this;
+        }
+        this.granted = true;
+
+        this.options = granted.getOptions();
+        return this
+            .withGrantedButtons(granted.getButtons());
+    }
+
+    /**
+     * 设置已经赋权的按钮到当前菜单
+     *
+     * @param grantedButtons 全部按钮
+     * @return 原始菜单
+     */
+    public MenuView withGrantedButtons(Collection<ButtonView> grantedButtons) {
+        if (CollectionUtils.isEmpty(grantedButtons) || CollectionUtils.isEmpty(this.buttons)) {
+            return this;
+        }
+        Map<String, ButtonView> grantedButtonMap =
+            grantedButtons
+                .stream()
+                .collect(Collectors.toMap(ButtonView::getId, Function.identity(), (a, b) -> a));
+
+        for (ButtonView button : this.buttons) {
+            button.enabled = button.granted = grantedButtonMap.containsKey(button.getId());
+        }
+        return this;
+    }
+
+    public Optional<ButtonView> getButton(String id) {
+        if (CollectionUtils.isEmpty(buttons)) {
+            return Optional.empty();
+        }
+        return buttons
+            .stream()
+            .filter(button -> Objects.equals(id, button.getId()))
+            .findFirst();
+    }
+
+    public void grantAll() {
+        this.granted = true;
+        if (CollectionUtils.isNotEmpty(getButtons())) {
+            for (ButtonView button : getButtons()) {
+                button.granted = true;
+            }
+        }
+    }
+
+    public void resetGrant() {
+        this.granted = false;
+        if (CollectionUtils.isNotEmpty(getButtons())) {
+            for (ButtonView button : getButtons()) {
+                button.granted = false;
+            }
+        }
+    }
+
     @Getter
     @Setter
     @AllArgsConstructor(staticName = "of")
     @NoArgsConstructor
-    public static class ButtonView{
+    public static class ButtonView implements Serializable {
+        private static final long serialVersionUID = 1L;
 
         @Schema(description = "按钮ID")
         private String id;
@@ -48,11 +128,67 @@ public class MenuView extends GenericTreeSortSupportEntity<String> {
         @Schema(description = "按钮名称")
         private String name;
 
+        @Schema(description = "说明")
+        private String description;
+
         @Schema(description = "其他配置")
-        private Map<String,Object> options;
+        private Map<String, Object> options;
+
+        @Schema(description = "是否启用")
+        @Deprecated
+        private boolean enabled;
+
+        @Schema(description = "是否已授权")
+        private boolean granted;
+
+        public static ButtonView of(String id, String name, String description, Map<String, Object> options) {
+            return ButtonView.of(id, name, description, options, true, true);
+        }
+
+
+        public ButtonView copy() {
+            return FastBeanCopier.copy(this, new ButtonView());
+        }
+    }
+
+    public static MenuView of(MenuEntity entity) {
+        return FastBeanCopier.copy(entity, new MenuView());
     }
 
-    public static MenuView of(MenuEntity entity){
-        return FastBeanCopier.copy(entity,new MenuView());
+    public static MenuView of(MenuEntity entity, List<MenuBindEntity> binds) {
+        MenuView view = of(entity);
+        if (binds == null) {
+            return view;
+        }
+        view.granted = true;
+        if (MapUtils.isEmpty(view.getOptions())) {
+            view.setOptions(new HashMap<>());
+        }
+        //重新排序
+        binds.sort(Comparator.comparing(MenuBindEntity::getPriority));
+        Map<String, ButtonView> buttons = new LinkedHashMap<>();
+
+        for (MenuBindEntity bind : binds) {
+            //不合并则清空之前的配置
+            if (!bind.getMerge()) {
+                view.setOptions(new HashMap<>());
+                buttons.clear();
+            }
+            if (MapUtils.isNotEmpty(bind.getOptions())) {
+                view.getOptions().putAll(bind.getOptions());
+            }
+            //按钮权限
+            if (CollectionUtils.isNotEmpty(bind.getButtons())) {
+                for (ButtonView button : bind.getButtons()) {
+                    if (button.isGranted()) {
+                        buttons.put(button.getId(), button);
+                    }
+                }
+            }
+
+        }
+        view.setButtons(new ArrayList<>(buttons.values()));
+        return view;
+
     }
 }

+ 84 - 0
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/OrganizationEntity.java

@@ -0,0 +1,84 @@
+package org.jetlinks.community.auth.entity;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
+import org.hswebframework.ezorm.rdb.mapping.annotation.Comment;
+import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue;
+import org.hswebframework.ezorm.rdb.mapping.annotation.JsonCodec;
+import org.hswebframework.web.api.crud.entity.GenericTreeSortSupportEntity;
+import org.hswebframework.web.api.crud.entity.RecordCreationEntity;
+import org.hswebframework.web.authorization.Dimension;
+import org.hswebframework.web.authorization.simple.SimpleDimension;
+import org.hswebframework.web.crud.annotation.EnableEntityEvent;
+import org.hswebframework.web.crud.generator.Generators;
+import org.hswebframework.web.validator.CreateGroup;
+import org.jetlinks.community.auth.dimension.OrgDimensionType;
+
+import javax.persistence.Column;
+import javax.persistence.Table;
+import javax.validation.constraints.Pattern;
+import java.sql.JDBCType;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Getter
+@Setter
+@Table(name = "s_organization")
+@Comment("机构信息表")
+@EnableEntityEvent
+public class OrganizationEntity extends GenericTreeSortSupportEntity<String> implements RecordCreationEntity {
+
+    @Override
+    @Pattern(regexp = "^[0-9a-zA-Z_\\-]+$", message = "ID只能由数字,字母,下划线和中划线组成", groups = CreateGroup.class)
+    @Schema(description = "机构ID(只能由数字,字母,下划线和中划线组成)")
+    public String getId() {
+        return super.getId();
+    }
+
+    @Column
+    @Schema(description = "编码")
+    private String code;
+
+    @Column
+    @Schema(description = "名称")
+    private String name;
+
+    @Column
+    @Schema(description = "类型")
+    private String type;
+
+    @Column
+    @Schema(description = "说明")
+    private String describe;
+
+    @Column
+    @ColumnType(jdbcType = JDBCType.LONGVARCHAR)
+    @JsonCodec
+    @Schema(description = "其他配置")
+    private Map<String, Object> properties;
+
+    @Column(updatable = false)
+    @Schema(
+        description = "创建者ID(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String creatorId;
+
+    @Column(updatable = false)
+    @DefaultValue(generator = Generators.CURRENT_TIME)
+    @Schema(description = "创建时间"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private Long createTime;
+
+    private List<OrganizationEntity> children;
+
+    public Dimension toDimension(boolean direct) {
+        Map<String, Object> options = new HashMap<>();
+        options.put("direct", direct);
+        return SimpleDimension.of(getId(), getName(), OrgDimensionType.org, options);
+    }
+}

+ 46 - 0
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/OrganizationInfo.java

@@ -0,0 +1,46 @@
+package org.jetlinks.community.auth.entity;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.web.authorization.Dimension;
+import org.jetlinks.reactor.ql.utils.CastUtils;
+
+@Getter
+@Setter
+public class OrganizationInfo {
+
+    @Schema(description = "机构(部门ID)")
+    private String id;
+
+    @Schema(description = "名称")
+    private String name;
+
+    @Schema(description = "编码")
+    private String code;
+
+    @Schema(description = "上级ID")
+    private String parentId;
+
+    @Schema(description = "序号")
+    private long sortIndex;
+
+    public static OrganizationInfo of(Dimension dimension) {
+        OrganizationInfo info = new OrganizationInfo();
+        info.setId(dimension.getId());
+        info.setName(dimension.getName());
+
+        dimension.getOption("parentId")
+                 .map(String::valueOf)
+                 .ifPresent(info::setParentId);
+
+        dimension.getOption("code")
+                 .map(String::valueOf)
+                 .ifPresent(info::setCode);
+        dimension.getOption("sortIndex")
+                 .map(sortIndex -> CastUtils.castNumber(sortIndex).longValue())
+                 .ifPresent(info::setSortIndex);
+
+        return info;
+    }
+}

+ 66 - 0
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/RoleEntity.java

@@ -0,0 +1,66 @@
+package org.jetlinks.community.auth.entity;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+import org.hibernate.validator.constraints.Length;
+import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
+import org.hswebframework.ezorm.rdb.mapping.annotation.Comment;
+import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue;
+import org.hswebframework.ezorm.rdb.mapping.annotation.EnumCodec;
+import org.hswebframework.web.api.crud.entity.GenericEntity;
+import org.hswebframework.web.api.crud.entity.RecordCreationEntity;
+import org.hswebframework.web.authorization.DefaultDimensionType;
+import org.hswebframework.web.authorization.Dimension;
+import org.hswebframework.web.authorization.simple.SimpleDimension;
+import org.hswebframework.web.crud.generator.Generators;
+import org.jetlinks.community.auth.enums.RoleState;
+
+import javax.persistence.Column;
+import javax.persistence.Table;
+
+@Getter
+@Setter
+@Table(name = "s_role")
+@Comment("角色信息表")
+public class RoleEntity extends GenericEntity<String> implements RecordCreationEntity {
+
+    @Column(length = 64)
+    @Length(min = 1, max = 64)
+    @Schema(description = "名称")
+    private String name;
+
+    @Column
+    @Length(max = 255)
+    @Schema(description = "说明")
+    private String description;
+
+    @Column(length = 32)
+    @EnumCodec
+    @ColumnType(javaType = String.class)
+    @Schema(description = "状态。enabled为正常,disabled为已禁用")
+    @DefaultValue("enabled")
+    private RoleState state;
+
+    @Column(updatable = false)
+    @Schema(
+        description = "创建者ID(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String creatorId;
+
+    @Column(updatable = false)
+    @DefaultValue(generator = Generators.CURRENT_TIME)
+    @Schema(description = "创建时间"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private Long createTime;
+
+    public Dimension toDimension() {
+        SimpleDimension dimension = new SimpleDimension();
+        dimension.setId(getId());
+        dimension.setName(name);
+        dimension.setType(DefaultDimensionType.role);
+        return dimension;
+    }
+}

+ 20 - 0
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/RoleInfo.java

@@ -0,0 +1,20 @@
+package org.jetlinks.community.auth.entity;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.web.authorization.Dimension;
+
+@Getter
+@Setter
+public class RoleInfo {
+
+    private String id;
+    private String name;
+
+    public static RoleInfo of(Dimension dimension) {
+        RoleInfo detail = new RoleInfo();
+        detail.setId(dimension.getId());
+        detail.setName(dimension.getName());
+        return detail;
+    }
+}

+ 0 - 36
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/SystemConfigEntity.java

@@ -1,36 +0,0 @@
-package org.jetlinks.community.auth.entity;
-
-import lombok.*;
-import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
-import org.hswebframework.ezorm.rdb.mapping.annotation.JsonCodec;
-import org.hswebframework.web.api.crud.entity.GenericEntity;
-
-import javax.persistence.Column;
-import javax.persistence.Table;
-import java.sql.JDBCType;
-import java.util.Map;
-
-@Getter
-@Setter
-@Table(name = "s_system_conf")
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-public class SystemConfigEntity extends GenericEntity<String> {
-
-    /**
-     * 前端配置
-     */
-    @Column
-    @ColumnType(jdbcType = JDBCType.CLOB)
-    @JsonCodec
-    private Map<String, Object> frontConfig;
-
-    public static SystemConfigEntity front(String id,Map<String, Object> front){
-        SystemConfigEntity entity=new SystemConfigEntity();
-        entity.setId(id);
-        entity.setFrontConfig(front);
-        return entity;
-    }
-
-}

+ 26 - 0
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/enums/RoleState.java

@@ -0,0 +1,26 @@
+package org.jetlinks.community.auth.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.hswebframework.web.dict.EnumDict;
+
+/**
+ * 角色状态
+ * @author zhouhao
+ * @since 2.0
+ */
+@Getter
+@AllArgsConstructor
+public enum RoleState implements EnumDict<String> {
+
+    enabled("正常"),
+    disabled("已禁用");
+
+    private final String text;
+
+    @Override
+    public String getValue() {
+        return name();
+    }
+
+}

+ 88 - 2
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/AuthorizationSettingDetailService.java

@@ -4,17 +4,32 @@ import lombok.AllArgsConstructor;
 import org.apache.commons.collections4.CollectionUtils;
 import org.hswebframework.web.authorization.Authentication;
 import org.hswebframework.web.authorization.DimensionProvider;
+import org.hswebframework.web.authorization.DimensionType;
 import org.hswebframework.web.system.authorization.api.entity.AuthorizationSettingEntity;
+import org.hswebframework.web.system.authorization.api.event.ClearUserAuthorizationCacheEvent;
 import org.hswebframework.web.system.authorization.defaults.configuration.PermissionProperties;
 import org.hswebframework.web.system.authorization.defaults.service.DefaultAuthorizationSettingService;
 import org.jetlinks.community.auth.web.request.AuthorizationSettingDetail;
+import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
+import reactor.util.function.Tuple2;
+import reactor.util.function.Tuples;
 
+import javax.annotation.Nullable;
 import java.util.List;
+import java.util.stream.Collectors;
 
+/**
+ * 授权设置管理,用于保存授权配置以及获取授权设置详情.
+ *
+ * @author zhouhao
+ * @see DefaultAuthorizationSettingService
+ * @see DimensionProvider
+ * @since 1.0
+ */
 @Component
 @AllArgsConstructor
 public class AuthorizationSettingDetailService {
@@ -23,8 +38,35 @@ public class AuthorizationSettingDetailService {
     private final List<DimensionProvider> providers;
     private final PermissionProperties permissionProperties;
 
+    private final ApplicationEventPublisher eventPublisher;
+
+    public Mono<Void> clearPermissionUserAuth(String type,String id){
+        return Flux
+            .fromIterable(providers)
+            .flatMap(provider ->
+                         //按维度类型进行映射
+                         provider.getAllType()
+                                 .map(DimensionType::getId)
+                                 .map(t -> Tuples.of(t, provider)))
+            .collectMap(Tuple2::getT1, Tuple2::getT2)
+            .flatMapMany(typeProviderMapping -> Mono
+                .justOrEmpty(typeProviderMapping.get(type))
+                .flatMapMany(provider -> provider.getUserIdByDimensionId(id)))
+            .collectList()
+            .flatMap(lst-> ClearUserAuthorizationCacheEvent.of(lst).publish(eventPublisher))
+            .then();
+    }
+
+    /**
+     * 保存授权设置详情,此操作会全量覆盖数据
+     *
+     * @param currentAuthentication 当前用户权限信息
+     * @param detailFlux            授权详情
+     * @return void
+     */
     @Transactional
-    public Mono<Void> saveDetail(Authentication authentication, Flux<AuthorizationSettingDetail> detailFlux) {
+    public Mono<Void> saveDetail(@Nullable Authentication currentAuthentication,
+                                 Flux<AuthorizationSettingDetail> detailFlux) {
         return detailFlux
             //先删除旧的权限设置
             .flatMap(detail -> settingService
@@ -34,6 +76,22 @@ public class AuthorizationSettingDetailService {
                 .and(AuthorizationSettingEntity::getDimensionTarget, detail.getTargetId())
                 .execute()
                 .thenReturn(detail))
+            .flatMap(detail -> addDetail(currentAuthentication, detailFlux))
+            .then();
+    }
+
+
+    /**
+     * 增量添加授权设置详情
+     *
+     * @param currentAuthentication 当前用户权限信息
+     * @param detailFlux            授权详情
+     * @return void
+     */
+    @Transactional
+    public Mono<Void> addDetail(@Nullable Authentication currentAuthentication,
+                                   Flux<AuthorizationSettingDetail> detailFlux) {
+        return detailFlux
             .flatMap(detail -> Flux
                 .fromIterable(providers)
                 .flatMap(provider -> provider
@@ -45,12 +103,40 @@ public class AuthorizationSettingDetailService {
                 .switchIfEmpty(Flux.defer(() -> Flux.fromIterable(detail.toEntity())))
                 .distinct(AuthorizationSettingEntity::getPermission)
             )
-            .map(entity -> permissionProperties.getFilter().handleSetting(authentication, entity))
+            .map(entity -> null == currentAuthentication
+                ? entity
+                : permissionProperties.getFilter().handleSetting(currentAuthentication, entity))
             .filter(e -> CollectionUtils.isNotEmpty(e.getActions()))
             .as(settingService::save)
             .then();
     }
 
+    /**
+     * 删除授权设置详情
+     *
+     * @param detailFlux   授权详情
+     * @return void
+     */
+    @Transactional
+    public Mono<Void> deleteDetail(Flux<AuthorizationSettingDetail> detailFlux) {
+        return detailFlux
+            .flatMap(detail -> settingService
+                .getRepository()
+                .createDelete()
+                .where(AuthorizationSettingEntity::getDimensionType, detail.getTargetType())
+                .and(AuthorizationSettingEntity::getDimensionTarget, detail.getTargetId())
+                .in(AuthorizationSettingEntity::getPermission,
+                    detail
+                        .getPermissionList()
+                        .stream()
+                        .map(AuthorizationSettingDetail.PermissionInfo::getId)
+                        .collect(Collectors.toList()))
+                .execute())
+            .then();
+
+    }
+
+    //获取权限详情
     public Mono<AuthorizationSettingDetail> getSettingDetail(String targetType,
                                                              String target) {
         return settingService

+ 165 - 3
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/DefaultMenuService.java

@@ -1,33 +1,195 @@
 package org.jetlinks.community.auth.service;
 
+import lombok.Generated;
+import org.apache.commons.collections4.CollectionUtils;
+import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
+import org.hswebframework.ezorm.rdb.operator.dml.query.SortOrder;
+import org.hswebframework.web.api.crud.entity.QueryParamEntity;
+import org.hswebframework.web.authorization.Dimension;
+import org.hswebframework.web.crud.events.EntityDeletedEvent;
+import org.hswebframework.web.crud.events.EntityModifyEvent;
+import org.hswebframework.web.crud.events.EntitySavedEvent;
 import org.hswebframework.web.crud.service.GenericReactiveCrudService;
 import org.hswebframework.web.crud.service.ReactiveTreeSortEntityService;
 import org.hswebframework.web.id.IDGenerator;
+import org.hswebframework.web.system.authorization.api.event.ClearUserAuthorizationCacheEvent;
+import org.jetlinks.community.auth.entity.MenuBindEntity;
 import org.jetlinks.community.auth.entity.MenuEntity;
+import org.jetlinks.community.auth.entity.MenuView;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.event.EventListener;
 import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
 
-import java.util.List;
+import java.util.*;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
 
 /**
+ * 菜单基础信息管理
+ * <p>
+ * 使用通用增删改查接口实现,同时实现通用树排序接口保证菜单数据的树状结构.
+ *
  * @author wangzheng
+ * @see MenuEntity
  * @since 1.0
  */
 @Service
 public class DefaultMenuService
-        extends GenericReactiveCrudService<MenuEntity, String>
-        implements ReactiveTreeSortEntityService<MenuEntity, String> {
+    extends GenericReactiveCrudService<MenuEntity, String>
+    implements ReactiveTreeSortEntityService<MenuEntity, String> {
+
+    private final ReactiveRepository<MenuBindEntity, String> bindRepository;
+
+    private final ApplicationEventPublisher eventPublisher;
+
+    public DefaultMenuService(ReactiveRepository<MenuBindEntity, String> bindRepository,
+                              ApplicationEventPublisher eventPublisher) {
+        this.bindRepository = bindRepository;
+        this.eventPublisher = eventPublisher;
+    }
+
     @Override
+    @Generated
     public IDGenerator<String> getIDGenerator() {
         return IDGenerator.MD5;
     }
 
     @Override
+    @Generated
     public void setChildren(MenuEntity menuEntity, List<MenuEntity> children) {
         menuEntity.setChildren(children);
     }
 
     @Override
+    @Generated
     public List<MenuEntity> getChildren(MenuEntity menuEntity) {
         return menuEntity.getChildren();
     }
+
+    public Flux<MenuView> getMenuViews(QueryParamEntity queryParam, Predicate<MenuEntity> menuPredicate) {
+        return this
+            .createQuery()
+            .setParam(queryParam.noPaging())
+            .orderBy(SortOrder.asc(MenuEntity::getSortIndex))
+            .fetch()
+            .collectMap(MenuEntity::getId, Function.identity())
+            .flatMapIterable(menus -> convertMenuView(menus, menuPredicate, MenuView::of));
+    }
+
+    /**
+     * 根据维度获取已经授权的菜单信息
+     *
+     * @param dimensions 维度信息
+     * @return 菜单信息
+     */
+    public Flux<MenuView> getGrantedMenus(QueryParamEntity queryParam, List<Dimension> dimensions) {
+        if (CollectionUtils.isEmpty(dimensions)) {
+            return Flux.empty();
+        }
+        List<String> keyList = dimensions
+            .stream()
+            .map(dimension -> MenuBindEntity.generateTargetKey(dimension.getType().getId(), dimension.getId()))
+            .collect(Collectors.toList());
+
+        return bindRepository
+            .createQuery()
+            .setParam(queryParam.noPaging())
+            .where()
+            .in(MenuBindEntity::getTargetKey, keyList)
+            .fetch()
+            .as(this::convertToView);
+    }
+
+    public Flux<MenuView> getGrantedMenus(String dimensionType, String dimensionId) {
+        return getGrantedMenus(dimensionType, Collections.singleton(dimensionId));
+    }
+
+    public Flux<MenuView> getGrantedMenus(String dimensionType, Collection<String> dimensionIds) {
+        return bindRepository
+            .createQuery()
+            .where()
+            .in(MenuBindEntity::getTargetKey, dimensionIds
+                .stream()
+                .map(dimensionId -> MenuBindEntity.generateTargetKey(dimensionType, dimensionId))
+                .collect(Collectors.toSet()))
+            .fetch()
+            .as(this::convertToView);
+    }
+
+
+    private Flux<MenuView> convertToView(Flux<MenuBindEntity> entityFlux) {
+        return Mono
+            .zip(
+                //全部菜单数据
+                this
+                    .createQuery()
+                    .where()
+                    .and(MenuEntity::getStatus, 1)
+                    .fetch()
+                    .collectMap(MenuEntity::getId, Function.identity(), LinkedHashMap::new),
+                //菜单绑定信息
+                entityFlux.collect(Collectors.groupingBy(MenuBindEntity::getMenuId)),
+                (menus, binds) -> convertMenuView(menus,
+                                                  menu -> binds.get(menu.getId()) != null,
+                                                  menu -> MenuView.of(menu, binds.get(menu.getId())))
+            )
+            .flatMapIterable(Function.identity());
+    }
+
+    public Collection<MenuView> convertMenuView(Map<String, MenuEntity> menuMap,
+                                                Predicate<MenuEntity> menuPredicate,
+                                                Function<MenuEntity, MenuView> converter) {
+        Map<String, MenuEntity> group = new HashMap<>();
+        for (MenuEntity menu : menuMap.values()) {
+            if (group.containsKey(menu.getId())) {
+                continue;
+            }
+            if (menuPredicate.test(menu)) {
+                String parentId = menu.getParentId();
+                MenuEntity parent;
+                group.put(menu.getId(), menu);
+                //有子菜单默认就有父菜单
+                while (StringUtils.hasText(parentId)) {
+                    parent = menuMap.get(parentId);
+                    if (parent == null) {
+                        break;
+                    }
+                    parentId = parent.getParentId();
+                    group.put(parent.getId(), parent);
+                }
+            }
+        }
+        return group
+            .values()
+            .stream()
+            .map(converter)
+            .sorted()
+            .collect(Collectors.toList());
+    }
+
+    @EventListener
+    public void handleMenuEntity(EntityModifyEvent<MenuEntity> e) {
+        e.async(
+            ClearUserAuthorizationCacheEvent.all().publish(eventPublisher)
+        );
+    }
+
+    @EventListener
+    public void handleMenuEntity(EntityDeletedEvent<MenuEntity> e) {
+        e.async(
+            ClearUserAuthorizationCacheEvent.all().publish(eventPublisher)
+        );
+    }
+
+    @EventListener
+    public void handleMenuEntity(EntitySavedEvent<MenuEntity> e) {
+        e.async(
+            ClearUserAuthorizationCacheEvent.all().publish(eventPublisher)
+        );
+    }
+
 }

+ 0 - 34
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/DimensionInitService.java

@@ -1,34 +0,0 @@
-package org.jetlinks.community.auth.service;
-
-import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
-import org.hswebframework.web.system.authorization.api.entity.DimensionTypeEntity;
-import org.springframework.boot.CommandLineRunner;
-import org.springframework.stereotype.Component;
-import reactor.core.publisher.Flux;
-
-@Component
-public class DimensionInitService implements CommandLineRunner {
-
-    private final ReactiveRepository<DimensionTypeEntity, String> dimensionTypeRepository;
-
-    public DimensionInitService(ReactiveRepository<DimensionTypeEntity, String> dimensionTypeRepository) {
-        this.dimensionTypeRepository = dimensionTypeRepository;
-    }
-
-    @Override
-    public void run(String... args) throws Exception {
-        DimensionTypeEntity org =new DimensionTypeEntity();
-        org.setId("org");
-        org.setName("机构");
-        org.setDescribe("机构维度");
-
-        DimensionTypeEntity role =new DimensionTypeEntity();
-        role.setId("role");
-        role.setName("角色");
-        role.setDescribe("角色维度");
-
-        dimensionTypeRepository.save(Flux.just(org,role))
-            .subscribe();
-
-    }
-}

+ 57 - 0
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/MenuGrantService.java

@@ -0,0 +1,57 @@
+package org.jetlinks.community.auth.service;
+
+import lombok.AllArgsConstructor;
+import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
+import org.jetlinks.community.auth.entity.MenuBindEntity;
+import org.jetlinks.community.auth.entity.MenuEntity;
+import org.jetlinks.community.auth.service.request.request.MenuGrantRequest;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+@Service
+@AllArgsConstructor
+public class MenuGrantService {
+
+    private final AuthorizationSettingDetailService settingService;
+    private final ReactiveRepository<MenuBindEntity, String> bindRepository;
+    private final ReactiveRepository<MenuEntity, String> menuRepository;
+
+
+    /**
+     * 对菜单进行授权
+     *
+     * @param request 授权请求
+     * @return void
+     */
+    @Transactional
+    public Mono<Void> grant(MenuGrantRequest request) {
+        return Flux
+            .concat(
+                //先删除原已保存的菜单信息
+                bindRepository
+                    .createDelete()
+                    .where(MenuBindEntity::getTargetType, request.getTargetType())
+                    .and(MenuBindEntity::getTargetId, request.getTargetId())
+                    .execute(),
+                //保存菜单信息
+                bindRepository.save(request.toBindEntities()),
+                settingService.clearPermissionUserAuth(request.getTargetType(), request.getTargetId())
+            )
+//                //保存权限信息
+//                Mono
+//                    .zip(Mono.just(request),
+//                         menuRepository
+//                             .createQuery()
+//                             .where(MenuEntity::getStatus, 1)
+//                             .fetch()
+//                             .collectList(),
+//                         MenuGrantRequest::toAuthorizationSettingDetail
+//                    )
+//                    //保存后端权限信息
+//                    .flatMap(setting -> settingService.saveDetail(null, Flux.just(setting))))
+            .then()
+            ;
+    }
+}

+ 95 - 0
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/OrganizationService.java

@@ -0,0 +1,95 @@
+package org.jetlinks.community.auth.service;
+
+import lombok.AllArgsConstructor;
+import org.apache.commons.collections4.CollectionUtils;
+import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult;
+import org.hswebframework.web.crud.service.GenericReactiveTreeSupportCrudService;
+import org.hswebframework.web.id.IDGenerator;
+import org.hswebframework.web.system.authorization.api.entity.DimensionUserEntity;
+import org.hswebframework.web.system.authorization.defaults.service.DefaultDimensionUserService;
+import org.jetlinks.community.auth.dimension.OrgDimensionType;
+import org.jetlinks.community.auth.entity.OrganizationEntity;
+import org.jetlinks.community.auth.utils.DimensionUserBindUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Collection;
+import java.util.List;
+
+@Service
+@AllArgsConstructor
+public class OrganizationService extends GenericReactiveTreeSupportCrudService<OrganizationEntity, String> {
+
+    private DefaultDimensionUserService dimensionUserService;
+
+    @Override
+    public IDGenerator<String> getIDGenerator() {
+        return IDGenerator.UUID;
+    }
+
+    @Override
+    public void setChildren(OrganizationEntity entity, List<OrganizationEntity> children) {
+        entity.setChildren(children);
+    }
+
+    @Transactional
+    public Mono<Integer> bindUser(String orgId, List<String> userIdList) {
+        Flux<String> userIdStream = Flux.fromIterable(userIdList);
+
+        return this
+            .findById(orgId)
+            .flatMap(org -> userIdStream
+                .map(userId -> {
+                    DimensionUserEntity userEntity = new DimensionUserEntity();
+                    userEntity.setUserId(userId);
+                    userEntity.setUserName(userId);
+                    userEntity.setDimensionId(orgId);
+                    userEntity.setDimensionTypeId(OrgDimensionType.org.getId());
+                    userEntity.setDimensionName(org.getName());
+                    return userEntity;
+                })
+                .as(dimensionUserService::save))
+            .map(SaveResult::getTotal);
+
+    }
+
+    @Transactional
+    public Mono<Integer> unbindUser(String orgId, List<String> userIdList) {
+        Flux<String> userIdStream = Flux.fromIterable(userIdList);
+
+        return userIdStream
+            .collectList()
+            .filter(CollectionUtils::isNotEmpty)
+            .flatMap(newUserIdList -> dimensionUserService
+                .createDelete()
+                .where(DimensionUserEntity::getDimensionTypeId, OrgDimensionType.org.getId())
+                .in(DimensionUserEntity::getUserId, newUserIdList)
+                .and(DimensionUserEntity::getDimensionId, orgId)
+                .execute())
+            ;
+    }
+
+
+    /**
+     * 绑定用户到机构(部门)
+     *
+     * @param userIdList    用户ID
+     * @param orgIdList     机构Id
+     * @param removeOldBind 是否删除旧的绑定信息
+     * @return void
+     */
+    @Transactional
+    public Mono<Void> bindUser(Collection<String> userIdList,
+                               Collection<String> orgIdList,
+                               boolean removeOldBind) {
+        if (CollectionUtils.isEmpty(userIdList)) {
+            return Mono.empty();
+        }
+        return DimensionUserBindUtils.bindUser(dimensionUserService, userIdList, OrgDimensionType.org.getId(), orgIdList, removeOldBind);
+    }
+
+
+
+}

+ 77 - 0
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/RoleService.java

@@ -0,0 +1,77 @@
+package org.jetlinks.community.auth.service;
+
+import lombok.AllArgsConstructor;
+import org.apache.commons.collections4.CollectionUtils;
+import org.hswebframework.web.authorization.DefaultDimensionType;
+import org.hswebframework.web.crud.service.GenericReactiveCrudService;
+import org.hswebframework.web.system.authorization.defaults.service.DefaultDimensionUserService;
+import org.jetlinks.community.auth.entity.RoleEntity;
+import org.jetlinks.community.auth.utils.DimensionUserBindUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nullable;
+import javax.validation.constraints.NotNull;
+import java.util.Collection;
+
+@Service
+@AllArgsConstructor
+public class RoleService extends GenericReactiveCrudService<RoleEntity, String> {
+
+
+    private final DefaultDimensionUserService dimensionUserService;
+
+
+    /**
+     * 绑定用户到角色
+     *
+     * @param userIdList    用户ID
+     * @param roleIdList    角色Id
+     * @param removeOldBind 是否删除旧的绑定信息
+     * @return void
+     * @see DimensionUserBindUtils#bindUser(DefaultDimensionUserService, Collection, String, Collection, boolean)
+     */
+    @Transactional
+    public Mono<Void> bindUser(@NotNull Collection<String> userIdList,
+                               @Nullable Collection<String> roleIdList,
+                               boolean removeOldBind) {
+
+        if (CollectionUtils.isEmpty(userIdList)) {
+            return Mono.empty();
+        }
+
+        return DimensionUserBindUtils
+            .bindUser(dimensionUserService,
+                      userIdList,
+                      DefaultDimensionType.role.getId(),
+                      roleIdList,
+                      removeOldBind);
+
+    }
+
+    /**
+     * 绑定用户到角色
+     *
+     * @param userIdList 用户ID
+     * @param roleIdList 角色Id
+     * @return void
+     * @see DimensionUserBindUtils#bindUser(DefaultDimensionUserService, Collection, String, Collection, boolean)
+     */
+    @Transactional
+    public Mono<Void> unbindUser(@NotNull Collection<String> userIdList,
+                                 @Nullable Collection<String> roleIdList) {
+
+        if (CollectionUtils.isEmpty(userIdList)) {
+            return Mono.empty();
+        }
+        return DimensionUserBindUtils
+            .unbindUser(dimensionUserService,
+                        userIdList,
+                        DefaultDimensionType.role.getId(),
+                        roleIdList);
+
+    }
+
+
+}

+ 35 - 6
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/request/MenuGrantRequest.java

@@ -1,4 +1,4 @@
-package org.jetlinks.community.auth.web.request;
+package org.jetlinks.community.auth.service.request.request;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.AllArgsConstructor;
@@ -9,9 +9,12 @@ import org.apache.commons.collections4.CollectionUtils;
 import org.hswebframework.web.api.crud.entity.TreeSupportEntity;
 import org.hswebframework.web.id.IDGenerator;
 import org.jetlinks.community.auth.entity.MenuView;
+import org.jetlinks.community.auth.entity.MenuBindEntity;
 import org.jetlinks.community.auth.entity.MenuEntity;
 import org.jetlinks.community.auth.entity.MenuView;
 import org.jetlinks.community.auth.entity.PermissionInfo;
+import org.jetlinks.community.auth.web.request.AuthorizationSettingDetail;
+import org.springframework.util.StringUtils;
 
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
@@ -24,9 +27,10 @@ import java.util.stream.Collectors;
 @NoArgsConstructor
 public class MenuGrantRequest {
 
-
+    @Schema(description = "权限类型,如: org,openApi")
     private String targetType;
 
+    @Schema(description = "权限类型对应的数据ID")
     private String targetId;
 
     /**
@@ -60,6 +64,9 @@ public class MenuGrantRequest {
             //平铺
             List<MenuView> expand = TreeSupportEntity.expandTree2List(menu, IDGenerator.MD5);
             for (MenuView menuView : expand) {
+                if (!menu.isGranted()) {
+                    continue;
+                }
                 MenuEntity entity = menuMap.get(menuView.getId());
                 if (entity == null) {
                     continue;
@@ -67,14 +74,19 @@ public class MenuGrantRequest {
                 //自动持有配置的权限
                 if (CollectionUtils.isNotEmpty(entity.getPermissions())) {
                     for (PermissionInfo permission : entity.getPermissions()) {
-                        permissionInfos
-                            .computeIfAbsent(permission.getPermission(), ignore -> new HashSet<>())
-                            .addAll(permission.getActions());
+                        if (StringUtils.hasText(permission.getPermission()) && CollectionUtils.isNotEmpty(permission.getActions())) {
+                            permissionInfos
+                                .computeIfAbsent(permission.getPermission(), ignore -> new HashSet<>())
+                                .addAll(permission.getActions());
+                        }
                     }
                 }
 
                 if (CollectionUtils.isNotEmpty(menuView.getButtons())) {
                     for (MenuView.ButtonView button : menuView.getButtons()) {
+                        if (!button.isGranted()) {
+                            continue;
+                        }
                         entity.getButton(button.getId())
                               .ifPresent(buttonInfo -> {
                                   if (CollectionUtils.isNotEmpty(buttonInfo.getPermissions())) {
@@ -99,9 +111,26 @@ public class MenuGrantRequest {
                                      .map(e -> AuthorizationSettingDetail.PermissionInfo.of(e.getKey(), e.getValue()))
                                      .collect(Collectors.toList()));
 
-
         return detail;
     }
 
 
+    public List<MenuBindEntity> toBindEntities() {
+        if (CollectionUtils.isEmpty(menus)) {
+            return Collections.emptyList();
+        }
+        List<MenuView> entities = new ArrayList<>();
+        for (MenuView menu : menus) {
+            TreeSupportEntity.expandTree2List(menu, entities, IDGenerator.MD5);
+        }
+        return entities
+            .stream()
+            .filter(MenuView::isGranted)
+            .map(menu -> MenuBindEntity
+                .of(menu)
+                .withTarget(targetType, targetId)
+                .withMerge(merge, priority))
+            .collect(Collectors.toList());
+
+    }
 }

+ 29 - 0
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/request/request/SaveUserDetailRequest.java

@@ -0,0 +1,29 @@
+package org.jetlinks.community.auth.service.request.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.NotBlank;
+
+@Getter
+@Setter
+public class SaveUserDetailRequest {
+
+    @NotBlank
+    @Schema(description = "姓名")
+    private String name;
+
+    @Schema(description = "email")
+    private String email;
+
+    @Schema(description = "联系电话")
+    private String telephone;
+
+    @Schema(description = "头像图片地址")
+    private String avatar;
+
+    @Schema(description = "说明")
+    private String description;
+
+}

+ 46 - 0
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/request/request/SaveUserRequest.java

@@ -0,0 +1,46 @@
+package org.jetlinks.community.auth.service.request.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.web.validator.CreateGroup;
+import org.hswebframework.web.validator.UpdateGroup;
+import org.hswebframework.web.validator.ValidatorUtils;
+import org.jetlinks.community.auth.entity.UserDetail;
+import org.springframework.util.StringUtils;
+
+import javax.validation.constraints.NotNull;
+import java.util.Set;
+
+/**
+ * 创建用户请求,用于新建用户操作
+ *
+ * @author zhouhao
+ * @since 2.0
+ */
+@Getter
+@Setter
+public class SaveUserRequest {
+
+    @Schema(description = "用户基本信息")
+    @NotNull
+    private UserDetail user;
+
+    @Schema(description = "角色ID列表")
+    private Set<String> roleIdList;
+
+    @Schema(description = "机构ID列表")
+    private Set<String> orgIdList;
+
+    public SaveUserRequest validate() {
+        if (user == null) {
+            throw new IllegalArgumentException("user can not be null");
+        }
+        if (StringUtils.hasText(user.getId())) {
+            ValidatorUtils.tryValidate(user, UpdateGroup.class);
+        } else {
+            ValidatorUtils.tryValidate(user, CreateGroup.class);
+        }
+        return this;
+    }
+}

+ 85 - 0
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/utils/DimensionUserBindUtils.java

@@ -0,0 +1,85 @@
+package org.jetlinks.community.auth.utils;
+
+import org.apache.commons.collections4.CollectionUtils;
+import org.hswebframework.web.authorization.DefaultDimensionType;
+import org.hswebframework.web.system.authorization.api.entity.DimensionUserEntity;
+import org.hswebframework.web.system.authorization.defaults.service.DefaultDimensionUserService;
+import org.jetlinks.community.auth.dimension.OrgDimensionType;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Collection;
+
+public class DimensionUserBindUtils {
+
+    /**
+     * 绑定用户到指定的维度中,removeOldBind设置为true时,在绑定前会删除旧的绑定信息(全量绑定).
+     * 否则不会删除旧的绑定信息(增量绑定)
+     *
+     * @param userIdList      用户ID列表
+     * @param dimensionType   维度类型,
+     *                        如:角色{@link  DefaultDimensionType#role},部门{@link  OrgDimensionType#org}.
+     * @param dimensionIdList 角色ID列表
+     * @param removeOldBind   是否删除旧的绑定信息
+     * @return void
+     */
+    public static Mono<Void> bindUser(DefaultDimensionUserService dimensionUserService,
+                                      Collection<String> userIdList,
+                                      String dimensionType,
+                                      Collection<String> dimensionIdList,
+                                      boolean removeOldBind) {
+
+        Mono<Void> before = Mono.empty();
+        if (removeOldBind) {
+            before = dimensionUserService
+                .createDelete()
+                .where()
+                .in(DimensionUserEntity::getUserId, userIdList)
+                .and(DimensionUserEntity::getDimensionTypeId, dimensionType)
+                .execute()
+                .then();
+        }
+        if (CollectionUtils.isEmpty(dimensionIdList)) {
+            return before;
+        }
+
+        return before.then(
+            Flux
+                .fromIterable(userIdList)
+                .flatMap(userId -> Flux
+                    .fromIterable(dimensionIdList)
+                    .map(dimensionId -> createEntity(dimensionType, dimensionId, userId)))
+                .as(dimensionUserService::save)
+                .then()
+        );
+
+    }
+
+    public static Mono<Void> unbindUser(DefaultDimensionUserService dimensionUserService,
+                                        Collection<String> userIdList,
+                                        String dimensionType,
+                                        Collection<String> dimensionIdList) {
+        return dimensionUserService
+            .createDelete()
+            .where()
+            .in(DimensionUserEntity::getUserId, userIdList)
+            .and(DimensionUserEntity::getDimensionTypeId, dimensionType)
+            .when(CollectionUtils.isNotEmpty(dimensionIdList),
+                  delete -> delete.in(DimensionUserEntity::getDimensionId, dimensionIdList))
+            .execute()
+            .then();
+    }
+
+
+    private static DimensionUserEntity createEntity(String dimensionType, String dimensionId, String userId) {
+        DimensionUserEntity entity = new DimensionUserEntity();
+        entity.setUserId(userId);
+        entity.setUserName(userId);
+        entity.setDimensionName(dimensionId);
+        entity.setDimensionTypeId(dimensionType);
+        entity.setDimensionId(dimensionId);
+        entity.generateId();
+        return entity;
+    }
+
+}

+ 226 - 113
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/MenuController.java

@@ -1,38 +1,41 @@
 package org.jetlinks.community.auth.web;
 
 import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import lombok.AllArgsConstructor;
-import org.hswebframework.web.api.crud.entity.TreeSupportEntity;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.hswebframework.ezorm.core.param.TermType;
+import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult;
+import org.hswebframework.web.api.crud.entity.*;
 import org.hswebframework.web.authorization.Authentication;
-import org.hswebframework.web.authorization.annotation.Authorize;
-import org.hswebframework.web.authorization.annotation.Resource;
-import org.hswebframework.web.authorization.annotation.ResourceAction;
+import org.hswebframework.web.authorization.annotation.*;
 import org.hswebframework.web.authorization.exception.UnAuthorizedException;
 import org.hswebframework.web.crud.service.ReactiveCrudService;
 import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController;
-import org.jetlinks.community.auth.entity.MenuButtonInfo;
+import org.hswebframework.web.i18n.LocaleUtils;
+import org.hswebframework.web.system.authorization.defaults.service.DefaultPermissionService;
+import org.jetlinks.community.auth.configuration.MenuProperties;
 import org.jetlinks.community.auth.entity.MenuEntity;
 import org.jetlinks.community.auth.entity.MenuView;
-import org.jetlinks.community.auth.service.AuthorizationSettingDetailService;
 import org.jetlinks.community.auth.service.DefaultMenuService;
-import org.jetlinks.community.auth.web.request.MenuGrantRequest;
-import org.springframework.util.StringUtils;
+import org.jetlinks.community.auth.service.MenuGrantService;
+import org.jetlinks.community.auth.service.request.request.MenuGrantRequest;
+import org.jetlinks.community.auth.web.request.AuthorizationSettingDetail;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.List;
 import java.util.function.Function;
-import java.util.function.Predicate;
-import java.util.stream.Collectors;
 
 /**
  * 菜单管理
  *
- * @author wangzheng
+ * @author zhouhao
  * @since 1.0
  */
 @RestController
@@ -45,150 +48,260 @@ public class MenuController implements ReactiveServiceCrudController<MenuEntity,
 
     private final DefaultMenuService defaultMenuService;
 
-    private final AuthorizationSettingDetailService settingService;
+    private final MenuGrantService grantService;
+
+    private final MenuProperties properties;
+
+    private final DefaultPermissionService permissionService;
 
     @Override
     public ReactiveCrudService<MenuEntity, String> getService() {
         return defaultMenuService;
     }
 
+
+    /**
+     * 获取获取全部菜单信息(树结构)
+     *
+     * @return 菜单列表
+     */
+    @QueryAction
+    @PostMapping("/_all/tree")
+    @Operation(summary = "获取获取全部菜单信息(树结构)")
+    public Flux<MenuEntity> getAllMenuAsTree(@RequestBody Mono<QueryParamEntity> queryMono) {
+        return queryMono
+            .doOnNext(query -> query
+                .toQuery()
+                .orderByAsc(MenuEntity::getSortIndex)
+                .noPaging())
+            .flatMapMany(defaultMenuService::query)
+            .collectList()
+            .flatMapIterable(list -> TreeSupportEntity.list2tree(list, MenuEntity::setChildren));
+    }
+
     /**
      * 获取用户自己的菜单列表
      *
      * @return 菜单列表
      */
-    @GetMapping("/user-own/tree")
+    @PostMapping("/user-own/tree")
     @Authorize(merge = false)
     @Operation(summary = "获取当前用户可访问的菜单(树结构)")
-    public Flux<MenuView> getUserMenuAsTree() {
+    public Flux<MenuView> getUserMenuAsTree(@RequestBody(required = false) Mono<QueryParamEntity> queryMono) {
+        return queryMono
+            .flatMapMany(queryParam -> getUserMenuAsList(queryParam)
+                .as(MenuController::listToTree));
+    }
+
+
+    /**
+     * 获取用户自己的菜单列表
+     *
+     * @return 菜单列表
+     */
+    @GetMapping("/user-own/tree")
+    @Authorize(merge = false)
+    @QueryNoPagingOperation(summary = "获取当前用户可访问的菜单(树结构)")
+    public Flux<MenuView> getUserMenuAsTree(QueryParamEntity queryParam) {
         return this
-            .getUserMenuAsList()
+            .getUserMenuAsList(queryParam)
             .as(MenuController::listToTree);
     }
 
-
     @GetMapping("/user-own/list")
     @Authorize(merge = false)
-    @Operation(summary = "获取当前用户可访问的菜单(列表结构)")
-    public Flux<MenuView> getUserMenuAsList() {
+    @QueryNoPagingOperation(summary = "获取当前用户可访问的菜单(列表结构)")
+    public Flux<MenuView> getUserMenuAsList(QueryParamEntity queryParam) {
         return Authentication
             .currentReactive()
             .switchIfEmpty(Mono.error(UnAuthorizedException::new))
-            .flatMapMany(autz -> defaultMenuService
-                .createQuery()
-                .where(MenuEntity::getStatus,1)
-                .fetch()
-                .collect(Collectors.toMap(MenuEntity::getId, Function.identity()))
-                .flatMapIterable(menuMap -> MenuController
-                    .convertMenuView(menuMap,
-                                     menu -> "admin".equals(autz.getUser().getUsername()) ||
-                                         menu.hasPermission(autz::hasPermission),
-                                     button -> "admin".equals(autz.getUser().getUsername()) ||
-                                         button.hasPermission(autz::hasPermission)
-                    )));
-    }
-
-    @PutMapping("/_grant")
-    @Operation(summary = "根据菜单进行授权")
+            .flatMapMany(autz -> properties.isAllowAllMenu(autz)
+                ?
+                defaultMenuService
+                    .getMenuViews(queryParam, menu -> true)
+                    .doOnNext(MenuView::grantAll)
+                :
+                defaultMenuService.getGrantedMenus(queryParam, autz.getDimensions())
+            );
+    }
+
+    /**
+     * 获取菜单所属系统
+     * 用于新增应用管理时,根据所属系统(owner)查询菜单信息
+     *
+     * @return 所属系统owner
+     */
+    @Authorize(ignore = true)
+    @PostMapping("/owner")
+    @Operation(summary = "获取菜单所属系统")
+    public Flux<String> getSystemMenuOwner(@RequestBody @Parameter(description = "需要去除的所属系统,例如当前系统")
+                                           Mono<List<String>> excludes) {
+        return excludes.flatMapMany(owner -> defaultMenuService
+                           .createQuery()
+                           .and(MenuEntity::getOwner, TermType.nin, owner)
+                           .fetch())
+                       .map(MenuEntity::getOwner)
+                       .distinct();
+    }
+
+    /**
+     * 获取本系统菜单信息(树结构)
+     * 用于应用管理之间同步菜单
+     *
+     * @return 菜单列表
+     */
+    @Authorize(ignore = true)
+    @PostMapping("/owner/tree/{owner}")
+    @Operation(summary = "获取本系统菜单信息(树结构)")
+    public Flux<MenuEntity> getSystemMenuAsTree(@PathVariable @Parameter(description = "菜单所属系统") String owner,
+                                                @RequestBody Mono<QueryParamEntity> queryMono) {
+        return queryMono
+            .doOnNext(query -> query
+                .toQuery()
+                .and(MenuEntity::getOwner, owner)
+                .orderByAsc(MenuEntity::getSortIndex)
+                .noPaging())
+            .flatMapMany(defaultMenuService::query)
+            .collectList()
+            .flatMapIterable(list -> TreeSupportEntity.list2tree(list, MenuEntity::setChildren));
+    }
+
+    @PutMapping("/{targetType}/{targetId}/_grant")
+    @Operation(summary = "对菜单进行授权")
     @ResourceAction(id = "grant", name = "授权")
-    public Mono<Void> grant(@RequestBody Mono<MenuGrantRequest> body) {
-        return Mono
-            .zip(
-                //T1: 当前用户权限信息
-                Authentication.currentReactive(),
-                //T2: 将菜单信息转为授权信息
-                Mono
-                    .zip(body,
-                         defaultMenuService
-                             .createQuery()
-                             .where(MenuEntity::getStatus,1)
-                             .fetch()
-                             .collectList(),
-                         MenuGrantRequest::toAuthorizationSettingDetail
-                    )
-                    .map(Flux::just),
-                //保存授权信息
-                settingService::saveDetail
-            )
-            .flatMap(Function.identity());
+    public Mono<Void> grant(@PathVariable String targetType,
+                            @PathVariable String targetId,
+                            @RequestBody Mono<MenuGrantRequest> body) {
+        //todo 防越权控制
+        return body
+            .doOnNext(request -> {
+                request.setTargetType(targetType);
+                request.setTargetId(targetId);
+            })
+            .flatMap(grantService::grant);
+    }
+
+    @PutMapping("/_batch/_grant")
+    @Operation(summary = "对菜单批量进行授权")
+    @ResourceAction(id = "grant", name = "授权")
+    public Mono<Void> grant(@RequestBody Flux<MenuGrantRequest> body) {
+        return body
+            .flatMap(grantService::grant)
+            .then();
     }
 
     @GetMapping("/{targetType}/{targetId}/_grant/tree")
     @ResourceAction(id = "grant", name = "授权")
-    @Operation(summary = "获取菜单授权信息(树结构)")
+    @QueryNoPagingOperation(summary = "获取菜单授权信息(树结构)")
     public Flux<MenuView> getGrantInfoTree(@PathVariable String targetType,
-                                           @PathVariable String targetId) {
+                                           @PathVariable String targetId,
+                                           QueryParamEntity query) {
 
         return this
-            .getGrantInfo(targetType, targetId)
+            .getGrantInfo(targetType, targetId, query)
             .as(MenuController::listToTree);
     }
 
     @GetMapping("/{targetType}/{targetId}/_grant/list")
     @ResourceAction(id = "grant", name = "授权")
-    @Operation(summary = "获取菜单授权信息(列表结构)")
+    @QueryNoPagingOperation(summary = "获取菜单授权信息(列表结构)")
     public Flux<MenuView> getGrantInfo(@PathVariable String targetType,
-                                       @PathVariable String targetId) {
+                                       @PathVariable String targetId,
+                                       QueryParamEntity query) {
 
+        Flux<MenuView> allMenu = this.getUserMenuAsList(query).cache();
         return Mono
             .zip(
-                //权限设置信息
-                settingService.getSettingDetail(targetType, targetId),
-                //菜单
                 defaultMenuService
-                    .createQuery()
-                    .where(MenuEntity::getStatus,1)
-                    .fetch()
-                    .collectMap(MenuEntity::getId, Function.identity()),
-                (detail, menuMap) -> MenuController
-                    .convertMenuView(menuMap,
-                                     menu -> menu.hasPermission(detail::hasPermission),
-                                     button -> button.hasPermission(detail::hasPermission)
-                    )
-            )
-            .flatMapIterable(Function.identity());
+                    .getGrantedMenus(targetType, targetId)
+                    .collectMap(GenericEntity::getId),
+                allMenu.collectMap(MenuView::getId, Function.identity()),
+                (granted, all) -> LocaleUtils
+                    .currentReactive()
+                    .flatMapMany(locale -> allMenu
+                        .doOnNext(MenuView::resetGrant)
+                        .map(view -> view
+                            .withGranted(granted.get(view.getId()))
+                        )))
+            .flatMapMany(Function.identity());
+
+    }
+
+
+    @PostMapping("/permissions")
+    @ResourceAction(id = "grant", name = "授权")
+    @Operation(summary = "根据菜单获取对应的权限")
+    public Flux<AuthorizationSettingDetail.PermissionInfo> getPermissionsByMenuGrant(@RequestBody Flux<MenuView> menus) {
+        return getAuthorizationSettingDetail(menus)
+            .flatMapIterable(AuthorizationSettingDetail::getPermissionList);
+    }
+
+
+    @PostMapping("/asset-types")
+    @ResourceAction(id = "grant", name = "授权")
+    @Operation(summary = "根据菜单获取对应的资产类型")
+    @Deprecated
+    public Flux<AssetTypeView> getAssetTypeByMenuGrant(@RequestBody Flux<MenuView> menus) {
+        // 社区版本目前不支持数据权限控制
+        return Flux.empty();
+
+    }
+
+    @PatchMapping("/_all")
+    @SaveAction
+    @Transactional
+    @Operation(summary = "全量保存数据", description = "先删除旧数据,再新增数据")
+    public Mono<SaveResult> saveAll(@RequestBody Flux<MenuEntity> menus) {
+        return this
+            .getService()
+            .createDelete()
+            .where(MenuEntity::getStatus, 1)
+            .execute()
+            .then(
+                this.save(menus)
+            );
+    }
+
+    private Mono<AuthorizationSettingDetail> getAuthorizationSettingDetail(Flux<MenuView> menus) {
+        return Mono
+            .zip(menus
+                     .doOnNext(view -> {
+                         view.setGranted(true);
+                         if (null != view.getButtons()) {
+                             for (MenuView.ButtonView button : view.getButtons()) {
+                                 button.setGranted(true);
+                             }
+                         }
+                     })
+                     .collectList()
+                     .map(list -> {
+                         MenuGrantRequest request = new MenuGrantRequest();
+                         request.setTargetType("temp");
+                         request.setTargetId("temp");
+                         request.setMenus(list);
+                         return request;
+                     }),
+                 defaultMenuService
+                     .createQuery()
+                     .fetch()
+                     .collectList(),
+                 MenuGrantRequest::toAuthorizationSettingDetail
+            );
     }
 
     private static Flux<MenuView> listToTree(Flux<MenuView> flux) {
         return flux
             .collectList()
-            .flatMapIterable(list -> TreeSupportEntity
-                .list2tree(list,
-                           MenuView::setChildren,
-                           (Predicate<MenuView>) n ->
-                               StringUtils.isEmpty(n.getParentId())
-                                   || "-1".equals(n.getParentId())));
-    }
-
-    private static Collection<MenuView> convertMenuView(Map<String, MenuEntity> menuMap,
-                                                        Predicate<MenuEntity> menuPredicate,
-                                                        Predicate<MenuButtonInfo> buttonPredicate) {
-        Map<String, MenuEntity> group = new HashMap<>();
-        for (MenuEntity menu : menuMap.values()) {
-            if (group.containsKey(menu.getId())) {
-                continue;
-            }
-            if (menuPredicate.test(menu)) {
-                String parentId = menu.getParentId();
-                MenuEntity parent;
-                group.put(menu.getId(), menu);
-                //有子菜单默认就有父菜单
-                while (!StringUtils.isEmpty(parentId)) {
-                    parent = menuMap.get(parentId);
-                    if (parent == null) {
-                        break;
-                    }
-                    parentId = parent.getParentId();
-                    group.put(parent.getId(), parent);
-                }
-            }
-        }
-        return group
-            .values()
-            .stream()
-            .map(menu -> MenuView.of(menu.copy(buttonPredicate)))
-            .sorted()
-            .collect(Collectors.toList());
+            .flatMapIterable(list -> TreeUtils.list2tree(list, MenuView::getId, MenuView::getParentId, MenuView::setChildren));
+    }
+
+    @Getter
+    @Setter
+    @AllArgsConstructor(staticName = "of")
+    @NoArgsConstructor
+    public static class AssetTypeView {
+        private String id;
+        private String name;
     }
 
 }

+ 50 - 121
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/OrganizationController.java

@@ -3,143 +3,87 @@ package org.jetlinks.community.auth.web;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
-import org.apache.commons.collections4.CollectionUtils;
-import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult;
-import org.hswebframework.web.api.crud.entity.PagerResult;
 import org.hswebframework.web.api.crud.entity.QueryOperation;
 import org.hswebframework.web.api.crud.entity.QueryParamEntity;
 import org.hswebframework.web.api.crud.entity.TreeSupportEntity;
-import org.hswebframework.web.authorization.Authentication;
-import org.hswebframework.web.authorization.Dimension;
-import org.hswebframework.web.authorization.annotation.*;
-import org.hswebframework.web.system.authorization.api.entity.DimensionEntity;
-import org.hswebframework.web.system.authorization.api.entity.DimensionUserEntity;
-import org.hswebframework.web.system.authorization.defaults.service.DefaultDimensionService;
-import org.hswebframework.web.system.authorization.defaults.service.DefaultDimensionUserService;
-import org.springframework.beans.factory.annotation.Autowired;
+import org.hswebframework.web.authorization.annotation.Authorize;
+import org.hswebframework.web.authorization.annotation.QueryAction;
+import org.hswebframework.web.authorization.annotation.Resource;
+import org.hswebframework.web.authorization.annotation.ResourceAction;
+import org.hswebframework.web.crud.service.ReactiveCrudService;
+import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController;
+import org.jetlinks.community.auth.entity.OrganizationEntity;
+import org.jetlinks.community.auth.service.OrganizationService;
 import org.springframework.web.bind.annotation.*;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 import java.util.List;
-import java.util.function.Function;
-import java.util.stream.Collectors;
 
 @RequestMapping("/organization")
 @RestController
-@Resource(id = "organization", name = "机构管理")
-@Tag(name = "机构管理")
-public class OrganizationController {
-    static String orgDimensionTypeId = "org";
-    @Autowired
-    private DefaultDimensionService dimensionService;
+@Resource(id = "organization", name = "部门管理")
+@Tag(name = "部门管理")
+public class OrganizationController implements ReactiveServiceCrudController<OrganizationEntity, String> {
 
-    @Autowired
-    private DefaultDimensionUserService dimensionUserService;
+    private final OrganizationService organizationService;
 
+    public OrganizationController(OrganizationService organizationService) {
+        this.organizationService = organizationService;
+    }
+
+    private Flux<OrganizationEntity> queryAll() {
+        return organizationService.createQuery().fetch();
+    }
 
-    public OrganizationController(DefaultDimensionService dimensionService) {
-        this.dimensionService = dimensionService;
+    private Flux<OrganizationEntity> queryAll(Mono<QueryParamEntity> queryParamEntity) {
+        return organizationService.query(queryParamEntity);
     }
 
     @GetMapping("/_all/tree")
     @Authorize(merge = false)
     @Operation(summary = "获取全部机构信息(树结构)")
-    public Flux<DimensionEntity> getAllOrgTree() {
-        return getAllOrg()
+    public Flux<OrganizationEntity> getAllOrgTree() {
+        return queryAll()
             .collectList()
-            .flatMapIterable(list -> TreeSupportEntity.list2tree(list, DimensionEntity::setChildren));
+            .flatMapIterable(list -> TreeSupportEntity.list2tree(list, OrganizationEntity::setChildren));
+    }
+
+    @PostMapping("/_all/tree")
+    @Authorize(merge = false)
+    @Operation(summary = "获取全部机构信息(树结构)")
+    public Flux<OrganizationEntity> getAllOrgTree(@RequestBody Mono<QueryParamEntity> query) {
+        return queryAll(query)
+            .collectList()
+            .flatMapIterable(list -> TreeSupportEntity.list2tree(list, OrganizationEntity::setChildren));
     }
 
     @GetMapping("/_all")
     @Authorize(merge = false)
     @Operation(summary = "获取全部机构信息")
-    public Flux<DimensionEntity> getAllOrg() {
-        return Authentication
-            .currentReactive()
-            .flatMapMany(auth -> {
-                List<String> list = auth.getDimensions(orgDimensionTypeId)
-                                        .stream()
-                                        .map(Dimension::getId)
-                                        .collect(Collectors.toList());
-                if (CollectionUtils.isNotEmpty(list)) {
-                    return dimensionService.findById(list);
-                }
-                return dimensionService
-                    .createQuery()
-                    .where(DimensionEntity::getTypeId, orgDimensionTypeId)
-                    .fetch();
-            });
+    public Flux<OrganizationEntity> getAllOrg() {
+        return queryAll();
     }
 
-    @GetMapping("/_query")
-    @QueryAction
-    @QueryOperation(summary = "查询机构列表")
-    public Mono<PagerResult<DimensionEntity>> queryDimension(@Parameter(hidden = true) QueryParamEntity entity) {
-        return entity
-            .toNestQuery(q -> q.where(DimensionEntity::getTypeId, orgDimensionTypeId))
-            .execute(Mono::just)
-            .as(dimensionService::queryPager);
+    @PostMapping("/_all")
+    @Authorize(merge = false)
+    @Operation(summary = "获取全部机构信息")
+    public Flux<OrganizationEntity> getAllOrg(@RequestBody Mono<QueryParamEntity> query) {
+        return queryAll(query);
     }
 
     @GetMapping("/_query/_children/tree")
     @QueryAction
     @QueryOperation(summary = "查询机构列表(包含子机构)树结构")
-    public Mono<List<DimensionEntity>> queryChildrenTree(@Parameter(hidden = true) QueryParamEntity entity) {
-        return entity
-            .toNestQuery(q -> q.where(DimensionEntity::getTypeId, orgDimensionTypeId))
-            .execute(dimensionService::queryIncludeChildrenTree);
+    public Mono<List<OrganizationEntity>> queryChildrenTree(@Parameter(hidden = true) QueryParamEntity entity) {
+        return organizationService.queryIncludeChildrenTree(entity);
     }
 
     @GetMapping("/_query/_children")
     @QueryAction
     @QueryOperation(summary = "查询机构列表(包含子机构)")
-    public Flux<DimensionEntity> queryChildren(@Parameter(hidden = true) QueryParamEntity entity) {
-        return entity
-            .toNestQuery(q -> q.where(DimensionEntity::getTypeId, orgDimensionTypeId))
-            .execute(dimensionService::queryIncludeChildren);
-    }
-
-    @PostMapping
-    @CreateAction
-    @Operation(summary = "新增机构信息")
-    public Mono<Void> addOrg(@RequestBody Flux<DimensionEntity> entityFlux) {
-        return entityFlux
-            .doOnNext(entity -> entity.setTypeId(orgDimensionTypeId))
-            .as(dimensionService::insert)
-            .then();
-    }
-
-    @PutMapping("/{id}")
-    @SaveAction
-    @Operation(summary = "更新机构信息")
-    public Mono<Void> updateOrg(@PathVariable String id, @RequestBody Mono<DimensionEntity> entityMono) {
-        return entityMono
-            .doOnNext(entity -> {
-                entity.setTypeId(orgDimensionTypeId);
-                entity.setId(id);
-            })
-            .as(payload -> dimensionService.updateById(id, payload))
-            .then();
-    }
-
-    @PatchMapping
-    @SaveAction
-    @Operation(summary = "保存机构信息")
-    public Mono<Void> saveOrg(@RequestBody Flux<DimensionEntity> entityFlux) {
-        return entityFlux
-            .doOnNext(entity -> entity.setTypeId(orgDimensionTypeId))
-            .as(dimensionService::save)
-            .then();
-    }
-
-    @DeleteMapping("/{id}")
-    @DeleteAction
-    @Operation(summary = "删除机构信息")
-    public Mono<Void> deleteOrg(@PathVariable String id) {
-        return dimensionService
-            .deleteById(Mono.just(id))
-            .then();
+    public Flux<OrganizationEntity> queryChildren(@Parameter(hidden = true) QueryParamEntity entity) {
+        return organizationService.queryIncludeChildren(entity);
     }
 
     @PostMapping("/{id}/users/_bind")
@@ -149,19 +93,7 @@ public class OrganizationController {
                                   @Parameter(description = "用户ID")
                                   @RequestBody Mono<List<String>> userId) {
 
-        return userId
-            .flatMapIterable(Function.identity())
-            .map(uId -> {
-                DimensionUserEntity userEntity = new DimensionUserEntity();
-                userEntity.setUserId(uId);
-                userEntity.setUserName(uId);
-                userEntity.setDimensionId(id);
-                userEntity.setDimensionTypeId(orgDimensionTypeId);
-                userEntity.setDimensionName(orgDimensionTypeId);
-                return userEntity;
-            })
-            .as(dimensionUserService::save)
-            .map(SaveResult::getTotal);
+        return userId.flatMap(list -> organizationService.bindUser(id, list));
 
     }
 
@@ -171,14 +103,11 @@ public class OrganizationController {
     public Mono<Integer> unbindUser(@Parameter(description = "机构ID") @PathVariable String id,
                                     @Parameter(description = "用户ID")
                                     @RequestBody Mono<List<String>> userId) {
-        return userId
-            .flatMap(newUserIdList -> dimensionUserService
-                .createDelete()
-                .where(DimensionUserEntity::getDimensionTypeId, orgDimensionTypeId)
-                .in(DimensionUserEntity::getUserId, newUserIdList)
-                .and(DimensionUserEntity::getDimensionId, id)
-                .execute());
+        return userId.flatMap(list -> organizationService.unbindUser(id, list));
     }
 
-
+    @Override
+    public ReactiveCrudService<OrganizationEntity, String> getService() {
+        return organizationService;
+    }
 }

+ 45 - 0
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/RoleController.java

@@ -0,0 +1,45 @@
+package org.jetlinks.community.auth.web;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.hswebframework.web.authorization.annotation.Resource;
+import org.hswebframework.web.authorization.annotation.SaveAction;
+import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController;
+import org.jetlinks.community.auth.entity.RoleEntity;
+import org.jetlinks.community.auth.service.RoleService;
+import org.springframework.web.bind.annotation.*;
+import reactor.core.publisher.Mono;
+
+import java.util.Collections;
+import java.util.List;
+
+@RestController
+@RequestMapping("/role")
+@Resource(id = "role", name = "角色管理")
+@AllArgsConstructor
+@Getter
+@Tag(name = "角色管理")
+public class RoleController implements ReactiveServiceCrudController<RoleEntity, String> {
+
+    private final RoleService service;
+
+    @PostMapping("/{roleId}/users/_bind")
+    @Operation(summary = "绑定用户")
+    @SaveAction
+    public Mono<Void> bindUser(@PathVariable String roleId,
+                               @RequestBody Mono<List<String>> userId) {
+        return userId
+            .flatMap(list -> service.bindUser(list, Collections.singleton(roleId), false));
+    }
+
+    @PostMapping("/{roleId}/users/_unbind")
+    @Operation(summary = "解绑用户")
+    @SaveAction
+    public Mono<Void> unbindUser(@PathVariable String roleId,
+                                 @RequestBody Mono<List<String>> userId) {
+        return userId
+            .flatMap(list -> service.unbindUser(list, Collections.singleton(roleId)));
+    }
+}

+ 0 - 50
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/SystemConfigController.java

@@ -1,50 +0,0 @@
-package org.jetlinks.community.auth.web;
-
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
-import org.hswebframework.web.authorization.annotation.Authorize;
-import org.hswebframework.web.authorization.annotation.QueryAction;
-import org.hswebframework.web.authorization.annotation.Resource;
-import org.hswebframework.web.authorization.annotation.SaveAction;
-import org.jetlinks.community.auth.entity.SystemConfigEntity;
-import org.springframework.web.bind.annotation.*;
-import reactor.core.publisher.Mono;
-
-import java.util.Collections;
-import java.util.Map;
-
-@RequestMapping("/system/config")
-@RestController
-@Resource(id = "system-config", name = "系统配置")
-@Authorize
-@Tag(name = "系统配置")
-public class SystemConfigController {
-
-    private final ReactiveRepository<SystemConfigEntity, String> repository;
-
-    public SystemConfigController(ReactiveRepository<SystemConfigEntity, String> repository) {
-        this.repository = repository;
-    }
-
-    @GetMapping("/front")
-    @QueryAction
-    @Authorize(ignore = true)
-    @Operation(summary = "获取前端配置信息")
-    public Mono<Map<String, Object>> getFrontConfig() {
-        return repository.findById("default")
-            .map(SystemConfigEntity::getFrontConfig)
-            .defaultIfEmpty(Collections.emptyMap());
-    }
-
-    @PostMapping("/front")
-    @SaveAction
-    @Operation(summary = "保存前端配置信息", description = "参数为json对象,可保存任意字段.")
-    public Mono<Void> saveFrontConfig(@RequestBody Mono<Map<String, Object>> config) {
-        return config
-            .map(front -> SystemConfigEntity.front("default", front))
-            .as(repository::save)
-            .then();
-    }
-
-}

+ 159 - 0
jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/WebFluxUserController.java

@@ -0,0 +1,159 @@
+package org.jetlinks.community.auth.web;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import org.apache.commons.lang3.StringUtils;
+import org.hswebframework.web.authorization.Authentication;
+import org.hswebframework.web.authorization.User;
+import org.hswebframework.web.authorization.annotation.Authorize;
+import org.hswebframework.web.authorization.annotation.SaveAction;
+import org.hswebframework.web.exception.ValidationException;
+import org.hswebframework.web.i18n.LocaleUtils;
+import org.hswebframework.web.system.authorization.api.PasswordValidator;
+import org.hswebframework.web.system.authorization.api.UsernameValidator;
+import org.hswebframework.web.system.authorization.api.entity.UserEntity;
+import org.hswebframework.web.system.authorization.defaults.service.DefaultReactiveUserService;
+import org.jetlinks.community.web.response.ValidationResult;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import reactor.core.publisher.Mono;
+
+public class WebFluxUserController extends org.hswebframework.web.system.authorization.defaults.webflux.WebFluxUserController {
+
+    @Autowired
+    private DefaultReactiveUserService reactiveUserService;
+
+    @Autowired(required = false)
+    private PasswordValidator passwordValidator = (password) -> {
+    };
+
+    @Autowired(required = false)
+    private UsernameValidator usernameValidator = (username) -> {
+        if (StringUtils.isEmpty(username)) {
+            throw new ValidationException("error.user_cannot_be_empty");
+        }
+    };
+
+
+    @PostMapping("/{id}/password/_reset")
+    @SaveAction
+    @Operation(summary = "重置密码")
+    public Mono<Boolean> resetPassword(@PathVariable String id,
+                                       @RequestBody String password) {
+        return Mono.defer(() -> {
+            passwordValidator.validate(password);
+            UserEntity user = new UserEntity();
+            user.setPassword(password);
+            user.setId(id);
+            return reactiveUserService.saveUser(Mono.just(user));
+        });
+    }
+
+    @PostMapping("/username/_validate")
+    @Authorize(merge = false)
+    @Operation(summary = "用户名验证")
+    public Mono<ValidationResult> usernameValidate(@RequestBody(required = false) String username) {
+
+        return LocaleUtils
+            .currentReactive()
+            .flatMap(locale -> {
+                usernameValidator.validate(username);
+                return reactiveUserService
+                    .findByUsername(username)
+                    .map(i -> ValidationResult.error(LocaleUtils.resolveMessage(
+                        "error.user_already_exist",
+                        locale,
+                        "用户" + username + "已存在", username))
+                    );
+            })
+            .defaultIfEmpty(ValidationResult.success())
+            .onErrorResume(ValidationException.class,
+                           e -> e.getLocalizedMessageReactive().map(ValidationResult::error));
+    }
+
+
+    @PostMapping("/password/_validate")
+    @Authorize(merge = false)
+    @Operation(summary = "密码验证")
+    public Mono<ValidationResult> passwordValidate(@RequestBody(required = false) String password) {
+        return Mono
+            .fromSupplier(() -> {
+                passwordValidator.validate(password);
+                return ValidationResult.success();
+            })
+            .onErrorResume(ValidationException.class,
+                           e -> e.getLocalizedMessageReactive().map(ValidationResult::error));
+    }
+
+    @PostMapping("/me/password/_validate")
+    @SaveAction
+    @Operation(summary = "校验当前用户的密码")
+    @Authorize(merge = false)
+    public Mono<ValidationResult> loginUserPasswordValidate(@RequestBody(required = false) String password) {
+        return LocaleUtils
+            .currentReactive()
+            .flatMap(locale -> {
+                // 不校验密码长度与强度,管理员初始密码可能无法通过校验
+//                passwordValidator.validate(password);
+
+                return Authentication
+                    .currentReactive()
+                    .map(Authentication::getUser)
+                    .map(User::getUsername)
+                    .flatMap(username -> reactiveUserService
+                        .findByUsernameAndPassword(username, password)
+                        .flatMap(ignore -> Mono.just(ValidationResult.success())))
+                    .switchIfEmpty(Mono.just(ValidationResult
+                                                 .error(LocaleUtils
+                                                            .resolveMessage("error.password_not_correct", locale, "密码错误"))));
+            })
+            .onErrorResume(ValidationException.class,
+                           e -> e.getLocalizedMessageReactive().map(ValidationResult::error));
+    }
+
+    @Override
+    public Mono<Boolean> saveUser(@RequestBody Mono<UserEntity> userMono) {
+        return userMono
+            .flatMap(user -> {
+                Mono<Void> before;
+                boolean isNew = StringUtils.isEmpty(user.getId());
+                if (!isNew) {
+                    //如果不是新创建用户,则判断是否有权限修改
+                    before = assertUserPermission(user.getId());
+                } else {
+                    before = Mono.empty();
+                }
+                return before
+                    .then(super.saveUser(Mono.just(user)))
+                    .thenReturn(true);
+            });
+    }
+
+    @Override
+    public Mono<Boolean> deleteUser(@PathVariable String id) {
+        return assertUserPermission(id)
+            .then(super.deleteUser(id))
+            ;
+    }
+
+    @Override
+    public Mono<Integer> changeState(@PathVariable @Parameter(description = "用户ID") String id,
+                                     @PathVariable @Parameter(description = "状态,0禁用,1启用") Byte state) {
+        return assertUserPermission(id)
+            .then(
+                super.changeState(id, state)
+            );
+    }
+
+    @Override
+    public Mono<UserEntity> getById(@PathVariable String id) {
+        return assertUserPermission(id)
+            .then(super.getById(id));
+    }
+
+    private Mono<Void> assertUserPermission(String userId) {
+        return Mono.empty();
+    }
+}