17 Commits

Author SHA1 Message Date
许晓东
571efe6ddc 接口权限过滤. 2023-05-18 22:56:00 +08:00
许晓东
7e98a58f60 eslint fixed and 页面权限配置 and 默认权限数据加载. 2023-05-17 22:29:04 +08:00
许晓东
b08be2aa65 权限配置、默认用户、角色配置. 2023-05-16 21:23:45 +08:00
许晓东
238507de19 权限配置. 2023-05-15 21:58:55 +08:00
许晓东
435a5ca2bc 支持登录 2023-05-14 21:25:13 +08:00
许晓东
be8e567684 个人设置更新密码. 2023-05-09 20:44:54 +08:00
许晓东
da1ddeb1e7 用户删除、重置密码,分配角色. 2023-05-09 07:16:55 +08:00
许晓东
38ca2cfc52 用户新增,删除. 2023-05-07 22:55:01 +08:00
许晓东
cc8f671fdb 角色列表页面权限分配. 2023-05-04 16:24:34 +08:00
许晓东
e3b0dd5d2a 角色列表页面权限分配雏形. 2023-04-24 22:08:05 +08:00
许晓东
a37664f6d5 角色列表页面雏形. 2023-04-19 22:10:08 +08:00
许晓东
af52e6bc61 用户权限展示. 2023-04-17 22:24:51 +08:00
许晓东
fce1154c36 新增用户管理:用户、角色、权限新增接口 2023-04-11 22:01:16 +08:00
许晓东
0bf5c6f46c 更新下载地址. 2023-04-11 13:30:53 +08:00
许晓东
fe759aaf74 集群字段长度增加到1024,修复漏洞(CVE-2023-20860):spring升级到5.3.26 2023-04-11 12:25:24 +08:00
许晓东
1e6a7bb269 polish 文档. 2023-02-10 20:41:15 +08:00
许晓东
fb440ae153 polish 文档. 2023-02-10 20:28:23 +08:00
105 changed files with 4077 additions and 358 deletions

View File

