1 Commits

Author SHA1 Message Date
yinuo
8942760a5a basic user auth 2022-05-06 10:36:00 +08:00
58 changed files with 2265 additions and 13977 deletions

View File

@@ -25,8 +25,14 @@
<maven.assembly.plugin.version>3.0.0</maven.assembly.plugin.version> <maven.assembly.plugin.version>3.0.0</maven.assembly.plugin.version>
<mybatis-plus-boot-starter.version>3.4.2</mybatis-plus-boot-starter.version> <mybatis-plus-boot-starter.version>3.4.2</mybatis-plus-boot-starter.version>
<scala.version>2.13.6</scala.version> <scala.version>2.13.6</scala.version>
<jwt.version>0.9.0</jwt.version>
</properties> </properties>
<dependencies> <dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.scala-lang</groupId> <groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId> <artifactId>scala-library</artifactId>

View File

@@ -0,0 +1,7 @@
package com.xuxd.kafka.console.beans;
public class KafkaConsoleException extends RuntimeException{
public KafkaConsoleException(String msg){
super(msg);
}
}

View File

@@ -11,7 +11,7 @@ import lombok.Setter;
**/ **/
public class ResponseData<T> { public class ResponseData<T> {
public static final int SUCCESS_CODE = 0, FAILED_CODE = -9999; public static final int SUCCESS_CODE = 0, TOKEN_ILLEGAL = -5000, FAILED_CODE = -9999;
public static final String SUCCESS_MSG = "success", FAILED_MSG = "failed"; public static final String SUCCESS_MSG = "success", FAILED_MSG = "failed";
@@ -58,6 +58,12 @@ public class ResponseData<T> {
return this; return this;
} }
public ResponseData<T> failed(int code) {
this.code = code;
this.msg = FAILED_MSG;
return this;
}
public ResponseData<T> failed(String msg) { public ResponseData<T> failed(String msg) {
this.code = FAILED_CODE; this.code = FAILED_CODE;
this.msg = msg; this.msg = msg;

View File

@@ -0,0 +1,9 @@
package com.xuxd.kafka.console.beans.annotation;
import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequiredAuthorize {
}

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 com.xuxd.kafka.console.beans.enums.Role;
import lombok.Data;
import java.util.Date;
@Data
@TableName("t_devops_user")
public class DevOpsUserDO {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private Role role;
private boolean delete;
private Date createTime;
private Date updateTime;
}

View File

@@ -0,0 +1,11 @@
package com.xuxd.kafka.console.beans.dto.user;
import com.xuxd.kafka.console.beans.enums.Role;
import lombok.Data;
@Data
public class AddUserDTO {
private String username;
private String password;
private Role role;
}

View File

@@ -0,0 +1,9 @@
package com.xuxd.kafka.console.beans.dto.user;
import lombok.Data;
@Data
public class ListUserDTO {
private Long id;
private String username;
}

View File

@@ -0,0 +1,9 @@
package com.xuxd.kafka.console.beans.dto.user;
import lombok.Data;
@Data
public class LoginDTO {
private String username;
private String password;
}

View File

@@ -0,0 +1,9 @@
package com.xuxd.kafka.console.beans.dto.user;
import lombok.Data;
@Data
public class PasswordDTO {
private Long userId;
private String password;
}

View File

@@ -0,0 +1,11 @@
package com.xuxd.kafka.console.beans.dto.user;
import com.xuxd.kafka.console.beans.enums.Role;
import lombok.Data;
@Data
public class UpdateUserDTO {
private String username;
private String password;
private Role role;
}

View File

@@ -0,0 +1,6 @@
package com.xuxd.kafka.console.beans.enums;
public enum Role {
developer,
manager
}

View File

@@ -0,0 +1,16 @@
package com.xuxd.kafka.console.beans.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.xuxd.kafka.console.beans.enums.Role;
import lombok.Data;
import java.util.Date;
@Data
public class DevOpsUserVO {
private Long id;
private String username;
private Role role;
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
private Date createTime;
}

View File

@@ -0,0 +1,16 @@
package com.xuxd.kafka.console.beans.vo;
import com.xuxd.kafka.console.beans.enums.Role;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class LoginVO {
private String token;
private Role role;
}

View File

@@ -2,15 +2,20 @@ package com.xuxd.kafka.console.boot;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xuxd.kafka.console.beans.dos.ClusterInfoDO; import com.xuxd.kafka.console.beans.dos.ClusterInfoDO;
import com.xuxd.kafka.console.beans.dos.DevOpsUserDO;
import com.xuxd.kafka.console.config.KafkaConfig; import com.xuxd.kafka.console.config.KafkaConfig;
import com.xuxd.kafka.console.dao.ClusterInfoMapper; import com.xuxd.kafka.console.dao.ClusterInfoMapper;
import com.xuxd.kafka.console.dao.DevOpsUserMapper;
import com.xuxd.kafka.console.utils.ConvertUtil; import com.xuxd.kafka.console.utils.ConvertUtil;
import java.util.List; import java.util.List;
import com.xuxd.kafka.console.utils.Md5Utils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**

View File

@@ -0,0 +1,42 @@
package com.xuxd.kafka.console.boot;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xuxd.kafka.console.beans.dos.DevOpsUserDO;
import com.xuxd.kafka.console.beans.enums.Role;
import com.xuxd.kafka.console.dao.DevOpsUserMapper;
import com.xuxd.kafka.console.utils.Md5Utils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
@Slf4j
@RequiredArgsConstructor
public class InitSuperDevOpsUser implements SmartInitializingSingleton {
private final DevOpsUserMapper devOpsUserMapper;
public final static String SUPER_USERNAME = "admin";
@Value("${devops.password:kafka-console-ui521}")
private String password;
@Override
public void afterSingletonsInstantiated() {
QueryWrapper<DevOpsUserDO> userDOQueryWrapper = new QueryWrapper<>();
userDOQueryWrapper.eq("username", SUPER_USERNAME);
DevOpsUserDO userDO = devOpsUserMapper.selectOne(userDOQueryWrapper);
if (userDO == null){
DevOpsUserDO devOpsUserDO = new DevOpsUserDO();
devOpsUserDO.setUsername(SUPER_USERNAME);
devOpsUserDO.setPassword(Md5Utils.MD5(password));
devOpsUserDO.setRole(Role.manager);
devOpsUserMapper.insert(devOpsUserDO);
} else {
userDO.setPassword(Md5Utils.MD5(password));
devOpsUserMapper.updateById(userDO);
}
log.info("init super devops user done, username = {}", SUPER_USERNAME);
}
}

View File

@@ -1,6 +1,7 @@
package com.xuxd.kafka.console.controller; package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.beans.AclUser; import com.xuxd.kafka.console.beans.AclUser;
import com.xuxd.kafka.console.beans.annotation.RequiredAuthorize;
import com.xuxd.kafka.console.service.AclService; import com.xuxd.kafka.console.service.AclService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
@@ -30,17 +31,20 @@ public class AclUserController {
} }
@PostMapping @PostMapping
@RequiredAuthorize
public Object addOrUpdateUser(@RequestBody AclUser user) { public Object addOrUpdateUser(@RequestBody AclUser user) {
return aclService.addOrUpdateUser(user.getUsername(), user.getPassword()); return aclService.addOrUpdateUser(user.getUsername(), user.getPassword());
} }
@DeleteMapping @DeleteMapping
@RequiredAuthorize
public Object deleteUser(@RequestBody AclUser user) { public Object deleteUser(@RequestBody AclUser user) {
return aclService.deleteUser(user.getUsername()); return aclService.deleteUser(user.getUsername());
} }
@DeleteMapping("/auth") @DeleteMapping("/auth")
@RequiredAuthorize
public Object deleteUserAndAuth(@RequestBody AclUser user) { public Object deleteUserAndAuth(@RequestBody AclUser user) {
return aclService.deleteUserAndAuth(user.getUsername()); return aclService.deleteUserAndAuth(user.getUsername());
} }

View File

@@ -3,13 +3,7 @@ package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.beans.dto.ClusterInfoDTO; import com.xuxd.kafka.console.beans.dto.ClusterInfoDTO;
import com.xuxd.kafka.console.service.ClusterService; import com.xuxd.kafka.console.service.ClusterService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/** /**
* kafka-console-ui. * kafka-console-ui.

View File

@@ -1,6 +1,7 @@
package com.xuxd.kafka.console.controller; package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.beans.ResponseData; import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.annotation.RequiredAuthorize;
import com.xuxd.kafka.console.beans.dto.AlterConfigDTO; import com.xuxd.kafka.console.beans.dto.AlterConfigDTO;
import com.xuxd.kafka.console.beans.enums.AlterType; import com.xuxd.kafka.console.beans.enums.AlterType;
import com.xuxd.kafka.console.config.KafkaConfig; import com.xuxd.kafka.console.config.KafkaConfig;
@@ -47,11 +48,13 @@ public class ConfigController {
} }
@PostMapping("/topic") @PostMapping("/topic")
@RequiredAuthorize
public Object setTopicConfig(@RequestBody AlterConfigDTO dto) { public Object setTopicConfig(@RequestBody AlterConfigDTO dto) {
return configService.alterTopicConfig(dto.getEntity(), dto.to(), AlterType.SET); return configService.alterTopicConfig(dto.getEntity(), dto.to(), AlterType.SET);
} }
@DeleteMapping("/topic") @DeleteMapping("/topic")
@RequiredAuthorize
public Object deleteTopicConfig(@RequestBody AlterConfigDTO dto) { public Object deleteTopicConfig(@RequestBody AlterConfigDTO dto) {
return configService.alterTopicConfig(dto.getEntity(), dto.to(), AlterType.DELETE); return configService.alterTopicConfig(dto.getEntity(), dto.to(), AlterType.DELETE);
} }
@@ -62,11 +65,13 @@ public class ConfigController {
} }
@PostMapping("/broker") @PostMapping("/broker")
@RequiredAuthorize
public Object setBrokerConfig(@RequestBody AlterConfigDTO dto) { public Object setBrokerConfig(@RequestBody AlterConfigDTO dto) {
return configService.alterBrokerConfig(dto.getEntity(), dto.to(), AlterType.SET); return configService.alterBrokerConfig(dto.getEntity(), dto.to(), AlterType.SET);
} }
@DeleteMapping("/broker") @DeleteMapping("/broker")
@RequiredAuthorize
public Object deleteBrokerConfig(@RequestBody AlterConfigDTO dto) { public Object deleteBrokerConfig(@RequestBody AlterConfigDTO dto) {
return configService.alterBrokerConfig(dto.getEntity(), dto.to(), AlterType.DELETE); return configService.alterBrokerConfig(dto.getEntity(), dto.to(), AlterType.DELETE);
} }
@@ -77,11 +82,13 @@ public class ConfigController {
} }
@PostMapping("/broker/logger") @PostMapping("/broker/logger")
@RequiredAuthorize
public Object setBrokerLoggerConfig(@RequestBody AlterConfigDTO dto) { public Object setBrokerLoggerConfig(@RequestBody AlterConfigDTO dto) {
return configService.alterBrokerLoggerConfig(dto.getEntity(), dto.to(), AlterType.SET); return configService.alterBrokerLoggerConfig(dto.getEntity(), dto.to(), AlterType.SET);
} }
@DeleteMapping("/broker/logger") @DeleteMapping("/broker/logger")
@RequiredAuthorize
public Object deleteBrokerLoggerConfig(@RequestBody AlterConfigDTO dto) { public Object deleteBrokerLoggerConfig(@RequestBody AlterConfigDTO dto) {
return configService.alterBrokerLoggerConfig(dto.getEntity(), dto.to(), AlterType.DELETE); return configService.alterBrokerLoggerConfig(dto.getEntity(), dto.to(), AlterType.DELETE);
} }

View File

@@ -1,6 +1,7 @@
package com.xuxd.kafka.console.controller; package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.beans.ResponseData; import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.annotation.RequiredAuthorize;
import com.xuxd.kafka.console.beans.dto.AddSubscriptionDTO; import com.xuxd.kafka.console.beans.dto.AddSubscriptionDTO;
import com.xuxd.kafka.console.beans.dto.QueryConsumerGroupDTO; import com.xuxd.kafka.console.beans.dto.QueryConsumerGroupDTO;
import com.xuxd.kafka.console.beans.dto.ResetOffsetDTO; import com.xuxd.kafka.console.beans.dto.ResetOffsetDTO;
@@ -67,11 +68,13 @@ public class ConsumerController {
} }
@PostMapping("/subscription") @PostMapping("/subscription")
@RequiredAuthorize
public Object addSubscription(@RequestBody AddSubscriptionDTO subscriptionDTO) { public Object addSubscription(@RequestBody AddSubscriptionDTO subscriptionDTO) {
return consumerService.addSubscription(subscriptionDTO.getGroupId(), subscriptionDTO.getTopic()); return consumerService.addSubscription(subscriptionDTO.getGroupId(), subscriptionDTO.getTopic());
} }
@PostMapping("/reset/offset") @PostMapping("/reset/offset")
@RequiredAuthorize
public Object restOffset(@RequestBody ResetOffsetDTO offsetDTO) { public Object restOffset(@RequestBody ResetOffsetDTO offsetDTO) {
ResponseData res = ResponseData.create().failed("unknown"); ResponseData res = ResponseData.create().failed("unknown");
switch (offsetDTO.getLevel()) { switch (offsetDTO.getLevel()) {

View File

@@ -0,0 +1,53 @@
package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.annotation.RequiredAuthorize;
import com.xuxd.kafka.console.beans.dto.user.*;
import com.xuxd.kafka.console.beans.vo.DevOpsUserVO;
import com.xuxd.kafka.console.beans.vo.LoginVO;
import com.xuxd.kafka.console.service.DevOpsUserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 用户管理
* @author dongyinuo
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/devops/user")
public class DevOpsUserController {
private final DevOpsUserService devOpsUserService;
@PostMapping("add")
@RequiredAuthorize
public ResponseData<Boolean> add(@RequestBody AddUserDTO addUserDTO){
return devOpsUserService.add(addUserDTO);
}
@PostMapping("update")
@RequiredAuthorize
public ResponseData<Boolean> update(@RequestBody UpdateUserDTO updateUserDTO){
return devOpsUserService.update(updateUserDTO);
}
@DeleteMapping
@RequiredAuthorize
public ResponseData<Boolean> delete(@RequestParam Long id){
return devOpsUserService.delete(id);
}
@GetMapping("list")
@RequiredAuthorize
public ResponseData<List<DevOpsUserVO>> list(@ModelAttribute ListUserDTO listUserDTO){
return devOpsUserService.list(listUserDTO);
}
@PostMapping("login")
public ResponseData<LoginVO> login(@RequestBody LoginDTO loginDTO){
return devOpsUserService.login(loginDTO.getUsername(), loginDTO.getPassword());
}
}

View File

@@ -1,6 +1,7 @@
package com.xuxd.kafka.console.controller; package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.beans.TopicPartition; import com.xuxd.kafka.console.beans.TopicPartition;
import com.xuxd.kafka.console.beans.annotation.RequiredAuthorize;
import com.xuxd.kafka.console.beans.dto.BrokerThrottleDTO; import com.xuxd.kafka.console.beans.dto.BrokerThrottleDTO;
import com.xuxd.kafka.console.beans.dto.ProposedAssignmentDTO; import com.xuxd.kafka.console.beans.dto.ProposedAssignmentDTO;
import com.xuxd.kafka.console.beans.dto.ReplicationDTO; import com.xuxd.kafka.console.beans.dto.ReplicationDTO;
@@ -30,12 +31,14 @@ public class OperationController {
private OperationService operationService; private OperationService operationService;
@PostMapping("/sync/consumer/offset") @PostMapping("/sync/consumer/offset")
@RequiredAuthorize
public Object syncConsumerOffset(@RequestBody SyncDataDTO dto) { public Object syncConsumerOffset(@RequestBody SyncDataDTO dto) {
dto.getProperties().put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, dto.getAddress()); dto.getProperties().put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, dto.getAddress());
return operationService.syncConsumerOffset(dto.getGroupId(), dto.getTopic(), dto.getProperties()); return operationService.syncConsumerOffset(dto.getGroupId(), dto.getTopic(), dto.getProperties());
} }
@PostMapping("/sync/min/offset/alignment") @PostMapping("/sync/min/offset/alignment")
@RequiredAuthorize
public Object minOffsetAlignment(@RequestBody SyncDataDTO dto) { public Object minOffsetAlignment(@RequestBody SyncDataDTO dto) {
dto.getProperties().put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, dto.getAddress()); dto.getProperties().put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, dto.getAddress());
return operationService.minOffsetAlignment(dto.getGroupId(), dto.getTopic(), dto.getProperties()); return operationService.minOffsetAlignment(dto.getGroupId(), dto.getTopic(), dto.getProperties());
@@ -47,6 +50,7 @@ public class OperationController {
} }
@DeleteMapping("/sync/alignment") @DeleteMapping("/sync/alignment")
@RequiredAuthorize
public Object deleteAlignment(@RequestParam Long id) { public Object deleteAlignment(@RequestParam Long id) {
return operationService.deleteAlignmentById(id); return operationService.deleteAlignmentById(id);
} }
@@ -57,6 +61,7 @@ public class OperationController {
} }
@PostMapping("/broker/throttle") @PostMapping("/broker/throttle")
@RequiredAuthorize
public Object configThrottle(@RequestBody BrokerThrottleDTO dto) { public Object configThrottle(@RequestBody BrokerThrottleDTO dto) {
return operationService.configThrottle(dto.getBrokerList(), dto.getUnit().toKb(dto.getThrottle())); return operationService.configThrottle(dto.getBrokerList(), dto.getUnit().toKb(dto.getThrottle()));
} }
@@ -77,6 +82,7 @@ public class OperationController {
} }
@PostMapping("/replication/reassignments/proposed") @PostMapping("/replication/reassignments/proposed")
@RequiredAuthorize
public Object proposedAssignments(@RequestBody ProposedAssignmentDTO dto) { public Object proposedAssignments(@RequestBody ProposedAssignmentDTO dto) {
return operationService.proposedAssignments(dto.getTopic(), dto.getBrokers()); return operationService.proposedAssignments(dto.getTopic(), dto.getBrokers());
} }

View File

@@ -1,6 +1,7 @@
package com.xuxd.kafka.console.controller; package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.beans.ReplicaAssignment; import com.xuxd.kafka.console.beans.ReplicaAssignment;
import com.xuxd.kafka.console.beans.annotation.RequiredAuthorize;
import com.xuxd.kafka.console.beans.dto.AddPartitionDTO; import com.xuxd.kafka.console.beans.dto.AddPartitionDTO;
import com.xuxd.kafka.console.beans.dto.NewTopicDTO; import com.xuxd.kafka.console.beans.dto.NewTopicDTO;
import com.xuxd.kafka.console.beans.dto.TopicThrottleDTO; import com.xuxd.kafka.console.beans.dto.TopicThrottleDTO;
@@ -43,6 +44,7 @@ public class TopicController {
} }
@DeleteMapping @DeleteMapping
@RequiredAuthorize
public Object deleteTopic(@RequestParam String topic) { public Object deleteTopic(@RequestParam String topic) {
return topicService.deleteTopic(topic); return topicService.deleteTopic(topic);
} }
@@ -53,11 +55,13 @@ public class TopicController {
} }
@PostMapping("/new") @PostMapping("/new")
@RequiredAuthorize
public Object createNewTopic(@RequestBody NewTopicDTO topicDTO) { public Object createNewTopic(@RequestBody NewTopicDTO topicDTO) {
return topicService.createTopic(topicDTO.toNewTopic()); return topicService.createTopic(topicDTO.toNewTopic());
} }
@PostMapping("/partition/new") @PostMapping("/partition/new")
@RequiredAuthorize
public Object addPartition(@RequestBody AddPartitionDTO partitionDTO) { public Object addPartition(@RequestBody AddPartitionDTO partitionDTO) {
String topic = partitionDTO.getTopic().trim(); String topic = partitionDTO.getTopic().trim();
int addNum = partitionDTO.getAddNum(); int addNum = partitionDTO.getAddNum();
@@ -80,11 +84,13 @@ public class TopicController {
} }
@PostMapping("/replica/assignment") @PostMapping("/replica/assignment")
@RequiredAuthorize
public Object updateReplicaAssignment(@RequestBody ReplicaAssignment assignment) { public Object updateReplicaAssignment(@RequestBody ReplicaAssignment assignment) {
return topicService.updateReplicaAssignment(assignment); return topicService.updateReplicaAssignment(assignment);
} }
@PostMapping("/replica/throttle") @PostMapping("/replica/throttle")
@RequiredAuthorize
public Object configThrottle(@RequestBody TopicThrottleDTO dto) { public Object configThrottle(@RequestBody TopicThrottleDTO dto) {
return topicService.configThrottle(dto.getTopic(), dto.getPartitions(), dto.getOperation()); return topicService.configThrottle(dto.getTopic(), dto.getPartitions(), dto.getOperation());
} }

View File

@@ -0,0 +1,7 @@
package com.xuxd.kafka.console.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xuxd.kafka.console.beans.dos.DevOpsUserDO;
public interface DevOpsUserMapper extends BaseMapper<DevOpsUserDO> {
}

View File

@@ -1,7 +1,6 @@
package com.xuxd.kafka.console.interceptor; package com.xuxd.kafka.console.interceptor;
import com.xuxd.kafka.console.beans.ResponseData; import com.xuxd.kafka.console.utils.ResponseUtil;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -14,14 +13,13 @@ import org.springframework.web.bind.annotation.ResponseBody;
* @date 2021-10-19 14:32:18 * @date 2021-10-19 14:32:18
**/ **/
@Slf4j @Slf4j
@ControllerAdvice(basePackages = "com.xuxd.kafka.console.controller") @ControllerAdvice(basePackages = "com.xuxd.kafka.console")
public class GlobalExceptionHandler { public class GlobalExceptionHandler {
@ExceptionHandler(value = Exception.class) @ExceptionHandler(value = Exception.class)
@ResponseBody @ResponseBody
public Object exceptionHandler(HttpServletRequest req, Exception ex) throws Exception { public Object exceptionHandler(Exception ex) {
log.error("exception handle: ", ex); log.error("exception handle: ", ex);
return ResponseData.create().failed(ex.getMessage()); return ResponseUtil.error(ex.getMessage());
} }
} }

View File

@@ -0,0 +1,84 @@
package com.xuxd.kafka.console.interceptor;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.annotation.RequiredAuthorize;
import com.xuxd.kafka.console.beans.enums.Role;
import com.xuxd.kafka.console.beans.vo.DevOpsUserVO;
import com.xuxd.kafka.console.service.DevOpsUserService;
import com.xuxd.kafka.console.utils.ContextUtil;
import com.xuxd.kafka.console.utils.ConvertUtil;
import com.xuxd.kafka.console.utils.JwtUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.AsyncHandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import static com.xuxd.kafka.console.beans.ResponseData.TOKEN_ILLEGAL;
@Component
@Slf4j
@RequiredArgsConstructor
public class TokenInterceptor implements AsyncHandlerInterceptor {
private final static String TOKEN = "token";
private final DevOpsUserService devOpsUserService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod){
String token = request.getHeader(TOKEN);
if (StringUtils.isBlank(token)){
log.info("token not exist");
write(response);
return false;
}
String username = JwtUtils.parse(token);
if (StringUtils.isBlank(username)){
log.info("{} is wrongful", token);
write(response);
return false;
}
ResponseData<DevOpsUserVO> userVORsp = devOpsUserService.detail(username);
if (userVORsp == null || userVORsp.getData() == null){
log.info("{} not exist", username);
write(response);
return false;
}
ContextUtil.set(ContextUtil.USERNAME, username);
HandlerMethod method = (HandlerMethod)handler;
RequiredAuthorize annotation = method.getMethodAnnotation(RequiredAuthorize.class);
if (annotation != null){
DevOpsUserVO userVO = userVORsp.getData();
if (!userVO.getRole().equals(Role.manager)){
log.info("{},{} no permission", username, request.getRequestURI());
write(response);
return false;
}
}
}
return true;
}
private void write(HttpServletResponse response){
PrintWriter writer = null;
try {
writer = response.getWriter();
writer.write(ConvertUtil.toJsonString(ResponseData.create().failed(TOKEN_ILLEGAL)));
} catch (Exception ignored){
} finally {
if (writer != null){
writer.flush();
writer.close();
}
}
}
}

View File

@@ -0,0 +1,20 @@
package com.xuxd.kafka.console.interceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
private final TokenInterceptor tokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/devops/user/login");
}
}

View File

@@ -0,0 +1,26 @@
package com.xuxd.kafka.console.service;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.dto.user.AddUserDTO;
import com.xuxd.kafka.console.beans.dto.user.ListUserDTO;
import com.xuxd.kafka.console.beans.dto.user.UpdateUserDTO;
import com.xuxd.kafka.console.beans.vo.DevOpsUserVO;
import com.xuxd.kafka.console.beans.vo.LoginVO;
import java.util.List;
public interface DevOpsUserService {
ResponseData<Boolean> add(AddUserDTO addUserDTO);
ResponseData<Boolean> update(UpdateUserDTO updateUserDTO);
ResponseData<Boolean> delete(Long id);
ResponseData<List<DevOpsUserVO>> list(ListUserDTO listUserDTO);
ResponseData<DevOpsUserVO> detail(String username);
ResponseData<LoginVO> login(String username, String password);
}

View File

@@ -0,0 +1,99 @@
package com.xuxd.kafka.console.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.xuxd.kafka.console.beans.KafkaConsoleException;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.dos.DevOpsUserDO;
import com.xuxd.kafka.console.beans.dto.user.AddUserDTO;
import com.xuxd.kafka.console.beans.dto.user.ListUserDTO;
import com.xuxd.kafka.console.beans.dto.user.UpdateUserDTO;
import com.xuxd.kafka.console.beans.vo.DevOpsUserVO;
import com.xuxd.kafka.console.beans.vo.LoginVO;
import com.xuxd.kafka.console.boot.InitSuperDevOpsUser;
import com.xuxd.kafka.console.dao.DevOpsUserMapper;
import com.xuxd.kafka.console.service.DevOpsUserService;
import com.xuxd.kafka.console.utils.ConvertUtil;
import com.xuxd.kafka.console.utils.JwtUtils;
import com.xuxd.kafka.console.utils.Md5Utils;
import com.xuxd.kafka.console.utils.ResponseUtil;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class DevOpsServiceImpl implements DevOpsUserService {
private final DevOpsUserMapper devOpsUserMapper;
@Override
public ResponseData<Boolean> add(AddUserDTO addUserDTO) {
QueryWrapper<DevOpsUserDO> queryWrapper = new QueryWrapper<DevOpsUserDO>();
queryWrapper.eq("username", addUserDTO.getUsername());
if (devOpsUserMapper.selectOne(queryWrapper) != null){
throw new KafkaConsoleException("账号已存在");
}
addUserDTO.setPassword(Md5Utils.MD5(addUserDTO.getPassword()));
int ret = devOpsUserMapper.insert(ConvertUtil.copy(addUserDTO, DevOpsUserDO.class));
return ResponseUtil.success(ret > 0);
}
@Override
public ResponseData<Boolean> update(UpdateUserDTO updateUserDTO) {
UpdateWrapper<DevOpsUserDO> updateWrapper = new UpdateWrapper<>();
if (updateUserDTO.getRole() != null){
updateWrapper.set("role", updateUserDTO.getRole());
}
if (StringUtils.isNotBlank(updateUserDTO.getPassword())){
updateWrapper.set("password", Md5Utils.MD5(updateUserDTO.getPassword()));
}
updateWrapper.eq("username", updateUserDTO.getUsername());
int ret = devOpsUserMapper.update(null, updateWrapper);
return ResponseUtil.success(ret > 0);
}
@Override
public ResponseData<Boolean> delete(Long id) {
int ret = devOpsUserMapper.deleteById(id);
return ResponseUtil.success(ret > 0);
}
@Override
public ResponseData<List<DevOpsUserVO>> list(ListUserDTO listUserDTO) {
QueryWrapper<DevOpsUserDO> queryWrapper = new QueryWrapper<DevOpsUserDO>();
if (listUserDTO.getId() != null){
queryWrapper.eq("id", listUserDTO.getId());
}
if (StringUtils.isNotBlank(listUserDTO.getUsername())){
queryWrapper.eq("username", listUserDTO.getUsername());
}
queryWrapper.ne("username", InitSuperDevOpsUser.SUPER_USERNAME);
List<DevOpsUserDO> userDOS = devOpsUserMapper.selectList(queryWrapper);
return ResponseUtil.success(ConvertUtil.copyList(userDOS, DevOpsUserVO.class));
}
@Override
public ResponseData<DevOpsUserVO> detail(String username) {
QueryWrapper<DevOpsUserDO> queryWrapper = new QueryWrapper<DevOpsUserDO>();
queryWrapper.eq("username", username);
DevOpsUserDO userDO = devOpsUserMapper.selectOne(queryWrapper);
return ResponseUtil.success(ConvertUtil.copy(userDO, DevOpsUserVO.class));
}
@Override
public ResponseData<LoginVO> login(String username, String password) {
QueryWrapper<DevOpsUserDO> queryWrapper = new QueryWrapper<DevOpsUserDO>();
queryWrapper.eq("username", username);
queryWrapper.eq("password", Md5Utils.MD5(password));
DevOpsUserDO userDO = devOpsUserMapper.selectOne(queryWrapper);
if (userDO == null){
throw new KafkaConsoleException("用户名或密码错误");
}
LoginVO loginVO = LoginVO.builder().role(userDO.getRole()).token(JwtUtils.sign(username)).build();
return ResponseUtil.success(loginVO);
}
}