@@ -18,17 +18,18 @@ v1.0.6版本之前如果kafka集群启用了ACL但是控制台没看到Acl
* 消费组管理
* 消息管理
* ACL
* 客户端限流
* 运维
功能明细看这个脑图:
![功能特性](./document/img/功能特性.png)
## 安装包下载
点击下载(v1.0.5版本)[kafka-console-ui.zip](https://github.com/xxd763795151/kafka-console-ui/releases/download/v1.0.5/kafka-console-ui.zip)
点击下载(v1.0.7版本)[kafka-console-ui.zip](https://github.com/xxd763795151/kafka-console-ui/releases/download/v1.0.7/kafka-console-ui.zip)
如果安装包下载的比较慢,可以查看下面的源码打包说明,把代码下载下来,快速打包不过最新main分支代码刚升级了kafka版本到3.2.0,还没有充分测试,如果需要稳定版本,可以下载 1.0.4-release分支代码
如果安装包下载的比较慢,可以查看下面的源码打包说明,把代码下载下来,本地快速打包.
github下载慢也可以试试从gitee下载点击下载[gitee来源kafka-console-ui.zip](https://gitee.com/xiaodong_xu/kafka-console-ui/attach_files/969018/download/kafka-console-ui.zip)
github下载慢也可以试试从gitee下载点击下载[gitee来源kafka-console-ui.zip](https://gitee.com/xiaodong_xu/kafka-console-ui/releases/download/v1.0.7/kafka-console-ui.zip)
## 快速使用
### Windows

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

After

Width:  |  Height:  |  Size: 178 KiB

View File

@@ -85,7 +85,7 @@ docker run -d -p 7766:7766 -v $PWD/data:/app/data -v $PWD/log:/app/log wdkang/ka
解压后 将Dockerfile放入文件夹的根目录
**Dockefile**
**Dockerfile**
```dockerfile
# jdk

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 439 KiB

View File

@@ -10,7 +10,7 @@
</parent>
<groupId>com.xuxd</groupId>
<artifactId>kafka-console-ui</artifactId>
<version>1.0.6</version>
<version>1.0.7</version>
<name>kafka-console-ui</name>
<description>Kafka console manage ui</description>
<properties>
@@ -25,6 +25,7 @@
<maven.assembly.plugin.version>3.0.0</maven.assembly.plugin.version>
<mybatis-plus-boot-starter.version>3.4.2</mybatis-plus-boot-starter.version>
<scala.version>2.13.6</scala.version>
<spring-framework.version>5.3.26</spring-framework.version>
</properties>
<dependencies>
<dependency>
@@ -48,6 +49,11 @@
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>

View File

@@ -6,6 +6,9 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* @author 晓东哥哥
*/
@MapperScan("com.xuxd.kafka.console.dao")
@SpringBootApplication
@EnableScheduling

View File

@@ -0,0 +1,117 @@
package com.xuxd.kafka.console.aspect;
import com.xuxd.kafka.console.aspect.annotation.ControllerLog;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author 晓东哥哥
*/
@Slf4j
@Order(-1)
@Aspect
@Component
public class ControllerLogAspect {
private Map<String, String> descMap = new HashMap<>();
private ReentrantLock lock = new ReentrantLock();
@Pointcut("@annotation(com.xuxd.kafka.console.aspect.annotation.ControllerLog)")
private void pointcut() {
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
StringBuilder params = new StringBuilder("[");
try {
String methodName = getMethodFullName(joinPoint.getTarget().getClass().getName(), joinPoint.getSignature().getName());
if (!descMap.containsKey(methodName)) {
cacheDescInfo(joinPoint);
}
Object[] args = joinPoint.getArgs();
long startTime = System.currentTimeMillis();
Object res = joinPoint.proceed();
long endTime = System.currentTimeMillis();
for (int i = 0; i < args.length; i++) {
params.append(args[i]);
}
params.append("]");
String resStr = "[" + (res != null ? res.toString() : "") + "]";
StringBuilder sb = new StringBuilder();
String shortMethodName = descMap.getOrDefault(methodName, ".-");
shortMethodName = shortMethodName.substring(shortMethodName.lastIndexOf(".") + 1);
sb.append("[").append(shortMethodName)
.append("调用完成: ")
.append("请求参数=").append(params).append(", ")
.append("响应值=").append(resStr).append(", ")
.append("耗时=").append(endTime - startTime)
.append(" ms");
log.info(sb.toString());
return res;
} catch (Throwable e) {
log.error("调用方法异常, 请求参数:" + params, e);
throw e;
}
}
private void cacheDescInfo(ProceedingJoinPoint joinPoint) {
lock.lock();
try {
String methodName = joinPoint.getSignature().getName();
Class<?> aClass = joinPoint.getTarget().getClass();
Method method = null;
try {
Object[] args = joinPoint.getArgs();
Class<?>[] clzArr = new Class[args.length];
for (int i = 0; i < args.length; i++) {
clzArr[i] = args[i].getClass();
}
method = aClass.getDeclaredMethod(methodName, clzArr);
} catch (Exception e) {
log.warn("cacheDescInfo error: {}", e.getMessage());
}
String fullMethodName = getMethodFullName(aClass.getName(), methodName);
String desc = "[" + fullMethodName + "]";
if (method == null) {
descMap.put(fullMethodName, desc);
return;
}
ControllerLog controllerLog = method.getAnnotation(ControllerLog.class);
String value = controllerLog.value();
if (StringUtils.isBlank(value)) {
descMap.put(fullMethodName, desc);
} else {
descMap.put(fullMethodName, value);
}
} finally {
lock.unlock();
}
}
private String getMethodFullName(String className, String methodName) {
return className + "#" + methodName;
}
}

View File

@@ -0,0 +1,127 @@
package com.xuxd.kafka.console.aspect;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xuxd.kafka.console.aspect.annotation.Permission;
import com.xuxd.kafka.console.beans.Credentials;
import com.xuxd.kafka.console.beans.dos.SysUserDO;
import com.xuxd.kafka.console.cache.RolePermCache;
import com.xuxd.kafka.console.config.AuthConfig;
import com.xuxd.kafka.console.dao.SysPermissionMapper;
import com.xuxd.kafka.console.dao.SysRoleMapper;
import com.xuxd.kafka.console.dao.SysUserMapper;
import com.xuxd.kafka.console.exception.UnAuthorizedException;
import com.xuxd.kafka.console.filter.CredentialsContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author: xuxd
* @date: 2023/5/17 22:32
**/
@Slf4j
@Order(1)
@Aspect
@Component
public class PermissionAspect {
private Map<String, Set<String>> permMap = new HashMap<>();
private final AuthConfig authConfig;
private final SysUserMapper userMapper;
private final SysRoleMapper roleMapper;
private final SysPermissionMapper permissionMapper;
private final RolePermCache rolePermCache;
public PermissionAspect(AuthConfig authConfig,
SysUserMapper userMapper,
SysRoleMapper roleMapper,
SysPermissionMapper permissionMapper,
RolePermCache rolePermCache) {
this.authConfig = authConfig;
this.userMapper = userMapper;
this.roleMapper = roleMapper;
this.permissionMapper = permissionMapper;
this.rolePermCache = rolePermCache;
}
@Pointcut("@annotation(com.xuxd.kafka.console.aspect.annotation.Permission)")
private void pointcut() {
}
@Before(value = "pointcut()")
public void before(JoinPoint joinPoint) {
if (!authConfig.isEnable()) {
return;
}
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Permission permission = method.getAnnotation(Permission.class);
if (permission == null) {
return;
}
String[] value = permission.value();
if (value == null || value.length == 0) {
return;
}
String name = method.getName() + "@" + method.hashCode();
Map<String, Set<String>> pm = checkPermMap(name, value);
Set<String> allowPermSet = pm.get(name);
if (allowPermSet == null) {
log.error("解析权限出现意外啦!!!");
return;
}
Credentials credentials = CredentialsContext.get();
if (credentials == null || credentials.isInvalid()) {
throw new UnAuthorizedException("credentials is invalid");
}
QueryWrapper<SysUserDO> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", credentials.getUsername());
SysUserDO userDO = userMapper.selectOne(queryWrapper);
if (userDO == null) {
throw new UnAuthorizedException(credentials.getUsername() + ":" + allowPermSet);
}
String roleIds = userDO.getRoleIds();
List<Long> roleIdList = Arrays.stream(roleIds.split(",")).map(String::trim).filter(StringUtils::isNotEmpty).map(Long::valueOf).collect(Collectors.toList());
for (Long roleId : roleIdList) {
Set<String> permSet = rolePermCache.getRolePermCache().getOrDefault(roleId, Collections.emptySet());
for (String p : allowPermSet) {
if (permSet.contains(p)) {
return;
}
}
}
throw new UnAuthorizedException(credentials.getUsername() + ":" + allowPermSet);
}
private Map<String, Set<String>> checkPermMap(String methodName, String[] value) {
if (!permMap.containsKey(methodName)) {
Map<String, Set<String>> map = new HashMap<>(permMap);
map.put(methodName, new HashSet<>(Arrays.asList(value)));
permMap = map;
return map;
}
return permMap;
}
}

View File

@@ -0,0 +1,15 @@
package com.xuxd.kafka.console.aspect.annotation;
import java.lang.annotation.*;
/**
* 该注解用到controller层的方法上
* @author 晓东哥哥
*/
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ControllerLog {
String value() default "";
}

View File

@@ -0,0 +1,17 @@
package com.xuxd.kafka.console.aspect.annotation;
import java.lang.annotation.*;
/**
* 权限注解,开启认证的时候拥有该权限的用户才能访问对应接口.
*
* @author: xuxd
* @date: 2023/5/17 22:30
**/
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Permission {
String[] value() default {};
}

View File

@@ -0,0 +1,21 @@
package com.xuxd.kafka.console.beans;
import lombok.Data;
/**
* @author: xuxd
* @date: 2023/5/14 19:37
**/
@Data
public class Credentials {
public static final Credentials INVALID = new Credentials();
private String username;
private long expiration;
public boolean isInvalid() {
return this == INVALID;
}
}

View File

@@ -0,0 +1,17 @@
package com.xuxd.kafka.console.beans;
import lombok.Data;
import java.util.List;
/**
* @author: xuxd
* @date: 2023/5/14 20:44
**/
@Data
public class LoginResult {
private String token;
private List<String> permissions;
}

View File

@@ -0,0 +1,22 @@
package com.xuxd.kafka.console.beans;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.context.ApplicationEvent;
/**
* @author: xuxd
* @date: 2023/5/18 15:49
**/
@ToString
public class RolePermUpdateEvent extends ApplicationEvent {
@Getter
@Setter
private boolean reload = false;
public RolePermUpdateEvent(Object source) {
super(source);
}
}

View File

@@ -0,0 +1,29 @@
package com.xuxd.kafka.console.beans.dos;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
* @author: xuxd
* @date: 2023/4/11 21:17
**/
@Data
@TableName("t_sys_permission")
public class SysPermissionDO {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
/**
* 权限类型: 0菜单1按钮
*/
private Integer type;
private Long parentId;
private String permission;
}

View File

@@ -0,0 +1,24 @@
package com.xuxd.kafka.console.beans.dos;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
* @author: xuxd
* @date: 2023/4/11 21:17
**/
@Data
@TableName("t_sys_role")
public class SysRoleDO {
@TableId(type = IdType.AUTO)
private Long id;
private String roleName;
private String description;
private String permissionIds;
}

View File

@@ -0,0 +1,26 @@
package com.xuxd.kafka.console.beans.dos;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
* @author: xuxd
* @date: 2023/4/11 21:17
**/
@Data
@TableName("t_sys_user")
public class SysUserDO {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String salt;
private String roleIds;
}

View File

@@ -0,0 +1,15 @@
package com.xuxd.kafka.console.beans.dto;
import lombok.Data;
/**
* @author: xuxd
* @date: 2023/5/14 18:59
**/
@Data
public class LoginUserDTO {
private String username;
private String password;
}

View File

@@ -0,0 +1,32 @@
package com.xuxd.kafka.console.beans.dto;
import com.xuxd.kafka.console.beans.dos.SysPermissionDO;
import lombok.Data;
/**
* @author: xuxd
* @date: 2023/4/11 21:17
**/
@Data
public class SysPermissionDTO {
private String name;
/**
* 权限类型: 0菜单1按钮
*/
private Integer type;
private Long parentId;
private String permission;
public SysPermissionDO toSysPermissionDO() {
SysPermissionDO permissionDO = new SysPermissionDO();
permissionDO.setName(this.name);
permissionDO.setType(this.type);
permissionDO.setParentId(this.parentId);
permissionDO.setPermission(this.permission);
return permissionDO;
}
}

View File

@@ -0,0 +1,35 @@
package com.xuxd.kafka.console.beans.dto;
import com.xuxd.kafka.console.beans.dos.SysRoleDO;
import lombok.Data;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
/**
* @author: xuxd
* @date: 2023/4/11 21:17
**/
@Data
public class SysRoleDTO {
private Long id;
private String roleName;
private String description;
private List<String> permissionIds;
public SysRoleDO toDO() {
SysRoleDO roleDO = new SysRoleDO();
roleDO.setId(this.id);
roleDO.setRoleName(this.roleName);
roleDO.setDescription(this.description);
if (CollectionUtils.isNotEmpty(permissionIds)) {
roleDO.setPermissionIds(StringUtils.join(this.permissionIds, ","));
}
return roleDO;
}
}

View File

@@ -0,0 +1,34 @@
package com.xuxd.kafka.console.beans.dto;
import com.xuxd.kafka.console.beans.dos.SysUserDO;
import lombok.Data;
/**
* @author: xuxd
* @date: 2023/4/11 21:17
**/
@Data
public class SysUserDTO {
private Long id;
private String username;
private String password;
private String salt;
private String roleIds;
private Boolean resetPassword = false;
public SysUserDO toDO() {
SysUserDO userDO = new SysUserDO();
userDO.setId(this.id);
userDO.setUsername(this.username);
userDO.setPassword(this.password);
userDO.setSalt(this.salt);
userDO.setRoleIds(this.roleIds);
return userDO;
}
}

View File

@@ -0,0 +1,43 @@
package com.xuxd.kafka.console.beans.vo;
import com.xuxd.kafka.console.beans.dos.SysPermissionDO;
import lombok.Data;
import java.util.List;
/**
* @author: xuxd
* @date: 2023/4/17 21:18
**/
@Data
public class SysPermissionVO {
private Long id;
private String name;
/**
* 权限类型: 0菜单1按钮
*/
private Integer type;
private Long parentId;
private String permission;
private Long key;
private List<SysPermissionVO> children;
public static SysPermissionVO from(SysPermissionDO permissionDO) {
SysPermissionVO permissionVO = new SysPermissionVO();
permissionVO.setPermission(permissionDO.getPermission());
permissionVO.setType(permissionDO.getType());
permissionVO.setName(permissionDO.getName());
permissionVO.setParentId(permissionDO.getParentId());
permissionVO.setKey(permissionDO.getId());
permissionVO.setId(permissionDO.getId());
return permissionVO;
}
}

View File

@@ -0,0 +1,38 @@
package com.xuxd.kafka.console.beans.vo;
import com.xuxd.kafka.console.beans.dos.SysRoleDO;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author: xuxd
* @date: 2023/4/19 21:12
**/
@Data
public class SysRoleVO {
private Long id;
private String roleName;
private String description;
private List<Long> permissionIds;
public static SysRoleVO from(SysRoleDO roleDO) {
SysRoleVO roleVO = new SysRoleVO();
roleVO.setId(roleDO.getId());
roleVO.setRoleName(roleDO.getRoleName());
roleVO.setDescription(roleDO.getDescription());
if (StringUtils.isNotEmpty(roleDO.getPermissionIds())) {
List<Long> list = Arrays.stream(roleDO.getPermissionIds().split(",")).
filter(StringUtils::isNotEmpty).map(e -> Long.valueOf(e.trim())).collect(Collectors.toList());
roleVO.setPermissionIds(list);
}
return roleVO;
}
}

View File

@@ -0,0 +1,31 @@
package com.xuxd.kafka.console.beans.vo;
import com.xuxd.kafka.console.beans.dos.SysUserDO;
import lombok.Data;
/**
* @author: xuxd
* @date: 2023/5/6 13:06
**/
@Data
public class SysUserVO {
private Long id;
private String username;
private String password;
private String roleIds;
private String roleNames;
public static SysUserVO from(SysUserDO userDO) {
SysUserVO userVO = new SysUserVO();
userVO.setId(userDO.getId());
userVO.setUsername(userDO.getUsername());
userVO.setRoleIds(userDO.getRoleIds());
userVO.setPassword(userDO.getPassword());
return userVO;
}
}

View File

@@ -0,0 +1,91 @@
package com.xuxd.kafka.console.cache;
import com.xuxd.kafka.console.beans.RolePermUpdateEvent;
import com.xuxd.kafka.console.beans.dos.SysPermissionDO;
import com.xuxd.kafka.console.beans.dos.SysRoleDO;
import com.xuxd.kafka.console.dao.SysPermissionMapper;
import com.xuxd.kafka.console.dao.SysRoleMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* @author: xuxd
* @date: 2023/5/18 15:47
**/
@DependsOn("dataInit")
@Slf4j
@Component
public class RolePermCache implements ApplicationListener<RolePermUpdateEvent>, SmartInitializingSingleton {
private Map<Long, SysPermissionDO> permCache = new HashMap<>();
private Map<Long, Set<String>> rolePermCache = new HashMap<>();
private final SysPermissionMapper permissionMapper;
private final SysRoleMapper roleMapper;
public RolePermCache(SysPermissionMapper permissionMapper, SysRoleMapper roleMapper) {
this.permissionMapper = permissionMapper;
this.roleMapper = roleMapper;
}
@Override
public void onApplicationEvent(RolePermUpdateEvent event) {
log.info("更新角色权限信息:{}", event);
if (event.isReload()) {
this.loadPermCache();
}
refresh();
}
public Map<Long, SysPermissionDO> getPermCache() {
return permCache;
}
public Map<Long, Set<String>> getRolePermCache() {
return rolePermCache;
}
private void refresh() {
List<SysRoleDO> roleDOS = roleMapper.selectList(null);
Map<Long, Set<String>> tmp = new HashMap<>();
for (SysRoleDO roleDO : roleDOS) {
String permissionIds = roleDO.getPermissionIds();
if (StringUtils.isEmpty(permissionIds)) {
continue;
}
List<Long> list = Arrays.stream(permissionIds.split(",")).map(String::trim).filter(StringUtils::isNotEmpty).map(Long::valueOf).collect(Collectors.toList());
Set<String> permSet = tmp.getOrDefault(roleDO.getId(), new HashSet<>());
for (Long permId : list) {
SysPermissionDO permissionDO = permCache.get(permId);
if (permissionDO != null) {
permSet.add(permissionDO.getPermission());
}
}
tmp.put(roleDO.getId(), permSet);
}
rolePermCache = tmp;
}
private void loadPermCache() {
List<SysPermissionDO> roleDOS = permissionMapper.selectList(null);
Map<Long, SysPermissionDO> map = roleDOS.stream().collect(Collectors.toMap(SysPermissionDO::getId, Function.identity(), (e1, e2) -> e1));
permCache = map;
}
@Override
public void afterSingletonsInstantiated() {
this.loadPermCache();
this.refresh();
}
}

View File

@@ -0,0 +1,21 @@
package com.xuxd.kafka.console.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @author: xuxd
* @date: 2023/5/9 21:08
**/
@Data
@Configuration
@ConfigurationProperties(prefix = "auth")
public class AuthConfig {
private boolean enable;
private String secret = "kafka-console-ui-default-secret";
private long expireHours;
}

View File

@@ -1,5 +1,6 @@
package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.aspect.annotation.Permission;
import com.xuxd.kafka.console.beans.AclEntry;
import com.xuxd.kafka.console.beans.dto.AddAuthDTO;
import com.xuxd.kafka.console.beans.dto.ConsumerAuthDTO;
@@ -28,6 +29,7 @@ public class AclAuthController {
@Autowired
private AclService aclService;
@Permission({"acl:authority:detail", "acl:sasl-scram:detail"})
@PostMapping("/detail")
public Object getAclDetailList(@RequestBody QueryAclDTO param) {
return aclService.getAclDetailList(param.toEntry());
@@ -38,11 +40,13 @@ public class AclAuthController {
return aclService.getOperationList();
}
@Permission("acl:authority")
@PostMapping("/list")
public Object getAclList(@RequestBody QueryAclDTO param) {
return aclService.getAclList(param.toEntry());
}
@Permission({"acl:authority:add-principal", "acl:authority:add", "acl:sasl-scram:add-auth"})
@PostMapping
public Object addAcl(@RequestBody AddAuthDTO param) {
return aclService.addAcl(param.toAclEntry());
@@ -54,6 +58,7 @@ public class AclAuthController {
* @param param entry.topic && entry.username must.
* @return
*/
@Permission({"acl:authority:producer", "acl:sasl-scram:producer"})
@PostMapping("/producer")
public Object addProducerAcl(@RequestBody ProducerAuthDTO param) {
@@ -66,6 +71,7 @@ public class AclAuthController {
* @param param entry.topic && entry.groupId entry.username must.
* @return
*/
@Permission({"acl:authority:consumer", "acl:sasl-scram:consumer"})
@PostMapping("/consumer")
public Object addConsumerAcl(@RequestBody ConsumerAuthDTO param) {
@@ -78,6 +84,7 @@ public class AclAuthController {
* @param entry entry
* @return
*/
@Permission({"acl:authority:clean", "acl:sasl-scram:pure"})
@DeleteMapping
public Object deleteAclByUser(@RequestBody AclEntry entry) {
return aclService.deleteAcl(entry);
@@ -89,6 +96,7 @@ public class AclAuthController {
* @param param entry.username
* @return
*/
@Permission({"acl:authority:clean", "acl:sasl-scram:pure"})
@DeleteMapping("/user")
public Object deleteAclByUser(@RequestBody DeleteAclDTO param) {
return aclService.deleteUserAcl(param.toUserEntry());
@@ -100,6 +108,7 @@ public class AclAuthController {
* @param param entry.topic && entry.username must.
* @return
*/
@Permission({"acl:authority:clean", "acl:sasl-scram:pure"})
@DeleteMapping("/producer")
public Object deleteProducerAcl(@RequestBody ProducerAuthDTO param) {
@@ -112,6 +121,7 @@ public class AclAuthController {
* @param param entry.topic && entry.groupId entry.username must.
* @return
*/
@Permission({"acl:authority:clean", "acl:sasl-scram:pure"})
@DeleteMapping("/consumer")
public Object deleteConsumerAcl(@RequestBody ConsumerAuthDTO param) {
@@ -124,6 +134,7 @@ public class AclAuthController {
* @param param acl principal.
* @return true or false.
*/
@Permission({"acl:authority:clean", "acl:sasl-scram:pure"})
@DeleteMapping("/clear")
public Object clearAcl(@RequestBody DeleteAclDTO param) {
return aclService.clearAcl(param.toUserEntry());

View File

@@ -1,5 +1,6 @@
package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.aspect.annotation.Permission;
import com.xuxd.kafka.console.beans.AclEntry;
import com.xuxd.kafka.console.beans.AclUser;
import com.xuxd.kafka.console.service.AclService;
@@ -26,27 +27,32 @@ public class AclUserController {
@Autowired
private AclService aclService;
@Permission("acl:sasl-scram")
@GetMapping
public Object getUserList() {
return aclService.getUserList();
}
@Permission({"acl:sasl-scram:add-update", "acl:sasl-scram:add-auth"})
@PostMapping
public Object addOrUpdateUser(@RequestBody AclUser user) {
return aclService.addOrUpdateUser(user.getUsername(), user.getPassword());
}
@Permission({"acl:sasl-scram:del", "acl:sasl-scram:pure"})
@DeleteMapping
public Object deleteUser(@RequestBody AclUser user) {
return aclService.deleteUser(user.getUsername());
}
@Permission({"acl:sasl-scram:del", "acl:sasl-scram:pure"})
@DeleteMapping("/auth")
public Object deleteUserAndAuth(@RequestBody AclUser user) {
return aclService.deleteUserAndAuth(user.getUsername());
}
@Permission("acl:sasl-scram:detail")
@GetMapping("/detail")
public Object getUserDetail(@RequestParam String username) {
return aclService.getUserDetail(username);

View File

@@ -0,0 +1,36 @@
package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.dto.LoginUserDTO;
import com.xuxd.kafka.console.config.AuthConfig;
import com.xuxd.kafka.console.service.AuthService;
import org.springframework.web.bind.annotation.*;
/**
* @author: xuxd
* @date: 2023/5/11 18:54
**/
@RestController
@RequestMapping("/auth")
public class AuthController {
private final AuthConfig authConfig;
private final AuthService authService;
public AuthController(AuthConfig authConfig, AuthService authService) {
this.authConfig = authConfig;
this.authService = authService;
}
@GetMapping("/enable")
public boolean enable() {
return authConfig.isEnable();
}
@PostMapping("/login")
public ResponseData login(@RequestBody LoginUserDTO userDTO) {
return authService.login(userDTO);
}
}

View File

@@ -1,5 +1,6 @@
package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.aspect.annotation.Permission;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.dto.AlterClientQuotaDTO;
import com.xuxd.kafka.console.beans.dto.QueryClientQuotaDTO;
@@ -21,11 +22,13 @@ public class ClientQuotaController {
this.clientQuotaService = clientQuotaService;
}
@Permission({"quota:user", "quota:client", "quota:user-client"})
@PostMapping("/list")
public Object getClientQuotaConfigs(@RequestBody QueryClientQuotaDTO request) {
return clientQuotaService.getClientQuotaConfigs(request.getTypes(), request.getNames());
}
@Permission({"quota:user:add", "quota:client:add", "quota:user-client:add", "quota:edit"})
@PostMapping
public Object alterClientQuotaConfigs(@RequestBody AlterClientQuotaDTO request) {
if (request.getTypes().size() != 2) {
@@ -38,6 +41,7 @@ public class ClientQuotaController {
return clientQuotaService.alterClientQuotaConfigs(request);
}
@Permission("quota:del")
@DeleteMapping
public Object deleteClientQuotaConfigs(@RequestBody AlterClientQuotaDTO request) {
if (request.getTypes().size() != 2) {

View File

@@ -1,5 +1,6 @@
package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.aspect.annotation.Permission;
import com.xuxd.kafka.console.beans.dto.ClusterInfoDTO;
import com.xuxd.kafka.console.service.ClusterService;
import org.springframework.beans.factory.annotation.Autowired;
@@ -34,16 +35,19 @@ public class ClusterController {
return clusterService.getClusterInfoList();
}
@Permission("op:cluster-switch:add")
@PostMapping("/info")
public Object addClusterInfo(@RequestBody ClusterInfoDTO dto) {
return clusterService.addClusterInfo(dto.to());
}
@Permission("op:cluster-switch:del")
@DeleteMapping("/info")
public Object deleteClusterInfo(@RequestBody ClusterInfoDTO dto) {
return clusterService.deleteClusterInfo(dto.getId());
}
@Permission("op:cluster-switch:edit")
@PutMapping("/info")
public Object updateClusterInfo(@RequestBody ClusterInfoDTO dto) {
return clusterService.updateClusterInfo(dto.to());

View File

@@ -1,5 +1,6 @@
package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.aspect.annotation.Permission;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.dto.AlterConfigDTO;
import com.xuxd.kafka.console.beans.enums.AlterType;
@@ -41,46 +42,55 @@ public class ConfigController {
return ResponseData.create().data(configMap).success();
}
@Permission("topic:property-config")
@GetMapping("/topic")
public Object getTopicConfig(String topic) {
return configService.getTopicConfig(topic);
}
@Permission("topic:property-config:edit")
@PostMapping("/topic")
public Object setTopicConfig(@RequestBody AlterConfigDTO dto) {
return configService.alterTopicConfig(dto.getEntity(), dto.to(), AlterType.SET);
}
@Permission("topic:property-config:del")
@DeleteMapping("/topic")
public Object deleteTopicConfig(@RequestBody AlterConfigDTO dto) {
return configService.alterTopicConfig(dto.getEntity(), dto.to(), AlterType.DELETE);
}
@Permission("cluster:property-config")
@GetMapping("/broker")
public Object getBrokerConfig(String brokerId) {
return configService.getBrokerConfig(brokerId);
}
@Permission("cluster:edit")
@PostMapping("/broker")
public Object setBrokerConfig(@RequestBody AlterConfigDTO dto) {
return configService.alterBrokerConfig(dto.getEntity(), dto.to(), AlterType.SET);
}
@Permission("cluster:edit")
@DeleteMapping("/broker")
public Object deleteBrokerConfig(@RequestBody AlterConfigDTO dto) {
return configService.alterBrokerConfig(dto.getEntity(), dto.to(), AlterType.DELETE);
}
@Permission("cluster:log-config")
@GetMapping("/broker/logger")
public Object getBrokerLoggerConfig(String brokerId) {
return configService.getBrokerLoggerConfig(brokerId);
}
@Permission("cluster:edit")
@PostMapping("/broker/logger")
public Object setBrokerLoggerConfig(@RequestBody AlterConfigDTO dto) {
return configService.alterBrokerLoggerConfig(dto.getEntity(), dto.to(), AlterType.SET);
}
@Permission("cluster:edit")
@DeleteMapping("/broker/logger")
public Object deleteBrokerLoggerConfig(@RequestBody AlterConfigDTO dto) {
return configService.alterBrokerLoggerConfig(dto.getEntity(), dto.to(), AlterType.DELETE);

View File

@@ -1,28 +1,20 @@
package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.aspect.annotation.Permission;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.dto.AddSubscriptionDTO;
import com.xuxd.kafka.console.beans.dto.QueryConsumerGroupDTO;
import com.xuxd.kafka.console.beans.dto.ResetOffsetDTO;
import com.xuxd.kafka.console.service.ConsumerService;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.kafka.clients.consumer.OffsetResetStrategy;
import org.apache.kafka.common.ConsumerGroupState;
import org.apache.kafka.common.TopicPartition;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* kafka-console-ui.
@@ -51,26 +43,34 @@ public class ConsumerController {
return consumerService.getConsumerGroupList(groupIdList, stateSet);
}
@Permission("group:del")
@DeleteMapping("/group")
public Object deleteConsumerGroup(@RequestParam String groupId) {
return consumerService.deleteConsumerGroup(groupId);
}
@Permission("group:client")
@GetMapping("/member")
public Object getConsumerMembers(@RequestParam String groupId) {
return consumerService.getConsumerMembers(groupId);
}
@Permission("group:consumer-detail")
@GetMapping("/detail")
public Object getConsumerDetail(@RequestParam String groupId) {
return consumerService.getConsumerDetail(groupId);
}
@Permission("group:add")
@PostMapping("/subscription")
public Object addSubscription(@RequestBody AddSubscriptionDTO subscriptionDTO) {
return consumerService.addSubscription(subscriptionDTO.getGroupId(), subscriptionDTO.getTopic());
}
@Permission({"group:consumer-detail:min",
"group:consumer-detail:last",
"group:consumer-detail:timestamp",
"group:consumer-detail:any"})
@PostMapping("/reset/offset")
public Object restOffset(@RequestBody ResetOffsetDTO offsetDTO) {
ResponseData res = ResponseData.create().failed("unknown");
@@ -78,7 +78,7 @@ public class ConsumerController {
case ResetOffsetDTO.Level.TOPIC:
switch (offsetDTO.getType()) {
case ResetOffsetDTO.Type
.EARLIEST:
.EARLIEST:
res = consumerService.resetOffsetToEndpoint(offsetDTO.getGroupId(), offsetDTO.getTopic(), OffsetResetStrategy.EARLIEST);
break;
case ResetOffsetDTO.Type.LATEST:
@@ -94,7 +94,7 @@ public class ConsumerController {
case ResetOffsetDTO.Level.PARTITION:
switch (offsetDTO.getType()) {
case ResetOffsetDTO.Type
.SPECIAL:
.SPECIAL:
res = consumerService.resetPartitionToTargetOffset(offsetDTO.getGroupId(), new TopicPartition(offsetDTO.getTopic(), offsetDTO.getPartition()), offsetDTO.getOffset());
break;
default:
@@ -118,11 +118,13 @@ public class ConsumerController {
return consumerService.getSubscribeTopicList(groupId);
}
@Permission({"topic:consumer-detail"})
@GetMapping("/topic/subscribed")
public Object getTopicSubscribedByGroups(@RequestParam String topic) {
return consumerService.getTopicSubscribedByGroups(topic);
}
@Permission("group:offset-partition")
@GetMapping("/offset/partition")
public Object getOffsetPartition(@RequestParam String groupId) {
return consumerService.getOffsetPartition(groupId);

View File

@@ -1,5 +1,6 @@
package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.aspect.annotation.Permission;
import com.xuxd.kafka.console.beans.QueryMessage;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.SendMessage;
@@ -24,16 +25,19 @@ public class MessageController {
@Autowired
private MessageService messageService;
@Permission("message:search-time")
@PostMapping("/search/time")
public Object searchByTime(@RequestBody QueryMessageDTO dto) {
return messageService.searchByTime(dto.toQueryMessage());
}
@Permission("message:search-offset")
@PostMapping("/search/offset")
public Object searchByOffset(@RequestBody QueryMessageDTO dto) {
return messageService.searchByOffset(dto.toQueryMessage());
}
@Permission("message:detail")
@PostMapping("/search/detail")
public Object searchDetail(@RequestBody QueryMessageDTO dto) {
return messageService.searchDetail(dto.toQueryMessage());
@@ -44,16 +48,19 @@ public class MessageController {
return messageService.deserializerList();
}
@Permission("message:send")
@PostMapping("/send")
public Object send(@RequestBody SendMessage message) {
return messageService.send(message);
}
@Permission("message:resend")
@PostMapping("/resend")
public Object resend(@RequestBody SendMessage message) {
return messageService.resend(message);
}
@Permission("message:del")
@DeleteMapping
public Object delete(@RequestBody List<QueryMessage> messages) {
if (CollectionUtils.isEmpty(messages)) {

View File

@@ -1,5 +1,6 @@
package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.aspect.annotation.Permission;
import com.xuxd.kafka.console.beans.TopicPartition;
import com.xuxd.kafka.console.beans.dto.BrokerThrottleDTO;
import com.xuxd.kafka.console.beans.dto.ProposedAssignmentDTO;
@@ -8,13 +9,7 @@ import com.xuxd.kafka.console.beans.dto.SyncDataDTO;
import com.xuxd.kafka.console.service.OperationService;
import org.apache.kafka.clients.admin.AdminClientConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
/**
* kafka-console-ui.
@@ -51,26 +46,31 @@ public class OperationController {
return operationService.deleteAlignmentById(id);
}
@Permission({"topic:partition-detail:preferred", "op:replication-preferred"})
@PostMapping("/replication/preferred")
public Object electPreferredLeader(@RequestBody ReplicationDTO dto) {
return operationService.electPreferredLeader(dto.getTopic(), dto.getPartition());
}
@Permission("op:config-throttle")
@PostMapping("/broker/throttle")
public Object configThrottle(@RequestBody BrokerThrottleDTO dto) {
return operationService.configThrottle(dto.getBrokerList(), dto.getUnit().toKb(dto.getThrottle()));
}
@Permission("op:remove-throttle")
@DeleteMapping("/broker/throttle")
public Object removeThrottle(@RequestBody BrokerThrottleDTO dto) {
return operationService.removeThrottle(dto.getBrokerList());
}
@Permission("op:replication-update-detail")
@GetMapping("/replication/reassignments")
public Object currentReassignments() {
return operationService.currentReassignments();
}
@Permission("op:replication-update-detail:cancel")
@DeleteMapping("/replication/reassignments")
public Object cancelReassignment(@RequestBody TopicPartition partition) {
return operationService.cancelReassignment(new org.apache.kafka.common.TopicPartition(partition.getTopic(), partition.getPartition()));

View File

@@ -1,23 +1,19 @@
package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.aspect.annotation.Permission;
import com.xuxd.kafka.console.beans.ReplicaAssignment;
import com.xuxd.kafka.console.beans.dto.AddPartitionDTO;
import com.xuxd.kafka.console.beans.dto.NewTopicDTO;
import com.xuxd.kafka.console.beans.dto.TopicThrottleDTO;
import com.xuxd.kafka.console.beans.enums.TopicType;
import com.xuxd.kafka.console.service.TopicService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* kafka-console-ui.
@@ -37,26 +33,31 @@ public class TopicController {
return topicService.getTopicNameList(false);
}
@Permission("topic:load")
@GetMapping("/list")
public Object getTopicList(@RequestParam(required = false) String topic, @RequestParam String type) {
return topicService.getTopicList(topic, TopicType.valueOf(type.toUpperCase()));
}
@Permission({"topic:batch-del", "topic:del"})
@DeleteMapping
public Object deleteTopic(@RequestBody List<String> topics) {
return topicService.deleteTopics(topics);
}
@Permission("topic:partition-detail")
@GetMapping("/partition")
public Object getTopicPartitionInfo(@RequestParam String topic) {
return topicService.getTopicPartitionInfo(topic.trim());
}
@Permission("topic:add")
@PostMapping("/new")
public Object createNewTopic(@RequestBody NewTopicDTO topicDTO) {
return topicService.createTopic(topicDTO.toNewTopic());
}
@Permission("topic:partition-add")
@PostMapping("/partition/new")
public Object addPartition(@RequestBody AddPartitionDTO partitionDTO) {
String topic = partitionDTO.getTopic().trim();
@@ -79,16 +80,19 @@ public class TopicController {
return topicService.getCurrentReplicaAssignment(topic);
}
@Permission({"topic:replication-modify", "op:replication-reassign"})
@PostMapping("/replica/assignment")
public Object updateReplicaAssignment(@RequestBody ReplicaAssignment assignment) {
return topicService.updateReplicaAssignment(assignment);
}
@Permission("topic:replication-sync-throttle")
@PostMapping("/replica/throttle")
public Object configThrottle(@RequestBody TopicThrottleDTO dto) {
return topicService.configThrottle(dto.getTopic(), dto.getPartitions(), dto.getOperation());
}
@Permission("topic:send-count")
@GetMapping("/send/stats")
public Object sendStats(@RequestParam String topic) {
return topicService.sendStats(topic);

View File

@@ -0,0 +1,97 @@
package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.aspect.annotation.ControllerLog;
import com.xuxd.kafka.console.aspect.annotation.Permission;
import com.xuxd.kafka.console.beans.Credentials;
import com.xuxd.kafka.console.beans.dto.SysPermissionDTO;
import com.xuxd.kafka.console.beans.dto.SysRoleDTO;
import com.xuxd.kafka.console.beans.dto.SysUserDTO;
import com.xuxd.kafka.console.service.UserManageService;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
/**
* @author: xuxd
* @date: 2023/4/11 21:34
**/
@RestController
@RequestMapping("/sys/user/manage")
public class UserManageController {
private final UserManageService userManageService;
public UserManageController(UserManageService userManageService) {
this.userManageService = userManageService;
}
@Permission({"user-manage:user:add", "user-manage:user:change-role", "user-manage:user:reset-pass"})
@ControllerLog("新增/更新用户")
@PostMapping("/user")
public Object addOrUpdateUser(@RequestBody SysUserDTO userDTO) {
return userManageService.addOrUpdateUser(userDTO);
}
@Permission("user-manage:role:save")
@ControllerLog("新增/更新角色")
@PostMapping("/role")
public Object addOrUpdateRole(@RequestBody SysRoleDTO roleDTO) {
return userManageService.addOrUdpateRole(roleDTO);
}
@ControllerLog("新增权限")
@PostMapping("/permission")
public Object addPermission(@RequestBody SysPermissionDTO permissionDTO) {
return userManageService.addPermission(permissionDTO);
}
@Permission("user-manage:role:save")
@ControllerLog("更新角色")
@PutMapping("/role")
public Object updateRole(@RequestBody SysRoleDTO roleDTO) {
return userManageService.updateRole(roleDTO);
}
@Permission({"user-manage:role"})
@GetMapping("/role")
public Object selectRole() {
return userManageService.selectRole();
}
@Permission({"user-manage:permission"})
@GetMapping("/permission")
public Object selectPermission() {
return userManageService.selectPermission();
}
@Permission({"user-manage:user"})
@GetMapping("/user")
public Object selectUser() {
return userManageService.selectUser();
}
@Permission("user-manage:role:del")
@ControllerLog("删除角色")
@DeleteMapping("/role")
public Object deleteRole(@RequestParam Long id) {
return userManageService.deleteRole(id);
}
@Permission("user-manage:user:del")
@ControllerLog("删除用户")
@DeleteMapping("/user")
public Object deleteUser(@RequestParam Long id) {
return userManageService.deleteUser(id);
}
@Permission("user-manage:setting")
@ControllerLog("更新密码")
@PostMapping("/user/password")
public Object updatePassword(@RequestBody SysUserDTO userDTO, HttpServletRequest request) {
Credentials credentials = (Credentials) request.getAttribute("credentials");
if (credentials != null && !credentials.isInvalid()) {
userDTO.setUsername(credentials.getUsername());
}
return userManageService.updatePassword(userDTO);
}
}

View File

@@ -0,0 +1,15 @@
package com.xuxd.kafka.console.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xuxd.kafka.console.beans.dos.SysPermissionDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 系统权限 .
*
* @author: xuxd
* @date: 2023/4/11 21:21
**/
@Mapper
public interface SysPermissionMapper extends BaseMapper<SysPermissionDO> {
}

View File

@@ -0,0 +1,13 @@
package com.xuxd.kafka.console.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xuxd.kafka.console.beans.dos.SysRoleDO;
import org.apache.ibatis.annotations.Mapper;
/**
* @author: xuxd
* @date: 2023/4/11 21:22
**/
@Mapper
public interface SysRoleMapper extends BaseMapper<SysRoleDO> {
}

View File

@@ -0,0 +1,13 @@
package com.xuxd.kafka.console.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xuxd.kafka.console.beans.dos.SysUserDO;
import org.apache.ibatis.annotations.Mapper;
/**
* @author: xuxd
* @date: 2023/4/11 21:22
**/
@Mapper
public interface SysUserMapper extends BaseMapper<SysUserDO> {
}

View File

@@ -0,0 +1,93 @@
package com.xuxd.kafka.console.dao.init;
import com.xuxd.kafka.console.beans.RolePermUpdateEvent;
import com.xuxd.kafka.console.config.AuthConfig;
import com.xuxd.kafka.console.dao.SysPermissionMapper;
import com.xuxd.kafka.console.dao.SysRoleMapper;
import com.xuxd.kafka.console.dao.SysUserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* @author: xuxd
* @date: 2023/5/17 13:10
**/
@Slf4j
@Component
public class DataInit implements SmartInitializingSingleton {
private final AuthConfig authConfig;
private final SysUserMapper userMapper;
private final SysRoleMapper roleMapper;
private final SysPermissionMapper permissionMapper;
private final DataSource dataSource;
private final SqlParse sqlParse;
private final ApplicationEventPublisher publisher;
public DataInit(AuthConfig authConfig,
SysUserMapper userMapper,
SysRoleMapper roleMapper,
SysPermissionMapper permissionMapper,
DataSource dataSource,
ApplicationEventPublisher publisher) {
this.authConfig = authConfig;
this.userMapper = userMapper;
this.roleMapper = roleMapper;
this.permissionMapper = permissionMapper;
this.dataSource = dataSource;
this.publisher = publisher;
this.sqlParse = new SqlParse();
}
@Override
public void afterSingletonsInstantiated() {
if (!authConfig.isEnable()) {
log.info("Disable login authentication, no longer try to initialize the data");
return;
}
try {
Connection connection = dataSource.getConnection();
Integer userCount = userMapper.selectCount(null);
if (userCount == null || userCount == 0) {
initData(connection, SqlParse.USER_TABLE);
}
Integer roleCount = roleMapper.selectCount(null);
if (roleCount == null || roleCount == 0) {
initData(connection, SqlParse.ROLE_TABLE);
}
Integer permCount = permissionMapper.selectCount(null);
if (permCount == null || permCount == 0) {
initData(connection, SqlParse.PERM_TABLE);
}
RolePermUpdateEvent event = new RolePermUpdateEvent(this);
event.setReload(true);
publisher.publishEvent(event);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private void initData(Connection connection, String table) throws SQLException {
log.info("Init default data for {}", table);
String sql = sqlParse.getMergeSql(table);
PreparedStatement statement = connection.prepareStatement(sql);
statement.execute();
}
}

View File

@@ -0,0 +1,85 @@
package com.xuxd.kafka.console.dao.init;
import com.google.common.io.Files;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.ResourceUtils;
import scala.collection.mutable.StringBuilder;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author: xuxd
* @date: 2023/5/17 21:22
**/
@Slf4j
public class SqlParse {
private final String FILE = "classpath:db/data-h2.sql";
private final Map<String, List<String>> sqlMap = new HashMap<>();
public static final String ROLE_TABLE = "t_sys_role";
public static final String USER_TABLE = "t_sys_user";
public static final String PERM_TABLE = "t_sys_permission";
public SqlParse() {
sqlMap.put(ROLE_TABLE, new ArrayList<>());
sqlMap.put(USER_TABLE, new ArrayList<>());
sqlMap.put(PERM_TABLE, new ArrayList<>());
String table = null;
try {
File file = ResourceUtils.getFile(FILE);
List<String> lines = Files.readLines(file, Charset.forName("UTF-8"));
for (String str : lines) {
if (StringUtils.isNotEmpty(str)) {
if (str.indexOf("start--") > 0) {
if (str.indexOf(ROLE_TABLE) > 0) {
table = ROLE_TABLE;
}
if (str.indexOf(USER_TABLE) > 0) {
table = USER_TABLE;
}
if (str.indexOf(PERM_TABLE) > 0) {
table = PERM_TABLE;
}
}
if (isSql(str)) {
if (table == null) {
log.error("Table is null, can not load sql: {}", str);
continue;
}
sqlMap.get(table).add(str);
}
}
}
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public List<String> getSqlList(String table) {
return sqlMap.get(table);
}
public String getMergeSql(String table) {
List<String> list = getSqlList(table);
StringBuilder sb = new StringBuilder();
list.forEach(sql -> sb.append(sql));
return sb.toString();
}
private boolean isSql(String str) {
return StringUtils.isNotEmpty(str) && str.startsWith("insert");
}
}

View File

@@ -1,11 +1,14 @@
package com.xuxd.kafka.console.interceptor;
package com.xuxd.kafka.console.exception;
import com.xuxd.kafka.console.beans.ResponseData;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import javax.servlet.http.HttpServletRequest;
/**
* kafka-console-ui.
@@ -17,6 +20,14 @@ import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice(basePackages = "com.xuxd.kafka.console.controller")
public class GlobalExceptionHandler {
@ResponseStatus(code = HttpStatus.FORBIDDEN)
@ExceptionHandler(value = UnAuthorizedException.class)
@ResponseBody
public Object unAuthorizedExceptionHandler(HttpServletRequest req, Exception ex) throws Exception {
log.error("unAuthorized: {}", ex.getMessage());
return ResponseData.create().failed("UnAuthorized: " + ex.getMessage());
}
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Object exceptionHandler(HttpServletRequest req, Exception ex) throws Exception {

View File

@@ -0,0 +1,12 @@
package com.xuxd.kafka.console.exception;
/**
* @author: xuxd
* @date: 2023/5/17 23:08
**/
public class UnAuthorizedException extends RuntimeException{
public UnAuthorizedException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,70 @@
package com.xuxd.kafka.console.filter;
import com.xuxd.kafka.console.beans.Credentials;
import com.xuxd.kafka.console.config.AuthConfig;
import com.xuxd.kafka.console.utils.AuthUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author: xuxd
* @date: 2023/5/9 21:20
**/
@Order(1)
@WebFilter(filterName = "auth-filter", urlPatterns = {"/*"})
@Slf4j
public class AuthFilter implements Filter {
private final AuthConfig authConfig;
private final String TOKEN_HEADER = "X-Auth-Token";
private final String AUTH_URI_PREFIX = "/auth";
public AuthFilter(AuthConfig authConfig) {
this.authConfig = authConfig;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if (!authConfig.isEnable()) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String accessToken = request.getHeader(TOKEN_HEADER);
String requestURI = request.getRequestURI();
if (requestURI.startsWith(AUTH_URI_PREFIX)) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
if (StringUtils.isEmpty(accessToken)) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return;
}
Credentials credentials = AuthUtil.parseToken(authConfig.getSecret(), accessToken);
if (credentials.isInvalid()) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return;
}
request.setAttribute("credentials", credentials);
try {
CredentialsContext.set(credentials);
filterChain.doFilter(servletRequest, servletResponse);
}finally {
CredentialsContext.remove();
}
}
}

View File

@@ -1,4 +1,4 @@
package com.xuxd.kafka.console.interceptor;
package com.xuxd.kafka.console.filter;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.dos.ClusterInfoDO;
@@ -9,6 +9,7 @@ import com.xuxd.kafka.console.utils.ConvertUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import javax.servlet.*;
@@ -24,6 +25,7 @@ import java.util.Set;
* @author xuxd
* @date 2022-01-05 19:56:25
**/
@Order(100)
@WebFilter(filterName = "context-set-filter", urlPatterns = {"/acl/*", "/user/*", "/cluster/*", "/config/*", "/consumer/*", "/message/*", "/topic/*", "/op/*", "/client/*"})
@Slf4j
public class ContextSetFilter implements Filter {

View File

@@ -0,0 +1,23 @@
package com.xuxd.kafka.console.filter;
import com.xuxd.kafka.console.beans.Credentials;
/**
* @author: xuxd
* @date: 2023/5/17 23:02
**/
public class CredentialsContext {
private static final ThreadLocal<Credentials> CREDENTIALS = new ThreadLocal<>();
public static void set(Credentials credentials) {
CREDENTIALS.set(credentials);
}
public static Credentials get() {
return CREDENTIALS.get();
}
public static void remove() {
CREDENTIALS.remove();
}
}

View File

@@ -0,0 +1,13 @@
package com.xuxd.kafka.console.service;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.dto.LoginUserDTO;
/**
* @author: xuxd
* @date: 2023/5/14 19:00
**/
public interface AuthService {
ResponseData login(LoginUserDTO userDTO);
}

View File

@@ -0,0 +1,40 @@
package com.xuxd.kafka.console.service;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.dto.SysPermissionDTO;
import com.xuxd.kafka.console.beans.dto.SysRoleDTO;
import com.xuxd.kafka.console.beans.dto.SysUserDTO;
/**
* 登录用户权限管理.
*
* @author: xuxd
* @date: 2023/4/11 21:24
**/
public interface UserManageService {
/**
* 增加权限
*/
ResponseData addPermission(SysPermissionDTO permissionDTO);
ResponseData addOrUdpateRole(SysRoleDTO roleDTO);
ResponseData addOrUpdateUser(SysUserDTO userDTO);
ResponseData selectRole();
ResponseData selectPermission();
ResponseData selectUser();
ResponseData updateUser(SysUserDTO userDTO);
ResponseData updateRole(SysRoleDTO roleDTO);
ResponseData deleteRole(Long id);
ResponseData deleteUser(Long id);
ResponseData updatePassword(SysUserDTO userDTO);
}

View File

@@ -0,0 +1,96 @@
package com.xuxd.kafka.console.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xuxd.kafka.console.beans.Credentials;
import com.xuxd.kafka.console.beans.LoginResult;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.dos.SysRoleDO;
import com.xuxd.kafka.console.beans.dos.SysUserDO;
import com.xuxd.kafka.console.beans.dto.LoginUserDTO;
import com.xuxd.kafka.console.cache.RolePermCache;
import com.xuxd.kafka.console.config.AuthConfig;
import com.xuxd.kafka.console.dao.SysRoleMapper;
import com.xuxd.kafka.console.dao.SysUserMapper;
import com.xuxd.kafka.console.service.AuthService;
import com.xuxd.kafka.console.utils.AuthUtil;
import com.xuxd.kafka.console.utils.UUIDStrUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author: xuxd
* @date: 2023/5/14 19:01
**/
@Slf4j
@Service
public class AuthServiceImpl implements AuthService {
private final SysUserMapper userMapper;
private final SysRoleMapper roleMapper;
private final AuthConfig authConfig;
private final RolePermCache rolePermCache;
public AuthServiceImpl(SysUserMapper userMapper,
SysRoleMapper roleMapper,
AuthConfig authConfig,
RolePermCache rolePermCache) {
this.userMapper = userMapper;
this.roleMapper = roleMapper;
this.authConfig = authConfig;
this.rolePermCache = rolePermCache;
}
@Override
public ResponseData login(LoginUserDTO userDTO) {
QueryWrapper<SysUserDO> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", userDTO.getUsername());
SysUserDO userDO = userMapper.selectOne(queryWrapper);
if (userDO == null) {
return ResponseData.create().failed("用户名/密码不正确");
}
String encrypt = UUIDStrUtil.generate(userDTO.getPassword(), userDO.getSalt());
if (!userDO.getPassword().equals(encrypt)) {
return ResponseData.create().failed("用户名/密码不正确");
}
Credentials credentials = new Credentials();
credentials.setUsername(userDO.getUsername());
credentials.setExpiration(System.currentTimeMillis() + authConfig.getExpireHours() * 3600 * 1000);
String token = AuthUtil.generateToken(authConfig.getSecret(), credentials);
LoginResult loginResult = new LoginResult();
List<String> permissions = new ArrayList<>();
String roleIds = userDO.getRoleIds();
if (StringUtils.isNotEmpty(roleIds)) {
List<String> roleIdList = Arrays.stream(roleIds.split(",")).map(String::trim).filter(StringUtils::isNotEmpty).collect(Collectors.toList());
roleIdList.forEach(roleId -> {
Long rId = Long.valueOf(roleId);
SysRoleDO roleDO = roleMapper.selectById(rId);
String permissionIds = roleDO.getPermissionIds();
if (StringUtils.isNotEmpty(permissionIds)) {
List<Long> permIds = Arrays.stream(permissionIds.split(",")).map(String::trim).
filter(StringUtils::isNotEmpty).map(Long::valueOf).collect(Collectors.toList());
permIds.forEach(id -> {
String permission = rolePermCache.getPermCache().get(id).getPermission();
if (StringUtils.isNotEmpty(permission)) {
permissions.add(permission);
} else {
log.error("角色:{}权限id: {},不存在", roleId, id);
}
});
}
});
}
loginResult.setToken(token);
loginResult.setPermissions(permissions);
return ResponseData.create().data(loginResult).success();
}
}

View File

@@ -0,0 +1,230 @@
package com.xuxd.kafka.console.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.RolePermUpdateEvent;
import com.xuxd.kafka.console.beans.dos.SysPermissionDO;
import com.xuxd.kafka.console.beans.dos.SysRoleDO;
import com.xuxd.kafka.console.beans.dos.SysUserDO;
import com.xuxd.kafka.console.beans.dto.SysPermissionDTO;
import com.xuxd.kafka.console.beans.dto.SysRoleDTO;
import com.xuxd.kafka.console.beans.dto.SysUserDTO;
import com.xuxd.kafka.console.beans.vo.SysPermissionVO;
import com.xuxd.kafka.console.beans.vo.SysRoleVO;
import com.xuxd.kafka.console.beans.vo.SysUserVO;
import com.xuxd.kafka.console.dao.SysPermissionMapper;
import com.xuxd.kafka.console.dao.SysRoleMapper;
import com.xuxd.kafka.console.dao.SysUserMapper;
import com.xuxd.kafka.console.service.UserManageService;
import com.xuxd.kafka.console.utils.RandomStringUtil;
import com.xuxd.kafka.console.utils.UUIDStrUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* @author: xuxd
* @date: 2023/4/11 21:24
**/
@Slf4j
@Service
public class UserManageServiceImpl implements UserManageService {
private final SysUserMapper userMapper;
private final SysRoleMapper roleMapper;
private final SysPermissionMapper permissionMapper;
private final ApplicationEventPublisher publisher;
public UserManageServiceImpl(ObjectProvider<SysUserMapper> userMapper,
ObjectProvider<SysRoleMapper> roleMapper,
ObjectProvider<SysPermissionMapper> permissionMapper,
ApplicationEventPublisher publisher) {
this.userMapper = userMapper.getIfAvailable();
this.roleMapper = roleMapper.getIfAvailable();
this.permissionMapper = permissionMapper.getIfAvailable();
this.publisher = publisher;
}
@Override
public ResponseData addPermission(SysPermissionDTO permissionDTO) {
permissionMapper.insert(permissionDTO.toSysPermissionDO());
return ResponseData.create().success();
}
@Override
public ResponseData addOrUdpateRole(SysRoleDTO roleDTO) {
SysRoleDO roleDO = roleDTO.toDO();
if (roleDO.getId() == null) {
roleMapper.insert(roleDO);
} else {
roleMapper.updateById(roleDO);
}
publisher.publishEvent(new RolePermUpdateEvent(this));
return ResponseData.create().success();
}
@Override
public ResponseData addOrUpdateUser(SysUserDTO userDTO) {
if (userDTO.getId() == null) {
if (StringUtils.isEmpty(userDTO.getPassword())) {
userDTO.setPassword(RandomStringUtil.random6Str());
}
SysUserDO userDO = userDTO.toDO();
QueryWrapper<SysUserDO> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(true, "username", userDO.getUsername());
SysUserDO exist = userMapper.selectOne(queryWrapper);
if (exist != null) {
return ResponseData.create().failed("用户已存在:" + userDO.getUsername());
}
userDO.setSalt(UUIDStrUtil.random());
userDO.setPassword(UUIDStrUtil.generate(userDTO.getPassword(), userDO.getSalt()));
userMapper.insert(userDO);
} else {
SysUserDO userDO = userMapper.selectById(userDTO.getId());
if (userDO == null) {
log.error("查不到用户: {}", userDTO.getId());
return ResponseData.create().failed("Unknown User.");
}
// 判断是否更新密码
if (userDTO.getResetPassword()) {
userDTO.setPassword(RandomStringUtil.random6Str());
userDO.setSalt(UUIDStrUtil.random());
userDO.setPassword(UUIDStrUtil.generate(userDTO.getPassword(), userDO.getSalt()));
}
userDO.setRoleIds(userDTO.getRoleIds());
userDO.setUsername(userDTO.getUsername());
userMapper.updateById(userDO);
}
return ResponseData.create().data(userDTO.getPassword()).success();
}
@Override
public ResponseData selectRole() {
List<SysRoleDO> dos = roleMapper.selectList(new QueryWrapper<>());
return ResponseData.create().data(dos.stream().map(SysRoleVO::from).collect(Collectors.toList())).success();
}
@Override
public ResponseData selectPermission() {
QueryWrapper<SysPermissionDO> queryWrapper = new QueryWrapper<>();
List<SysPermissionDO> permissionDOS = permissionMapper.selectList(queryWrapper);
List<SysPermissionVO> vos = new ArrayList<>();
Map<Long, Integer> posMap = new HashMap<>();
Map<Long, SysPermissionVO> voMap = new HashMap<>();
Iterator<SysPermissionDO> iterator = permissionDOS.iterator();
while (iterator.hasNext()) {
SysPermissionDO permissionDO = iterator.next();
if (permissionDO.getParentId() == null) {
// 菜单
SysPermissionVO vo = SysPermissionVO.from(permissionDO);
vos.add(vo);
int index = vos.size() - 1;
// 记录位置
posMap.put(permissionDO.getId(), index);
iterator.remove();
}
}
// 上面把菜单都处理过了
while (!permissionDOS.isEmpty()) {
iterator = permissionDOS.iterator();
while (iterator.hasNext()) {
SysPermissionDO permissionDO = iterator.next();
Long parentId = permissionDO.getParentId();
if (posMap.containsKey(parentId)) {
// 菜单下的按扭
SysPermissionVO vo = SysPermissionVO.from(permissionDO);
Integer index = posMap.get(parentId);
SysPermissionVO menuVO = vos.get(index);
if (menuVO.getChildren() == null) {
menuVO.setChildren(new ArrayList<>());
}
menuVO.getChildren().add(vo);
voMap.put(permissionDO.getId(), vo);
iterator.remove();
} else if (voMap.containsKey(parentId)) {
// 按钮下的按扭
SysPermissionVO vo = SysPermissionVO.from(permissionDO);
SysPermissionVO buttonVO = voMap.get(parentId);
if (buttonVO.getChildren() == null) {
buttonVO.setChildren(new ArrayList<>());
}
buttonVO.getChildren().add(vo);
voMap.put(permissionDO.getId(), vo);
iterator.remove();
}
}
}
return ResponseData.create().data(vos).success();
}
@Override
public ResponseData selectUser() {
QueryWrapper<SysUserDO> queryWrapper = new QueryWrapper<>();
List<SysUserDO> userDOS = userMapper.selectList(queryWrapper);
List<SysRoleDO> roleDOS = roleMapper.selectList(null);
Map<Long, SysRoleDO> roleDOMap = roleDOS.stream().collect(Collectors.toMap(SysRoleDO::getId, Function.identity(), (e1, e2) -> e1));
List<SysUserVO> voList = userDOS.stream().map(SysUserVO::from).collect(Collectors.toList());
voList.forEach(vo -> {
if (vo.getRoleIds() != null) {
Long roleId = Long.valueOf(vo.getRoleIds());
vo.setRoleNames(roleDOMap.containsKey(roleId) ? roleDOMap.get(roleId).getRoleName() : null);
}
});
return ResponseData.create().data(voList).success();
}
@Override
public ResponseData updateUser(SysUserDTO userDTO) {
userMapper.updateById(userDTO.toDO());
return ResponseData.create().success();
}
@Override
public ResponseData updateRole(SysRoleDTO roleDTO) {
roleMapper.updateById(roleDTO.toDO());
publisher.publishEvent(new RolePermUpdateEvent(this));
return ResponseData.create().success();
}
@Override
public ResponseData deleteRole(Long id) {
QueryWrapper<SysUserDO> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(true, "role_ids", id);
Integer count = userMapper.selectCount(queryWrapper);
if (count > 0) {
return ResponseData.create().failed("存在用户被分配为当前角色,不允许删除");
}
roleMapper.deleteById(id);
publisher.publishEvent(new RolePermUpdateEvent(this));
return ResponseData.create().success();
}
@Override
public ResponseData deleteUser(Long id) {
userMapper.deleteById(id);
return ResponseData.create().success();
}
@Override
public ResponseData updatePassword(SysUserDTO userDTO) {
SysUserDO userDO = userDTO.toDO();
userDO.setSalt(UUIDStrUtil.random());
userDO.setPassword(UUIDStrUtil.generate(userDTO.getPassword(), userDO.getSalt()));
QueryWrapper<SysUserDO> wrapper = new QueryWrapper<>();
wrapper.eq("username", userDTO.getUsername());
userMapper.update(userDO, wrapper);
return ResponseData.create().success();
}
}

View File

@@ -0,0 +1,54 @@
package com.xuxd.kafka.console.utils;
import com.google.gson.Gson;
import com.xuxd.kafka.console.beans.Credentials;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Base64Utils;
import java.nio.charset.StandardCharsets;
/**
* @author: xuxd
* @date: 2023/5/14 19:34
**/
@Slf4j
public class AuthUtil {
private static Gson gson = GsonUtil.INSTANCE.get();
public static String generateToken(String secret, Credentials info) {
String json = gson.toJson(info);
String str = json + secret;
String signature = MD5Util.md5(str);
return Base64Utils.encodeToString(json.getBytes(StandardCharsets.UTF_8)) + "." +
Base64Utils.encodeToString(signature.getBytes(StandardCharsets.UTF_8));
}
public static boolean isToken(String token) {
return token.split("\\.").length == 2;
}
public static Credentials parseToken(String secret, String token) {
if (!isToken(token)) {
return Credentials.INVALID;
}
String[] arr = token.split("\\.");
String infoStr = new String(Base64Utils.decodeFromString(arr[0]), StandardCharsets.UTF_8);
String signature = new String(Base64Utils.decodeFromString(arr[1]), StandardCharsets.UTF_8);
String encrypt = MD5Util.md5(infoStr + secret);
if (!encrypt.equals(signature)) {
return Credentials.INVALID;
}
try {
Credentials credentials = gson.fromJson(infoStr, Credentials.class);
if (credentials.getExpiration() < System.currentTimeMillis()) {
return Credentials.INVALID;
}
return credentials;
} catch (Exception e) {
log.error("解析token失败: {}", token, e);
return Credentials.INVALID;
}
}
}

View File

@@ -0,0 +1,32 @@
package com.xuxd.kafka.console.utils;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* @author: xuxd
* @date: 2023/5/14 20:25
**/
@Slf4j
public class MD5Util {
public static MessageDigest getInstance() {
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
return md5;
} catch (NoSuchAlgorithmException e) {
return null;
}
}
public static String md5(String str) {
MessageDigest digest = getInstance();
if (digest == null) {
return null;
}
return new String(digest.digest(str.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
}
}

View File

@@ -0,0 +1,26 @@
package com.xuxd.kafka.console.utils;
import java.util.Random;
/**
* @author: xuxd
* @date: 2023/5/8 9:19
**/
public class RandomStringUtil {
private final static String ALLOWED_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
public static String random6Str() {
return generateRandomString(6);
}
public static String generateRandomString(int length) {
Random random = new Random();
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
int index = random.nextInt(ALLOWED_CHARS.length());
sb.append(ALLOWED_CHARS.charAt(index));
}
return sb.toString();
}
}

View File

@@ -0,0 +1,22 @@
package com.xuxd.kafka.console.utils;
import java.util.UUID;
/**
* @author: xuxd
* @date: 2023/5/6 13:30
**/
public class UUIDStrUtil {
public static String random() {
return UUID.randomUUID().toString();
}
public static String generate(String ... strs) {
StringBuilder sb = new StringBuilder();
for (String str : strs) {
sb.append(str);
}
return UUID.nameUUIDFromBytes(sb.toString().getBytes()).toString();
}
}

View File

@@ -47,3 +47,9 @@ logging:
cron:
# clear-dirty-user: 0 * * * * ?
clear-dirty-user: 0 0 1 * * ?
# 权限认证设置设置为true需要先登录才能访问
auth:
enable: false
# 登录用户token的过期时间单位小时
expire-hours: 24

View File

@@ -1,5 +1,109 @@
-- DELETE FROM t_kafka_user;
--
-- INSERT INTO t_kafka_user (id, username, password) VALUES
-- (1, 'Jone', 'p1'),
-- (2, 'Jack', 'p2');
-- 不要随便修改下面的注释,要根据这个注释初始化加载数据
-- t_sys_permission start--
insert into t_sys_permission(id, name,type,parent_id,permission) values(0,'主页',0,null,'home');
insert into t_sys_permission(id, name,type,parent_id,permission) values(11,'集群',0,null,'cluster');
insert into t_sys_permission(id, name,type,parent_id,permission) values(12,'属性配置',1,11,'cluster:property-config');
insert into t_sys_permission(id, name,type,parent_id,permission) values(13,'日志配置',1,11,'cluster:log-config');
insert into t_sys_permission(id, name,type,parent_id,permission) values(14,'编辑配置',1,11,'cluster:edit');
insert into t_sys_permission(id, name,type,parent_id,permission) values(21,'Topic',0,null,'topic');
insert into t_sys_permission(id, name,type,parent_id,permission) values(22,'刷新',1,21,'topic:load');
insert into t_sys_permission(id, name,type,parent_id,permission) values(23,'新增',1,21,'topic:add');
insert into t_sys_permission(id, name,type,parent_id,permission) values(24,'批量删除',1,21,'topic:batch-del');
insert into t_sys_permission(id, name,type,parent_id,permission) values(25,'删除',1,21,'topic:del');
insert into t_sys_permission(id, name,type,parent_id,permission) values(26,'分区详情',1,21,'topic:partition-detail');
insert into t_sys_permission(id, name,type,parent_id,permission) values(27,'首选副本作leader',1,26,'topic:partition-detail:preferred');
insert into t_sys_permission(id, name,type,parent_id,permission) values(28,'增加分区',1,21,'topic:partition-add');
insert into t_sys_permission(id, name,type,parent_id,permission) values(29,'消费详情',1,21,'topic:consumer-detail');
insert into t_sys_permission(id, name,type,parent_id,permission) values(30,'属性配置',1,21,'topic:property-config');
insert into t_sys_permission(id, name,type,parent_id,permission) values(31,'变更副本',1,21,'topic:replication-modify');
insert into t_sys_permission(id, name,type,parent_id,permission) values(32,'发送统计',1,21,'topic:send-count');
insert into t_sys_permission(id, name,type,parent_id,permission) values(33,'限流',1,21,'topic:replication-sync-throttle');
insert into t_sys_permission(id, name,type,parent_id,permission) values(34,'编辑属性配置',1,30,'topic:property-config:edit');
insert into t_sys_permission(id, name,type,parent_id,permission) values(35,'删除属性配置',1,30,'topic:property-config:del');
insert into t_sys_permission(id, name,type,parent_id,permission) values(41,'消费组',0,null,'group');
insert into t_sys_permission(id, name,type,parent_id,permission) values(42,'新增订阅',1,41,'group:add');
insert into t_sys_permission(id, name,type,parent_id,permission) values(43,'删除',1,41,'group:del');
insert into t_sys_permission(id, name,type,parent_id,permission) values(44,'消费端',1,41,'group:client');
insert into t_sys_permission(id, name,type,parent_id,permission) values(45,'消费详情',1,41,'group:consumer-detail');
insert into t_sys_permission(id, name,type,parent_id,permission) values(46,'最小位点',1,45,'group:consumer-detail:min');
insert into t_sys_permission(id, name,type,parent_id,permission) values(47,'最新位点',1,45,'group:consumer-detail:last');
insert into t_sys_permission(id, name,type,parent_id,permission) values(48,'时间戳',1,45,'group:consumer-detail:timestamp');
insert into t_sys_permission(id, name,type,parent_id,permission) values(49,'重置位点',1,45,'group:consumer-detail:any');
insert into t_sys_permission(id, name,type,parent_id,permission) values(50,'位移分区',1,41,'group:offset-partition');
insert into t_sys_permission(id, name,type,parent_id,permission) values(61,'消息',0,null,'message');
insert into t_sys_permission(id, name,type,parent_id,permission) values(62,'根据时间查询',1,61,'message:search-time');
insert into t_sys_permission(id, name,type,parent_id,permission) values(63,'根据偏移查询',1,61,'message:search-offset');
insert into t_sys_permission(id, name,type,parent_id,permission) values(64,'在线发送',1,61,'message:send');
insert into t_sys_permission(id, name,type,parent_id,permission) values(65,'在线删除',1,61,'message:del');
insert into t_sys_permission(id, name,type,parent_id,permission) values(66,'消息详情',1,61,'message:detail');
insert into t_sys_permission(id, name,type,parent_id,permission) values(67,'重新发送',1,61,'message:resend');
insert into t_sys_permission(id, name,type,parent_id,permission) values(80,'限流',0,null,'quota');
insert into t_sys_permission(id, name,type,parent_id,permission) values(81,'用户',1,80,'quota:user');
insert into t_sys_permission(id, name,type,parent_id,permission) values(82,'新增配置',1,81,'quota:user:add');
insert into t_sys_permission(id, name,type,parent_id,permission) values(83,'客户端ID',1,80,'quota:client');
insert into t_sys_permission(id, name,type,parent_id,permission) values(84,'新增配置',1,83,'quota:client:add');
insert into t_sys_permission(id, name,type,parent_id,permission) values(85,'用户和客户端ID',1,80,'quota:user-client');
insert into t_sys_permission(id, name,type,parent_id,permission) values(86,'新增配置',1,85,'quota:user-client:add');
insert into t_sys_permission(id, name,type,parent_id,permission) values(87,'删除',1,80,'quota:del');
insert into t_sys_permission(id, name,type,parent_id,permission) values(88,'修改',1,80,'quota:edit');
insert into t_sys_permission(id, name,type,parent_id,permission) values(100,'Acl',0,null,'acl');
insert into t_sys_permission(id, name,type,parent_id,permission) values(101,'资源授权',1,100,'acl:authority');
insert into t_sys_permission(id, name,type,parent_id,permission) values(102,'新增主体权限',1,101,'acl:authority:add-principal');
insert into t_sys_permission(id, name,type,parent_id,permission) values(103,'权限详情',1,101,'acl:authority:detail');
insert into t_sys_permission(id, name,type,parent_id,permission) values(104,'管理生产权限',1,101,'acl:authority:producer');
insert into t_sys_permission(id, name,type,parent_id,permission) values(105,'管理消费权限',1,101,'acl:authority:consumer');
insert into t_sys_permission(id, name,type,parent_id,permission) values(106,'增加权限',1,101,'acl:authority:add');
insert into t_sys_permission(id, name,type,parent_id,permission) values(107,'清除权限',1,101,'acl:authority:clean');
insert into t_sys_permission(id, name,type,parent_id,permission) values(108,'SaslScram用户管理',1,100,'acl:sasl-scram');
insert into t_sys_permission(id, name,type,parent_id,permission) values(109,'新增/更新用户',1,108,'acl:sasl-scram:add-update');
insert into t_sys_permission(id, name,type,parent_id,permission) values(110,'详情',1,108,'acl:sasl-scram:detail');
insert into t_sys_permission(id, name,type,parent_id,permission) values(111,'删除',1,108,'acl:sasl-scram:del');
insert into t_sys_permission(id, name,type,parent_id,permission) values(112,'管理生产权限',1,108,'acl:sasl-scram:producer');
insert into t_sys_permission(id, name,type,parent_id,permission) values(113,'管理消费权限',1,108,'acl:sasl-scram:consumer');
insert into t_sys_permission(id, name,type,parent_id,permission) values(114,'增加权限',1,108,'acl:sasl-scram:add-auth');
insert into t_sys_permission(id, name,type,parent_id,permission) values(115,'彻底删除',1,108,'acl:sasl-scram:pure');
insert into t_sys_permission(id, name,type,parent_id,permission) values(140,'用户',0,null,'user-manage');
insert into t_sys_permission(id, name,type,parent_id,permission) values(141,'用户列表',1,140,'user-manage:user');
insert into t_sys_permission(id, name,type,parent_id,permission) values(142,'新增',1,141,'user-manage:user:add');
insert into t_sys_permission(id, name,type,parent_id,permission) values(143,'删除',1,141,'user-manage:user:del');
insert into t_sys_permission(id, name,type,parent_id,permission) values(144,'重置密码',1,141,'user-manage:user:reset-pass');
insert into t_sys_permission(id, name,type,parent_id,permission) values(145,'分配角色',1,141,'user-manage:user:change-role');
insert into t_sys_permission(id, name,type,parent_id,permission) values(146,'角色列表',1,140,'user-manage:role');
insert into t_sys_permission(id, name,type,parent_id,permission) values(147,'保存',1,146,'user-manage:role:save');
insert into t_sys_permission(id, name,type,parent_id,permission) values(148,'删除',1,146,'user-manage:role:del');
insert into t_sys_permission(id, name,type,parent_id,permission) values(149,'权限列表',1,140,'user-manage:permission');
insert into t_sys_permission(id, name,type,parent_id,permission) values(150,'个人设置',1,140,'user-manage:setting');
insert into t_sys_permission(id, name,type,parent_id,permission) values(160,'运维',0,null,'op');
insert into t_sys_permission(id, name,type,parent_id,permission) values(161,'集群切换',1,160,'op:cluster-switch');
insert into t_sys_permission(id, name,type,parent_id,permission) values(162,'新增集群',1,161,'op:cluster-switch:add');
insert into t_sys_permission(id, name,type,parent_id,permission) values(163,'切换',1,161,'op:cluster-switch:switch');
insert into t_sys_permission(id, name,type,parent_id,permission) values(164,'编辑',1,161,'op:cluster-switch:edit');
insert into t_sys_permission(id, name,type,parent_id,permission) values(165,'删除',1,161,'op:cluster-switch:del');
insert into t_sys_permission(id, name,type,parent_id,permission) values(166,'配置限流',1,160,'op:config-throttle');
insert into t_sys_permission(id, name,type,parent_id,permission) values(167,'解除限流',1,160,'op:remove-throttle');
insert into t_sys_permission(id, name,type,parent_id,permission) values(168,'首选副本作leader',1,160,'op:replication-preferred');
insert into t_sys_permission(id, name,type,parent_id,permission) values(169,'副本变更详情',1,160,'op:replication-update-detail');
insert into t_sys_permission(id, name,type,parent_id,permission) values(170,'副本重分配',1,160,'op:replication-reassign');
insert into t_sys_permission(id, name,type,parent_id,permission) values(171,'取消副本重分配',1,169,'op:replication-update-detail:cancel');
-- t_sys_permission end--
-- t_sys_role start--
insert into t_sys_role(id, role_name, description, permission_ids) VALUES (1,'超级管理员','超级管理员','12,13,14,22,23,24,25,26,27,28,29,30,34,35,31,32,33,42,43,44,45,46,47,48,49,50,62,63,64,65,66,67,81,82,83,84,85,86,87,88,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,141,142,143,144,145,146,147,148,149,150,161,162,163,164,165,166,167,168,169,171,170');
insert into t_sys_role(id, role_name, description, permission_ids) VALUES (2,'普通管理员','普通管理员,不能更改用户信息','12,13,14,22,23,24,25,26,27,28,29,30,34,35,31,32,33,42,43,44,45,46,47,48,49,50,62,63,64,65,66,67,81,82,83,84,85,86,87,88,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,141,146,149,150,161,162,163,164,165,166,167,168,169,171,170');
-- insert into t_sys_role(id, role_name, description, permission_ids) VALUES (2,'访客','访客','12,13,22,26,29,32,44,45,50,62,63,81,83,85,141,146,149,150,161,163');
-- t_sys_role end--
-- t_sys_user start--
insert into t_sys_user(id, username, password, salt, role_ids) VALUES (1,'super-admin','3a3e4d32-5247-321b-9efb-9cbf60b2bf6c','e6973cfc-7583-4baa-8802-65ded1268ab6','1' );
insert into t_sys_user(id, username, password, salt, role_ids) VALUES (2,'admin','3a3e4d32-5247-321b-9efb-9cbf60b2bf6c','e6973cfc-7583-4baa-8802-65ded1268ab6','2' );
-- t_sys_user end--

View File

@@ -29,9 +29,40 @@ CREATE TABLE IF NOT EXISTS T_CLUSTER_INFO
(
ID IDENTITY NOT NULL COMMENT '主键ID',
CLUSTER_NAME VARCHAR(128) NOT NULL DEFAULT '' COMMENT '集群名',
ADDRESS VARCHAR(256) NOT NULL DEFAULT '' COMMENT '集群地址',
PROPERTIES VARCHAR(512) NOT NULL DEFAULT '' COMMENT '集群的其它属性配置',
ADDRESS VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '集群地址',
PROPERTIES VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '集群的其它属性配置',
UPDATE_TIME TIMESTAMP NOT NULL DEFAULT NOW() COMMENT '更新时间',
PRIMARY KEY (ID),
UNIQUE (CLUSTER_NAME)
);
-- 登录用户的角色权限配置
CREATE TABLE IF NOT EXISTS t_sys_permission
(
ID IDENTITY NOT NULL COMMENT '主键ID',
name varchar(100) DEFAULT NULL COMMENT '权限名称',
type tinyint(1) NOT NULL DEFAULT 0 COMMENT '权限类型: 0菜单1按钮',
parent_id bigint(20) DEFAULT NULL COMMENT '所属父权限ID',
permission varchar(100) DEFAULT NULL COMMENT '权限字符串',
PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS t_sys_role
(
ID IDENTITY NOT NULL COMMENT '主键ID',
role_name varchar(100) NOT NULL COMMENT '角色名称',
description varchar(100) DEFAULT NULL COMMENT '角色描述',
permission_ids varchar(500) DEFAULT NULL COMMENT '分配的权限ID',
PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS t_sys_user
(
ID IDENTITY NOT NULL COMMENT '主键ID',
username varchar(100) DEFAULT NULL COMMENT '用户名',
password varchar(100) DEFAULT NULL COMMENT '用户密码',
salt varchar(100) DEFAULT NULL COMMENT '加密的盐值',
role_ids varchar(100) DEFAULT NULL COMMENT '分配角色的ID',
PRIMARY KEY (id),
UNIQUE (username)
);

View File

@@ -3,9 +3,9 @@
<springProperty scope="context" name="LOGGING_HOME" source="logging.home"/>
<!-- 日志目录 -->
<property name="LOG_HOME" value="${LOGGING_HOME}/logs"/>
<property name="LOG_HOME" value="${LOGGING_HOME:-${user.dir}}/logs"/>
<!-- 日志文件名-->
<property name="APP_NAME" value="${APPLICATION_NAME:-.}"/>
<property name="APP_NAME" value="${APPLICATION_NAME:-kafka-console-ui}"/>
<!-- 使用默认的输出格式-->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>

View File

@@ -15,20 +15,46 @@
><router-link to="/client-quota-page" class="pad-l-r">限流</router-link>
<span>|</span
><router-link to="/acl-page" class="pad-l-r">Acl</router-link>
<span v-show="showUserMenu">|</span
><router-link to="/user-page" class="pad-l-r" v-show="showUserMenu"
>用户</router-link
>
<span>|</span
><router-link to="/op-page" class="pad-l-r">运维</router-link>
<span class="right">集群{{ clusterName }}</span>
<div class="right">
<span>集群{{ clusterName }} </span>
<a-dropdown v-show="showUsername">
<span>
<span> | </span><a-icon type="smile"></a-icon>
<span>{{ username }}</span>
</span>
<a-menu slot="overlay">
<a-menu-item key="1">
<a href="javascript:;" @click="logout">
<!-- <a-icon type="logout"/>-->
<span>退出</span>
</a>
</a-menu-item>
</a-menu>
</a-dropdown>
</div>
</div>
<router-view class="content" />
</div>
</template>
<script>
import { KafkaClusterApi } from "@/utils/api";
import { KafkaClusterApi, AuthApi } from "@/utils/api";
import request from "@/utils/request";
import { mapMutations, mapState } from "vuex";
import { getClusterInfo } from "@/utils/local-cache";
import {
deleteToken,
deleteUsername,
getClusterInfo,
getPermissions,
getUsername,
} from "@/utils/local-cache";
import notification from "ant-design-vue/lib/notification";
import { CLUSTER } from "@/store/mutation-types";
import { AUTH, CLUSTER } from "@/store/mutation-types";
export default {
data() {
@@ -37,35 +63,69 @@ export default {
};
},
created() {
const clusterInfo = getClusterInfo();
if (!clusterInfo) {
request({
url: KafkaClusterApi.peekClusterInfo.url,
method: KafkaClusterApi.peekClusterInfo.method,
}).then((res) => {
if (res.code == 0) {
this.switchCluster(res.data);
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
} else {
this.switchCluster(clusterInfo);
}
this.intAuthState();
this.initClusterInfo();
},
computed: {
...mapState({
clusterName: (state) => state.clusterInfo.clusterName,
enableSasl: (state) => state.clusterInfo.enableSasl,
showUsername: (state) => state.auth.enable && state.auth.username,
username: (state) => state.auth.username,
showUserMenu: (state) => state.auth.enable,
}),
},
methods: {
...mapMutations({
switchCluster: CLUSTER.SWITCH,
enableAuth: AUTH.ENABLE,
setUsername: AUTH.SET_USERNAME,
setPermissions: AUTH.SET_PERMISSIONS,
}),
beforeLoadFn() {
this.setUsername(getUsername());
this.setPermissions(getPermissions());
},
intAuthState() {
request({
url: AuthApi.enable.url,
method: AuthApi.enable.method,
}).then((res) => {
const enable = res;
this.enableAuth(enable);
// if (!enable){
// this.initClusterInfo();
// }
});
},
initClusterInfo() {
const clusterInfo = getClusterInfo();
if (!clusterInfo) {
request({
url: KafkaClusterApi.peekClusterInfo.url,
method: KafkaClusterApi.peekClusterInfo.method,
}).then((res) => {
if (res.code == 0) {
this.switchCluster(res.data);
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
} else {
this.switchCluster(clusterInfo);
}
},
logout() {
deleteToken();
deleteUsername();
this.$router.push("/login-page");
},
},
mounted() {
this.beforeLoadFn();
},
};
</script>

View File

@@ -1,32 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String,
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@@ -0,0 +1,40 @@
<template>
<a-modal
title="消息提示"
:visible="visible"
:width="400"
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
@cancel="handleCancel"
>
<div>
{{ message }}
</div>
</a-modal>
</template>
<script>
export default {
name: "MessageBox",
props: {
message: {
type: String,
default: "",
},
visible: {
type: Boolean,
default: false,
},
},
methods: {
handleCancel() {
this.visible = false;
this.$emit("closeMessageBox", {});
},
},
};
</script>
<style scoped></style>

View File

@@ -0,0 +1,16 @@
import Vue from "vue";
import Store from "@/store";
const action = Vue.directive("action", {
inserted: function (el, binding) {
const actionName = binding.arg;
const enableAuth = Store.state.auth.enable;
const permissions = Store.state.auth.permissions;
if (enableAuth && (!permissions || permissions.indexOf(actionName) < 0)) {
(el.parentNode && el.parentNode.removeChild(el)) ||
(el.style.display = "none");
}
},
});
export default action;

View File

@@ -6,6 +6,7 @@ import store from "./store";
import Antd from "ant-design-vue";
import "ant-design-vue/dist/antd.css";
import { VueAxios } from "./utils/request";
import "@/directives/action";
Vue.config.productionTip = false;
Vue.use(Antd);

View File

@@ -1,6 +1,7 @@
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";
import Store from "@/store";
Vue.use(VueRouter);
@@ -53,7 +54,21 @@ const routes = [
path: "/client-quota-page",
name: "ClientQuota",
component: () =>
import(/* webpackChunkName: "cluster" */ "../views/quota/ClientQuota.vue"),
import(
/* webpackChunkName: "cluster" */ "../views/quota/ClientQuota.vue"
),
},
{
path: "/user-page",
name: "UserManage",
component: () =>
import(/* webpackChunkName: "cluster" */ "../views/user/UserManage.vue"),
},
{
path: "/login-page",
name: "Login",
component: () =>
import(/* webpackChunkName: "cluster" */ "../views/login/Login.vue"),
},
];
@@ -64,4 +79,49 @@ const router = new VueRouter({
routes,
});
router.beforeEach((to, from, next) => {
const enableAuth = Store.state.auth.enable;
if (!enableAuth) {
next();
} else {
if (to.path === "/login-page") {
next();
} else {
let token = localStorage.getItem("access_token");
if (token === null || token === "") {
next("/login-page");
} else {
next();
}
}
}
});
let originPush = VueRouter.prototype.push;
let originReplace = VueRouter.prototype.replace;
VueRouter.prototype.push = function (location, resolve, reject) {
if (resolve && reject) {
originPush.call(this, location, resolve, reject);
} else {
originPush.call(
this,
location,
() => {},
() => {}
);
}
};
VueRouter.prototype.replace = function (location, resolve, reject) {
if (resolve && reject) {
originReplace.call(this, location, resolve, reject);
} else {
originReplace.call(
this,
location,
() => {},
() => {}
);
}
};
export default router;

View File

@@ -1,7 +1,12 @@
import Vue from "vue";
import Vuex from "vuex";
import { CLUSTER } from "@/store/mutation-types";
import { setClusterInfo } from "@/utils/local-cache";
import { CLUSTER, AUTH } from "@/store/mutation-types";
import {
setClusterInfo,
setPermissions,
setToken,
setUsername,
} from "@/utils/local-cache";
Vue.use(Vuex);
@@ -12,6 +17,11 @@ export default new Vuex.Store({
clusterName: undefined,
enableSasl: false,
},
auth: {
enable: false,
username: "",
permissions: [],
},
},
mutations: {
[CLUSTER.SWITCH](state, clusterInfo) {
@@ -28,6 +38,20 @@ export default new Vuex.Store({
state.clusterInfo.enableSasl = enableSasl;
setClusterInfo(clusterInfo);
},
[AUTH.ENABLE](state, enable) {
state.auth.enable = enable;
},
[AUTH.SET_TOKEN](state, info) {
setToken(info);
},
[AUTH.SET_USERNAME](state, username) {
setUsername(username);
state.auth.username = username;
},
[AUTH.SET_PERMISSIONS](state, permissions) {
setPermissions(permissions);
state.auth.permissions = permissions;
},
},
actions: {},
modules: {},

View File

@@ -1,3 +1,10 @@
export const CLUSTER = {
SWITCH: "switchCluster",
};
export const AUTH = {
ENABLE: "enable",
SET_TOKEN: "setToken",
SET_USERNAME: "setUsername",
SET_PERMISSIONS: "setPermissions",
};

View File

@@ -307,4 +307,54 @@ export const KafkaClientQuotaApi = {
url: "/client/quota",
method: "delete",
},
};
};
export const UserManageApi = {
getPermissions: {
url: "/sys/user/manage/permission",
method: "get",
},
addPermission: {
url: "/sys/user/manage/permission",
method: "post",
},
getRole: {
url: "/sys/user/manage/role",
method: "get",
},
addOrUpdateRole: {
url: "/sys/user/manage/role",
method: "post",
},
deleteRole: {
url: "/sys/user/manage/role",
method: "delete",
},
getUsers: {
url: "/sys/user/manage/user",
method: "get",
},
addOrUpdateUser: {
url: "/sys/user/manage/user",
method: "post",
},
deleteUser: {
url: "/sys/user/manage/user",
method: "delete",
},
updatePassword: {
url: "/sys/user/manage/user/password",
method: "post",
},
};
export const AuthApi = {
enable: {
url: "/auth/enable",
method: "get",
},
login: {
url: "/auth/login",
method: "post",
},
};

16
ui/src/utils/auth.js Normal file
View File

@@ -0,0 +1,16 @@
import Store from "@/store";
export function isUnauthorized(permission) {
const enableAuth = Store.state.auth.enable;
const permissions = Store.state.auth.permissions;
return enableAuth && (!permissions || permissions.indexOf(permission) < 0);
}
export function isAuthorized(permission) {
const enableAuth = Store.state.auth.enable;
if (!enableAuth) {
return true;
}
const permissions = Store.state.auth.permissions;
return permissions && permissions.indexOf(permission) >= 0;
}

View File

@@ -4,4 +4,9 @@ export const ConstantEvent = {
export const Cache = {
clusterInfo: "clusterInfo",
auth: "auth",
token: "access_token",
username: "login_user",
enableAuth: "enable_auth",
permissions: "permissions",
};

View File

@@ -8,3 +8,43 @@ export function getClusterInfo() {
const str = localStorage.getItem(Cache.clusterInfo);
return str ? JSON.parse(str) : undefined;
}
// export function setAuth(auth) {
// localStorage.setItem(Cache.auth, JSON.stringify(auth));
// }
export function setToken(token) {
localStorage.setItem(Cache.token, token);
}
export function getToken() {
return localStorage.getItem(Cache.token);
}
export function deleteToken() {
localStorage.removeItem(Cache.token);
}
export function deleteUsername() {
localStorage.removeItem(Cache.username);
}
export function setUsername(username) {
localStorage.setItem(Cache.username, username);
}
export function getUsername() {
return localStorage.getItem(Cache.username);
}
export function setPermissions(permissions) {
localStorage.setItem(Cache.permissions, permissions);
}
export function getPermissions() {
return localStorage.getItem(Cache.permissions);
}
// export function setEnableAuth(enable) {
// localStorage.setItem()
// }

View File

@@ -2,7 +2,7 @@ import axios from "axios";
import notification from "ant-design-vue/es/notification";
import { VueAxios } from "./axios";
import { getClusterInfo } from "@/utils/local-cache";
import Router from "@/router";
// 创建 axios 实例
const request = axios.create({
// API 请求的默认前缀
@@ -10,14 +10,30 @@ const request = axios.create({
timeout: 120000, // 请求超时时间
});
// axios.defaults.headers.common['X-Auth-Token'] = localStorage.getItem('access_token');
// 异常拦截处理器
const errorHandler = (error) => {
if (error.response) {
const data = error.response.data;
notification.error({
message: error.response.status,
description: JSON.stringify(data),
});
if (error.response.status == 401) {
notification.error({
message: error.response.status,
description: "请登录",
});
Router.push({ path: "/login-page" });
} else if (error.response.status == 403) {
// const data = error.response.data;
// notification.error({
// message: error.response.status,
// description: data.msg,
// });
} else {
const data = error.response.data;
notification.error({
message: error.response.status,
description: JSON.stringify(data),
});
}
}
return Promise.reject(error);
};
@@ -29,6 +45,10 @@ request.interceptors.request.use((config) => {
config.headers["X-Cluster-Info-Id"] = clusterInfo.id;
// config.headers["X-Cluster-Info-Name"] = encodeURIComponent(clusterInfo.clusterName);
}
const token = localStorage.getItem("access_token");
if (token) {
config.headers["X-Auth-Token"] = token;
}
return config;
}, errorHandler);

View File

@@ -1,10 +1,14 @@
<template>
<div class="content">
<a-tabs default-active-key="1" size="large" tabPosition="top">
<a-tab-pane key="1" tab="资源授权">
<a-tab-pane key="1" tab="资源授权" v-if="isAuthorized('acl:authority')">
<acl-list></acl-list>
</a-tab-pane>
<a-tab-pane key="2" tab="SaslScram用户管理">
<a-tab-pane
key="2"
tab="SaslScram用户管理"
v-if="isAuthorized('acl:sasl-scram')"
>
<sasl-scram></sasl-scram>
</a-tab-pane>
</a-tabs>
@@ -14,9 +18,11 @@
<script>
import AclList from "@/views/acl/AclList";
import SaslScram from "@/views/acl/SaslScram";
import { isAuthorized } from "@/utils/auth";
export default {
name: "Acl",
methods: { isAuthorized },
components: {
AclList,
SaslScram,

View File

@@ -27,6 +27,7 @@
ok-text="确认"
cancel-text="取消"
@confirm="onDelete(record)"
v-action:acl:authority:clean
>
<a-button>删除</a-button>
</a-popconfirm>

View File

@@ -47,7 +47,10 @@
</a-form>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="onAddPrincipalAuth"
<a-button
type="primary"
@click="onAddPrincipalAuth"
v-action:acl:authority:add-principal
>新增主体权限</a-button
>
<span v-show="hint != ''" class="hint"

View File

@@ -27,7 +27,12 @@
</a-form>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="updateUser">新增/更新用户</a-button>
<a-button
type="primary"
@click="updateUser"
v-action:acl:sasl-scram:add-update
>新增/更新用户</a-button
>
<span class="hint" v-show="!enableSasl"
>未启用SASL SCRAM认证不支持相关操作</span
>
@@ -45,6 +50,7 @@
type="dashed"
style="float: right"
@click="onUserDetail(username)"
v-action:acl:sasl-scram:detail
>详情</a-button
>
</div>
@@ -58,6 +64,7 @@
ok-text="确认"
cancel-text="取消"
@confirm="onDeleteUser(record)"
v-action:acl:sasl-scram:del
>
<a-button size="small" href="javascript:;" class="operation-btn"
>删除</a-button
@@ -68,6 +75,7 @@
href="javascript:;"
class="operation-btn"
@click="onManageProducerAuth(record)"
v-action:acl:sasl-scram:producer
>管理生产权限
</a-button>
@@ -76,6 +84,7 @@
href="javascript:;"
class="operation-btn"
@click="onManageConsumerAuth(record)"
v-action:acl:sasl-scram:consumer
>管理消费权限
</a-button>
<a-button
@@ -83,6 +92,7 @@
href="javascript:;"
class="operation-btn"
@click="onAddAuth(record)"
v-action:acl:sasl-scram:add-auth
>增加权限
</a-button>
<a-popconfirm
@@ -91,7 +101,11 @@
cancel-text="取消"
@confirm="onDeleteUserAndAuth(record)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
<a-button
size="small"
href="javascript:;"
class="operation-btn"
v-action:acl:sasl-scram:pure
>彻底删除</a-button
>
</a-popconfirm>

View File

@@ -35,6 +35,7 @@
class="operation-btn"
v-show="!record.readOnly"
@click="openEditConfigDialog(record)"
v-action:cluster:edit
>编辑
</a-button>
<a-popconfirm
@@ -44,7 +45,11 @@
v-show="isDynamic(record.source)"
@confirm="deleteBrokerConfig(record)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
<a-button
size="small"
href="javascript:;"
class="operation-btn"
v-action:cluster:edit
>删除
</a-button>
</a-popconfirm>

View File

@@ -19,6 +19,7 @@
href="javascript:;"
class="operation-btn"
@click="openBrokerConfigDialog(record, false)"
v-action:cluster:property-config
>属性配置
</a-button>
<a-button
@@ -26,6 +27,7 @@
href="javascript:;"
class="operation-btn"
@click="openBrokerConfigDialog(record, true)"
v-action:cluster:log-config
>日志配置
</a-button>
</div>

View File

@@ -23,7 +23,11 @@
cancel-text="取消"
@confirm="resetTopicOffsetToEndpoint(group, k, 1)"
>
<a-button size="small" type="danger" style="margin-right: 1%"
<a-button
size="small"
type="danger"
style="margin-right: 1%"
v-action:group:consumer-detail:min
>最小位点
</a-button>
</a-popconfirm>
@@ -35,7 +39,11 @@
cancel-text="取消"
@confirm="resetTopicOffsetToEndpoint(group, k, 2)"
>
<a-button size="small" type="danger" style="margin-right: 1%"
<a-button
size="small"
type="danger"
style="margin-right: 1%"
v-action:group:consumer-detail:last
>最新位点
</a-button>
</a-popconfirm>
@@ -45,6 +53,7 @@
type="danger"
style="margin-right: 1%"
@click="openResetOffsetByTimeDialog(k)"
v-action:group:consumer-detail:timestamp
>时间戳
</a-button>
<a-button
@@ -75,6 +84,7 @@
@click="
openResetPartitionOffsetDialog(record.topic, record.partition)
"
v-action:group:consumer-detail:any
>重置位点
</a-button>
</div>

View File

@@ -61,7 +61,10 @@
</a-form>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="openAddSubscriptionDialog"
<a-button
type="primary"
@click="openAddSubscriptionDialog"
v-action:group:add
>新增订阅</a-button
>
</div>
@@ -89,7 +92,11 @@
cancel-text="取消"
@confirm="deleteGroup(record.groupId)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
<a-button
size="small"
href="javascript:;"
class="operation-btn"
v-action:group:del
>删除
</a-button>
</a-popconfirm>
@@ -98,6 +105,7 @@
href="javascript:;"
class="operation-btn"
@click="openConsumerMemberDialog(record.groupId)"
v-action:group:client
>消费端
</a-button>
<a-button
@@ -105,6 +113,7 @@
href="javascript:;"
class="operation-btn"
@click="openConsumerDetailDialog(record.groupId)"
v-action:group:consumer-detail
>消费详情
</a-button>
<a-button
@@ -112,6 +121,7 @@
href="javascript:;"
class="operation-btn"
@click="openOffsetPartitionDialog(record.groupId)"
v-action:group:offset-partition
>位移分区
</a-button>
</div>

View File

@@ -0,0 +1,107 @@
<template>
<a-form
:form="form"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 12 }"
@submit="handleSubmit"
class="login-box"
>
<h3 class="login-title">登录kafka-console-ui</h3>
<a-form-item label="账号">
<a-input
style="width: 200px"
allowClear
v-decorator="[
'username',
{ rules: [{ required: true, message: '请输入账号' }] },
]"
>
</a-input>
</a-form-item>
<a-form-item label="密码">
<a-input-password
style="width: 200px"
v-decorator="[
'password',
{ rules: [{ required: true, message: '请输入密码' }] },
]"
/>
</a-form-item>
<a-form-item :wrapper-col="{ span: 16, offset: 5 }">
<a-button type="primary" @click="handleSubmit" :loading="loading"
>登录</a-button
>
</a-form-item>
</a-form>
</template>
<script>
import request from "@/utils/request";
import { AuthApi } from "@/utils/api";
import notification from "ant-design-vue/lib/notification";
import { mapMutations } from "vuex";
import { AUTH } from "@/store/mutation-types";
export default {
name: "Login",
data() {
return {
form: this.$form.createForm(this, { name: "login-form" }),
loading: false,
};
},
methods: {
handleSubmit(e) {
e.preventDefault();
this.form.validateFields((err, values) => {
if (!err) {
this.loading = true;
const params = Object.assign({}, values);
request({
url: AuthApi.login.url,
method: AuthApi.login.method,
data: params,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.setToken(res.data.token);
this.setUsername(params.username);
this.setPermissions(res.data.permissions);
this.$router.push("/");
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
}
});
},
...mapMutations({
setToken: AUTH.SET_TOKEN,
setUsername: AUTH.SET_USERNAME,
setPermissions: AUTH.SET_PERMISSIONS,
}),
},
};
</script>
<style scoped>
.login-box {
border: 1px solid #dcdfe6;
width: 350px;
height: 300px;
margin: 120px auto;
padding: 35px 35px 15px 35px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
box-shadow: 0 0 25px #909399;
}
.login-title {
text-align: center;
margin: 0 auto 40px auto;
color: #303133;
}
</style>

View File

@@ -2,16 +2,25 @@
<div class="content">
<a-spin :spinning="loading">
<a-tabs default-active-key="1" size="large" tabPosition="top">
<a-tab-pane key="1" tab="根据时间查询消息">
<a-tab-pane
v-if="isAuthorized('message:search-time')"
key="1"
tab="根据时间查询消息"
>
<SearchByTime :topic-list="topicList"></SearchByTime>
</a-tab-pane>
<a-tab-pane key="2" tab="根据偏移查询消息">
<a-tab-pane
key="2"
v-if="isAuthorized('message:search-offset')"
tab="根据偏移查询消息"
>
<SearchByOffset :topic-list="topicList"></SearchByOffset>
</a-tab-pane>
<a-tab-pane key="3" tab="在线发送">
<a-tab-pane key="3" tab="在线发送" v-if="isAuthorized('message:send')">
<SendMessage :topic-list="topicList"></SendMessage>
</a-tab-pane>
<a-tab-pane key="4" tab="在线删除">
<a-tab-pane key="4" tab="在线删除" v-if="isAuthorized('message:del')">
<DeleteMessage :topic-list="topicList"></DeleteMessage>
</a-tab-pane>
</a-tabs>
@@ -27,6 +36,7 @@ import { KafkaTopicApi } from "@/utils/api";
import notification from "ant-design-vue/lib/notification";
import SendMessage from "@/views/message/SendMessage";
import DeleteMessage from "./DeleteMessage";
import { isAuthorized, isUnauthorized } from "@/utils/auth";
export default {
name: "Message",
components: { DeleteMessage, SearchByTime, SearchByOffset, SendMessage },
@@ -37,6 +47,8 @@ export default {
};
},
methods: {
isAuthorized,
isUnauthorized,
getTopicNameList() {
request({
url: KafkaTopicApi.getTopicNameList.url,

View File

@@ -116,7 +116,9 @@
cancel-text="取消"
@confirm="resend"
>
<a-button type="primary" icon="reload"> 重新发送 </a-button>
<a-button type="primary" icon="reload" v-action:message:resend>
重新发送
</a-button>
</a-popconfirm>
</div>
</a-spin>

View File

@@ -17,6 +17,7 @@
href="javascript:;"
class="operation-btn"
@click="openDetailDialog(record)"
v-action:message:detail
>消息详情
</a-button>
</div>

View File

@@ -17,6 +17,7 @@
href="javascript:;"
class="operation-btn"
@click="openAddClusterInfoDialog"
v-action:op:cluster-switch:add
>新增集群
</a-button>
<br /><br />
@@ -38,6 +39,7 @@
href="javascript:;"
class="operation-btn"
@click="switchCluster(record)"
v-action:op:cluster-switch:switch
>切换
</a-button>
<a-button
@@ -45,6 +47,7 @@
href="javascript:;"
class="operation-btn"
@click="openUpdateClusterInfoDialog(record)"
v-action:op:cluster-switch:edit
>编辑
</a-button>
<a-popconfirm
@@ -58,6 +61,7 @@
href="javascript:;"
class="operation-btn"
type="danger"
v-action:op:cluster-switch:del
>删除
</a-button>
</a-popconfirm>

View File

@@ -39,7 +39,11 @@
cancel-text="取消"
@confirm="cancelReassignment(record)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
<a-button
size="small"
href="javascript:;"
class="operation-btn"
v-action:op:replication-update-detail:cancel
>取消
</a-button>
</a-popconfirm>

View File

@@ -2,7 +2,7 @@
<div class="content">
<div class="content-module">
<a-card title="集群管理" style="width: 100%; text-align: left">
<p>
<p v-action:op:cluster-switch>
<a-button type="primary" @click="openClusterInfoDialog">
集群切换
</a-button>
@@ -15,7 +15,7 @@
</div>
<div class="content-module">
<a-card title="Broker管理" style="width: 100%; text-align: left">
<p>
<p v-action:op:config-throttle>
<a-button type="primary" @click="openConfigThrottleDialog">
配置限流
</a-button>
@@ -24,7 +24,7 @@
>设置指定broker上的topic的副本之间数据同步占用的带宽这个设置是broker级别的但是设置后还要去对应的topic上进行限流配置指定对这个topic的相关副本进行限制</span
>
</p>
<p>
<p v-action:op:remove-throttle>
<a-button type="primary" @click="openRemoveThrottleDialog">
解除限流
</a-button>
@@ -35,21 +35,21 @@
</div>
<div class="content-module">
<a-card title="副本管理" style="width: 100%; text-align: left">
<p>
<p v-action:op:replication-preferred>
<a-button type="primary" @click="openElectPreferredLeaderDialog">
首选副本作为leader
</a-button>
<label>说明</label>
<span>将集群中所有分区leader副本设置为首选副本</span>
</p>
<p>
<p v-action:op:replication-update-detail>
<a-button type="primary" @click="openCurrentReassignmentsDialog">
副本变更详情
</a-button>
<label>说明</label>
<span>查看正在进行副本变更/重分配的任务或者将其取消</span>
</p>
<p>
<p v-action:op:replication-reassign>
<a-button type="primary" @click="openReplicaReassignDialog">
副本重分配
</a-button>

View File

@@ -1,56 +1,49 @@
<script src="../../store/index.js"></script>
<template>
<a-modal
title="新增配置"
:visible="show"
:width="800"
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
@cancel="handleCancel"
title="新增配置"
:visible="show"
:width="800"
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
@cancel="handleCancel"
>
<div>
<a-spin :spinning="loading">
<a-form
:form="form"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 12 }"
@submit="handleSubmit"
:form="form"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 12 }"
@submit="handleSubmit"
>
<a-form-item label="用户" v-show="showUser">
<a-input
v-decorator="[
'user',
]"
placeholder="输入用户主体标识,比如:用户名,未指定表示用户默认设置"
v-decorator="['user']"
placeholder="输入用户主体标识,比如:用户名,未指定表示用户默认设置"
/>
</a-form-item>
<a-form-item label="客户端ID" v-show="showClientId">
<a-input
v-decorator="[
'client',
]"
placeholder="输入用户客户端ID未指定表示默认客户端设置"
v-decorator="['client']"
placeholder="输入用户客户端ID未指定表示默认客户端设置"
/>
</a-form-item>
<a-form-item label="IP" v-show="showIP">
<a-input
v-decorator="[
'ip',
]"
placeholder="输入客户端IP"
/>
<a-input v-decorator="['ip']" placeholder="输入客户端IP" />
</a-form-item>
<a-form-item label="生产速率">
<a-input-number
:min="1"
:max="102400000"
v-decorator="[
'producerRate',
]"
:min="1"
:max="102400000"
v-decorator="['producerRate']"
/>
<a-select default-value="MB" v-model="producerRateUnit" style="width: 100px">
<a-select
default-value="MB"
v-model="producerRateUnit"
style="width: 100px"
>
<a-select-option value="MB"> MB/s</a-select-option>
<a-select-option value="KB"> KB/s</a-select-option>
<a-select-option value="Byte"> Byte/s</a-select-option>
@@ -58,13 +51,15 @@
</a-form-item>
<a-form-item label="消费速率">
<a-input-number
:min="1"
:max="102400000"
v-decorator="[
'consumerRate',
]"
:min="1"
:max="102400000"
v-decorator="['consumerRate']"
/>
<a-select default-value="MB" v-model="consumerRateUnit" style="width: 100px">
<a-select
default-value="MB"
v-model="consumerRateUnit"
style="width: 100px"
>
<a-select-option value="MB"> MB/s</a-select-option>
<a-select-option value="KB"> KB/s</a-select-option>
<a-select-option value="Byte"> Byte/s</a-select-option>
@@ -72,11 +67,9 @@
</a-form-item>
<a-form-item label="吞吐量">
<a-input-number
:min="1"
:max="102400000"
v-decorator="[
'requestPercentage',
]"
:min="1"
:max="102400000"
v-decorator="['requestPercentage']"
/>
</a-form-item>
<a-form-item :wrapper-col="{ span: 12, offset: 5 }">
@@ -90,7 +83,7 @@
<script>
import request from "@/utils/request";
import {KafkaClientQuotaApi} from "@/utils/api";
import { KafkaClientQuotaApi } from "@/utils/api";
import notification from "ant-design-vue/es/notification";
export default {
@@ -122,7 +115,7 @@ export default {
show: this.visible,
data: [],
loading: false,
form: this.$form.createForm(this, {name: "coordinated"}),
form: this.$form.createForm(this, { name: "coordinated" }),
producerRateUnit: "MB",
consumerRateUnit: "MB",
};
@@ -136,13 +129,15 @@ export default {
handleSubmit() {
this.form.validateFields((err, values) => {
if (!err) {
const params = Object.assign({type: this.type}, values);
const unitMap = {MB: 1024 * 1024, KB: 1024, Byte: 1};
const params = Object.assign({ type: this.type }, values);
const unitMap = { MB: 1024 * 1024, KB: 1024, Byte: 1 };
if (values.consumerRate) {
params.consumerRate = params.consumerRate * unitMap[this.consumerRateUnit];
params.consumerRate =
params.consumerRate * unitMap[this.consumerRateUnit];
}
if (values.producerRate) {
params.producerRate = params.producerRate * unitMap[this.producerRateUnit];
params.producerRate =
params.producerRate * unitMap[this.producerRateUnit];
}
params.types = [];
params.names = [];
@@ -179,7 +174,7 @@ export default {
this.loading = false;
if (res.code == 0) {
this.$message.success(res.msg);
this.$emit("closeAddQuotaDialog", {refresh: true});
this.$emit("closeAddQuotaDialog", { refresh: true });
} else {
notification.error({
message: "error",
@@ -192,7 +187,7 @@ export default {
},
handleCancel() {
this.data = [];
this.$emit("closeAddQuotaDialog", {refresh: false});
this.$emit("closeAddQuotaDialog", { refresh: false });
this.producerRateUnit = "MB";
this.consumerRateUnit = "MB";
},

View File

@@ -3,18 +3,16 @@
<a-spin :spinning="loading">
<div id="search-offset-form-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<a-row :gutter="24">
<a-col :span="16">
<a-form-item label="客户端ID">
<a-input
v-decorator="[
'id',
]"
placeholder="请输入生产者/消费者客户端ID!"
v-decorator="['id']"
placeholder="请输入生产者/消费者客户端ID!"
/>
</a-form-item>
</a-col>
@@ -27,26 +25,39 @@
</a-form>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="openAddQuotaDialog"
>新增配置
<a-button
type="primary"
@click="openAddQuotaDialog"
v-action:quota:client:add
>新增配置
</a-button>
</div>
<QuotaList type="client-id" :columns="columns" :data="data" @refreshQuotaList="refresh"></QuotaList>
<AddQuotaConfig type="client-id" :visible="showAddQuotaDialog" :showClientId="true" @closeAddQuotaDialog="closeAddQuotaDialog"></AddQuotaConfig>
<QuotaList
type="client-id"
:columns="columns"
:data="data"
@refreshQuotaList="refresh"
></QuotaList>
<AddQuotaConfig
type="client-id"
:visible="showAddQuotaDialog"
:showClientId="true"
@closeAddQuotaDialog="closeAddQuotaDialog"
></AddQuotaConfig>
</a-spin>
</div>
</template>
<script>
import request from "@/utils/request";
import {KafkaClientQuotaApi} from "@/utils/api";
import { KafkaClientQuotaApi } from "@/utils/api";
import notification from "ant-design-vue/lib/notification";
import QuotaList from "@/views/quota/QuotaList.vue";
import AddQuotaConfig from "@/views/quota/AddQuotaConfig.vue";
export default {
name: "ClientIDQuota",
components: {QuotaList, AddQuotaConfig},
components: { QuotaList, AddQuotaConfig },
props: {
topicList: {
type: Array,
@@ -55,7 +66,7 @@ export default {
data() {
return {
loading: false,
form: this.$form.createForm(this, {name: "client_id_quota"}),
form: this.$form.createForm(this, { name: "client_id_quota" }),
data: [],
showAlterQuotaDialog: false,
showAddQuotaDialog: false,
@@ -64,8 +75,8 @@ export default {
title: "客户端ID",
dataIndex: "client",
key: "client",
slots: {title: "client"},
scopedSlots: {customRender: "client"},
slots: { title: "client" },
scopedSlots: { customRender: "client" },
},
{
title: "生产速率(带宽/秒)",
@@ -90,7 +101,7 @@ export default {
this.form.validateFields((err, values) => {
if (!err) {
this.loading = true;
const params = {types: ["client-id"]};
const params = { types: ["client-id"] };
if (values.id) {
params.names = [values.id.trim()];
}

View File

@@ -5,13 +5,17 @@
<a-tab-pane key="1" tab="使用说明">
<ClientQuotaIntroduce></ClientQuotaIntroduce>
</a-tab-pane>
<a-tab-pane key="2" tab="用户">
<a-tab-pane v-if="isAuthorized('quota:user')" key="2" tab="用户">
<UserQuota></UserQuota>
</a-tab-pane>
<a-tab-pane key="3" tab="客户端ID">
<a-tab-pane key="3" tab="客户端ID" v-if="isAuthorized('quota:client')">
<ClientIDQuota></ClientIDQuota>
</a-tab-pane>
<a-tab-pane key="4" tab="用户_客户端ID">
<a-tab-pane
key="4"
tab="用户_客户端ID"
v-if="isAuthorized('quota:user-client')"
>
<UserAndClientIDQuota></UserAndClientIDQuota>
</a-tab-pane>
<!-- <a-tab-pane key="5" tab="IP">-->
@@ -27,10 +31,17 @@ import ClientIDQuota from "@/views/quota/ClientIDQuota.vue";
import UserQuota from "@/views/quota/UserQuota.vue";
import UserAndClientIDQuota from "@/views/quota/UserAndClientIDQuota.vue";
import ClientQuotaIntroduce from "@/views/quota/ClientQuotaIntroduce.vue";
import { isAuthorized } from "@/utils/auth";
export default {
name: "ClientQuota",
components: {ClientIDQuota, UserQuota, UserAndClientIDQuota, ClientQuotaIntroduce},
methods: { isAuthorized },
components: {
ClientIDQuota,
UserQuota,
UserAndClientIDQuota,
ClientQuotaIntroduce,
},
data() {
return {
loading: false,

View File

@@ -1,21 +1,24 @@
<template>
<div class="content">
<h1>客户端限流说明资源限额</h1>
<hr/>
<hr/>
<hr />
<hr />
<h2>支持类型</h2>
<ul>
<li>基于用户主体标识的配置</li>
<li>基于客户端ID的配置</li>
<li>基于用户主体+客户端ID的配置</li>
</ul>
<hr/>
<hr />
<h2>默认配置</h2>
<p>比如基于用户的配置在新增配置的时候未设置用户名称则默认应用于所有用户</p>
<p>
比如基于用户的配置在新增配置的时候未设置用户名称则默认应用于所有用户
</p>
<p>基于客户端ID的配置新增的时候未指定客户端ID则应用于所有客户端</p>
<hr/>
<hr />
<h2>配置优先级</h2>
下面的展示中数字越小即越靠上的优先级越高相同的用户名称或者客户端ID优先级最高的配置生效下面的未指定表示 默认配置
下面的展示中数字越小即越靠上的优先级越高相同的用户名称或者客户端ID优先级最高的配置生效下面的未指定表示
默认配置
<ol>
<li>[用户+客户端ID] 指定用户名称并且指定客户端ID优先级最高</li>
<li>[用户+客户端ID] 指定用户名称未指定客户端ID</li>
@@ -26,18 +29,17 @@
<li>[客户端ID] 指定客户端ID</li>
<li>[客户端ID] 未指定客户端ID优先级最低</li>
</ol>
<hr/>
<hr />
<h2>使用注意</h2>
大多数集群一般没有开启认证所以用户配置可能不支持限流可以使用基于客户端ID的配置但是对于优先级最低的"[客户端ID]
未指定客户端ID"默认对于所有客户端生效万一速率配置过小可能无意间影响生产业务所以尽量避免默认配置
<hr/>
<hr />
<h2>查询</h2>
查询默认配置在查询项的输入框内输入一个空格即可
</div>
</template>
<script>
export default {
name: "ClientQuotaIntroduce",
};

View File

@@ -3,19 +3,14 @@
<a-spin :spinning="loading">
<div id="search-offset-form-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<a-row :gutter="24">
<a-col :span="16">
<a-form-item label="IP">
<a-input
v-decorator="[
'ip',
]"
placeholder="请输入ip!"
/>
<a-input v-decorator="['ip']" placeholder="请输入ip!" />
</a-form-item>
</a-col>
<a-col :span="2" :style="{ textAlign: 'right' }">
@@ -28,25 +23,35 @@
</div>
<div class="operation-row-button">
<a-button type="primary" @click="openAddQuotaDialog"
>新增配置
>新增配置
</a-button>
</div>
<QuotaList type="ip" :columns="columns" :data="data" @refreshQuotaList="refresh"></QuotaList>
<AddQuotaConfig type="ip" :visible="showAddQuotaDialog" :showIP="true" @closeAddQuotaDialog="closeAddQuotaDialog"></AddQuotaConfig>
<QuotaList
type="ip"
:columns="columns"
:data="data"
@refreshQuotaList="refresh"
></QuotaList>
<AddQuotaConfig
type="ip"
:visible="showAddQuotaDialog"
:showIP="true"
@closeAddQuotaDialog="closeAddQuotaDialog"
></AddQuotaConfig>
</a-spin>
</div>
</template>
<script>
import request from "@/utils/request";
import {KafkaClientQuotaApi} from "@/utils/api";
import { KafkaClientQuotaApi } from "@/utils/api";
import notification from "ant-design-vue/lib/notification";
import QuotaList from "@/views/quota/QuotaList.vue";
import AddQuotaConfig from "@/views/quota/AddQuotaConfig.vue";
export default {
name: "IpQuota",
components: {QuotaList, AddQuotaConfig},
components: { QuotaList, AddQuotaConfig },
props: {
topicList: {
type: Array,
@@ -55,7 +60,7 @@ export default {
data() {
return {
loading: false,
form: this.$form.createForm(this, {name: "ip_quota"}),
form: this.$form.createForm(this, { name: "ip_quota" }),
data: [],
showAlterQuotaDialog: false,
showAddQuotaDialog: false,
@@ -64,8 +69,8 @@ export default {
title: "IP",
dataIndex: "ip",
key: "ip",
slots: {title: "ip"},
scopedSlots: {customRender: "ip"},
slots: { title: "ip" },
scopedSlots: { customRender: "ip" },
},
{
title: "生产速率(带宽/秒)",
@@ -90,7 +95,7 @@ export default {
this.form.validateFields((err, values) => {
if (!err) {
this.loading = true;
const params = {types: ["ip"]};
const params = { types: ["ip"] };
if (values.ip) {
params.names = [values.ip.trim()];
}

View File

@@ -2,58 +2,69 @@
<div>
<a-spin :spinning="loading">
<a-table
:columns="columns"
:data-source="data"
bordered
:row-key="
(record, index) => {
return index;
}
"
@change="handleChange"
:columns="columns"
:data-source="data"
bordered
:row-key="
(record, index) => {
return index;
}
"
@change="handleChange"
>
<div slot="client" slot-scope="text">
<span v-if="text">{{ text }}</span><span v-else style="color: red">默认配置</span>
<span v-if="text">{{ text }}</span
><span v-else style="color: red">默认配置</span>
</div>
<div slot="user" slot-scope="text">
<span v-if="text">{{ text }}</span><span v-else style="color: red">默认配置</span>
<span v-if="text">{{ text }}</span
><span v-else style="color: red">默认配置</span>
</div>
<div slot="operation" slot-scope="record">
<a-popconfirm
:title="'删除当前配置?'"
ok-text="确认"
cancel-text="取消"
@confirm="deleteConfig(record)"
:title="'删除当前配置?'"
ok-text="确认"
cancel-text="取消"
@confirm="deleteConfig(record)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
>删除
</a-button>
</a-popconfirm>
<a-button
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openUpdateDialog(record)"
>修改
v-action:quota:del
>删除
</a-button>
</a-popconfirm>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openUpdateDialog(record)"
v-action:quota:edit
>修改
</a-button>
</div>
</a-table>
<UpdateQuotaConfig :type="type" :record="selectRow" :visible="showUpdateDialog"
@closeUpdateQuotaDialog="closeUpdateQuotaDialog"></UpdateQuotaConfig>
<UpdateQuotaConfig
:type="type"
:record="selectRow"
:visible="showUpdateDialog"
@closeUpdateQuotaDialog="closeUpdateQuotaDialog"
></UpdateQuotaConfig>
</a-spin>
</div>
</template>
<script>
import {KafkaClientQuotaApi} from "@/utils/api";
import { KafkaClientQuotaApi } from "@/utils/api";
import request from "@/utils/request";
import notification from "ant-design-vue/lib/notification";
import UpdateQuotaConfig from "@/views/quota/UpdateQuotaConfig.vue";
export default {
name: "QuotaList",
components: {UpdateQuotaConfig},
components: { UpdateQuotaConfig },
props: {
columns: {
type: Array,
@@ -88,7 +99,7 @@ export default {
},
deleteConfig(record) {
this.loading = true;
const params = {type: this.type};
const params = { type: this.type };
params.types = [];
params.names = [];
if (this.type == "user") {
@@ -161,7 +172,7 @@ export default {
this.columns.push({
title: "操作",
key: "operation",
scopedSlots: {customRender: "operation"},
scopedSlots: { customRender: "operation" },
});
},
};

View File

@@ -1,59 +1,58 @@
<script src="../../store/index.js"></script>
<template>
<a-modal
title="修改配置"
:visible="show"
:width="800"
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
@cancel="handleCancel"
title="修改配置"
:visible="show"
:width="800"
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
@cancel="handleCancel"
>
<div>
<a-spin :spinning="loading">
<a-form
:form="form"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 12 }"
@submit="handleSubmit"
:form="form"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 12 }"
@submit="handleSubmit"
>
<a-form-item label="用户" v-show="showUser">
<a-input
:disabled="true"
v-decorator="[
'user', { initialValue: record.user }
]"
placeholder="输入用户主体标识,比如:用户名,未指定表示用户默认设置"
:disabled="true"
v-decorator="['user', { initialValue: record.user }]"
placeholder="输入用户主体标识,比如:用户名,未指定表示用户默认设置"
/>
</a-form-item>
<a-form-item label="客户端ID" v-show="showClientId">
<a-input
:disabled="true"
v-decorator="[
'client', { initialValue: record.client }
]"
placeholder="输入用户客户端ID未指定表示默认客户端设置"
:disabled="true"
v-decorator="['client', { initialValue: record.client }]"
placeholder="输入用户客户端ID未指定表示默认客户端设置"
/>
</a-form-item>
<a-form-item label="IP" v-show="showIP">
<a-input
:disabled="true"
v-decorator="[
'ip', { initialValue: record.ip }
]"
placeholder="输入客户端IP"
:disabled="true"
v-decorator="['ip', { initialValue: record.ip }]"
placeholder="输入客户端IP"
/>
</a-form-item>
<a-form-item label="生产速率">
<a-input-number
:min="1"
:max="102400000"
v-decorator="[
'producerRate', { initialValue: record.producerRate }
:min="1"
:max="102400000"
v-decorator="[
'producerRate',
{ initialValue: record.producerRate },
]"
/>
<a-select default-value="MB" v-model="producerRateUnit" style="width: 100px">
<a-select
default-value="MB"
v-model="producerRateUnit"
style="width: 100px"
>
<a-select-option value="MB"> MB/s</a-select-option>
<a-select-option value="KB"> KB/s</a-select-option>
<a-select-option value="Byte"> Byte/s</a-select-option>
@@ -61,13 +60,18 @@
</a-form-item>
<a-form-item label="消费速率">
<a-input-number
:min="1"
:max="102400000"
v-decorator="[
'consumerRate', { initialValue: record.consumerRate }
:min="1"
:max="102400000"
v-decorator="[
'consumerRate',
{ initialValue: record.consumerRate },
]"
/>
<a-select default-value="MB" v-model="consumerRateUnit" style="width: 100px">
<a-select
default-value="MB"
v-model="consumerRateUnit"
style="width: 100px"
>
<a-select-option value="MB"> MB/s</a-select-option>
<a-select-option value="KB"> KB/s</a-select-option>
<a-select-option value="Byte"> Byte/s</a-select-option>
@@ -75,10 +79,11 @@
</a-form-item>
<a-form-item label="吞吐量">
<a-input-number
:min="1"
:max="102400000"
v-decorator="[
'requestPercentage', { initialValue: record.requestPercentage }
:min="1"
:max="102400000"
v-decorator="[
'requestPercentage',
{ initialValue: record.requestPercentage },
]"
/>
</a-form-item>
@@ -93,7 +98,7 @@
<script>
import request from "@/utils/request";
import {KafkaClientQuotaApi} from "@/utils/api";
import { KafkaClientQuotaApi } from "@/utils/api";
import notification from "ant-design-vue/es/notification";
export default {
@@ -110,7 +115,7 @@ export default {
record: {
type: Object,
default: function () {
return {}
return {};
},
},
},
@@ -119,7 +124,7 @@ export default {
show: this.visible,
data: [],
loading: false,
form: this.$form.createForm(this, {name: "coordinated"}),
form: this.$form.createForm(this, { name: "coordinated" }),
producerRateUnit: "MB",
consumerRateUnit: "MB",
showUser: false,
@@ -139,16 +144,24 @@ export default {
handleSubmit() {
this.form.validateFields((err, values) => {
if (!err) {
const params = {type: this.type, deleteConfigs: []};
const unitMap = {MB: 1024 * 1024, KB: 1024, Byte: 1};
const params = { type: this.type, deleteConfigs: [] };
const unitMap = { MB: 1024 * 1024, KB: 1024, Byte: 1 };
if (values.consumerRate) {
const num = typeof (values.consumerRate) == "string" && values.consumerRate.indexOf(" ") > 0 ? values.consumerRate.split(" ")[0] : values.consumerRate;
const num =
typeof values.consumerRate == "string" &&
values.consumerRate.indexOf(" ") > 0
? values.consumerRate.split(" ")[0]
: values.consumerRate;
params.consumerRate = num * unitMap[this.consumerRateUnit];
} else {
params.deleteConfigs.push("consumerRate");
}
if (values.producerRate) {
const num = typeof (values.producerRate) == "string" && values.producerRate.indexOf(" ") > 0 ? values.producerRate.split(" ")[0] : values.producerRate;
const num =
typeof values.producerRate == "string" &&
values.producerRate.indexOf(" ") > 0
? values.producerRate.split(" ")[0]
: values.producerRate;
params.producerRate = num * unitMap[this.producerRateUnit];
} else {
params.deleteConfigs.push("producerRate");
@@ -207,7 +220,7 @@ export default {
this.loading = false;
if (res.code == 0) {
this.$message.success(res.msg);
this.$emit("closeUpdateQuotaDialog", {refresh: true});
this.$emit("closeUpdateQuotaDialog", { refresh: true });
} else {
notification.error({
message: "error",
@@ -220,7 +233,7 @@ export default {
},
handleCancel() {
this.data = [];
this.$emit("closeUpdateQuotaDialog", {refresh: false});
this.$emit("closeUpdateQuotaDialog", { refresh: false });
},
init() {
this.producerRateUnit = "MB";
@@ -243,9 +256,7 @@ export default {
}
},
},
created() {
},
created() {},
};
</script>

View File

@@ -3,28 +3,24 @@
<a-spin :spinning="loading">
<div id="search-offset-form-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<a-row :gutter="24">
<a-col :span="10">
<a-form-item label="用户标识">
<a-input
v-decorator="[
'user',
]"
placeholder="请输入用户标识,如:用户名!"
v-decorator="['user']"
placeholder="请输入用户标识,如:用户名!"
/>
</a-form-item>
</a-col>
<a-col :span="10">
<a-form-item label="客户端ID">
<a-input
v-decorator="[
'client',
]"
placeholder="请输入客户端ID!"
v-decorator="['client']"
placeholder="请输入客户端ID!"
/>
</a-form-item>
</a-col>
@@ -37,27 +33,40 @@
</a-form>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="openAddQuotaDialog"
>新增配置
<a-button
type="primary"
@click="openAddQuotaDialog"
v-action:quota:user-client:add
>新增配置
</a-button>
</div>
<QuotaList type="user&client-id" :columns="columns" :data="data" @refreshQuotaList="refresh"></QuotaList>
<AddQuotaConfig type="user&client-id" :visible="showAddQuotaDialog" :showUser="true" :showClientId="true"
@closeAddQuotaDialog="closeAddQuotaDialog"></AddQuotaConfig>
<QuotaList
type="user&client-id"
:columns="columns"
:data="data"
@refreshQuotaList="refresh"
></QuotaList>
<AddQuotaConfig
type="user&client-id"
:visible="showAddQuotaDialog"
:showUser="true"
:showClientId="true"
@closeAddQuotaDialog="closeAddQuotaDialog"
></AddQuotaConfig>
</a-spin>
</div>
</template>
<script>
import request from "@/utils/request";
import {KafkaClientQuotaApi} from "@/utils/api";
import { KafkaClientQuotaApi } from "@/utils/api";
import notification from "ant-design-vue/lib/notification";
import QuotaList from "@/views/quota/QuotaList.vue";
import AddQuotaConfig from "@/views/quota/AddQuotaConfig.vue";
export default {
name: "UserAndClientIDQuota",
components: {QuotaList, AddQuotaConfig},
components: { QuotaList, AddQuotaConfig },
props: {
topicList: {
type: Array,
@@ -66,7 +75,7 @@ export default {
data() {
return {
loading: false,
form: this.$form.createForm(this, {name: "user_client_id_quota"}),
form: this.$form.createForm(this, { name: "user_client_id_quota" }),
data: [],
showAlterQuotaDialog: false,
showAddQuotaDialog: false,
@@ -75,15 +84,15 @@ export default {
title: "用户标识",
dataIndex: "user",
key: "user",
slots: {title: "user"},
scopedSlots: {customRender: "user"},
slots: { title: "user" },
scopedSlots: { customRender: "user" },
},
{
title: "客户端ID",
dataIndex: "client",
key: "client",
slots: {title: "client"},
scopedSlots: {customRender: "client"},
slots: { title: "client" },
scopedSlots: { customRender: "client" },
},
{
title: "生产速率(带宽/秒)",
@@ -108,7 +117,7 @@ export default {
this.form.validateFields((err, values) => {
if (!err) {
this.loading = true;
const params = {types: ["user", "client-id"], names: []};
const params = { types: ["user", "client-id"], names: [] };
if (values.user) {
params.names.push(values.user.trim());
}

View File

@@ -3,18 +3,16 @@
<a-spin :spinning="loading">
<div id="search-offset-form-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<a-row :gutter="24">
<a-col :span="16">
<a-form-item label="用户标识">
<a-input
v-decorator="[
'user',
]"
placeholder="请输入用户标识,如:用户名!"
v-decorator="['user']"
placeholder="请输入用户标识,如:用户名!"
/>
</a-form-item>
</a-col>
@@ -27,26 +25,39 @@
</a-form>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="openAddQuotaDialog"
>新增配置
<a-button
type="primary"
@click="openAddQuotaDialog"
v-action:quota:user:add
>新增配置
</a-button>
</div>
<QuotaList type="user" :columns="columns" :data="data" @refreshQuotaList="refresh"></QuotaList>
<AddQuotaConfig type="user" :visible="showAddQuotaDialog" :showUser="true" @closeAddQuotaDialog="closeAddQuotaDialog"></AddQuotaConfig>
<QuotaList
type="user"
:columns="columns"
:data="data"
@refreshQuotaList="refresh"
></QuotaList>
<AddQuotaConfig
type="user"
:visible="showAddQuotaDialog"
:showUser="true"
@closeAddQuotaDialog="closeAddQuotaDialog"
></AddQuotaConfig>
</a-spin>
</div>
</template>
<script>
import request from "@/utils/request";
import {KafkaClientQuotaApi} from "@/utils/api";
import { KafkaClientQuotaApi } from "@/utils/api";
import notification from "ant-design-vue/lib/notification";
import QuotaList from "@/views/quota/QuotaList.vue";
import AddQuotaConfig from "@/views/quota/AddQuotaConfig.vue";
export default {
name: "UserQuota",
components: {QuotaList, AddQuotaConfig},
components: { QuotaList, AddQuotaConfig },
props: {
topicList: {
type: Array,
@@ -55,7 +66,7 @@ export default {
data() {
return {
loading: false,
form: this.$form.createForm(this, {name: "user_quota"}),
form: this.$form.createForm(this, { name: "user_quota" }),
data: [],
showAlterQuotaDialog: false,
showAddQuotaDialog: false,
@@ -65,8 +76,8 @@ export default {
dataIndex: "user",
key: "user",
width: 300,
slots: {title: "user"},
scopedSlots: {customRender: "user"},
slots: { title: "user" },
scopedSlots: { customRender: "user" },
},
{
title: "生产速率(带宽/秒)",
@@ -91,7 +102,7 @@ export default {
this.form.validateFields((err, values) => {
if (!err) {
this.loading = true;
const params = {types: ["user"]};
const params = { types: ["user"] };
if (values.user) {
params.names = [values.user.trim()];
}

View File

@@ -44,7 +44,11 @@
cancel-text="取消"
@confirm="electPreferredLeader(record)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
<a-button
size="small"
href="javascript:;"
class="operation-btn"
v-action:topic:partition-detail:preferred
>首选副本作为leader
</a-button>
</a-popconfirm>

View File

@@ -36,7 +36,13 @@
<a-col :span="8" :style="{ textAlign: 'right' }">
<a-form-item>
<a-button type="primary" html-type="submit"> 刷新</a-button>
<a-button
type="primary"
html-type="submit"
v-action:topic:load
>
刷新</a-button
>
<!-- <a-button :style="{ marginLeft: '8px' }" @click="handleReset">-->
<!-- 重置-->
<!-- </a-button>-->
@@ -46,7 +52,10 @@
</a-form>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="openCreateTopicDialog"
<a-button
type="primary"
@click="openCreateTopicDialog"
v-action:topic:add
>新增</a-button
>
<a-popconfirm
@@ -60,6 +69,7 @@
class="btn-left"
:disabled="!hasSelected"
:loading="loading"
v-action:topic:batch-del
>
批量删除
</a-button>
@@ -97,7 +107,11 @@
cancel-text="取消"
@confirm="deleteTopic(record.name)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
<a-button
size="small"
href="javascript:;"
class="operation-btn"
v-action:topic:del
>删除
</a-button>
</a-popconfirm>
@@ -106,6 +120,7 @@
href="javascript:;"
class="operation-btn"
@click="openPartitionInfoDialog(record.name)"
v-action:topic:partition-detail
>分区详情
</a-button>
<a-button
@@ -113,6 +128,7 @@
href="javascript:;"
class="operation-btn"
@click="openAddPartitionDialog(record.name)"
v-action:topic:partition-add
>增加分区
</a-button>
<a-button
@@ -120,6 +136,7 @@
href="javascript:;"
class="operation-btn"
@click="openConsumedDetailDialog(record.name)"
v-action:topic:consumer-detail
>消费详情
</a-button>
<a-button
@@ -127,6 +144,7 @@
href="javascript:;"
class="operation-btn"
@click="openTopicConfigDialog(record.name)"
v-action:topic:property-config
>属性配置
</a-button>
<a-button
@@ -134,6 +152,7 @@
href="javascript:;"
class="operation-btn"
@click="openUpdateReplicaDialog(record.name)"
v-action:topic:replication-modify
>变更副本
</a-button>
<a-button
@@ -141,6 +160,7 @@
href="javascript:;"
class="operation-btn"
@click="openMessageStatsDialog(record.name)"
v-action:topic:send-count
>发送统计
</a-button>
<a-button
@@ -148,6 +168,7 @@
href="javascript:;"
class="operation-btn"
@click="openThrottleDialog(record.name)"
v-action:topic:replication-sync-throttle
>限流
</a-button>
</div>

View File

@@ -35,6 +35,7 @@
class="operation-btn"
v-show="!record.readOnly"
@click="openEditConfigDialog(record)"
v-action:topic:property-config:edit
>编辑
</a-button>
<a-popconfirm
@@ -44,7 +45,11 @@
v-show="isDynamic(record.source)"
@confirm="deleteTopicConfig(record)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
<a-button
size="small"
href="javascript:;"
class="operation-btn"
v-action:topic:property-config:del
>删除
</a-button>
</a-popconfirm>

View File

@@ -0,0 +1,143 @@
<template>
<a-modal
title="新增用户"
:visible="show"
:width="800"
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
@cancel="handleCancel"
>
<div>
<a-spin :spinning="loading">
<a-form
:form="form"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 12 }"
@submit="handleSubmit"
>
<a-form-item label="用户名">
<a-input
v-decorator="[
'username',
{ rules: [{ required: true, message: '输入用户名' }] },
]"
placeholder="输入用户名"
/>
</a-form-item>
<a-form-item label="角色">
<a-select
show-search
option-filter-prop="children"
v-decorator="[
'roleIds',
{ rules: [{ required: true, message: '请选择一个角色!' }] },
]"
placeholder="请选择一个角色"
>
<a-select-option
v-for="role in roles"
:key="role.id"
:value="role.id"
>
{{ role.roleName }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item :wrapper-col="{ span: 12, offset: 5 }">
<a-button type="primary" html-type="submit"> 提交</a-button>
</a-form-item>
</a-form>
</a-spin>
</div>
</a-modal>
</template>
<script>
import request from "@/utils/request";
import notification from "ant-design-vue/es/notification";
import { UserManageApi } from "@/utils/api";
export default {
name: "CreateUser",
props: {
visible: {
type: Boolean,
default: false,
},
},
data() {
return {
show: this.visible,
data: [],
loading: false,
form: this.$form.createForm(this, { name: "coordinated" }),
roles: [],
};
},
watch: {
visible(v) {
this.show = v;
if (this.show) {
this.getRoles();
}
},
},
methods: {
handleSubmit(e) {
e.preventDefault();
this.form.validateFields((err, values) => {
if (!err) {
this.loading = true;
request({
url: UserManageApi.addOrUpdateUser.url,
method: UserManageApi.addOrUpdateUser.method,
data: values,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.$message.success(res.msg);
this.$emit("closeCreateUserDialog", {
refresh: true,
data: res.data,
});
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
}
});
},
getRoles() {
this.loading = true;
request({
url: UserManageApi.getRole.url,
method: UserManageApi.getRole.method,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.roles = res.data;
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
},
handleCancel() {
this.data = [];
this.$emit("closeCreateUserDialog", { refresh: false });
},
},
created() {
this.getRoles();
},
};
</script>
<style scoped></style>

View File

@@ -0,0 +1,80 @@
<template>
<div class="content">
<a-spin :spinning="loading">
<a-table
:columns="columns"
:data-source="data"
:expanded-row-keys.sync="expandedRowKeys"
>
<div slot="type" slot-scope="text">
<span v-if="text == 0" style="color: darkgreen">菜单</span
><span v-else>按钮</span>
</div>
</a-table>
</a-spin>
</div>
</template>
<script>
import request from "@/utils/request";
const columns = [
{
title: "权限名称",
dataIndex: "name",
key: "name",
},
{
title: "类型",
dataIndex: "type",
key: "type",
width: "12%",
slots: { title: "type" },
scopedSlots: { customRender: "type" },
},
];
import { UserManageApi } from "@/utils/api";
import notification from "ant-design-vue/lib/notification";
export default {
name: "Permission",
components: {},
data() {
return {
loading: false,
data: [],
columns,
expandedRowKeys: [],
};
},
methods: {
getPermissions() {
this.loading = true;
request({
url: UserManageApi.getPermissions.url,
method: UserManageApi.getPermissions.method,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.data = res.data;
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
},
},
created() {
this.getPermissions();
},
};
</script>
<style scoped>
.editable-row-operations a {
margin-right: 8px;
}
</style>

Some files were not shown because too many files have changed in this diff Show More