View File

@@ -0,0 +1,23 @@
package com.xuxd.kafka.console.utils;
import java.util.HashMap;
import java.util.Map;
public class ContextUtil {
public static final String USERNAME = "username" ;
private static ThreadLocal<Map<String, Object>> context = ThreadLocal.withInitial(() -> new HashMap<>());
public static void set(String key, Object value){
context.get().put(key, value);
}
public static String get(String key){
return (String) context.get().get(key);
}
public static void clear(){
context.remove();
}
}

View File

@@ -1,17 +1,15 @@
package com.xuxd.kafka.console.utils; package com.xuxd.kafka.console.utils;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cglib.beans.BeanCopier;
import org.springframework.objenesis.ObjenesisStd;
import org.springframework.util.ClassUtils;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.*;
import java.util.HashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ClassUtils;
/** /**
* kafka-console-ui. * kafka-console-ui.
@@ -22,6 +20,47 @@ import org.springframework.util.ClassUtils;
@Slf4j @Slf4j
public class ConvertUtil { public class ConvertUtil {
private static ThreadLocal<ObjenesisStd> objenesisStdThreadLocal = ThreadLocal.withInitial(ObjenesisStd::new);
private static ConcurrentHashMap<Class<?>, ConcurrentHashMap<Class<?>, BeanCopier>> cache = new ConcurrentHashMap<>();
public static <T> T copy(Object source, Class<T> target) {
return copy(source, objenesisStdThreadLocal.get().newInstance(target));
}
public static <T> T copy(Object source, T target) {
if (null == source) {
return null;
}
BeanCopier beanCopier = getCacheBeanCopier(source.getClass(), target.getClass());
beanCopier.copy(source, target, null);
return target;
}
public static <T> List<T> copyList(List<?> sources, Class<T> target) {
if (sources.isEmpty()) {
return Collections.emptyList();
}
ArrayList<T> list = new ArrayList<>(sources.size());
ObjenesisStd objenesisStd = objenesisStdThreadLocal.get();
for (Object source : sources) {
if (source == null) {
break;
}
T newInstance = objenesisStd.newInstance(target);
BeanCopier beanCopier = getCacheBeanCopier(source.getClass(), target);
beanCopier.copy(source, newInstance, null);
list.add(newInstance);
}
return list;
}
private static <S, T> BeanCopier getCacheBeanCopier(Class<S> source, Class<T> target) {
ConcurrentHashMap<Class<?>, BeanCopier> copierConcurrentHashMap =
cache.computeIfAbsent(source, aClass -> new ConcurrentHashMap<>(16));
return copierConcurrentHashMap.computeIfAbsent(target, aClass -> BeanCopier.create(source, target, false));
}
public static Map<String, Object> toMap(Object src) { public static Map<String, Object> toMap(Object src) {
Preconditions.checkNotNull(src); Preconditions.checkNotNull(src);
Map<String, Object> res = new HashMap<>(); Map<String, Object> res = new HashMap<>();

View File

@@ -0,0 +1,43 @@
package com.xuxd.kafka.console.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtUtils {
private static final String ISSUER = "kafka-console-ui";
private static final long EXPIRE_TIME = 5 * 24 * 60 * 60 * 1000;
private static final String PRIVATE_KEY = "~hello!kafka=console^ui";
public static String sign(String username){
Map<String,Object> header = new HashMap<>();
header.put("typ","JWT");
header.put("alg","HS256");
Map<String,Object> claims = new HashMap<>();
claims.put("username", username);
return Jwts.builder()
.setIssuer(ISSUER)
.setHeader(header)
.setClaims(claims)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME))
.signWith(SignatureAlgorithm.HS256, PRIVATE_KEY)
.compact();
}
public static String parse(String token){
try{
Claims claims = Jwts.parser()
.setSigningKey(PRIVATE_KEY)
.parseClaimsJws(token).getBody();
return (String) claims.get("username");
}catch (Exception e){
return null;
}
}
}

View File

@@ -0,0 +1,12 @@
package com.xuxd.kafka.console.utils;
import org.springframework.util.DigestUtils;
import java.nio.charset.StandardCharsets;
public class Md5Utils {
public static String MD5(String s) {
return DigestUtils.md5DigestAsHex(s.getBytes(StandardCharsets.UTF_8));
}
}

View File

@@ -0,0 +1,15 @@
package com.xuxd.kafka.console.utils;
import com.xuxd.kafka.console.beans.ResponseData;
public class ResponseUtil {
public static <T> ResponseData<T> success(T data) {
return ResponseData.create().data(data);
}
public static ResponseData<String> error(String msg) {
return ResponseData.create().failed(msg);
}
}

View File

@@ -35,3 +35,17 @@ CREATE TABLE IF NOT EXISTS T_CLUSTER_INFO
PRIMARY KEY (ID), PRIMARY KEY (ID),
UNIQUE (CLUSTER_NAME) UNIQUE (CLUSTER_NAME)
); );
-- 用户表
CREATE TABLE IF NOT EXISTS T_DEVOPS_USER
(
ID IDENTITY NOT NULL COMMENT '主键ID',
USERNAME VARCHAR(50) NOT NULL DEFAULT '' COMMENT '用户名',
PASSWORD VARCHAR(50) NOT NULL DEFAULT '' COMMENT '密码',
`ROLE` VARCHAR(16) NOT NULL DEFAULT 'developer' COMMENT '角色',
`DELETE` TINYINT(1) NOT NULL DEFAULT '' COMMENT '删除标记',
CREATE_TIME TIMESTAMP NOT NULL DEFAULT NOW() COMMENT '创建时间',
UPDATE_TIME TIMESTAMP NOT NULL DEFAULT NOW() COMMENT '更新时间',
PRIMARY KEY (ID),
UNIQUE (USERNAME)
);

13125
ui/package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -17,11 +17,6 @@
"vuex": "^3.4.0" "vuex": "^3.4.0"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/eslint-config-prettier": "^6.0.0", "@vue/eslint-config-prettier": "^6.0.0",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"eslint": "^6.7.2", "eslint": "^6.7.2",

View File

@@ -1,127 +1,5 @@
<template> <template>
<div id="app"> <div id="app">
<div id="nav"> <router-view />
<h2 class="logo">Kafka 控制台</h2>
<router-link to="/" class="pad-l-r">主页</router-link>
<span>|</span
><router-link to="/cluster-page" class="pad-l-r">集群</router-link>
<span>|</span
><router-link to="/topic-page" class="pad-l-r">Topic</router-link>
<span>|</span
><router-link to="/group-page" class="pad-l-r">消费组</router-link>
<span>|</span
><router-link to="/message-page" class="pad-l-r">消息</router-link>
<span v-show="enableSasl">|</span
><router-link to="/acl-page" class="pad-l-r" v-show="enableSasl"
>Acl</router-link
>
<span>|</span
><router-link to="/op-page" class="pad-l-r">运维</router-link>
<span class="right">集群{{ clusterName }}</span>
</div>
<router-view class="content" />
</div> </div>
</template> </template>
<script>
import { KafkaClusterApi } from "@/utils/api";
import request from "@/utils/request";
import { mapMutations, mapState } from "vuex";
import { getClusterInfo } from "@/utils/local-cache";
import notification from "ant-design-vue/lib/notification";
import { CLUSTER } from "@/store/mutation-types";
export default {
data() {
return {
config: {},
};
},
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);
}
},
computed: {
...mapState({
clusterName: (state) => state.clusterInfo.clusterName,
enableSasl: (state) => state.clusterInfo.enableSasl,
}),
},
methods: {
...mapMutations({
switchCluster: CLUSTER.SWITCH,
}),
},
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}
#app {
width: 100%;
height: 100%;
}
#nav {
background-color: #9fe0e0;
font-size: large;
padding-top: 1%;
padding-bottom: 1%;
margin-bottom: 1%;
text-align: center;
}
#nav a {
font-weight: bold;
color: #2c3e50;
}
#nav a.router-link-exact-active {
color: #61c126;
}
.pad-l-r {
padding-left: 10px;
padding-right: 10px;
}
.content {
padding-left: 2%;
padding-right: 2%;
height: 90%;
width: 100%;
}
.logo {
float: left;
left: 1%;
top: 1%;
position: absolute;
}
.right {
float: right;
right: 1%;
top: 2%;
position: absolute;
}
</style>

BIN
ui/src/assets/bg.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

View File

@@ -0,0 +1,162 @@
<template>
<div id="main">
<div id="nav">
<h2 class="logo">Kafka 控制台</h2>
<span v-show="manager">
<router-link to="/home" class="pad-l-r">主页</router-link>
<span>|</span>
</span>
<span>
<router-link to="/cluster-page" class="pad-l-r">集群</router-link>
</span>
<span>
<span>|</span>
<router-link to="/topic-page" class="pad-l-r">Topic</router-link>
</span>
<span>
<span>|</span>
<router-link to="/group-page" class="pad-l-r">消费组</router-link>
</span>
<span>
<span>|</span>
<router-link to="/message-page" class="pad-l-r">消息</router-link>
</span>
<span v-show="manager && enableSasl">
<span>|</span>
<router-link to="/acl-page" class="pad-l-r">Acl</router-link>
</span>
<span>
<span>|</span>
<router-link to="/op-page" class="pad-l-r">运维</router-link>
</span>
<span v-show="manager">
<span>|</span>
<router-link to="/devops/user" class="pad-l-r">用户</router-link>
</span>
<span class="right">
<span>集群{{ clusterName }}</span>
<span> | </span>
<span @click="logout" style="cursor: pointer">登出</span>
</span>
</div>
<router-view class="content" />
</div>
</template>
<script>
import { KafkaClusterApi } from "@/utils/api";
import request from "@/utils/request";
import { mapMutations, mapState } from "vuex";
import { getClusterInfo } from "@/utils/local-cache";
import notification from "ant-design-vue/lib/notification";
import { CLUSTER } from "@/store/mutation-types";
import {isManager} from "../utils/role";
import router from "../router";
export default {
name: "Header",
data() {
return {
manager: isManager(),
config: {},
};
},
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);
}
},
computed: {
...mapState({
clusterName: (state) => state.clusterInfo.clusterName,
enableSasl: (state) => state.clusterInfo.enableSasl,
}),
},
methods: {
...mapMutations({
switchCluster: CLUSTER.SWITCH,
}),
logout: function (){
localStorage.clear();
router.push("/")
}
},
};
</script>
<style>
#main {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}
#main {
width: 100%;
height: 100%;
}
#nav {
background-color: #9fe0e0;
font-size: large;
padding-top: 1%;
padding-bottom: 1%;
margin-bottom: 1%;
text-align: center;
}
#nav a {
font-weight: bold;
color: #2c3e50;
}
#nav a.router-link-exact-active {
color: #61c126;
}
.pad-l-r {
padding-left: 10px;
padding-right: 10px;
}
.content {
padding-left: 2%;
padding-right: 2%;
height: 90%;
width: 100%;
}
.logo {
float: left;
left: 1%;
top: 1%;
position: absolute;
}
.cluster {
float: right;
right: 8%;
top: 2%;
position: absolute;
}
.right {
float: right;
right: 2%;
top: 2%;
position: absolute;
}
</style>

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

@@ -1,12 +1,30 @@
import Vue from "vue"; import Vue from "vue";
import VueRouter from "vue-router"; import VueRouter from "vue-router";
import Home from "../views/Home.vue"; import Home from "../views/home/Home.vue";
import Login from "@/views/login/index";
Vue.use(VueRouter); Vue.use(VueRouter);
const routes = [ const routes = [
{ {
path: "/", path: "/",
name: "Login",
component: Login,
},
{
path: "/main",
name: "Main",
component: () =>
import( "../components/Header"),
},
{
path: "/devops/user",
name: "DevOpsUser",
component: () =>
import( "../views/user/index"),
},
{
path: "/home",
name: "Home", name: "Home",
component: Home, component: Home,
}, },

View File

@@ -92,6 +92,29 @@ export const KafkaConfigApi = {
}, },
}; };
export const DevOpsUserAPi = {
createUser: {
url: "/devops/user/add",
method: "post",
},
userList: {
url: "/devops/user/list",
method: "get",
},
deleteUser: {
url: "/devops/user/",
method: "delete",
},
updateUser: {
url: "/devops/user/update",
method: "post",
},
login: {
url: "/devops/user/login",
method: "post",
},
}
export const KafkaTopicApi = { export const KafkaTopicApi = {
getTopicNameList: { getTopicNameList: {
url: "/topic", url: "/topic",

View File

@@ -1,4 +1,5 @@
import axios from "axios"; import axios from "axios";
import router from "../router";
import notification from "ant-design-vue/es/notification"; import notification from "ant-design-vue/es/notification";
import { VueAxios } from "./axios"; import { VueAxios } from "./axios";
import { getClusterInfo } from "@/utils/local-cache"; import { getClusterInfo } from "@/utils/local-cache";
@@ -25,6 +26,7 @@ const errorHandler = (error) => {
// request interceptor // request interceptor
request.interceptors.request.use((config) => { request.interceptors.request.use((config) => {
const clusterInfo = getClusterInfo(); const clusterInfo = getClusterInfo();
config.headers["token"] = localStorage.getItem('token');
if (clusterInfo) { if (clusterInfo) {
config.headers["X-Cluster-Info-Id"] = clusterInfo.id; config.headers["X-Cluster-Info-Id"] = clusterInfo.id;
// config.headers["X-Cluster-Info-Name"] = encodeURIComponent(clusterInfo.clusterName); // config.headers["X-Cluster-Info-Name"] = encodeURIComponent(clusterInfo.clusterName);
@@ -34,6 +36,10 @@ request.interceptors.request.use((config) => {
// response interceptor // response interceptor
request.interceptors.response.use((response) => { request.interceptors.response.use((response) => {
if (response.data.code === -5000){
router.push({ path:'/'})
return
}
return response.data; return response.data;
}, errorHandler); }, errorHandler);

3
ui/src/utils/role.js Normal file
View File

@@ -0,0 +1,3 @@
export function isManager() {
return 'manager' === localStorage.getItem("role");
}

87
ui/src/utils/validate.js Normal file
View File

@@ -0,0 +1,87 @@
/**
* Created by PanJiaChen on 16/11/18.
*/
/**
* @param {string} path
* @returns {Boolean}
*/
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validUsername(str) {
const valid_map = ['admin', 'editor']
return valid_map.indexOf(str.trim()) >= 0
}
/**
* @param {string} url
* @returns {Boolean}
*/
export function validURL(url) {
const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
return reg.test(url)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validLowerCase(str) {
const reg = /^[a-z]+$/
return reg.test(str)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validUpperCase(str) {
const reg = /^[A-Z]+$/
return reg.test(str)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validAlphabets(str) {
const reg = /^[A-Za-z]+$/
return reg.test(str)
}
/**
* @param {string} email
* @returns {Boolean}
*/
export function validEmail(email) {
const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
return reg.test(email)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function isString(str) {
if (typeof str === 'string' || str instanceof String) {
return true
}
return false
}
/**
* @param {Array} arg
* @returns {Boolean}
*/
export function isArray(arg) {
if (typeof Array.isArray === 'undefined') {
return Object.prototype.toString.call(arg) === '[object Array]'
}
return Array.isArray(arg)
}

View File

@@ -1,4 +1,6 @@
<template> <template>
<div>
<Header/>
<div class="content"> <div class="content">
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<div class="acl"> <div class="acl">
@@ -153,6 +155,7 @@
</div> </div>
</a-spin> </a-spin>
</div> </div>
</div>
</template> </template>
<script> <script>
@@ -165,7 +168,7 @@ import ManageConsumerAuth from "@/views/acl/ManageConsumerAuth";
import AddAuth from "@/views/acl/AddAuth"; import AddAuth from "@/views/acl/AddAuth";
import AclDetail from "@/views/acl/AclDetail"; import AclDetail from "@/views/acl/AclDetail";
import UserDetail from "@/views/acl/UserDetail"; import UserDetail from "@/views/acl/UserDetail";
import Header from "@/components/Header"
export default { export default {
name: "Acl", name: "Acl",
components: { components: {
@@ -175,6 +178,7 @@ export default {
AddAuth, AddAuth,
AclDetail, AclDetail,
UserDetail, UserDetail,
Header
}, },
data() { data() {
return { return {

View File

@@ -1,4 +1,6 @@
<template> <template>
<div>
<Header/>
<div class="content"> <div class="content">
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<div class="body-c"> <div class="body-c">
@@ -15,6 +17,7 @@
</div> </div>
<div slot="operation" slot-scope="record" v-show="!record.internal"> <div slot="operation" slot-scope="record" v-show="!record.internal">
<a-button <a-button
v-show="manager"
size="small" size="small"
href="javascript:;" href="javascript:;"
class="operation-btn" class="operation-btn"
@@ -39,6 +42,7 @@
></BrokerConfig> ></BrokerConfig>
</a-spin> </a-spin>
</div> </div>
</div>
</template> </template>
<script> <script>
@@ -46,12 +50,14 @@ import request from "@/utils/request";
import { KafkaClusterApi } from "@/utils/api"; import { KafkaClusterApi } from "@/utils/api";
import BrokerConfig from "@/views/cluster/BrokerConfig"; import BrokerConfig from "@/views/cluster/BrokerConfig";
import notification from "ant-design-vue/lib/notification"; import notification from "ant-design-vue/lib/notification";
import Header from "@/components/Header"
import {isManager} from "../../utils/role";
export default { export default {
name: "Topic", name: "Topic",
components: { BrokerConfig }, components: { BrokerConfig, Header },
data() { data() {
return { return {
manager: isManager(),
data: [], data: [],
columns, columns,
loading: false, loading: false,

View File

@@ -1,4 +1,6 @@
<template> <template>
<div>
<Header/>
<div class="content"> <div class="content">
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<div class="topic"> <div class="topic">
@@ -60,7 +62,7 @@
</a-row> </a-row>
</a-form> </a-form>
</div> </div>
<div class="operation-row-button"> <div v-show="manager" class="operation-row-button">
<a-button type="primary" @click="openAddSubscriptionDialog" <a-button type="primary" @click="openAddSubscriptionDialog"
>新增订阅</a-button >新增订阅</a-button
> >
@@ -89,7 +91,7 @@
cancel-text="取消" cancel-text="取消"
@confirm="deleteGroup(record.groupId)" @confirm="deleteGroup(record.groupId)"
> >
<a-button size="small" href="javascript:;" class="operation-btn" <a-button v-show="manager" size="small" href="javascript:;" class="operation-btn"
>删除 >删除
</a-button> </a-button>
</a-popconfirm> </a-popconfirm>
@@ -101,6 +103,7 @@
>消费端 >消费端
</a-button> </a-button>
<a-button <a-button
v-show="manager"
size="small" size="small"
href="javascript:;" href="javascript:;"
class="operation-btn" class="operation-btn"
@@ -140,6 +143,7 @@
</div> </div>
</a-spin> </a-spin>
</div> </div>
</div>
</template> </template>
<script> <script>
@@ -150,12 +154,14 @@ import Member from "@/views/group/Member";
import ConsumerDetail from "@/views/group/ConsumerDetail"; import ConsumerDetail from "@/views/group/ConsumerDetail";
import AddSupscription from "@/views/group/AddSupscription"; import AddSupscription from "@/views/group/AddSupscription";
import OffsetTopicPartition from "@/views/group/OffsetTopicPartition"; import OffsetTopicPartition from "@/views/group/OffsetTopicPartition";
import Header from "@/components/Header"
import {isManager} from "../../utils/role";
export default { export default {
name: "ConsumerGroup", name: "ConsumerGroup",
components: { Member, ConsumerDetail, AddSupscription, OffsetTopicPartition }, components: { Member, ConsumerDetail, AddSupscription, OffsetTopicPartition, Header },
data() { data() {
return { return {
manager: isManager(),
queryParam: {}, queryParam: {},
data: [], data: [],
columns, columns,

View File

@@ -1,5 +1,7 @@
<template> <template>
<div class="home"> <div>
<Header/>
<div class="content">
<a-card title="控制台默认配置" class="card-style"> <a-card title="控制台默认配置" class="card-style">
<p v-for="(v, k) in config" :key="k">{{ k }}={{ v }}</p> <p v-for="(v, k) in config" :key="k">{{ k }}={{ v }}</p>
</a-card> </a-card>
@@ -31,6 +33,7 @@
> >
</VersionInfo> </VersionInfo>
</div> </div>
</div>
</template> </template>
<script> <script>
@@ -39,9 +42,10 @@ import request from "@/utils/request";
import { KafkaConfigApi, KafkaClusterApi } from "@/utils/api"; import { KafkaConfigApi, KafkaClusterApi } from "@/utils/api";
import notification from "ant-design-vue/lib/notification"; import notification from "ant-design-vue/lib/notification";
import VersionInfo from "@/views/home/VersionInfo"; import VersionInfo from "@/views/home/VersionInfo";
import Header from "@/components/Header"
export default { export default {
name: "Home", name: "Home",
components: { VersionInfo }, components: { VersionInfo, Header },
data() { data() {
return { return {
config: {}, config: {},

View File

@@ -0,0 +1,99 @@
<template>
<div id="login">
<div class="kafka-console-ui">
<span style="font-size: xxx-large; font-weight: bold">kafka-console-ui</span>
</div>
<div>
<a-form
:form="form"
:label-col="{ span: 10 }"
:wrapper-col="{ span: 4 }"
@submit="handleSubmit"
>
<a-form-item label="账号">
<a-input
v-decorator="[
'username',
{ rules: [{ required: true, message: '请输入账号' }] },
]"
placeholder="请输入账号"
/>
</a-form-item>
<a-form-item label="密码">
<a-input
v-decorator="[
'password',
{ rules: [{ required: true, message: '请输入密码' }] },
]"
placeholder="请输入密码"
/>
</a-form-item>
<a-form-item :wrapper-col="{ span: 10, offset: 10 }">
<a-button type="primary" html-type="submit"> 提交 </a-button>
</a-form-item>
</a-form>
</div>
<router-view></router-view>
</div>
</template>
<script>
import notification from "ant-design-vue/lib/notification";
import request from "@/utils/request";
import { DevOpsUserAPi } from "@/utils/api";
export default {
name: 'login',
data(){
return{
form: this.$form.createForm(this, { name: "coordinated" }),
}
},methods:{
handleSubmit(e){
e.preventDefault();
this.form.validateFields((err, values) => {
if (!err) {
if (values.configs) {
const config = {};
values.configs.split("\n").forEach((e) => {
const c = e.split("=");
if (c.length > 1) {
let k = c[0].trim(),
v = c[1].trim();
if (k && v) {
config[k] = v;
}
}
});
values.configs = config;
}
request({
url: DevOpsUserAPi.login.url,
method: DevOpsUserAPi.login.method,
data: values,
}).then((res) => {
if (res.code == 0) {
localStorage.setItem('token', res.data.token)
localStorage.setItem('role', res.data.role)
this.$router.push({ path:'/main'})
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
}
});
}
}
}
</script>
<style>
.kafka-console-ui{
text-align: center;
height: 100px;
margin-top: 50px;
margin-bottom: 20px;
}
</style>

View File

@@ -1,4 +1,6 @@
<template> <template>
<div>
<Header/>
<div class="content"> <div class="content">
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<a-tabs default-active-key="1" size="large" tabPosition="top"> <a-tabs default-active-key="1" size="large" tabPosition="top">
@@ -14,6 +16,7 @@
</a-tabs> </a-tabs>
</a-spin> </a-spin>
</div> </div>
</div>
</template> </template>
<script> <script>
@@ -23,9 +26,10 @@ import request from "@/utils/request";
import { KafkaTopicApi } from "@/utils/api"; import { KafkaTopicApi } from "@/utils/api";
import notification from "ant-design-vue/lib/notification"; import notification from "ant-design-vue/lib/notification";
import SendMessage from "@/views/message/SendMessage"; import SendMessage from "@/views/message/SendMessage";
import Header from "@/components/Header"
export default { export default {
name: "Message", name: "Message",
components: { SearchByTime, SearchByOffset, SendMessage }, components: { SearchByTime, SearchByOffset, SendMessage, Header },
data() { data() {
return { return {
loading: false, loading: false,

View File

@@ -1,4 +1,6 @@
<template> <template>
<div>
<Header/>
<div class="content"> <div class="content">
<div class="content-module"> <div class="content-module">
<a-card title="集群管理" style="width: 100%; text-align: left"> <a-card title="集群管理" style="width: 100%; text-align: left">
@@ -13,7 +15,7 @@
</p> </p>
</a-card> </a-card>
</div> </div>
<div class="content-module"> <div class="content-module" v-show="manager">
<a-card title="Broker管理" style="width: 100%; text-align: left"> <a-card title="Broker管理" style="width: 100%; text-align: left">
<p> <p>
<a-button type="primary" @click="openConfigThrottleDialog"> <a-button type="primary" @click="openConfigThrottleDialog">
@@ -33,7 +35,7 @@
</p> </p>
</a-card> </a-card>
</div> </div>
<div class="content-module"> <div class="content-module" v-show="manager">
<a-card title="副本管理" style="width: 100%; text-align: left"> <a-card title="副本管理" style="width: 100%; text-align: left">
<p> <p>
<a-button type="primary" @click="openElectPreferredLeaderDialog"> <a-button type="primary" @click="openElectPreferredLeaderDialog">
@@ -140,6 +142,7 @@
> >
</ReplicaReassign> </ReplicaReassign>
</div> </div>
</div>
</template> </template>
<script> <script>
@@ -153,6 +156,8 @@ import RemoveThrottle from "@/views/op/RemoveThrottle";
import CurrentReassignments from "@/views/op/CurrentReassignments"; import CurrentReassignments from "@/views/op/CurrentReassignments";
import ClusterInfo from "@/views/op/ClusterInfo"; import ClusterInfo from "@/views/op/ClusterInfo";
import ReplicaReassign from "@/views/op/ReplicaReassign"; import ReplicaReassign from "@/views/op/ReplicaReassign";
import Header from "@/components/Header"
import {isManager} from "../../utils/role";
export default { export default {
name: "Operation", name: "Operation",
components: { components: {
@@ -166,9 +171,11 @@ export default {
CurrentReassignments, CurrentReassignments,
ClusterInfo, ClusterInfo,
ReplicaReassign, ReplicaReassign,
Header
}, },
data() { data() {
return { return {
manager: isManager(),
syncData: { syncData: {
showSyncConsumerOffsetDialog: false, showSyncConsumerOffsetDialog: false,
showMinOffsetAlignmentDialog: false, showMinOffsetAlignmentDialog: false,

View File

@@ -31,7 +31,7 @@
{{ i }} {{ i }}
</span> </span>
</div> </div>
<div slot="operation" slot-scope="record" v-show="!record.internal"> <div slot="operation" slot-scope="record" v-show="!record.internal && manager">
<a-popconfirm <a-popconfirm
:title=" :title="
'topic: ' + 'topic: ' +
@@ -68,6 +68,7 @@ import request from "@/utils/request";
import { KafkaOpApi, KafkaTopicApi } from "@/utils/api"; import { KafkaOpApi, KafkaTopicApi } from "@/utils/api";
import notification from "ant-design-vue/es/notification"; import notification from "ant-design-vue/es/notification";
import moment from "moment"; import moment from "moment";
import {isManager} from "../../utils/role";
export default { export default {
name: "PartitionInfo", name: "PartitionInfo",
props: { props: {
@@ -82,6 +83,7 @@ export default {
}, },
data() { data() {
return { return {
manager: isManager(),
columns: columns, columns: columns,
show: this.visible, show: this.visible,
data: [], data: [],

View File

@@ -1,4 +1,6 @@
<template> <template>
<div>
<Header/>
<div class="content"> <div class="content">
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<div class="topic"> <div class="topic">
@@ -45,7 +47,7 @@
</a-row> </a-row>
</a-form> </a-form>
</div> </div>
<div class="operation-row-button"> <div v-show="manager" class="operation-row-button">
<a-button type="primary" @click="openCreateTopicDialog" <a-button type="primary" @click="openCreateTopicDialog"
>新增</a-button >新增</a-button
> >
@@ -68,6 +70,7 @@
<div slot="operation" slot-scope="record" v-show="!record.internal"> <div slot="operation" slot-scope="record" v-show="!record.internal">
<a-popconfirm <a-popconfirm
v-show="manager"
:title="'删除topic: ' + record.name + ''" :title="'删除topic: ' + record.name + ''"
ok-text="确认" ok-text="确认"
cancel-text="取消" cancel-text="取消"
@@ -78,6 +81,7 @@
</a-button> </a-button>
</a-popconfirm> </a-popconfirm>
<a-button <a-button
v-show="manager"
size="small" size="small"
href="javascript:;" href="javascript:;"
class="operation-btn" class="operation-btn"
@@ -85,6 +89,7 @@
>分区详情 >分区详情
</a-button> </a-button>
<a-button <a-button
v-show="manager"
size="small" size="small"
href="javascript:;" href="javascript:;"
class="operation-btn" class="operation-btn"
@@ -99,6 +104,7 @@
>消费详情 >消费详情
</a-button> </a-button>
<a-button <a-button
v-show="manager"
size="small" size="small"
href="javascript:;" href="javascript:;"
class="operation-btn" class="operation-btn"
@@ -106,6 +112,7 @@
>属性配置 >属性配置
</a-button> </a-button>
<a-button <a-button
v-show="manager"
size="small" size="small"
href="javascript:;" href="javascript:;"
class="operation-btn" class="operation-btn"
@@ -120,6 +127,7 @@
>发送统计 >发送统计
</a-button> </a-button>
<a-button <a-button
v-show="manager"
size="small" size="small"
href="javascript:;" href="javascript:;"
class="operation-btn" class="operation-btn"
@@ -172,6 +180,7 @@
</div> </div>
</a-spin> </a-spin>
</div> </div>
</div>
</template> </template>
<script> <script>
@@ -186,7 +195,8 @@ import TopicConfig from "@/views/topic/TopicConfig";
import UpdateReplica from "@/views/topic/UpdateReplica"; import UpdateReplica from "@/views/topic/UpdateReplica";
import ConfigTopicThrottle from "@/views/topic/ConfigTopicThrottle"; import ConfigTopicThrottle from "@/views/topic/ConfigTopicThrottle";
import SendStats from "@/views/topic/SendStats"; import SendStats from "@/views/topic/SendStats";
import Header from "@/components/Header"
import {isManager} from "../../utils/role";
export default { export default {
name: "Topic", name: "Topic",
components: { components: {
@@ -198,9 +208,11 @@ export default {
UpdateReplica, UpdateReplica,
ConfigTopicThrottle, ConfigTopicThrottle,
SendStats, SendStats,
Header
}, },
data() { data() {
return { return {
manager: isManager(),
queryParam: { type: "normal" }, queryParam: { type: "normal" },
data: [], data: [],
columns, columns,

View File

@@ -0,0 +1,135 @@
<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-input
v-decorator="[
'password',
{ rules: [{ required: true, message: '请输入密码!' }] },
]"
placeholder="请输入密码"
/>
</a-form-item>
<a-form-item label="角色">
<a-select
option-filter-prop="role"
v-decorator="[
'role',
{ rules: [{ required: true, message: '请选择一个角色!' }] },
]"
placeholder="请选择一个角色"
>
<a-select-option v-for="v in roleList" :key="v" :value="v">
{{ v }}
</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 {DevOpsUserAPi} from "../../utils/api";
export default {
name: "CreateUser",
props: {
visible: {
type: Boolean,
default: false,
},
},
data() {
return {
show: this.visible,
data: [],
roleList: roleList,
loading: false,
form: this.$form.createForm(this, { name: "coordinated" }),
};
},
watch: {
visible(v) {
this.show = v;
},
},
methods: {
handleSubmit(e) {
e.preventDefault();
this.form.validateFields((err, values) => {
if (!err) {
if (values.configs) {
const config = {};
values.configs.split("\n").forEach((e) => {
const c = e.split("=");
if (c.length > 1) {
let k = c[0].trim(),
v = c[1].trim();
if (k && v) {
config[k] = v;
}
}
});
values.configs = config;
}
this.loading = true;
request({
url: DevOpsUserAPi.createUser.url,
method: DevOpsUserAPi.createUser.method,
data: values,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.$message.success(res.msg);
this.$emit("closeCreateUserDialog", { refresh: true });
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
}
});
},
handleCancel() {
this.data = [];
this.$emit("closeCreateUserDialog", { refresh: false });
},
},
};
const roleList = ["developer", "manager"]
</script>
<style scoped></style>

View File

@@ -0,0 +1,123 @@
<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
:disabled="true"
v-decorator="['username', { initialValue: username }]"
placeholder="username"
/>
</a-form-item>
<a-form-item label="密码">
<a-input
v-decorator="[
'password',
{ rules: [{ required: true, message: '请输入密码!' }] },
]"
placeholder="请输入密码"
/>
</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 { DevOpsUserAPi } from "@/utils/api";
import notification from "ant-design-vue/es/notification";
export default {
name: "UpdatePassword",
props: {
username: {
type: String,
default: "",
},
visible: {
type: Boolean,
default: false,
},
},
data() {
return {
show: this.visible,
data: [],
loading: false,
form: this.$form.createForm(this, { name: "coordinated" }),
};
},
watch: {
visible(v) {
this.show = v;
},
},
methods: {
handleSubmit(e) {
e.preventDefault();
this.form.validateFields((err, values) => {
if (!err) {
if (values.assignment) {
const assignment = {};
values.assignment.split("\n").forEach((e) => {
const c = e.split("=");
if (c.length > 1) {
let k = c[0];
let v = c[1];
let arr = v.split(",");
if (arr.length > 0) {
assignment[k] = arr;
}
}
});
values.assignment = assignment;
}
this.loading = true;
request({
url: DevOpsUserAPi.updateUser.url,
method: DevOpsUserAPi.updateUser.method,
data: values,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.$message.success(res.msg);
this.$emit("closeResetPasswordDialog", { refresh: true });
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
}
});
},
handleCancel() {
this.data = [];
this.$emit("closeResetPasswordDialog", { refresh: false });
},
},
};
</script>
<style scoped></style>

227
ui/src/views/user/index.vue Normal file
View File

@@ -0,0 +1,227 @@
<template>
<div>
<Header/>
<div class="content">
<a-spin :spinning="loading">
<div class="user">
<div class="operation-row-button">
<a-button type="primary" @click="openCreateUserDialog"
>新增</a-button
>
</div>
<a-table
:columns="columns"
:data-source="data"
bordered
row-key="name"
>
<div slot="role" slot-scope="text, record">
<a-select
@change="handleRoleChange(record.username, text)"
v-model="text"
option-filter-prop="role"
v-decorator="['role']"
style="width: 200px"
>
<a-select-option v-for="v in roleList" :key="v" :value="v">
{{ v }}
</a-select-option>
</a-select>
</div>
<div slot="operation" slot-scope="record">
<a-popconfirm
:title="'删除用户: ' + record.username + ' '"
ok-text="确认"
cancel-text="取消"
@confirm="deleteUser(record.id)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
>删除
</a-button>
</a-popconfirm>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="resetPassword(record.username)"
>重置密码
</a-button>
</div>
</a-table>
<CreateUser
:visible="showCreateUser"
@closeCreateUserDialog="closeCreateUserDialog"
>
</CreateUser>
<ResetPassword
:visible="showResetPassword"
:username="selectDetail.resourceName"
@closeResetPasswordDialog="closeResetPasswordDialog"
></ResetPassword>
</div>
</a-spin>
</div>
</div>
</template>
<script>
import request from "@/utils/request";
import notification from "ant-design-vue/es/notification";
import CreateUser from "@/views/user/CreateUser";
import ResetPassword from "@/views/user/ResetPassword"
import Header from "@/components/Header"
import {DevOpsUserAPi} from "../../utils/api";
export default {
name: "DevOpsUser",
components: {
CreateUser,
ResetPassword,
Header
},
data() {
return {
queryParam: { type: "normal" },
roleList: ["developer", "manager"],
columns,
showUpdateUser: false,
deleteUserConfirm: false,
selectDetail: {
resourceName: "",
resourceType: "",
username: "",
},
loading: false,
showCreateUser: false,
showResetPassword: false,
type: "normal",
};
},
methods: {
handleRoleChange(username, role) {
this.loading = true;
request({
url: DevOpsUserAPi.updateUser.url,
method: DevOpsUserAPi.updateUser.method,
data: {
"username": username,
"role": role
}
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.$message.success(res.msg);
this.getDevOpsUserList();
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
},
getDevOpsUserList() {
Object.assign(this.queryParam, { type: this.type });
this.loading = true;
request({
url: DevOpsUserAPi.userList.url,
method: DevOpsUserAPi.userList.method,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.data = res.data;
//this.filter();
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
},
deleteUser(id) {
request({
url: DevOpsUserAPi.deleteUser.url + "?id=" + id,
method: DevOpsUserAPi.deleteUser.method,
}).then((res) => {
if (res.code == 0) {
this.$message.success(res.msg);
this.getDevOpsUserList();
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
},
openCreateUserDialog() {
this.showCreateUser = true;
},
closeCreateUserDialog(res) {
this.showCreateUser = false;
if (res.refresh) {
this.getDevOpsUserList();
}
},
resetPassword(username) {
this.selectDetail.resourceName = username;
this.showResetPassword = true;
},
closeResetPasswordDialog(res) {
this.showResetPassword = false;
if (res.refresh) {
this.getDevOpsUserList();
}
},
},
created() {
this.getDevOpsUserList();
},
};
const columns = [
{
title: "账号",
dataIndex: "username",
key: "username",
},
{
title: "角色",
dataIndex: "role",
key: "role",
slots: { title: "role" },
scopedSlots: { customRender: "role" },
width: 300
},
{
title: "创建时间",
dataIndex: "createTime",
key: "createTime",
slots: { title: "createTime" },
width: 300
},
{
title: "操作",
key: "operation",
scopedSlots: { customRender: "operation" },
},
];
</script>
<style scoped>
.user {
width: 100%;
height: 100%;
}
.operation-row-button {
height: 4%;
text-align: left;
margin-bottom: 8px;
}
.operation-btn {
margin-right: 3%;
}
</style>