mirror of https://gitee.com/topiam/eiam
Merge remote-tracking branch 'origin/master'
commit
726d928527
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
* eiam-audit - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.audit.configuration;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
import cn.topiam.employee.support.autoconfiguration.SupportProperties;
|
||||
import static cn.topiam.employee.support.constant.EiamConstants.DEFAULT_DATE_FORMATTER_PATTERN;
|
||||
|
||||
/**
|
||||
* ElasticsearchConfiguration
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/11/3 23:31
|
||||
*/
|
||||
public class AuditDynamicIndexName {
|
||||
|
||||
private final SupportProperties supportProperties;
|
||||
|
||||
public AuditDynamicIndexName(SupportProperties supportProperties) {
|
||||
this.supportProperties = supportProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取索引
|
||||
*
|
||||
* @return {@link String}
|
||||
*/
|
||||
public String getIndexName() {
|
||||
return supportProperties.getAudit().getIndexPrefix() + LocalDate.now()
|
||||
.format(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMATTER_PATTERN));
|
||||
}
|
||||
}
|
|
@ -1,245 +0,0 @@
|
|||
/*
|
||||
* eiam-audit - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.audit.configuration;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.convert.ReadingConverter;
|
||||
import org.springframework.data.convert.WritingConverter;
|
||||
import org.springframework.data.elasticsearch.core.convert.ElasticsearchCustomConversions;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import cn.topiam.employee.audit.enums.EventStatus;
|
||||
import cn.topiam.employee.audit.enums.TargetType;
|
||||
import cn.topiam.employee.audit.event.type.EventType;
|
||||
import cn.topiam.employee.support.autoconfiguration.SupportProperties;
|
||||
import cn.topiam.employee.support.exception.TopIamException;
|
||||
import cn.topiam.employee.support.geo.GeoLocationProvider;
|
||||
import cn.topiam.employee.support.security.userdetails.UserType;
|
||||
import cn.topiam.employee.support.util.JsonUtils;
|
||||
import static cn.topiam.employee.common.geo.maxmind.MaxmindGeoLocationServiceImpl.MAXMIND;
|
||||
import static cn.topiam.employee.support.security.userdetails.UserType.*;
|
||||
import static cn.topiam.employee.support.security.userdetails.UserType.UNKNOWN;
|
||||
|
||||
/**
|
||||
* ElasticsearchConfiguration
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/11/3 23:31
|
||||
*/
|
||||
@Configuration
|
||||
public class AuditElasticsearchConfiguration {
|
||||
|
||||
@Bean
|
||||
public AuditDynamicIndexName auditDynamicIndexName(SupportProperties supportProperties) {
|
||||
return new AuditDynamicIndexName(supportProperties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ElasticsearchCustomConversions elasticsearchCustomConversions() {
|
||||
return new ElasticsearchCustomConversions(
|
||||
Lists.newArrayList(AuditTypeToStringConverter.INSTANCE,
|
||||
StringToAuditTypeConverter.INSTANCE, EventStatusToStringConverter.INSTANCE,
|
||||
StringToEventStatusConverter.INSTANCE, ActorTypeToStringConverter.INSTANCE,
|
||||
StringToActorTypeConverter.INSTANCE, GeoLocationProviderToStringConverter.INSTANCE,
|
||||
StringToGeoLocationProviderConverter.INSTANCE, TargetTypeToStringConverter.INSTANCE,
|
||||
StringToTargetTypeConverter.INSTANCE, StringToSetConverter.INSTANCE,
|
||||
SetToStringConverter.INSTANCE));
|
||||
}
|
||||
|
||||
@WritingConverter
|
||||
enum AuditTypeToStringConverter implements Converter<EventType, String> {
|
||||
/**
|
||||
* INSTANCE
|
||||
*/
|
||||
INSTANCE,;
|
||||
|
||||
@Override
|
||||
public String convert(EventType source) {
|
||||
return source.getCode();
|
||||
}
|
||||
}
|
||||
|
||||
@ReadingConverter
|
||||
enum StringToAuditTypeConverter implements Converter<String, EventType> {
|
||||
/**
|
||||
*INSTANCE
|
||||
*/
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public EventType convert(@NotNull String source) {
|
||||
return EventType.getType(source);
|
||||
}
|
||||
}
|
||||
|
||||
@WritingConverter
|
||||
enum ActorTypeToStringConverter implements Converter<UserType, String> {
|
||||
/**
|
||||
* INSTANCE
|
||||
*/
|
||||
INSTANCE,;
|
||||
|
||||
@Override
|
||||
public String convert(UserType source) {
|
||||
return source.getType();
|
||||
}
|
||||
}
|
||||
|
||||
@ReadingConverter
|
||||
enum StringToActorTypeConverter implements Converter<String, UserType> {
|
||||
/**
|
||||
* INSTANCE
|
||||
*/
|
||||
INSTANCE,;
|
||||
|
||||
@Override
|
||||
public UserType convert(@NotNull String source) {
|
||||
if (source.equals(ADMIN.getType())) {
|
||||
return ADMIN;
|
||||
}
|
||||
if (source.equals(USER.getType())) {
|
||||
return USER;
|
||||
}
|
||||
if (source.equals(DEVELOPER.getType())) {
|
||||
return DEVELOPER;
|
||||
}
|
||||
if (source.equals(UNKNOWN.getType())) {
|
||||
return UNKNOWN;
|
||||
}
|
||||
throw new TopIamException("未知用户类型");
|
||||
}
|
||||
}
|
||||
|
||||
@WritingConverter
|
||||
enum TargetTypeToStringConverter implements Converter<TargetType, String> {
|
||||
/**
|
||||
* INSTANCE
|
||||
*/
|
||||
INSTANCE,;
|
||||
|
||||
@Override
|
||||
public String convert(TargetType source) {
|
||||
return source.getCode();
|
||||
}
|
||||
}
|
||||
|
||||
@ReadingConverter
|
||||
enum StringToTargetTypeConverter implements Converter<String, TargetType> {
|
||||
/**
|
||||
*INSTANCE
|
||||
*/
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public TargetType convert(@NotNull String source) {
|
||||
return TargetType.getType(source);
|
||||
}
|
||||
}
|
||||
|
||||
@WritingConverter
|
||||
enum GeoLocationProviderToStringConverter implements Converter<GeoLocationProvider, String> {
|
||||
/**
|
||||
* INSTANCE
|
||||
*/
|
||||
INSTANCE,;
|
||||
|
||||
@Override
|
||||
public String convert(GeoLocationProvider source) {
|
||||
return source.getProvider();
|
||||
}
|
||||
}
|
||||
|
||||
@ReadingConverter
|
||||
enum StringToGeoLocationProviderConverter implements Converter<String, GeoLocationProvider> {
|
||||
/**
|
||||
*INSTANCE
|
||||
*/
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public GeoLocationProvider convert(@NotNull String source) {
|
||||
if (MAXMIND.getProvider().equals(source)) {
|
||||
return MAXMIND;
|
||||
}
|
||||
if (GeoLocationProvider.NONE.getProvider().equals(source)) {
|
||||
return GeoLocationProvider.NONE;
|
||||
}
|
||||
throw new TopIamException("未找到提供商");
|
||||
}
|
||||
}
|
||||
|
||||
@WritingConverter
|
||||
enum EventStatusToStringConverter implements Converter<EventStatus, String> {
|
||||
/**
|
||||
* INSTANCE
|
||||
*/
|
||||
INSTANCE,;
|
||||
|
||||
@Override
|
||||
public String convert(@NotNull EventStatus source) {
|
||||
return source.getCode();
|
||||
}
|
||||
}
|
||||
|
||||
@ReadingConverter
|
||||
enum StringToEventStatusConverter implements Converter<String, EventStatus> {
|
||||
/**
|
||||
*INSTANCE
|
||||
*/
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public EventStatus convert(@NotNull String source) {
|
||||
return EventStatus.getType(source);
|
||||
}
|
||||
}
|
||||
|
||||
@WritingConverter
|
||||
enum SetToStringConverter implements Converter<Set<String>, String> {
|
||||
/**
|
||||
* INSTANCE
|
||||
*/
|
||||
INSTANCE,;
|
||||
|
||||
@Override
|
||||
public String convert(@NotNull Set<String> source) {
|
||||
return JsonUtils.writeValueAsString(source);
|
||||
}
|
||||
}
|
||||
|
||||
@ReadingConverter
|
||||
enum StringToSetConverter implements Converter<String, Set<String>> {
|
||||
/**
|
||||
*INSTANCE
|
||||
*/
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public Set<String> convert(@NotNull String source) {
|
||||
return JsonUtils.readValue(source, new TypeReference<>() {
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,9 +20,6 @@ package cn.topiam.employee.audit.entity;
|
|||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.data.elasticsearch.annotations.Field;
|
||||
import org.springframework.data.elasticsearch.annotations.FieldType;
|
||||
|
||||
import cn.topiam.employee.support.security.userdetails.UserType;
|
||||
|
||||
import lombok.Builder;
|
||||
|
@ -39,32 +36,24 @@ import lombok.NonNull;
|
|||
@Builder
|
||||
public class Actor implements Serializable {
|
||||
|
||||
public static final String ACTOR_ID = "actor.id";
|
||||
public static final String ACTOR_TYPE = "actor.type";
|
||||
|
||||
public static final String ACTOR_AUTH_TYPE = "actor.auth_type.keyword";
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = -1144169992714000310L;
|
||||
private static final long serialVersionUID = -1144169992714000310L;
|
||||
|
||||
/**
|
||||
* 行动者ID
|
||||
*/
|
||||
@NonNull
|
||||
@Field(type = FieldType.Keyword, name = "id")
|
||||
private String id;
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 行动者类型
|
||||
*/
|
||||
@NonNull
|
||||
@Field(type = FieldType.Keyword, name = "type")
|
||||
private UserType type;
|
||||
private UserType type;
|
||||
|
||||
/**
|
||||
* 身份验证类型
|
||||
*/
|
||||
@Field(type = FieldType.Keyword, name = "auth_type")
|
||||
private String authType;
|
||||
private String authType;
|
||||
|
||||
}
|
||||
|
|
|
@ -20,9 +20,6 @@ package cn.topiam.employee.audit.entity;
|
|||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.data.elasticsearch.annotations.Field;
|
||||
import org.springframework.data.elasticsearch.annotations.FieldType;
|
||||
import org.springframework.data.elasticsearch.annotations.GeoPointField;
|
||||
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
|
||||
|
||||
import cn.topiam.employee.support.geo.GeoLocationProvider;
|
||||
|
@ -52,67 +49,56 @@ public class GeoLocation implements Serializable {
|
|||
/**
|
||||
* IP
|
||||
*/
|
||||
@Field(type = FieldType.Ip, name = "ip")
|
||||
private String ip;
|
||||
|
||||
/**
|
||||
* continent code
|
||||
*/
|
||||
@Field(type = FieldType.Keyword, name = "continent_code")
|
||||
private String continentCode;
|
||||
|
||||
/**
|
||||
* continent Name
|
||||
*/
|
||||
@Field(type = FieldType.Text, name = "continent_code")
|
||||
private String continentName;
|
||||
|
||||
/**
|
||||
* 国家code
|
||||
*/
|
||||
@Field(type = FieldType.Keyword, name = "country_code")
|
||||
private String countryCode;
|
||||
|
||||
/**
|
||||
* 国家名称
|
||||
*/
|
||||
@Field(type = FieldType.Text, name = "country_name")
|
||||
private String countryName;
|
||||
|
||||
/**
|
||||
* 省份code
|
||||
*/
|
||||
@Field(type = FieldType.Keyword, name = "province_code")
|
||||
private String provinceCode;
|
||||
|
||||
/**
|
||||
* 省份
|
||||
*/
|
||||
@Field(type = FieldType.Text, name = "province_name")
|
||||
private String provinceName;
|
||||
|
||||
/**
|
||||
* 城市code
|
||||
*/
|
||||
@Field(type = FieldType.Keyword, name = "city_code")
|
||||
private String cityCode;
|
||||
|
||||
/**
|
||||
* 城市名称
|
||||
*/
|
||||
@Field(type = FieldType.Text, name = "city_name")
|
||||
private String cityName;
|
||||
|
||||
/**
|
||||
* 地理坐标
|
||||
*/
|
||||
@GeoPointField
|
||||
private GeoPoint point;
|
||||
|
||||
/**
|
||||
* 提供商
|
||||
*/
|
||||
@Field(type = FieldType.Keyword, name = "provider")
|
||||
private GeoLocationProvider provider;
|
||||
|
||||
}
|
||||
|
|
|
@ -20,9 +20,6 @@ package cn.topiam.employee.audit.entity;
|
|||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.data.elasticsearch.annotations.Field;
|
||||
import org.springframework.data.elasticsearch.annotations.FieldType;
|
||||
|
||||
import cn.topiam.employee.audit.enums.TargetType;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
|
@ -50,24 +47,20 @@ public class Target implements Serializable {
|
|||
/**
|
||||
* 目标 ID
|
||||
*/
|
||||
@Field(type = FieldType.Keyword, name = "id")
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 目标名称
|
||||
*/
|
||||
@Field(type = FieldType.Keyword, name = "name")
|
||||
private String name;
|
||||
/**
|
||||
*
|
||||
* 目标类型
|
||||
*/
|
||||
@Field(type = FieldType.Keyword, name = "type")
|
||||
private TargetType type;
|
||||
|
||||
/**
|
||||
* 目标类型名称
|
||||
*/
|
||||
@Field(type = FieldType.Keyword, name = "type_name")
|
||||
private String typeName;
|
||||
}
|
||||
|
|
|
@ -19,9 +19,6 @@ package cn.topiam.employee.audit.entity;
|
|||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.data.elasticsearch.annotations.Field;
|
||||
import org.springframework.data.elasticsearch.annotations.FieldType;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
@ -38,21 +35,15 @@ import lombok.NoArgsConstructor;
|
|||
@NoArgsConstructor
|
||||
public class UserAgent implements Serializable {
|
||||
|
||||
@Field(type = FieldType.Keyword, name = "device_type")
|
||||
private String deviceType;
|
||||
|
||||
@Field(type = FieldType.Keyword, name = "platform")
|
||||
private String platform;
|
||||
|
||||
@Field(type = FieldType.Keyword, name = "platform_version")
|
||||
private String platformVersion;
|
||||
|
||||
@Field(type = FieldType.Keyword, name = "browser")
|
||||
private String browser;
|
||||
|
||||
@Field(type = FieldType.Keyword, name = "browser_type")
|
||||
private String browserType;
|
||||
|
||||
@Field(type = FieldType.Keyword, name = "browser_major_version")
|
||||
private String browserMajorVersion;
|
||||
}
|
||||
|
|
|
@ -17,28 +17,22 @@
|
|||
*/
|
||||
package cn.topiam.employee.audit.service.converter;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.mapstruct.Mapper;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.elasticsearch.client.elc.NativeQuery;
|
||||
import org.springframework.data.elasticsearch.client.elc.NativeQueryBuilder;
|
||||
import org.springframework.data.elasticsearch.client.elc.Queries;
|
||||
import org.springframework.data.elasticsearch.core.SearchHits;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.querydsl.core.types.ExpressionUtils;
|
||||
import com.querydsl.core.types.Predicate;
|
||||
|
||||
import cn.topiam.employee.audit.controller.pojo.AuditListQuery;
|
||||
import cn.topiam.employee.audit.controller.pojo.AuditListResult;
|
||||
import cn.topiam.employee.audit.entity.AuditEntity;
|
||||
import cn.topiam.employee.audit.entity.QAuditEntity;
|
||||
import cn.topiam.employee.audit.entity.Target;
|
||||
import cn.topiam.employee.audit.enums.TargetType;
|
||||
import cn.topiam.employee.common.entity.account.OrganizationEntity;
|
||||
|
@ -61,19 +55,6 @@ import cn.topiam.employee.support.repository.page.domain.Page;
|
|||
import cn.topiam.employee.support.repository.page.domain.PageModel;
|
||||
import cn.topiam.employee.support.security.userdetails.UserType;
|
||||
|
||||
import co.elastic.clients.elasticsearch._types.FieldSort;
|
||||
import co.elastic.clients.elasticsearch._types.FieldValue;
|
||||
import co.elastic.clients.elasticsearch._types.SortOptions;
|
||||
import co.elastic.clients.elasticsearch._types.SortOrder;
|
||||
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
|
||||
import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders;
|
||||
import co.elastic.clients.elasticsearch._types.query_dsl.TermsQueryField;
|
||||
import co.elastic.clients.json.JsonData;
|
||||
import static cn.topiam.employee.audit.entity.Actor.ACTOR_ID;
|
||||
import static cn.topiam.employee.audit.entity.Actor.ACTOR_TYPE;
|
||||
import static cn.topiam.employee.audit.entity.Event.*;
|
||||
import static cn.topiam.employee.support.constant.EiamConstants.DEFAULT_DATE_TIME_FORMATTER_PATTERN;
|
||||
|
||||
/**
|
||||
* 审计数据转换
|
||||
*
|
||||
|
@ -87,46 +68,45 @@ public interface AuditDataConverter {
|
|||
/**
|
||||
* searchHits 转审计列表
|
||||
*
|
||||
* @param search {@link SearchHits}
|
||||
* @param auditEntityPage {@link Page}
|
||||
* @param page {@link PageModel}
|
||||
* @return {@link Page}
|
||||
*/
|
||||
default Page<AuditListResult> searchHitsConvertToAuditListResult(SearchHits<AuditEntity> search,
|
||||
PageModel page) {
|
||||
default Page<AuditListResult> entityConvertToAuditListResult(org.springframework.data.domain.Page<AuditEntity> auditEntityPage,
|
||||
PageModel page) {
|
||||
List<AuditListResult> list = new ArrayList<>();
|
||||
//总记录数
|
||||
search.forEach(hit -> {
|
||||
AuditEntity content = hit.getContent();
|
||||
auditEntityPage.forEach(audit -> {
|
||||
AuditListResult result = new AuditListResult();
|
||||
result.setId(content.getId().toString());
|
||||
result.setEventStatus(content.getEventStatus());
|
||||
result.setEventType(content.getEventType().getDesc());
|
||||
result.setEventTime(content.getEventTime());
|
||||
result.setId(audit.getId().toString());
|
||||
result.setEventStatus(audit.getEventStatus());
|
||||
result.setEventType(audit.getEventType().getDesc());
|
||||
result.setEventTime(audit.getEventTime());
|
||||
//用户代理
|
||||
result.setUserAgent(content.getUserAgent());
|
||||
result.setGeoLocation(content.getGeoLocation());
|
||||
result.setUserAgent(audit.getUserAgent());
|
||||
result.setGeoLocation(audit.getGeoLocation());
|
||||
//用户ID
|
||||
result.setUserId(content.getActorId());
|
||||
result.setUsername(getUsername(content.getActorType(), content.getActorId()));
|
||||
result.setUserId(audit.getActorId());
|
||||
result.setUsername(getUsername(audit.getActorType(), audit.getActorId()));
|
||||
//用户类型
|
||||
result.setUserType(content.getActorType().getType());
|
||||
result.setUserType(audit.getActorType().getType());
|
||||
//操作对象
|
||||
if (Objects.nonNull(content.getTargets())) {
|
||||
for (Target target : content.getTargets()) {
|
||||
if (Objects.nonNull(audit.getTargets())) {
|
||||
for (Target target : audit.getTargets()) {
|
||||
if (Objects.nonNull(target.getId())) {
|
||||
target.setName(getTargetName(target.getType(), target.getId()));
|
||||
}
|
||||
target.setTypeName(target.getType().getDesc());
|
||||
}
|
||||
}
|
||||
result.setTargets(content.getTargets());
|
||||
result.setTargets(audit.getTargets());
|
||||
list.add(result);
|
||||
});
|
||||
//@formatter:off
|
||||
Page<AuditListResult> result = new Page<>();
|
||||
result.setPagination(Page.Pagination.builder()
|
||||
.total(search.getTotalHits())
|
||||
.totalPages(Math.toIntExact(search.getTotalHits() / page.getPageSize()))
|
||||
.total(auditEntityPage.getTotalElements())
|
||||
.totalPages(auditEntityPage.getTotalPages())
|
||||
.current(page.getCurrent() + 1)
|
||||
.build());
|
||||
result.setList(list);
|
||||
|
@ -165,13 +145,12 @@ public interface AuditDataConverter {
|
|||
* 审计列表请求到本机搜索查询
|
||||
*
|
||||
* @param query {@link AuditListQuery}
|
||||
* @param page {@link PageModel}
|
||||
* @return {@link NativeQuery}
|
||||
* @return {@link Predicate}
|
||||
*/
|
||||
default NativeQuery auditListRequestConvertToNativeQuery(AuditListQuery query, PageModel page) {
|
||||
//构建查询 builder下有 must、should 以及 mustNot 相当于 sql 中的 and、or 以及 not
|
||||
BoolQuery.Builder queryBuilder = QueryBuilders.bool();
|
||||
List<SortOptions> fieldSortBuilders = Lists.newArrayList();
|
||||
default Predicate auditListRequestConvertToPredicate(AuditListQuery query) {
|
||||
QAuditEntity auditEntity = QAuditEntity.auditEntity;
|
||||
Predicate predicate = ExpressionUtils.and(auditEntity.isNotNull(),
|
||||
auditEntity.deleted.eq(Boolean.FALSE));
|
||||
//用户名存在,查询用户ID
|
||||
if (StringUtils.hasText(query.getUsername())) {
|
||||
String actorId = "";
|
||||
|
@ -182,6 +161,8 @@ public interface AuditDataConverter {
|
|||
if (!Objects.isNull(user)) {
|
||||
actorId = user.getId().toString();
|
||||
}
|
||||
// 用户类型
|
||||
predicate = ExpressionUtils.and(predicate, auditEntity.actorType.eq(UserType.USER));
|
||||
}
|
||||
if (UserType.ADMIN.getType().equals(query.getUserType())) {
|
||||
AdministratorRepository administratorRepository = ApplicationContextHelp
|
||||
|
@ -191,61 +172,29 @@ public interface AuditDataConverter {
|
|||
if (optional.isPresent()) {
|
||||
actorId = optional.get().getId().toString();
|
||||
}
|
||||
// 用户类型
|
||||
predicate = ExpressionUtils.and(predicate,
|
||||
auditEntity.actorType.eq(UserType.ADMIN));
|
||||
}
|
||||
queryBuilder.must(Queries.termQueryAsQuery(ACTOR_ID, actorId));
|
||||
predicate = ExpressionUtils.and(predicate, auditEntity.actorId.eq(actorId));
|
||||
}
|
||||
//用户类型
|
||||
queryBuilder.must(Queries.termQueryAsQuery(ACTOR_TYPE, query.getUserType()));
|
||||
//事件类型
|
||||
if (!CollectionUtils.isEmpty(query.getEventType())) {
|
||||
queryBuilder.must(QueryBuilders.terms(builder -> {
|
||||
builder
|
||||
.terms(
|
||||
new TermsQueryField.Builder()
|
||||
.value(query.getEventType().stream()
|
||||
.map(t -> FieldValue.of(t.getCode())).collect(Collectors.toList()))
|
||||
.build());
|
||||
builder.field(EVENT_TYPE);
|
||||
return builder;
|
||||
}));
|
||||
predicate = ExpressionUtils.and(predicate,
|
||||
auditEntity.eventType.in(query.getEventType()));
|
||||
}
|
||||
//事件状态
|
||||
if (Objects.nonNull(query.getEventStatus())) {
|
||||
queryBuilder
|
||||
.must(Queries.termQueryAsQuery(EVENT_STATUS, query.getEventStatus().getCode()));
|
||||
predicate = ExpressionUtils.and(predicate,
|
||||
auditEntity.eventStatus.in(query.getEventStatus()));
|
||||
}
|
||||
//字段排序
|
||||
page.getSorts().forEach(sort -> {
|
||||
SortOrder sortOrder;
|
||||
if (org.apache.commons.lang3.StringUtils.equals(sort.getSorter(), SORT_EVENT_TIME)) {
|
||||
if (sort.getAsc()) {
|
||||
sortOrder = SortOrder.Asc;
|
||||
} else {
|
||||
sortOrder = SortOrder.Desc;
|
||||
}
|
||||
} else {
|
||||
sortOrder = SortOrder.Desc;
|
||||
}
|
||||
SortOptions eventTimeSortBuilder = SortOptions
|
||||
.of(s -> s.field(FieldSort.of(f -> f.field(EVENT_TIME).order(sortOrder))));
|
||||
fieldSortBuilders.add(eventTimeSortBuilder);
|
||||
});
|
||||
//事件时间
|
||||
if (!Objects.isNull(query.getStartEventTime())
|
||||
&& !Objects.isNull(query.getEndEventTime())) {
|
||||
queryBuilder.must(QueryBuilders.range(r -> r.field(EVENT_TIME)
|
||||
.gte(JsonData.of(query.getStartEventTime()
|
||||
.format(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMATTER_PATTERN))))
|
||||
.lte(JsonData.of(query.getEndEventTime()
|
||||
.format(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMATTER_PATTERN))))
|
||||
.timeZone(ZoneId.systemDefault().getId())
|
||||
.format(DEFAULT_DATE_TIME_FORMATTER_PATTERN)));
|
||||
predicate = ExpressionUtils.and(predicate,
|
||||
auditEntity.eventTime.between(query.getStartEventTime(), query.getEndEventTime()));
|
||||
}
|
||||
return new NativeQueryBuilder().withQuery(queryBuilder.build()._toQuery())
|
||||
//分页参数
|
||||
.withPageable(PageRequest.of(page.getCurrent(), page.getPageSize()))
|
||||
//排序
|
||||
.withSort(fieldSortBuilders).build();
|
||||
return predicate;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,33 +17,36 @@
|
|||
*/
|
||||
package cn.topiam.employee.audit.service.impl;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate;
|
||||
import org.springframework.data.elasticsearch.client.elc.NativeQuery;
|
||||
import org.springframework.data.elasticsearch.core.SearchHits;
|
||||
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
|
||||
import org.springframework.data.querydsl.QPageRequest;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import com.querydsl.core.types.OrderSpecifier;
|
||||
import com.querydsl.core.types.Predicate;
|
||||
|
||||
import cn.topiam.employee.audit.controller.pojo.AuditListQuery;
|
||||
import cn.topiam.employee.audit.controller.pojo.AuditListResult;
|
||||
import cn.topiam.employee.audit.controller.pojo.DictResult;
|
||||
import cn.topiam.employee.audit.entity.AuditEntity;
|
||||
import cn.topiam.employee.audit.entity.QAuditEntity;
|
||||
import cn.topiam.employee.audit.event.type.EventType;
|
||||
import cn.topiam.employee.audit.repository.AuditRepository;
|
||||
import cn.topiam.employee.audit.service.AuditService;
|
||||
import cn.topiam.employee.audit.service.converter.AuditDataConverter;
|
||||
import cn.topiam.employee.support.autoconfiguration.SupportProperties;
|
||||
import cn.topiam.employee.support.exception.BadParamsException;
|
||||
import cn.topiam.employee.support.repository.page.domain.Page;
|
||||
import cn.topiam.employee.support.repository.page.domain.PageModel;
|
||||
import cn.topiam.employee.support.security.userdetails.UserType;
|
||||
import cn.topiam.employee.support.security.util.SecurityUtils;
|
||||
import static cn.topiam.employee.common.constant.AuditConstants.getAuditIndexPrefix;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import static cn.topiam.employee.audit.service.converter.AuditDataConverter.SORT_EVENT_TIME;
|
||||
import static cn.topiam.employee.support.security.userdetails.UserType.USER;
|
||||
|
||||
/**
|
||||
|
@ -53,6 +56,7 @@ import static cn.topiam.employee.support.security.userdetails.UserType.USER;
|
|||
* Created by support@topiam.cn on 2021/9/10 23:06
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AuditServiceImpl implements AuditService {
|
||||
|
||||
/**
|
||||
|
@ -69,13 +73,21 @@ public class AuditServiceImpl implements AuditService {
|
|||
throw new BadParamsException("用户类型错误");
|
||||
}
|
||||
//查询入参转查询条件
|
||||
NativeQuery nsq = auditDataConverter.auditListRequestConvertToNativeQuery(query, page);
|
||||
Predicate predicate = auditDataConverter.auditListRequestConvertToPredicate(query);
|
||||
// 字段排序
|
||||
OrderSpecifier<LocalDateTime> order = QAuditEntity.auditEntity.eventTime.desc();
|
||||
for (PageModel.Sort sort : page.getSorts()) {
|
||||
if (org.apache.commons.lang3.StringUtils.equals(sort.getSorter(), SORT_EVENT_TIME)) {
|
||||
if (sort.getAsc()) {
|
||||
order = QAuditEntity.auditEntity.eventTime.asc();
|
||||
}
|
||||
}
|
||||
}
|
||||
//分页条件
|
||||
QPageRequest request = QPageRequest.of(page.getCurrent(), page.getPageSize(), order);
|
||||
//查询列表
|
||||
SearchHits<AuditEntity> search = elasticsearchTemplate.search(nsq, AuditEntity.class,
|
||||
IndexCoordinates
|
||||
.of(getAuditIndexPrefix(supportProperties.getAudit().getIndexPrefix()) + "*"));
|
||||
//结果转返回结果
|
||||
return auditDataConverter.searchHitsConvertToAuditListResult(search, page);
|
||||
return auditDataConverter
|
||||
.entityConvertToAuditListResult(auditRepository.findAll(predicate, request), page);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -116,27 +128,13 @@ public class AuditServiceImpl implements AuditService {
|
|||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* AuditProperties
|
||||
*/
|
||||
private final SupportProperties supportProperties;
|
||||
|
||||
/**
|
||||
* ElasticsearchTemplate
|
||||
*/
|
||||
private final ElasticsearchTemplate elasticsearchTemplate;
|
||||
|
||||
/**
|
||||
* AuditDataConverter
|
||||
*/
|
||||
private final AuditDataConverter auditDataConverter;
|
||||
|
||||
public AuditServiceImpl(SupportProperties supportProperties,
|
||||
ElasticsearchTemplate elasticsearchTemplate,
|
||||
AuditDataConverter auditDataConverter) {
|
||||
this.supportProperties = supportProperties;
|
||||
this.elasticsearchTemplate = elasticsearchTemplate;
|
||||
this.auditDataConverter = auditDataConverter;
|
||||
}
|
||||
private final AuditDataConverter auditDataConverter;
|
||||
|
||||
/**
|
||||
* AuditRepository
|
||||
*/
|
||||
private final AuditRepository auditRepository;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* eiam-common - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.common.constant;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author SanLi
|
||||
* Created by qinggang.zuo@gmail.com / 2689170096@qq.com on 2023/10/5 15:11
|
||||
*/
|
||||
public class UserConstants {
|
||||
|
||||
/**
|
||||
* 忘记密码预认证
|
||||
*/
|
||||
public static final String PREPARE_FORGET_PASSWORD = "/prepare_forget_password";
|
||||
|
||||
/**
|
||||
* 忘记密码
|
||||
*/
|
||||
public static final String FORGET_PASSWORD = "/forget_password";
|
||||
|
||||
/**
|
||||
* 忘记密码发送验证码
|
||||
*/
|
||||
public static final String FORGET_PASSWORD_CODE = "/forget_password_code";
|
||||
}
|
|
@ -187,7 +187,7 @@ public class UserEntity extends LogicDeleteEntity<Long> {
|
|||
*/
|
||||
@Transient
|
||||
@JsonIgnore
|
||||
private String plaintext;
|
||||
private String passwordPlainText;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
|
|
|
@ -64,6 +64,18 @@ public class AdministratorEntity extends LogicDeleteEntity<Long> {
|
|||
@Column(name = "username_")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 姓名
|
||||
*/
|
||||
@Column(name = "full_name")
|
||||
private String fullName;
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
@Column(name = "nick_name")
|
||||
private String nickName;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
|
|
|
@ -53,45 +53,7 @@ public class UserGroupMemberRepositoryCustomizedImpl implements
|
|||
@Override
|
||||
public Page<UserPO> getUserGroupMemberList(UserGroupMemberListQuery query, Pageable pageable) {
|
||||
//@formatter:off
|
||||
StringBuilder builder = new StringBuilder("""
|
||||
SELECT
|
||||
`u`.id_,
|
||||
`u`.username_,
|
||||
`u`.password_,
|
||||
`u`.email_,
|
||||
`u`.phone_,
|
||||
`u`.phone_area_code,
|
||||
`u`.full_name,
|
||||
`u`.nick_name,
|
||||
`u`.avatar_,
|
||||
`u`.status_,
|
||||
`u`.data_origin,
|
||||
`u`.email_verified,
|
||||
`u`.phone_verified,
|
||||
`u`.auth_total,
|
||||
`u`.last_auth_ip,
|
||||
`u`.last_auth_time,
|
||||
`u`.expand_,
|
||||
`u`.external_id,
|
||||
`u`.expire_date,
|
||||
`u`.create_by,
|
||||
`u`.create_time,
|
||||
`u`.update_by,
|
||||
`u`.update_time,
|
||||
`u`.remark_,
|
||||
group_concat( IF(organization_member.primary_ = 1, null, organization_.display_path ) ) AS primary_org_display_path,
|
||||
group_concat( IF(organization_member.primary_ IS NULL, null, organization_.display_path ) ) AS org_display_path
|
||||
FROM
|
||||
user_group_member ugm
|
||||
INNER JOIN user u ON ugm.user_id = u.id_ AND u.is_deleted = '0'
|
||||
INNER JOIN user_group ug ON ug.id_ = ugm.group_id AND ug.is_deleted = '0'
|
||||
LEFT JOIN organization_member ON ( u.id_ = organization_member.user_id AND organization_member.is_deleted = '0')
|
||||
LEFT JOIN organization organization_ ON ( organization_.id_ = organization_member.org_id AND organization_.is_deleted = '0')
|
||||
WHERE
|
||||
ugm.is_deleted = '0'
|
||||
AND ugm.group_id = '%s'
|
||||
AND ug.id_ = '%s'
|
||||
""".formatted(query.getId(), query.getId()));
|
||||
StringBuilder builder = new StringBuilder("SELECT `u`.id_, `u`.username_, `u`.password_, `u`.email_, `u`.phone_, `u`.phone_area_code, `u`.full_name, `u`.nick_name, `u`.avatar_, `u`.status_, `u`.data_origin, `u`.email_verified, `u`.phone_verified, `u`.auth_total, `u`.last_auth_ip, `u`.last_auth_time, `u`.expand_, `u`.external_id, `u`.expire_date, `u`.create_by, `u`.create_time, `u`.update_by, `u`.update_time, `u`.remark_, group_concat( IF( organization_member.primary_ = TRUE, organization_.display_path, NULL) ) AS primary_org_display_path, group_concat( IF ( organization_member.primary_ IS NULL, organization_.display_path, NULL ) ) AS org_display_path FROM user_group_member ugm LEFT JOIN user u ON ugm.user_id = u.id_ LEFT JOIN user_group ug ON ug.id_ = ugm.group_id LEFT JOIN organization_member ON ( u.id_ = organization_member.user_id) LEFT JOIN organization organization_ ON ( organization_.id_ = organization_member.org_id) WHERE ugm.is_deleted = '0' AND u.is_deleted = '0' AND ug.is_deleted = '0' AND organization_.is_deleted = '0' AND organization_member.is_deleted = '0' AND ugm.group_id = '%s' AND ug.id_ = '%s'".formatted(query.getId(), query.getId()));
|
||||
//用户名
|
||||
if (StringUtils.isNoneBlank(query.getFullName())) {
|
||||
builder.append(" AND full_name like '%").append(query.getFullName()).append("%'");
|
||||
|
|
|
@ -31,7 +31,6 @@ import org.springframework.cache.annotation.CacheEvict;
|
|||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate;
|
||||
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
@ -69,7 +68,7 @@ public class UserRepositoryCustomizedImpl implements UserRepositoryCustomized {
|
|||
@Override
|
||||
public Page<UserPO> getUserList(UserListQuery query, Pageable pageable) {
|
||||
//@formatter:off
|
||||
StringBuilder builder = new StringBuilder("SELECT `user`.id_, `user`.username_,`user`.password_, `user`.email_, `user`.phone_,`user`.phone_area_code, `user`.full_name ,`user`.nick_name, `user`.avatar_ , `user`.status_, `user`.data_origin, `user`.email_verified, `user`.phone_verified, `user`.auth_total, `user`.last_auth_ip, `user`.last_auth_time, `user`.expand_, `user`.external_id , `user`.expire_date,`user`.create_by, `user`.create_time, `user`.update_by , `user`.update_time, `user`.remark_, group_concat( IF(organization_member.primary_ = 1, null, organization_.display_path ) ) AS primary_org_display_path, group_concat( IF(organization_member.primary_ IS NULL, null, organization_.display_path ) ) AS org_display_path FROM `user` INNER JOIN `organization_member` ON (`user`.id_ = organization_member.user_id) INNER JOIN `organization` organization_ ON (organization_.id_ = organization_member.org_id) WHERE `user`.is_deleted = 0");
|
||||
StringBuilder builder = new StringBuilder("SELECT `user`.id_, `user`.username_, `user`.password_, `user`.email_, `user`.phone_, `user`.phone_area_code, `user`.full_name, `user`.nick_name, `user`.avatar_, `user`.status_, `user`.data_origin, `user`.email_verified, `user`.phone_verified, `user`.auth_total, `user`.last_auth_ip, `user`.last_auth_time, `user`.expand_, `user`.external_id, `user`.expire_date, `user`.create_by, `user`.create_time, `user`.update_by, `user`.update_time, `user`.remark_, group_concat( IF( organization_member.primary_ = TRUE, organization_.display_path, NULL) ) AS primary_org_display_path, group_concat( IF ( organization_member.primary_ IS NULL, organization_.display_path, NULL ) ) AS org_display_path FROM `user` INNER JOIN `organization_member` ON ( `user`.id_ = organization_member.user_id ) INNER JOIN `organization` organization_ ON ( organization_.id_ = organization_member.org_id ) WHERE `user`.is_deleted = 0 AND organization_member.is_deleted = 0 ");
|
||||
//组织条件
|
||||
if (StringUtils.isNotBlank(query.getOrganizationId())) {
|
||||
if (Boolean.TRUE.equals(query.getInclSubOrganization())) {
|
||||
|
@ -128,51 +127,7 @@ public class UserRepositoryCustomizedImpl implements UserRepositoryCustomized {
|
|||
@Override
|
||||
public Page<UserPO> getUserListNotInGroupId(UserListNotInGroupQuery query, Pageable pageable) {
|
||||
//@formatter:off
|
||||
StringBuilder builder = new StringBuilder(
|
||||
"""
|
||||
SELECT
|
||||
`user`.id_,
|
||||
`user`.username_,
|
||||
`user`.password_,
|
||||
`user`.email_,
|
||||
`user`.phone_,
|
||||
`user`.phone_area_code,
|
||||
`user`.full_name,
|
||||
`user`.nick_name,
|
||||
`user`.avatar_,
|
||||
`user`.status_,
|
||||
`user`.data_origin,
|
||||
`user`.email_verified,
|
||||
`user`.phone_verified,
|
||||
`user`.auth_total,
|
||||
`user`.last_auth_ip,
|
||||
`user`.last_auth_time,
|
||||
`user`.expand_,
|
||||
`user`.external_id,
|
||||
`user`.expire_date,
|
||||
`user`.create_by,
|
||||
`user`.create_time,
|
||||
`user`.update_by,
|
||||
`user`.update_time,
|
||||
`user`.remark_,
|
||||
group_concat( IF(organization_member.primary_ = 1, null, organization_.display_path ) ) AS primary_org_display_path,
|
||||
group_concat( IF(organization_member.primary_ IS NULL, null, organization_.display_path ) ) AS org_display_path
|
||||
FROM `user`
|
||||
LEFT JOIN `organization_member` ON ( `user`.id_ = organization_member.user_id AND organization_member.is_deleted = '0' )
|
||||
LEFT JOIN `organization` organization_ ON ( organization_.id_ = organization_member.org_id AND organization_.is_deleted = '0' )
|
||||
WHERE
|
||||
user.is_deleted = 0 AND
|
||||
user.id_ NOT IN (
|
||||
SELECT
|
||||
u.id_
|
||||
FROM
|
||||
user u
|
||||
INNER JOIN user_group_member ugm ON ugm.user_id = u.id_
|
||||
INNER JOIN user_group ug ON ug.id_ = ugm.group_id
|
||||
WHERE
|
||||
u.is_deleted = '0' AND ugm.is_deleted = '0'
|
||||
AND ug.id_ = '%s' AND ugm.group_id = '%s')
|
||||
""".formatted(query.getId(), query.getId()));
|
||||
StringBuilder builder = new StringBuilder("SELECT `user`.id_, `user`.username_, `user`.password_, `user`.email_, `user`.phone_, `user`.phone_area_code, `user`.full_name, `user`.nick_name, `user`.avatar_, `user`.status_, `user`.data_origin, `user`.email_verified, `user`.phone_verified, `user`.auth_total, `user`.last_auth_ip, `user`.last_auth_time, `user`.expand_, `user`.external_id, `user`.expire_date, `user`.create_by, `user`.create_time, `user`.update_by, `user`.update_time, `user`.remark_, group_concat( IF( organization_member.primary_ = TRUE, organization_.display_path, NULL) ) AS primary_org_display_path, group_concat( IF ( organization_member.primary_ IS NULL, organization_.display_path, NULL ) ) AS org_display_path FROM `user` LEFT JOIN `organization_member` ON ( `user`.id_ = organization_member.user_id AND organization_member.is_deleted = '0' ) LEFT JOIN `organization` organization_ ON ( organization_.id_ = organization_member.org_id AND organization_.is_deleted = '0' ) WHERE user.is_deleted = 0 AND organization_member.is_deleted = 0 AND user.id_ NOT IN ( SELECT u.id_ FROM user u INNER JOIN user_group_member ugm ON ugm.user_id = u.id_ INNER JOIN user_group ug ON ug.id_ = ugm.group_id WHERE u.is_deleted = '0' AND ugm.is_deleted = '0' AND ug.id_ = '%s' AND ugm.group_id = '%s')".formatted(query.getId(), query.getId()));
|
||||
if (StringUtils.isNoneBlank(query.getKeyword())) {
|
||||
builder.append(" AND user.username_ LIKE '%").append(query.getKeyword()).append("%'");
|
||||
builder.append(" OR user.full_name LIKE '%").append(query.getKeyword()).append("%'");
|
||||
|
@ -352,10 +307,5 @@ public class UserRepositoryCustomizedImpl implements UserRepositoryCustomized {
|
|||
/**
|
||||
* JdbcTemplate
|
||||
*/
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/**
|
||||
* ElasticsearchTemplate
|
||||
*/
|
||||
private final ElasticsearchTemplate elasticsearchTemplate;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
}
|
||||
|
|
|
@ -105,7 +105,7 @@ public interface AppRepository extends LogicDeleteRepository<AppEntity, Long>,
|
|||
*/
|
||||
@NotNull
|
||||
@Cacheable
|
||||
@Query(value = "SELECT AppEntity FROM AppEntity WHERE id = :id")
|
||||
@Query(value = "FROM AppEntity WHERE id = :id")
|
||||
Optional<AppEntity> findByIdContainsDeleted(@NotNull @Param(value = "id") Long id);
|
||||
|
||||
/**
|
||||
|
|
|
@ -20,6 +20,7 @@ package cn.topiam.employee.common.repository.identitysource;
|
|||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.cache.annotation.CacheConfig;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
|
@ -55,9 +56,10 @@ public interface IdentitySourceRepository extends LogicDeleteRepository<Identity
|
|||
* @param id {@link Long}
|
||||
* @return {@link IdentitySourceEntity}
|
||||
*/
|
||||
@NotNull
|
||||
@Override
|
||||
@Cacheable(key = "#p0", unless = "#result==null")
|
||||
Optional<IdentitySourceEntity> findById(@Param(value = "id") Long id);
|
||||
Optional<IdentitySourceEntity> findById(@NotNull @Param(value = "id") Long id);
|
||||
|
||||
/**
|
||||
* 根据ID查询
|
||||
|
@ -66,7 +68,7 @@ public interface IdentitySourceRepository extends LogicDeleteRepository<Identity
|
|||
* @return {@link IdentitySourceEntity}
|
||||
*/
|
||||
@Cacheable(key = "#p0", unless = "#result==null")
|
||||
@Query(value = "SELECT * FROM identity_source WHERE id_ = :id", nativeQuery = true)
|
||||
@Query(value = "SELECT IdentitySourceEntity FROM IdentitySourceEntity WHERE id = :id")
|
||||
Optional<IdentitySourceEntity> findByIdContainsDeleted(@Param(value = "id") Long id);
|
||||
|
||||
/**
|
||||
|
@ -123,7 +125,7 @@ public interface IdentitySourceRepository extends LogicDeleteRepository<Identity
|
|||
@Transactional(rollbackFor = Exception.class)
|
||||
@Modifying
|
||||
@CacheEvict(allEntries = true)
|
||||
@Query(value = "UPDATE identity_source SET strategy_config = :strategyConfig where id_ = :id", nativeQuery = true)
|
||||
@Query(value = "UPDATE IdentitySourceEntity SET strategyConfig = :strategyConfig where id = :id")
|
||||
void updateStrategyConfig(@Param(value = "id") Long id,
|
||||
@Param(value = "strategyConfig") String strategyConfig);
|
||||
|
||||
|
|
|
@ -139,8 +139,9 @@ public interface AdministratorRepository extends LogicDeleteRepository<Administr
|
|||
@Transactional(rollbackFor = Exception.class)
|
||||
@Modifying
|
||||
@CacheEvict(allEntries = true)
|
||||
@Query(value = "update AdministratorEntity set password = :password where id = :id")
|
||||
void updatePassword(@Param(value = "id") String id, @Param(value = "password") String password);
|
||||
@Query(value = "update AdministratorEntity set password =:password,lastUpdatePasswordTime = :lastUpdatePasswordTime where id=:id")
|
||||
Integer updatePassword(@Param(value = "id") Long id, @Param(value = "password") String password,
|
||||
@Param(value = "lastUpdatePasswordTime") LocalDateTime lastUpdatePasswordTime);
|
||||
|
||||
/**
|
||||
* 更新认证成功信息
|
||||
|
@ -154,4 +155,29 @@ public interface AdministratorRepository extends LogicDeleteRepository<Administr
|
|||
@Modifying
|
||||
@Query(value = "UPDATE administrator SET auth_total = (IFNULL(auth_total,0) +1),last_auth_ip = ?2,last_auth_time = ?3 WHERE id_ = ?1", nativeQuery = true)
|
||||
void updateAuthSucceedInfo(String id, String ip, LocalDateTime loginTime);
|
||||
|
||||
/**
|
||||
* 更新邮箱
|
||||
*
|
||||
* @param id {@link Long}
|
||||
* @param email {@link String}
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Modifying
|
||||
@CacheEvict(allEntries = true)
|
||||
@Query(value = "UPDATE AdministratorEntity SET email =:email WHERE id=:id")
|
||||
Integer updateEmail(@Param(value = "id") Long id, @Param(value = "email") String email);
|
||||
|
||||
/**
|
||||
* 更新用户手机号
|
||||
*
|
||||
* @param id {@link Long}
|
||||
* @param phone {@link String}
|
||||
* @return {@link Integer}
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Modifying
|
||||
@CacheEvict(allEntries = true)
|
||||
@Query(value = "update AdministratorEntity set phone =:phone where id=:id")
|
||||
Integer updatePhone(@Param(value = "id") Long id, @Param(value = "phone") String phone);
|
||||
}
|
||||
|
|
|
@ -39,11 +39,11 @@ public class NoneStorage extends AbstractStorage {
|
|||
@Override
|
||||
public String upload(@NotNull String fileName,
|
||||
InputStream inputStream) throws StorageProviderException {
|
||||
throw new StorageProviderException("暂未配置存储提供商或提供商异常,请联系管理员");
|
||||
throw new StorageProviderException("暂未配置存储提供商");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String download(String path) throws StorageProviderException {
|
||||
throw new StorageProviderException("暂未配置存储提供商或提供商异常,请联系管理员");
|
||||
throw new StorageProviderException("暂未配置存储提供商");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -88,6 +88,10 @@
|
|||
<constraints nullable="true"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
<addColumn tableName="administrator">
|
||||
<column name="full_name" remarks="姓名" type="VARCHAR(100)"/>
|
||||
<column name="nick_name" remarks="昵称" type="VARCHAR(50)"/>
|
||||
</addColumn>
|
||||
<!--创建索引-->
|
||||
<createIndex tableName="app_group_association" indexName="uk_app_group_association" unique="true">
|
||||
<column name="group_id"/>
|
||||
|
|
|
@ -40,6 +40,12 @@ export default [
|
|||
hideInMenu: true,
|
||||
component: './user/SessionExpired',
|
||||
},
|
||||
{
|
||||
name: 'account.profile',
|
||||
path: '/user/profile',
|
||||
hideInMenu: true,
|
||||
component: './user/Profile',
|
||||
},
|
||||
/*欢迎页*/
|
||||
{
|
||||
name: 'welcome',
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
"dayjs": "^1.11.10",
|
||||
"echarts": "^5.4.3",
|
||||
"fetch-jsonp": "^1.3.0",
|
||||
"form-render": "^2.2.16",
|
||||
"form-render": "^2.2.19",
|
||||
"google-libphonenumber": "^3.2.33",
|
||||
"js-base64": "^3.7.5",
|
||||
"js-yaml": "^4.1.0",
|
||||
|
@ -71,7 +71,7 @@
|
|||
"qs": "^6.11.2",
|
||||
"query-string": "^8.1.0",
|
||||
"rc-field-form": "^1.38.2",
|
||||
"rc-menu": "^9.12.0",
|
||||
"rc-menu": "^9.12.2",
|
||||
"rc-select": "^14.9.0",
|
||||
"rc-tree": "^5.7.12",
|
||||
"react": "^18.2.0",
|
||||
|
@ -93,13 +93,13 @@
|
|||
"@types/numeral": "^2.0.3",
|
||||
"@types/qs": "^6.9.8",
|
||||
"@types/react": "^18.2.25",
|
||||
"@types/react-dom": "^18.2.10",
|
||||
"@types/react-dom": "^18.2.11",
|
||||
"@types/react-helmet": "^6.1.7",
|
||||
"@umijs/lint": "^4.0.83",
|
||||
"@umijs/max": "^4.0.83",
|
||||
"cross-env": "^7.0.3",
|
||||
"cross-port-killer": "^1.4.0",
|
||||
"eslint": "^8.50.0",
|
||||
"eslint": "^8.51.0",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^14.0.1",
|
||||
"prettier": "^3.0.3",
|
||||
|
|
|
@ -113,7 +113,7 @@ export const layout: RunTimeLayoutConfig = ({ initialState, loading }) => {
|
|||
heightLayoutHeader: showBanner ? 78 : 56,
|
||||
},
|
||||
pageContainer: {
|
||||
paddingBlockPageContainerContent: 6,
|
||||
paddingBlockPageContainerContent: 12,
|
||||
paddingInlinePageContainerContent: 24,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -120,9 +120,9 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ children }) =
|
|||
|
||||
const menuItems: ItemType[] = [
|
||||
{
|
||||
key: 'center',
|
||||
key: 'profile',
|
||||
icon: <UserOutlined />,
|
||||
label: intl.formatMessage({ id: 'components.right_content.center' }),
|
||||
label: intl.formatMessage({ id: 'components.right_content.profile' }),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
|
|
|
@ -19,7 +19,7 @@ export default {
|
|||
'component.tagSelect.expand': '展开',
|
||||
'component.tagSelect.collapse': '收起',
|
||||
'component.tagSelect.all': '全部',
|
||||
'components.right_content.center': '个人中心',
|
||||
'components.right_content.profile': '个人中心',
|
||||
'components.right_content.change-password': '修改密码',
|
||||
'components.right_content.logout': '退出登录',
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ export default {
|
|||
'menu.exception.not-permission': '403',
|
||||
'menu.exception.not-find': '404',
|
||||
'menu.exception.server-error': '500',
|
||||
'menu.account.center': '个人中心',
|
||||
'menu.account.profile': '个人中心',
|
||||
'menu.account.settings': '个人设置',
|
||||
'menu.account.logout': '退出登录',
|
||||
'menu.social-bind': '用户绑定',
|
||||
|
|
|
@ -54,6 +54,13 @@ export default (props: { userId: string }) => {
|
|||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'pages.account.user_detail.login_audit.columns.platform' }),
|
||||
ellipsis: true,
|
||||
dataIndex: 'platform',
|
||||
width: 110,
|
||||
search: false,
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'pages.account.user_detail.login_audit.columns.browser' }),
|
||||
dataIndex: 'browser',
|
||||
|
@ -70,6 +77,7 @@ export default (props: { userId: string }) => {
|
|||
sorter: true,
|
||||
valueType: 'dateTime',
|
||||
search: false,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
|
@ -96,6 +104,8 @@ export default (props: { userId: string }) => {
|
|||
<ProTable
|
||||
columns={columns}
|
||||
search={false}
|
||||
rowKey={'id'}
|
||||
scroll={{ x: 900 }}
|
||||
request={getLoginAuditList}
|
||||
params={{ userId: userId }}
|
||||
pagination={{ pageSize: 10 }}
|
||||
|
|
|
@ -31,7 +31,6 @@ import { QuestionCircleOutlined } from '@ant-design/icons';
|
|||
import { omit } from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import { ParamCheckType } from '@/constant';
|
||||
import AppAccess from '../AppAccess';
|
||||
import { useIntl } from '@umijs/max';
|
||||
|
||||
const prefixCls = 'user-detail-info';
|
||||
|
@ -289,11 +288,6 @@ export default (props: { userId: string }) => {
|
|||
id: 'pages.account.user_detail.user_info.data_origin.value_enum.feishu',
|
||||
}),
|
||||
},
|
||||
ldap: {
|
||||
text: intl.formatMessage({
|
||||
id: 'pages.account.user_detail.user_info.data_origin.value_enum.ldap',
|
||||
}),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<ProDescriptions.Item
|
||||
|
|
|
@ -20,9 +20,9 @@
|
|||
*/
|
||||
export enum UserDetailTabs {
|
||||
//用户信息
|
||||
user_info = 'user-info',
|
||||
user_info = 'user_info',
|
||||
//应用授权
|
||||
app_access = 'app_access',
|
||||
//登录审计
|
||||
login_audit = 'login-audit',
|
||||
login_audit = 'login_audit',
|
||||
}
|
||||
|
|
|
@ -45,7 +45,6 @@ export default {
|
|||
'pages.account.user_detail.user_info.data_origin.value_enum.dingtalk': '钉钉导入',
|
||||
'pages.account.user_detail.user_info.data_origin.value_enum.wechat': '企业微信导入',
|
||||
'pages.account.user_detail.user_info.data_origin.value_enum.feishu': '飞书导入',
|
||||
'pages.account.user_detail.user_info.data_origin.value_enum.ldap': 'LDAP导入',
|
||||
'pages.account.user_detail.user_info.id': '账户 ID',
|
||||
'pages.account.user_detail.user_info.external_id': '外部 ID',
|
||||
'pages.account.user_detail.user_info.phone': '手机号',
|
||||
|
@ -67,6 +66,7 @@ export default {
|
|||
'pages.account.user_detail.login_audit.columns.app_name': '应用名称',
|
||||
'pages.account.user_detail.login_audit.columns.client_ip': '客户端IP',
|
||||
'pages.account.user_detail.login_audit.columns.browser': '浏览器',
|
||||
'pages.account.user_detail.login_audit.columns.platform': '操作系统',
|
||||
'pages.account.user_detail.login_audit.columns.location': '地理位置',
|
||||
'pages.account.user_detail.login_audit.columns.event_time': '登录时间',
|
||||
'pages.account.user_detail.login_audit.columns.event_status': '登录结果',
|
||||
|
|
|
@ -20,9 +20,9 @@
|
|||
*/
|
||||
export enum UserGroupDetailTabs {
|
||||
//app_access
|
||||
app_access = 'app-access',
|
||||
app_access = 'app_access',
|
||||
//权限
|
||||
permission_info = 'permission-info',
|
||||
permission_info = 'permission_info',
|
||||
//member
|
||||
member = 'member',
|
||||
}
|
||||
|
|
|
@ -39,7 +39,6 @@ import {
|
|||
Table,
|
||||
Tooltip,
|
||||
Typography,
|
||||
Tag,
|
||||
Popover,
|
||||
} from 'antd';
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
@ -172,11 +171,6 @@ export default (props: UserListProps) => {
|
|||
id: 'pages.account.user_list.user.columns.data_origin.value_enum.feishu',
|
||||
}),
|
||||
},
|
||||
ldap: {
|
||||
text: intl.formatMessage({
|
||||
id: 'pages.account.user_list.user.columns.data_origin.value_enum.ldap',
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -184,44 +178,41 @@ export default (props: UserListProps) => {
|
|||
dataIndex: 'orgDisplayPath',
|
||||
search: false,
|
||||
ellipsis: true,
|
||||
render: (_, record) => [
|
||||
<Popover
|
||||
key="pop"
|
||||
title={
|
||||
<Tag color={'geekblue'} key={record.orgDisplayPath}>
|
||||
{record.primaryOrgDisplayPath}
|
||||
</Tag>
|
||||
}
|
||||
content={
|
||||
<Space direction="vertical" size="small" style={{ display: 'flex' }}>
|
||||
{record.orgDisplayPath.split(',')?.map((p: string) => {
|
||||
return (
|
||||
<Tag color={'green'} key={p}>
|
||||
{p}
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Space key="primary_path">
|
||||
{
|
||||
<Tag color={'geekblue'} key={record.primaryOrgDisplayPath}>
|
||||
{record.primaryOrgDisplayPath}
|
||||
</Tag>
|
||||
}
|
||||
</Space>
|
||||
<Space key="path" direction="vertical" size="small" style={{ display: 'flex' }}>
|
||||
{record.orgDisplayPath.split(',')?.map((p: string) => {
|
||||
return (
|
||||
<Tag color={'green'} key={p}>
|
||||
{p}
|
||||
</Tag>
|
||||
);
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<Popover
|
||||
key="pop"
|
||||
title={intl.formatMessage({
|
||||
id: 'pages.account.user_list.user.columns.org_display_path',
|
||||
})}
|
||||
</Space>
|
||||
</Popover>,
|
||||
],
|
||||
content={
|
||||
<>
|
||||
{record.primaryOrgDisplayPath && (
|
||||
<span>主组织:{record.primaryOrgDisplayPath}</span>
|
||||
)}
|
||||
{record.orgDisplayPath && (
|
||||
<Space key="path" direction="vertical" size="small" style={{ display: 'flex' }}>
|
||||
{record.orgDisplayPath?.split(',')?.map((p: string) => {
|
||||
return p;
|
||||
})}
|
||||
</Space>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{record.primaryOrgDisplayPath && (
|
||||
<Space key="primary_path">{record.primaryOrgDisplayPath}</Space>
|
||||
)}
|
||||
{record.orgDisplayPath && (
|
||||
<Space key="path" direction="vertical" size="small" style={{ display: 'flex' }}>
|
||||
{record.orgDisplayPath?.split(',')?.map((p: string) => {
|
||||
return p;
|
||||
})}
|
||||
</Space>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'pages.account.user_list.user.columns.status' }),
|
||||
|
|
|
@ -33,7 +33,6 @@ export default {
|
|||
'pages.account.user_list.user.columns.data_origin.value_enum.dingtalk': '钉钉导入',
|
||||
'pages.account.user_list.user.columns.data_origin.value_enum.wechat': '企业微信导入',
|
||||
'pages.account.user_list.user.columns.data_origin.value_enum.feishu': '飞书导入',
|
||||
'pages.account.user_list.user.columns.data_origin.value_enum.ldap': 'LDAP导入',
|
||||
'pages.account.user_list.user.columns.org_display_path': '所属组织',
|
||||
'pages.account.user_list.user.columns.status': '状态',
|
||||
'pages.account.user_list.user.columns.status.value_enum.expired_locked': '过期锁定',
|
||||
|
|
|
@ -33,11 +33,13 @@ export default (props: {
|
|||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const formRef = useRef<ProFormInstance>();
|
||||
const intl = useIntl();
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
formRef.current?.setFieldsValue({ id });
|
||||
setLoading(false);
|
||||
}, [visible, id]);
|
||||
|
||||
return (
|
||||
<ModalForm
|
||||
title={intl.formatMessage({ id: 'pages.setting.administrator.reset_password_modal' })}
|
||||
|
@ -52,7 +54,7 @@ export default (props: {
|
|||
destroyOnClose: true,
|
||||
onCancel: onCancel,
|
||||
}}
|
||||
onFinish={(formData: { password: string }) => {
|
||||
onFinish={async (formData: { password: string }) => {
|
||||
setLoading(true);
|
||||
const password = Base64.encode(formData.password, true);
|
||||
onFinish({ id, password }).finally(() => {
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { history } from '@@/core/history';
|
||||
import { GridContent, PageContainer } from '@ant-design/pro-components';
|
||||
import { useAsyncEffect } from 'ahooks';
|
||||
import { Menu } from 'antd';
|
||||
import type { ItemType } from 'antd/es/menu/hooks/useItems';
|
||||
import { useLayoutEffect, useRef, useState } from 'react';
|
||||
import BaseView from './components/Base';
|
||||
import SecurityView from './components/Security';
|
||||
import { AccountSettingsStateKey } from './data.d';
|
||||
import classnames from 'classnames';
|
||||
import useStyle from './style';
|
||||
import queryString from 'query-string';
|
||||
import { useIntl, useLocation } from '@umijs/max';
|
||||
|
||||
const prefixCls = 'account';
|
||||
|
||||
type AccountSettingState = {
|
||||
mode: 'inline' | 'horizontal';
|
||||
selectKey: AccountSettingsStateKey;
|
||||
};
|
||||
|
||||
const AccountSettings = () => {
|
||||
const { wrapSSR, hashId } = useStyle(prefixCls);
|
||||
const location = useLocation();
|
||||
const query = queryString.parse(location.search);
|
||||
const { type } = query as { type: AccountSettingsStateKey };
|
||||
const intl = useIntl();
|
||||
const [initConfig, setInitConfig] = useState<AccountSettingState>({
|
||||
mode: 'inline',
|
||||
selectKey: AccountSettingsStateKey.base,
|
||||
});
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
if (!type || !AccountSettingsStateKey[type]) {
|
||||
setInitConfig({ ...initConfig, selectKey: AccountSettingsStateKey.base });
|
||||
history.replace({
|
||||
pathname: location.pathname,
|
||||
search: queryString.stringify({ type: AccountSettingsStateKey.base }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
setInitConfig({ ...initConfig, selectKey: type });
|
||||
}, [type]);
|
||||
|
||||
const menu: ItemType[] = [
|
||||
{
|
||||
key: AccountSettingsStateKey.base,
|
||||
label: intl.formatMessage({
|
||||
id: 'page.user.profile.menu.base',
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: AccountSettingsStateKey.security,
|
||||
label: intl.formatMessage({
|
||||
id: 'page.user.profile.menu.security',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const dom = useRef<HTMLDivElement>();
|
||||
|
||||
const resize = () => {
|
||||
requestAnimationFrame(() => {
|
||||
if (!dom.current) {
|
||||
return;
|
||||
}
|
||||
let mode: 'inline' | 'horizontal' = 'inline';
|
||||
const { offsetWidth } = dom.current;
|
||||
if (dom.current.offsetWidth < 641 && offsetWidth > 400) {
|
||||
mode = 'horizontal';
|
||||
}
|
||||
if (window.innerWidth < 768 && offsetWidth > 400) {
|
||||
mode = 'horizontal';
|
||||
}
|
||||
setInitConfig({ ...initConfig, selectKey: type, mode: mode as AccountSettingState['mode'] });
|
||||
});
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (dom.current) {
|
||||
window.addEventListener('resize', resize);
|
||||
resize();
|
||||
}
|
||||
return () => {
|
||||
window.removeEventListener('resize', resize);
|
||||
};
|
||||
}, [type]);
|
||||
|
||||
const renderChildren = () => {
|
||||
const { selectKey } = initConfig;
|
||||
switch (selectKey) {
|
||||
case AccountSettingsStateKey.base:
|
||||
return <BaseView />;
|
||||
case AccountSettingsStateKey.security:
|
||||
return <SecurityView />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return wrapSSR(
|
||||
<PageContainer className={classnames(`${prefixCls}`, hashId)}>
|
||||
<GridContent>
|
||||
<div
|
||||
className={classnames(`${prefixCls}-main`, hashId)}
|
||||
ref={(ref) => {
|
||||
if (ref) {
|
||||
dom.current = ref;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={classnames(`${prefixCls}-left`, hashId)}>
|
||||
<Menu
|
||||
mode={initConfig.mode}
|
||||
selectedKeys={[initConfig.selectKey]}
|
||||
onClick={({ key }) => {
|
||||
setInitConfig({
|
||||
...initConfig,
|
||||
selectKey: key as AccountSettingsStateKey,
|
||||
});
|
||||
history.replace({
|
||||
pathname: location.pathname,
|
||||
search: queryString.stringify({ type: key }),
|
||||
});
|
||||
}}
|
||||
items={menu}
|
||||
/>
|
||||
</div>
|
||||
<div className={classnames(`${prefixCls}-right`, hashId)}>
|
||||
<div className={classnames(`${prefixCls}-right-title`, hashId)}>
|
||||
{menu.map((i: any) => {
|
||||
if (i?.key === initConfig.selectKey) {
|
||||
return <div key={i}>{i.label}</div>;
|
||||
}
|
||||
return undefined;
|
||||
})}
|
||||
</div>
|
||||
{renderChildren()}
|
||||
</div>
|
||||
</div>
|
||||
</GridContent>
|
||||
</PageContainer>,
|
||||
);
|
||||
};
|
||||
export default AccountSettings;
|
|
@ -0,0 +1,303 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import { ProForm, ProFormText, useStyle as useAntdStyle } from '@ant-design/pro-components';
|
||||
import { App, Avatar, Button, Form, Skeleton, Upload } from 'antd';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { changeBaseInfo } from '../service';
|
||||
import { useAsyncEffect } from 'ahooks';
|
||||
import ImgCrop from 'antd-img-crop';
|
||||
import { uploadFile } from '@/services/upload';
|
||||
import { useModel } from '@umijs/max';
|
||||
import classnames from 'classnames';
|
||||
import { useIntl } from '@@/exports';
|
||||
|
||||
const prefixCls = 'account-base';
|
||||
|
||||
function useStyle() {
|
||||
return useAntdStyle('AccountBaseComponent', (token) => {
|
||||
return [
|
||||
{
|
||||
[`.${prefixCls}`]: {
|
||||
display: 'flex',
|
||||
'padding-top': '12px',
|
||||
[`&-left`]: {
|
||||
minWidth: '224px',
|
||||
maxWidth: '448px',
|
||||
},
|
||||
[`&-right`]: {
|
||||
flex: 1,
|
||||
'padding-inline-start': '104px',
|
||||
},
|
||||
[`&-avatar`]: {
|
||||
marginBottom: '12px',
|
||||
overflow: 'hidden',
|
||||
img: {
|
||||
width: '100%',
|
||||
},
|
||||
[`&-name`]: {
|
||||
verticalAlign: 'middle',
|
||||
backgroundColor: `${token.colorPrimary} !important`,
|
||||
},
|
||||
[`${token.antCls}-avatar`]: {
|
||||
[`&-string`]: {
|
||||
fontSize: '75px',
|
||||
},
|
||||
},
|
||||
[`&-title`]: {
|
||||
height: '22px',
|
||||
marginBottom: '8px',
|
||||
color: '@heading-color',
|
||||
fontSize: '@font-size-base',
|
||||
lineHeight: '22px',
|
||||
},
|
||||
[`&-button-view`]: {
|
||||
width: '144px',
|
||||
textAlign: 'center',
|
||||
},
|
||||
},
|
||||
},
|
||||
[`@media screen and (max-width: ${token.screenXL}px)`]: {
|
||||
[`.${prefixCls}`]: {
|
||||
flexDirection: 'column-reverse',
|
||||
[`&-right`]: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
maxWidth: '448px',
|
||||
padding: '20px',
|
||||
},
|
||||
['&-avatar']: {
|
||||
['&-title']: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
export const FORM_ITEM_LAYOUT = {
|
||||
labelCol: {
|
||||
span: 5,
|
||||
},
|
||||
wrapperCol: {
|
||||
span: 19,
|
||||
},
|
||||
};
|
||||
|
||||
const BaseView = () => {
|
||||
const intl = useIntl();
|
||||
const useApp = App.useApp();
|
||||
const { wrapSSR, hashId } = useStyle();
|
||||
const [loading, setLoading] = useState<boolean>();
|
||||
const { initialState, setInitialState } = useModel('@@initialState');
|
||||
const [avatarURL, setAvatarURL] = useState<string | undefined>(initialState?.currentUser?.avatar);
|
||||
const [avatarUploaded, setAvatarUploaded] = useState<boolean>(false);
|
||||
const [name, setName] = useState<string>('');
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
setLoading(true);
|
||||
if (initialState && initialState.currentUser) {
|
||||
setAvatarURL(initialState?.currentUser?.avatar);
|
||||
setName(initialState?.currentUser?.fullName || initialState?.currentUser?.username);
|
||||
setTimeout(async () => {
|
||||
setLoading(false);
|
||||
}, 500);
|
||||
}
|
||||
}, [initialState]);
|
||||
|
||||
const handleFinish = async (values: Record<string, string>) => {
|
||||
const { success } = await changeBaseInfo({
|
||||
fullName: values.fullName,
|
||||
nickName: values.nickName,
|
||||
avatar: avatarUploaded ? avatarURL : undefined,
|
||||
});
|
||||
if (success) {
|
||||
useApp.message.success(intl.formatMessage({ id: 'app.update_success' }));
|
||||
//获取当前用户信息
|
||||
const currentUser = await initialState?.fetchUserInfo?.();
|
||||
await setInitialState((s: any) => ({ ...s, currentUser: currentUser }));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 头像组件 方便以后独立,增加裁剪之类的功能
|
||||
*
|
||||
* @param avatar
|
||||
* @param name
|
||||
* @param callBack
|
||||
* @constructor
|
||||
*/
|
||||
const AvatarView = ({
|
||||
avatar,
|
||||
name,
|
||||
callBack,
|
||||
}: {
|
||||
avatar: string | undefined;
|
||||
name: string;
|
||||
callBack: any;
|
||||
}) => (
|
||||
<>
|
||||
<div className={classnames(`${prefixCls}-avatar-title`, hashId)}>
|
||||
{intl.formatMessage({ id: 'page.user.profile.base.avatar_title' })}
|
||||
</div>
|
||||
<div className={classnames(`${prefixCls}-avatar`, hashId)}>
|
||||
{avatar ? (
|
||||
<Avatar alt="avatar" shape={'circle'} size={144} src={avatar} />
|
||||
) : (
|
||||
<Avatar
|
||||
shape={'circle'}
|
||||
className={classnames(`${prefixCls}-avatar-name`, hashId)}
|
||||
size={144}
|
||||
>
|
||||
{name.substring(0, 1)}
|
||||
</Avatar>
|
||||
)}
|
||||
</div>
|
||||
<ImgCrop
|
||||
rotationSlider
|
||||
aspectSlider
|
||||
modalOk={intl.formatMessage({ id: 'app.confirm' })}
|
||||
modalCancel={intl.formatMessage({ id: 'app.cancel' })}
|
||||
>
|
||||
<Upload
|
||||
name="file"
|
||||
showUploadList={false}
|
||||
accept="image/png, image/jpeg"
|
||||
customRequest={async (files) => {
|
||||
if (!files.file) {
|
||||
return;
|
||||
}
|
||||
const { success, result, message } = await uploadFile(files.file);
|
||||
if (success && result) {
|
||||
callBack(result);
|
||||
return;
|
||||
}
|
||||
useApp.message.error(message);
|
||||
}}
|
||||
>
|
||||
<div className={classnames(`${prefixCls}-avatar-button-view`, hashId)}>
|
||||
<Button>
|
||||
<UploadOutlined />
|
||||
{intl.formatMessage({ id: 'page.user.profile.base.avatar_change_title' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Upload>
|
||||
</ImgCrop>
|
||||
</>
|
||||
);
|
||||
|
||||
return wrapSSR(
|
||||
<div className={classnames(`${prefixCls}`, hashId)}>
|
||||
{loading ? (
|
||||
<Skeleton paragraph={{ rows: 8 }} active />
|
||||
) : (
|
||||
<>
|
||||
<div className={classnames(`${prefixCls}-left`, hashId)}>
|
||||
<ProForm
|
||||
layout="horizontal"
|
||||
labelAlign={'left'}
|
||||
{...FORM_ITEM_LAYOUT}
|
||||
onFinish={handleFinish}
|
||||
submitter={{
|
||||
render: (p, dom) => {
|
||||
return <Form.Item wrapperCol={{ span: 19, offset: 5 }}>{dom}</Form.Item>;
|
||||
},
|
||||
searchConfig: {
|
||||
submitText: intl.formatMessage({
|
||||
id: 'page.user.profile.base.form.update_button',
|
||||
}),
|
||||
},
|
||||
resetButtonProps: {
|
||||
style: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
}}
|
||||
initialValues={{
|
||||
...initialState?.currentUser,
|
||||
phone: initialState?.currentUser?.phone?.split('-'),
|
||||
}}
|
||||
requiredMark={false}
|
||||
>
|
||||
<ProFormText
|
||||
width="md"
|
||||
name="accountId"
|
||||
readonly
|
||||
label={intl.formatMessage({ id: 'page.user.profile.base.form.account_id' })}
|
||||
/>
|
||||
<ProFormText
|
||||
width="md"
|
||||
name="username"
|
||||
readonly
|
||||
label={intl.formatMessage({ id: 'page.user.profile.base.form.username' })}
|
||||
/>
|
||||
<ProFormText
|
||||
width="md"
|
||||
name="email"
|
||||
readonly
|
||||
label={intl.formatMessage({ id: 'page.user.profile.base.form.email' })}
|
||||
/>
|
||||
<ProFormText
|
||||
width="md"
|
||||
name="phone"
|
||||
readonly
|
||||
label={intl.formatMessage({ id: 'page.user.profile.base.form.phone' })}
|
||||
/>
|
||||
<ProFormText
|
||||
width="md"
|
||||
name="fullName"
|
||||
label={intl.formatMessage({ id: 'page.user.profile.base.form.full_name' })}
|
||||
allowClear={false}
|
||||
/>
|
||||
<ProFormText
|
||||
width="md"
|
||||
name="nickName"
|
||||
label={intl.formatMessage({ id: 'page.user.profile.base.form.nick_name' })}
|
||||
allowClear={false}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'page.user.profile.base.form.nick_name.rule.0',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</ProForm>
|
||||
</div>
|
||||
<div className={classnames(`${prefixCls}-right`, hashId)}>
|
||||
<AvatarView
|
||||
avatar={avatarURL}
|
||||
callBack={(avatarUrl: string) => {
|
||||
setAvatarURL(avatarUrl);
|
||||
setAvatarUploaded(true);
|
||||
}}
|
||||
name={name}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>,
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseView;
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { FieldNames, ServerExceptionStatus } from '../constant';
|
||||
import { changeEmail, prepareChangeEmail } from '../service';
|
||||
import type { CaptFieldRef, ProFormInstance } from '@ant-design/pro-components';
|
||||
import { ModalForm, ProFormCaptcha, ProFormText } from '@ant-design/pro-components';
|
||||
import { App, Spin } from 'antd';
|
||||
import { omit } from 'lodash';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useIntl } from '@@/exports';
|
||||
|
||||
export default (props: {
|
||||
visible: boolean;
|
||||
prefixCls: string;
|
||||
setVisible: (visible: boolean) => void;
|
||||
setRefresh: (visible: boolean) => void;
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const useApp = App.useApp();
|
||||
const { visible, setVisible, setRefresh } = props;
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
/**已发送验证码*/
|
||||
const [hasSendCaptcha, setHasSendCaptcha] = useState<boolean>(false);
|
||||
const captchaRef = useRef<CaptFieldRef>();
|
||||
const formRef = useRef<ProFormInstance>();
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
setLoading(false);
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalForm
|
||||
title={intl.formatMessage({ id: 'page.user.profile.bind.totp.form.update_email' })}
|
||||
width={'560px'}
|
||||
formRef={formRef}
|
||||
labelAlign={'right'}
|
||||
preserve={false}
|
||||
layout={'horizontal'}
|
||||
labelCol={{
|
||||
span: 4,
|
||||
}}
|
||||
wrapperCol={{
|
||||
span: 20,
|
||||
}}
|
||||
autoFocusFirstInput
|
||||
open={visible}
|
||||
modalProps={{
|
||||
destroyOnClose: true,
|
||||
maskClosable: false,
|
||||
onCancel: async () => {
|
||||
setVisible(false);
|
||||
setHasSendCaptcha(false);
|
||||
},
|
||||
}}
|
||||
onFinish={async (formData: Record<string, any>) => {
|
||||
if (!hasSendCaptcha) {
|
||||
useApp.message.error(
|
||||
intl.formatMessage({ id: 'page.user.profile.please_send_code.message' }),
|
||||
);
|
||||
return Promise.reject();
|
||||
}
|
||||
const { success } = await changeEmail(omit(formData, FieldNames.PASSWORD));
|
||||
if (success) {
|
||||
useApp.message.success(intl.formatMessage({ id: 'app.update_success' }));
|
||||
setVisible(false);
|
||||
setRefresh(true);
|
||||
setHasSendCaptcha(false);
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject();
|
||||
}}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
<ProFormText.Password
|
||||
name={FieldNames.PASSWORD}
|
||||
label={intl.formatMessage({ id: 'page.user.profile.common.form.password' })}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'page.user.profile.common.form.password.placeholder',
|
||||
})}
|
||||
fieldProps={{ autoComplete: 'off' }}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'page.user.profile.common.form.password.rule.0',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ProFormCaptcha
|
||||
name={FieldNames.EMAIL}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'page.user.profile.modify_email.form.email.placeholder',
|
||||
})}
|
||||
label={intl.formatMessage({ id: 'page.user.profile.modify_email.form.email' })}
|
||||
fieldRef={captchaRef}
|
||||
phoneName={FieldNames.EMAIL}
|
||||
fieldProps={{ autoComplete: 'off' }}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'page.user.profile.modify_email.form.email.rule.0',
|
||||
}),
|
||||
},
|
||||
{
|
||||
type: 'email',
|
||||
message: intl.formatMessage({
|
||||
id: 'page.user.profile.modify_email.form.email.rule.1',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
onGetCaptcha={async (email) => {
|
||||
if (!(await formRef.current?.validateFields([FieldNames.PASSWORD]))) {
|
||||
return Promise.reject();
|
||||
}
|
||||
const { success, message, result, status } = await prepareChangeEmail({
|
||||
email: email,
|
||||
password: formRef.current?.getFieldValue(FieldNames.PASSWORD),
|
||||
});
|
||||
if (!success && status === ServerExceptionStatus.PASSWORD_VALIDATED_FAIL_ERROR) {
|
||||
formRef.current?.setFields([{ name: FieldNames.PASSWORD, errors: [`${message}`] }]);
|
||||
return Promise.reject();
|
||||
}
|
||||
if (success && result) {
|
||||
setHasSendCaptcha(true);
|
||||
useApp.message.success(intl.formatMessage({ id: 'app.send_successfully' }));
|
||||
return Promise.resolve();
|
||||
}
|
||||
useApp.message.error(message);
|
||||
captchaRef.current?.endTiming();
|
||||
return Promise.reject();
|
||||
}}
|
||||
/>
|
||||
<ProFormText
|
||||
label={intl.formatMessage({ id: 'page.user.profile.common.form.code' })}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'page.user.profile.common.form.code.placeholder',
|
||||
})}
|
||||
name={FieldNames.OTP}
|
||||
fieldProps={{ autoComplete: 'off' }}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({ id: 'page.user.profile.common.form.code.rule.0' }),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Spin>
|
||||
</ModalForm>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { FieldNames, ServerExceptionStatus } from '../constant';
|
||||
import { changePassword } from '../service';
|
||||
import { ModalForm, ProFormInstance, ProFormText } from '@ant-design/pro-components';
|
||||
import { App, Spin } from 'antd';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import * as React from 'react';
|
||||
import { useIntl } from '@umijs/max';
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
* @param props
|
||||
* @constructor
|
||||
*/
|
||||
const ModifyPassword = (props: {
|
||||
visible: boolean;
|
||||
prefixCls: string;
|
||||
setRefresh: (visible: boolean) => void;
|
||||
setVisible: (visible: boolean) => void;
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const useApp = App.useApp();
|
||||
const { visible, setVisible, setRefresh } = props;
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const formRef = useRef<ProFormInstance>();
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
setLoading(false);
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<ModalForm
|
||||
title={intl.formatMessage({ id: 'page.user.profile.modify_password.form' })}
|
||||
initialValues={{ channel: 'sms' }}
|
||||
width={'560px'}
|
||||
formRef={formRef}
|
||||
labelAlign={'right'}
|
||||
preserve={false}
|
||||
layout={'horizontal'}
|
||||
labelCol={{
|
||||
span: 5,
|
||||
}}
|
||||
wrapperCol={{
|
||||
span: 19,
|
||||
}}
|
||||
autoFocusFirstInput
|
||||
open={visible}
|
||||
modalProps={{
|
||||
destroyOnClose: true,
|
||||
maskClosable: false,
|
||||
onCancel: async () => {
|
||||
setVisible(false);
|
||||
},
|
||||
}}
|
||||
onFinish={async (formData: Record<string, any>) => {
|
||||
const { success, result, status, message } = await changePassword({
|
||||
oldPassword: formData[FieldNames.NEW_PASSWORD] as string,
|
||||
newPassword: formData[FieldNames.OLD_PASSWORD] as string,
|
||||
});
|
||||
if (!success && status === ServerExceptionStatus.PASSWORD_VALIDATED_FAIL_ERROR) {
|
||||
formRef.current?.setFields([{ name: FieldNames.OLD_PASSWORD, errors: [`${message}`] }]);
|
||||
return Promise.reject();
|
||||
}
|
||||
if (success && result) {
|
||||
setVisible(false);
|
||||
useApp.message.success(
|
||||
intl.formatMessage({ id: 'page.user.profile.modify_password.success' }),
|
||||
);
|
||||
setRefresh(true);
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject();
|
||||
}}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
<ProFormText.Password
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'page.user.profile.modify_password.form.old_password.placeholder',
|
||||
})}
|
||||
label={intl.formatMessage({ id: 'page.user.profile.modify_password.form.old_password' })}
|
||||
name={FieldNames.OLD_PASSWORD}
|
||||
fieldProps={{ autoComplete: 'off' }}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'page.user.profile.modify_password.form.old_password.rule.0',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ProFormText.Password
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'page.user.profile.modify_password.form.new_password.placeholder',
|
||||
})}
|
||||
label={intl.formatMessage({ id: 'page.user.profile.modify_password.form.new_password' })}
|
||||
name={FieldNames.NEW_PASSWORD}
|
||||
fieldProps={{ autoComplete: 'off' }}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'page.user.profile.modify_password.form.new_password.rule.0',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ProFormText.Password
|
||||
label={intl.formatMessage({
|
||||
id: 'pages.setting.administrator.reset_password_modal.from.confirm_password',
|
||||
})}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'pages.setting.administrator.reset_password_modal.from.confirm_password.placeholder',
|
||||
})}
|
||||
name={'confirmPassword'}
|
||||
fieldProps={{ autoComplete: 'off' }}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'pages.setting.administrator.reset_password_modal.from.confirm_password.rule.0.message',
|
||||
}),
|
||||
},
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue(FieldNames.NEW_PASSWORD) === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(
|
||||
new Error(intl.formatMessage({ id: 'app.password.not_match' })),
|
||||
);
|
||||
},
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
</Spin>
|
||||
</ModalForm>
|
||||
);
|
||||
};
|
||||
export default ModifyPassword;
|
|
@ -0,0 +1,228 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { changePhone, prepareChangePhone } from '../service';
|
||||
import { phoneIsValidNumber } from '@/utils/utils';
|
||||
import { FormattedMessage } from '@@/plugin-locale/localeExports';
|
||||
import type { CaptFieldRef, ProFormInstance } from '@ant-design/pro-components';
|
||||
import {
|
||||
ModalForm,
|
||||
ProFormCaptcha,
|
||||
ProFormDependency,
|
||||
ProFormText,
|
||||
useStyle as useAntdStyle,
|
||||
} from '@ant-design/pro-components';
|
||||
import { App, ConfigProvider, Spin } from 'antd';
|
||||
import { omit } from 'lodash';
|
||||
import { useContext, useEffect, useRef, useState } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { ConfigContext } from 'antd/es/config-provider';
|
||||
import { useIntl } from '@@/exports';
|
||||
import FormPhoneAreaCodeSelect from '@/components/FormPhoneAreaCodeSelect';
|
||||
import { FieldNames, ServerExceptionStatus } from '../constant';
|
||||
import * as React from 'react';
|
||||
|
||||
function useStyle(prefixCls: string) {
|
||||
const { getPrefixCls } = useContext(ConfigContext || ConfigProvider.ConfigContext);
|
||||
const antCls = `.${getPrefixCls()}`;
|
||||
return useAntdStyle('AccountModifyPhoneComponent', () => {
|
||||
return [
|
||||
{
|
||||
[`.${prefixCls}`]: {
|
||||
['&-captcha']: {
|
||||
[`div${antCls}-form-item-control-input`]: {
|
||||
width: '100%',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
export default (props: {
|
||||
visible: boolean;
|
||||
prefixCls: string;
|
||||
setVisible: (visible: boolean) => void;
|
||||
setRefresh: (visible: boolean) => void;
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const useApp = App.useApp();
|
||||
const { visible, setVisible, setRefresh, prefixCls } = props;
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const captchaRef = useRef<CaptFieldRef>();
|
||||
/**已发送验证码*/
|
||||
const [hasSendCaptcha, setHasSendCaptcha] = useState<boolean>(false);
|
||||
const formRef = useRef<ProFormInstance>();
|
||||
const { wrapSSR, hashId } = useStyle(prefixCls);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
setLoading(false);
|
||||
}, [visible]);
|
||||
|
||||
return wrapSSR(
|
||||
<ModalForm
|
||||
title={intl.formatMessage({ id: 'page.user.profile.modify_email.form' })}
|
||||
width={'560px'}
|
||||
className={classnames(`${prefixCls}`, hashId)}
|
||||
formRef={formRef}
|
||||
labelAlign={'right'}
|
||||
preserve={false}
|
||||
layout={'horizontal'}
|
||||
labelCol={{
|
||||
span: 4,
|
||||
}}
|
||||
wrapperCol={{
|
||||
span: 20,
|
||||
}}
|
||||
autoFocusFirstInput
|
||||
open={visible}
|
||||
modalProps={{
|
||||
destroyOnClose: true,
|
||||
maskClosable: false,
|
||||
onCancel: async () => {
|
||||
setVisible(false);
|
||||
setHasSendCaptcha(false);
|
||||
},
|
||||
}}
|
||||
onFinish={async (formData: Record<string, any>) => {
|
||||
if (!hasSendCaptcha) {
|
||||
useApp.message.error(
|
||||
intl.formatMessage({ id: 'page.user.profile.please_send_code.message' }),
|
||||
);
|
||||
return Promise.reject();
|
||||
}
|
||||
const { success } = await changePhone(omit(formData, FieldNames.PASSWORD));
|
||||
if (success) {
|
||||
useApp.message.success(intl.formatMessage({ id: 'app.update_success' }));
|
||||
setVisible(false);
|
||||
setRefresh(true);
|
||||
setHasSendCaptcha(false);
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject();
|
||||
}}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
<ProFormText.Password
|
||||
name={FieldNames.PASSWORD}
|
||||
label={intl.formatMessage({ id: 'page.user.profile.common.form.password' })}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'page.user.profile.common.form.password.placeholder',
|
||||
})}
|
||||
fieldProps={{ autoComplete: 'off' }}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({ id: 'page.user.profile.common.form.password.rule.0' }),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ProFormDependency name={['phoneAreaCode']}>
|
||||
{({ phoneAreaCode }) => {
|
||||
return (
|
||||
<ProFormCaptcha
|
||||
name={FieldNames.PHONE}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'page.user.profile.common.form.phone.placeholder',
|
||||
})}
|
||||
label={intl.formatMessage({ id: 'page.user.profile.common.form.phone' })}
|
||||
fieldProps={{ autoComplete: 'off' }}
|
||||
fieldRef={captchaRef}
|
||||
formItemProps={{ className: classnames(`${prefixCls}-captcha`, hashId) }}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: <FormattedMessage id={'page.user.profile.common.form.phone.rule.0'} />,
|
||||
},
|
||||
{
|
||||
validator: async (rule, value) => {
|
||||
if (!value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
//校验手机号格式
|
||||
const isValidNumber = await phoneIsValidNumber(value, phoneAreaCode);
|
||||
if (!isValidNumber) {
|
||||
return Promise.reject<any>(
|
||||
new Error(
|
||||
intl.formatMessage({
|
||||
id: 'page.user.profile.common.form.phone.rule.1',
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
validateTrigger: ['onBlur'],
|
||||
},
|
||||
]}
|
||||
phoneName={FieldNames.PHONE}
|
||||
addonBefore={
|
||||
<FormPhoneAreaCodeSelect
|
||||
name={'phoneAreaCode'}
|
||||
showSearch
|
||||
noStyle
|
||||
allowClear={false}
|
||||
style={{ maxWidth: '200px' }}
|
||||
fieldProps={{
|
||||
placement: 'bottomLeft',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onGetCaptcha={async (mobile) => {
|
||||
if (!(await formRef.current?.validateFields([FieldNames.PASSWORD]))) {
|
||||
return Promise.reject();
|
||||
}
|
||||
const { success, message, status, result } = await prepareChangePhone({
|
||||
phone: mobile as string,
|
||||
phoneRegion: phoneAreaCode,
|
||||
password: formRef.current?.getFieldValue(FieldNames.PASSWORD),
|
||||
});
|
||||
if (!success && status === ServerExceptionStatus.PASSWORD_VALIDATED_FAIL_ERROR) {
|
||||
formRef.current?.setFields([
|
||||
{ name: FieldNames.PASSWORD, errors: [`${message}`] },
|
||||
]);
|
||||
return Promise.reject();
|
||||
}
|
||||
if (success && result) {
|
||||
setHasSendCaptcha(true);
|
||||
useApp.message.success(intl.formatMessage({ id: 'app.send_successfully' }));
|
||||
return Promise.resolve();
|
||||
}
|
||||
useApp.message.error(message);
|
||||
captchaRef.current?.endTiming();
|
||||
return Promise.reject();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</ProFormDependency>
|
||||
<ProFormText
|
||||
placeholder={intl.formatMessage({ id: 'page.user.profile.common.form.code.placeholder' })}
|
||||
label={intl.formatMessage({ id: 'page.user.profile.common.form.code' })}
|
||||
name={FieldNames.OTP}
|
||||
fieldProps={{ autoComplete: 'off' }}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({ id: 'page.user.profile.common.form.code.rule.0' }),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Spin>
|
||||
</ModalForm>,
|
||||
);
|
||||
};
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { useModel } from '@umijs/max';
|
||||
import { useAsyncEffect } from 'ahooks';
|
||||
import { List, Skeleton } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import ModifyEmail from './ModifyEmail';
|
||||
import ModifyPassword from './ModifyPassword';
|
||||
import ModifyPhone from './ModifyPhone';
|
||||
import classnames from 'classnames';
|
||||
import { useStyle as useAntdStyle } from '@ant-design/pro-components';
|
||||
|
||||
type Unpacked<T> = T extends (infer U)[] ? U : T;
|
||||
|
||||
function useStyle(prefixCls: string) {
|
||||
return useAntdStyle('AccountSecurityComponent', (token) => {
|
||||
return [
|
||||
{
|
||||
[`.${prefixCls}`]: {
|
||||
'&-strong': {
|
||||
color: `${token.colorSuccess}`,
|
||||
},
|
||||
'&-medium': {
|
||||
color: `${token.colorWarning}`,
|
||||
},
|
||||
'&-weak': {
|
||||
color: `${token.colorError}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
const SecurityView = () => {
|
||||
const prefixCls = 'account-security';
|
||||
const { wrapSSR, hashId } = useStyle(prefixCls);
|
||||
|
||||
/**更新密码*/
|
||||
const [modifyPasswordVisible, setModifyPasswordVisible] = useState<boolean>(false);
|
||||
/**更新手机号*/
|
||||
const [modifyPhoneVisible, setModifyPhoneVisible] = useState<boolean>(false);
|
||||
/**更新邮箱*/
|
||||
const [modifyEmailVisible, setModifyEmailVisible] = useState<boolean>(false);
|
||||
/**刷新*/
|
||||
const [refresh, setRefresh] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>();
|
||||
const { initialState, setInitialState } = useModel('@@initialState');
|
||||
useAsyncEffect(async () => {
|
||||
setLoading(true);
|
||||
if (initialState && initialState?.currentUser) {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [initialState]);
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
if (refresh) {
|
||||
setLoading(true);
|
||||
//获取当前用户信息
|
||||
const currentUser = await initialState?.fetchUserInfo?.();
|
||||
await setInitialState((s: any) => ({ ...s, currentUser: currentUser }));
|
||||
setRefresh(false);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [refresh]);
|
||||
|
||||
const getData = () => [
|
||||
{
|
||||
title: '账户密码',
|
||||
actions: [
|
||||
<a
|
||||
key="Modify"
|
||||
onClick={() => {
|
||||
setModifyPasswordVisible(true);
|
||||
}}
|
||||
>
|
||||
修改
|
||||
</a>,
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '账户手机',
|
||||
description: initialState?.currentUser?.phone
|
||||
? `已绑定手机:${initialState?.currentUser?.phone}`
|
||||
: `暂未绑定`,
|
||||
actions: [
|
||||
<a
|
||||
key="Modify"
|
||||
onClick={() => {
|
||||
setModifyPhoneVisible(true);
|
||||
}}
|
||||
>
|
||||
修改
|
||||
</a>,
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '账户邮箱',
|
||||
description: initialState?.currentUser?.email
|
||||
? `已绑定邮箱:${initialState?.currentUser?.email}`
|
||||
: `暂未绑定`,
|
||||
actions: [
|
||||
<a
|
||||
key="Modify"
|
||||
onClick={() => {
|
||||
setModifyEmailVisible(true);
|
||||
}}
|
||||
>
|
||||
修改
|
||||
</a>,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const data = getData();
|
||||
|
||||
return wrapSSR(
|
||||
<Skeleton loading={loading} paragraph={{ rows: 8 }}>
|
||||
<List<Unpacked<typeof data>>
|
||||
itemLayout="horizontal"
|
||||
className={classnames(`${prefixCls}`, hashId)}
|
||||
dataSource={data}
|
||||
renderItem={(item) => (
|
||||
<List.Item actions={item.actions}>
|
||||
<List.Item.Meta title={item.title} description={item.description} />
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
{/*更新密码*/}
|
||||
<ModifyPassword
|
||||
visible={modifyPasswordVisible}
|
||||
setRefresh={setRefresh}
|
||||
setVisible={(visible) => {
|
||||
setModifyPasswordVisible(visible);
|
||||
}}
|
||||
prefixCls={prefixCls}
|
||||
/>
|
||||
{/*更新手机号*/}
|
||||
<ModifyPhone
|
||||
visible={modifyPhoneVisible}
|
||||
setRefresh={setRefresh}
|
||||
setVisible={(visible) => {
|
||||
setModifyPhoneVisible(visible);
|
||||
}}
|
||||
prefixCls={prefixCls}
|
||||
/>
|
||||
{/*更新手机号*/}
|
||||
<ModifyEmail
|
||||
visible={modifyEmailVisible}
|
||||
setRefresh={setRefresh}
|
||||
setVisible={(visible) => {
|
||||
setModifyEmailVisible(visible);
|
||||
}}
|
||||
prefixCls={prefixCls}
|
||||
/>
|
||||
</Skeleton>,
|
||||
);
|
||||
};
|
||||
|
||||
export default SecurityView;
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/**
|
||||
* 服务异常状态
|
||||
*/
|
||||
export enum ServerExceptionStatus {
|
||||
/**密码验证失败错误 */
|
||||
PASSWORD_VALIDATED_FAIL_ERROR = 'password_validated_fail_error',
|
||||
/**无效的 MFA 代码错误 */
|
||||
INVALID_MFA_CODE_ERROR = 'invalid_mfa_code_error',
|
||||
/**MFA 未发现秘密错误 */
|
||||
BIND_MFA_NOT_FOUND_SECRET_ERROR = 'bind_mfa_not_found_secret_error',
|
||||
}
|
||||
|
||||
/**
|
||||
* 字段名称
|
||||
*/
|
||||
export enum FieldNames {
|
||||
/**密码 */
|
||||
PASSWORD = 'password',
|
||||
/**OTP */
|
||||
OTP = 'otp',
|
||||
/**手机号 */
|
||||
PHONE = 'phone',
|
||||
/**邮箱 */
|
||||
EMAIL = 'email',
|
||||
/**旧密码 */
|
||||
OLD_PASSWORD = 'oldPassword',
|
||||
/**新密码 */
|
||||
NEW_PASSWORD = 'newPassword',
|
||||
/**验证码*/
|
||||
VERIFY_CODE = 'verifyCode',
|
||||
CHANNEL = 'channel',
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/**
|
||||
* 账户信息
|
||||
*/
|
||||
export type AccountInfo = {
|
||||
/** 用户ID */
|
||||
id: string;
|
||||
avatar: string;
|
||||
username: string;
|
||||
phone: string;
|
||||
access: string;
|
||||
};
|
||||
|
||||
export interface GetBoundIdpList {
|
||||
code: string;
|
||||
name: string;
|
||||
type: string;
|
||||
category: string;
|
||||
bound: boolean;
|
||||
idpId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 账户菜单类型
|
||||
*/
|
||||
export enum AccountSettingsStateKey {
|
||||
base = 'base',
|
||||
security = 'security',
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* eiam-portal - Employee Identity and Access Management
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
@ -15,18 +15,6 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.portal.controller;
|
||||
import Profile from './Profile';
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* message
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2021/9/12 21:42
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/notice")
|
||||
public class NoticeController {
|
||||
}
|
||||
export default Profile;
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export default {
|
||||
'page.user.profile.menu.base': '基础信息',
|
||||
'page.user.profile.menu.security': '安全设置',
|
||||
'page.user.profile.menu.bind': '账号绑定',
|
||||
'page.user.profile.base.avatar_title': '头像',
|
||||
'page.user.profile.base.avatar_change_title': '更换头像',
|
||||
'page.user.profile.base.form.account_id': '账户ID',
|
||||
'page.user.profile.base.form.username': '用户名',
|
||||
'page.user.profile.base.form.email': '邮箱',
|
||||
'page.user.profile.base.form.phone': '手机号',
|
||||
'page.user.profile.base.form.full_name': '姓名',
|
||||
'page.user.profile.base.form.nick_name': '昵称',
|
||||
'page.user.profile.base.form.nick_name.rule.0': '请输入您的昵称',
|
||||
'page.user.profile.base.form.update_button': '更新',
|
||||
'page.user.profile.common.form.password': '密码',
|
||||
'page.user.profile.common.form.password.placeholder': '请输入密码',
|
||||
'page.user.profile.common.form.password.rule.0': '请输入密码',
|
||||
|
||||
'page.user.profile.common.form.phone': '手机号',
|
||||
'page.user.profile.common.form.phone.placeholder': '请输入手机号',
|
||||
'page.user.profile.common.form.phone.rule.0': '手机号未填写',
|
||||
'page.user.profile.common.form.phone.rule.1': '手机号不合法',
|
||||
|
||||
'page.user.profile.common.form.code': '验证码',
|
||||
'page.user.profile.common.form.code.placeholder': '请输入验证码',
|
||||
'page.user.profile.common.form.code.rule.0': '请输入验证码',
|
||||
'page.user.profile.please_send_code.message': '请发送验证码',
|
||||
'page.user.profile.unbind': '解绑',
|
||||
'pages.account.unbind.confirm': '您确定要解除该平台绑定吗?',
|
||||
'page.user.profile.bind': '绑定',
|
||||
'page.user.profile.bind.success': '绑定成功',
|
||||
'page.user.profile.bind.totp': '绑定动态口令',
|
||||
'page.user.profile.bind.totp.form.verify': '身份验证',
|
||||
'page.user.profile.bind.totp.form.verify.placeholder': '输入密码确认身份',
|
||||
'page.user.profile.bind.totp.form.bind': '绑定动态口令',
|
||||
'page.user.profile.bind.totp.form.bind.placeholder': '使用移动端认证器绑定口令',
|
||||
'page.user.profile.bind.totp.form.bind.alert':
|
||||
'请使用市面常见认证器 APP,扫描下方二维码,完成绑定。',
|
||||
'page.user.profile.bind.totp.form.bind.paragraph':
|
||||
'扫码绑定后,请您输入移动端 APP 中的六位动态口令,完成本次绑定。',
|
||||
'page.user.profile.bind.totp.form.update_email': '修改邮箱',
|
||||
'page.user.profile.modify_email.form.email': '邮箱',
|
||||
'page.user.profile.modify_email.form.email.placeholder': '请输入邮箱',
|
||||
'page.user.profile.modify_email.form.email.rule.0': '请输入邮箱',
|
||||
'page.user.profile.modify_email.form.email.rule.1': '邮箱格式不正确',
|
||||
'page.user.profile.modify_password.form': '修改密码',
|
||||
'page.user.profile.modify_password.success': '修改成功,请重新登录',
|
||||
|
||||
'page.user.profile.modify_password.form.old_password': '旧密码',
|
||||
'page.user.profile.modify_password.form.old_password.placeholder': '请输入旧密码',
|
||||
'page.user.profile.modify_password.form.old_password.rule.0': '请输入旧密码',
|
||||
|
||||
'page.user.profile.modify_password.form.new_password': '新密码',
|
||||
'page.user.profile.modify_password.form.new_password.placeholder': '请输入新密码',
|
||||
'page.user.profile.modify_password.form.new_password.rule.0': '请输入新密码',
|
||||
'page.user.profile.modify_password.form.verify-code': '验证码',
|
||||
'page.user.profile.modify_password.form.verify-code-type.label': '验证方式',
|
||||
'page.user.profile.modify_password.form.verify-code-type.rule.0': '请选择验证方式',
|
||||
'page.user.profile.modify_password.form.phone.label': '短信',
|
||||
'page.user.profile.modify_password.form.mail.label': '邮件',
|
||||
'page.user.profile.modify_password.form.phone': '手机号',
|
||||
'page.user.profile.modify_password.form.mail': '邮箱',
|
||||
'page.user.profile.modify_email.form': '修改手机号',
|
||||
};
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { request } from '@@/plugin-request/request';
|
||||
|
||||
/**
|
||||
* 准备修改手机号
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
export async function prepareChangePhone(data: {
|
||||
phone: string;
|
||||
phoneRegion: string;
|
||||
password: string;
|
||||
}): Promise<API.ApiResult<boolean>> {
|
||||
return request(`/api/v1/user/profile/prepare_change_phone`, {
|
||||
data: data,
|
||||
method: 'POST',
|
||||
skipErrorHandler: true,
|
||||
}).catch(({ response: { data } }) => {
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更换手机
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
export async function changePhone(data: Record<string, string>): Promise<API.ApiResult<boolean>> {
|
||||
return request(`/api/v1/user/profile/change_phone`, {
|
||||
data: data,
|
||||
method: 'PUT',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备更换邮箱
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
export async function prepareChangeEmail(data: {
|
||||
password: string;
|
||||
email: string;
|
||||
}): Promise<API.ApiResult<boolean>> {
|
||||
return request(`/api/v1/user/profile/prepare_change_email`, {
|
||||
data: data,
|
||||
method: 'POST',
|
||||
skipErrorHandler: true,
|
||||
}).catch(({ response: { data } }) => {
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更换邮箱
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
export async function changeEmail(data: Record<string, string>): Promise<API.ApiResult<boolean>> {
|
||||
return request(`/api/v1/user/profile/change_email`, {
|
||||
data: data,
|
||||
method: 'PUT',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更换密码
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
export async function changePassword(data: {
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
}): Promise<API.ApiResult<boolean>> {
|
||||
return request(`/api/v1/user/profile/change_password`, {
|
||||
data: data,
|
||||
method: 'PUT',
|
||||
skipErrorHandler: true,
|
||||
}).catch(({ response: { data } }) => {
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更改基础信息
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
export async function changeBaseInfo(
|
||||
data: Record<string, string | undefined>,
|
||||
): Promise<API.ApiResult<boolean>> {
|
||||
return request(`/api/v1/user/profile/change_info`, {
|
||||
data: data,
|
||||
method: 'PUT',
|
||||
});
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import type { GenerateStyle, ProAliasToken } from '@ant-design/pro-components';
|
||||
import { useStyle as useAntdStyle } from '@ant-design/pro-components';
|
||||
import { ConfigProvider } from 'antd';
|
||||
import { useContext } from 'react';
|
||||
|
||||
const { ConfigContext } = ConfigProvider;
|
||||
|
||||
interface AccountToken extends ProAliasToken {
|
||||
antCls: string;
|
||||
prefixCls: string;
|
||||
}
|
||||
|
||||
const genActionsStyle: GenerateStyle<AccountToken> = (token) => {
|
||||
const { prefixCls, antCls } = token;
|
||||
|
||||
return {
|
||||
[`${prefixCls}`]: {
|
||||
['&-main']: {
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
paddingTop: '16px',
|
||||
paddingBottom: '16px',
|
||||
'background-color': `${token.colorBgBase}`,
|
||||
},
|
||||
['&-left']: {
|
||||
width: '224px',
|
||||
[`${antCls}-menu-light${antCls}-menu-root${antCls}-menu-inline`]: {
|
||||
'border-inline-end': '1px solid rgba(5, 5, 5, 0.06)',
|
||||
height: '100%',
|
||||
},
|
||||
[`${antCls}-menu-light:not(${antCls}-menu-horizontal) ${antCls}-menu-item-selected`]: {
|
||||
'background-color': `${token.layout?.sider?.colorBgMenuItemSelected}`,
|
||||
color: `${token.layout?.sider?.colorTextMenuSelected}`,
|
||||
},
|
||||
},
|
||||
['&-right']: {
|
||||
flex: 1,
|
||||
padding: '8px 40px',
|
||||
[`${antCls}-list ${antCls}-list-item`]: {
|
||||
'padding-inline-start': 0,
|
||||
},
|
||||
['&-title']: {
|
||||
marginBottom: '12px',
|
||||
color: `${token.colorTextHeading}`,
|
||||
fontWeight: 500,
|
||||
fontSize: '20px',
|
||||
lineHeight: '28px',
|
||||
},
|
||||
},
|
||||
},
|
||||
[`@media screen and (max-width: ${token.screenMD}px)`]: {
|
||||
[`${prefixCls}`]: {
|
||||
['&-main']: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
['&-left']: {
|
||||
width: '100%',
|
||||
border: 'none',
|
||||
},
|
||||
['&-right']: {
|
||||
padding: '40px',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default function useStyle(prefixCls?: string) {
|
||||
const { getPrefixCls } = useContext(ConfigContext || ConfigProvider.ConfigContext);
|
||||
const antCls = `.${getPrefixCls()}`;
|
||||
|
||||
return useAntdStyle('AccountToken', (token) => {
|
||||
const accountToken: AccountToken = {
|
||||
...token,
|
||||
prefixCls: `.${prefixCls}`,
|
||||
antCls,
|
||||
};
|
||||
|
||||
return [genActionsStyle(accountToken)];
|
||||
});
|
||||
}
|
|
@ -67,7 +67,6 @@ declare namespace API {
|
|||
secret: string;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 加密秘钥
|
||||
*/
|
||||
|
@ -181,6 +180,7 @@ declare namespace AccountAPI {
|
|||
dataOrigin: string;
|
||||
authTotal: string;
|
||||
lastAuthTime: string;
|
||||
primaryOrgDisplayPath: string;
|
||||
orgDisplayPath: string;
|
||||
remark: string;
|
||||
custom?: Record<string, any>;
|
||||
|
@ -220,6 +220,10 @@ declare namespace AccountAPI {
|
|||
export interface UserLoginAuditList {
|
||||
appName: string;
|
||||
clientIp: string;
|
||||
userAgent: {
|
||||
platformVersion: string;
|
||||
platform: string;
|
||||
};
|
||||
browser: string;
|
||||
eventStatus: string;
|
||||
eventTime: string;
|
||||
|
|
|
@ -20,6 +20,6 @@ import { request } from '@umijs/max';
|
|||
/**
|
||||
* 获取当前用户
|
||||
*/
|
||||
export async function getCurrent(): Promise<API.ApiResult<API.CurrentUser> | undefined> {
|
||||
export async function getCurrent(): Promise<API.ApiResult<API.CurrentUser>> {
|
||||
return request<API.ApiResult<API.CurrentUser>>('/api/v1/session/current_user');
|
||||
}
|
||||
|
|
|
@ -76,6 +76,7 @@ import static cn.topiam.employee.common.constant.AuthorizeConstants.FE_LOGIN;
|
|||
import static cn.topiam.employee.common.constant.AuthorizeConstants.FORM_LOGIN;
|
||||
import static cn.topiam.employee.common.constant.ConfigBeanNameConstants.DEFAULT_SECURITY_FILTER_CHAIN;
|
||||
import static cn.topiam.employee.common.constant.SessionConstants.CURRENT_STATUS;
|
||||
import static cn.topiam.employee.common.constant.SynchronizerConstants.EVENT_RECEIVE_PATH;
|
||||
import static cn.topiam.employee.core.endpoint.security.PublicSecretEndpoint.PUBLIC_SECRET_PATH;
|
||||
import static cn.topiam.employee.core.setting.constant.SecuritySettingConstants.*;
|
||||
import static cn.topiam.employee.protocol.code.util.ProtocolConfigUtils.getAuthenticationDetailsSource;
|
||||
|
@ -99,12 +100,8 @@ public class ConsoleSecurityConfiguration implements BeanClassLoaderAware {
|
|||
*/
|
||||
@Bean
|
||||
public WebSecurityCustomizer webSecurityCustomizer() {
|
||||
return (web) -> web.ignoring().requestMatchers(
|
||||
new AntPathRequestMatcher("/css/**", HttpMethod.GET.name()),
|
||||
new AntPathRequestMatcher("/js/**", HttpMethod.GET.name()),
|
||||
new AntPathRequestMatcher("/webjars/**", HttpMethod.GET.name()),
|
||||
new AntPathRequestMatcher("/images/**", HttpMethod.GET.name()),
|
||||
new AntPathRequestMatcher("/favicon.ico", HttpMethod.GET.name()));
|
||||
return web -> {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -134,7 +131,7 @@ public class ConsoleSecurityConfiguration implements BeanClassLoaderAware {
|
|||
//记住我
|
||||
.rememberMe(withRememberMeConfigurerDefaults(settingRepository))
|
||||
//CSRF
|
||||
.csrf(withCsrfConfigurerDefaults())
|
||||
.csrf(withCsrfConfigurerDefaults(new AntPathRequestMatcher(EVENT_RECEIVE_PATH+"/{code}")))
|
||||
//headers
|
||||
.headers(withHeadersConfigurerDefaults(settingRepository))
|
||||
//cors
|
||||
|
@ -157,6 +154,7 @@ public class ConsoleSecurityConfiguration implements BeanClassLoaderAware {
|
|||
public Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> authorizeHttpRequests() {
|
||||
//@formatter:off
|
||||
return registry -> {
|
||||
registry.requestMatchers(new AntPathRequestMatcher(EVENT_RECEIVE_PATH+"/{code}")).permitAll();
|
||||
registry.requestMatchers(new AntPathRequestMatcher(CURRENT_STATUS, HttpMethod.GET.name())).permitAll();
|
||||
registry.requestMatchers(new AntPathRequestMatcher(PUBLIC_SECRET_PATH, HttpMethod.GET.name())).permitAll();
|
||||
registry.anyRequest().authenticated();
|
||||
|
|
|
@ -71,6 +71,10 @@ public class CurrentUserEndpoint {
|
|||
result.setAccountId(userDetails.getId());
|
||||
//用户名
|
||||
result.setUsername(administrator.getUsername());
|
||||
//姓名
|
||||
result.setFullName(administrator.getFullName());
|
||||
//昵称
|
||||
result.setNickName(administrator.getNickName());
|
||||
//头像
|
||||
if (StringUtils.isEmpty(administrator.getAvatar())) {
|
||||
result.setAvatar(bufferedImageToBase64(generateAvatarImg(administrator.getUsername())));
|
||||
|
@ -105,6 +109,18 @@ public class CurrentUserEndpoint {
|
|||
@Schema(description = "用户名")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 姓名
|
||||
*/
|
||||
@Schema(description = "姓名")
|
||||
private String fullName;
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
@Schema(description = "昵称")
|
||||
private String nickName;
|
||||
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
|
|
|
@ -226,24 +226,6 @@ public class UserController {
|
|||
return ApiRestResult.<Boolean> builder().result(result).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户转岗
|
||||
*
|
||||
* @param userId {@link String}
|
||||
* @param orgId {@link String}
|
||||
* @return {@link Boolean}
|
||||
*/
|
||||
@Lock
|
||||
@Preview
|
||||
@Operation(summary = "用户转岗")
|
||||
@PutMapping(value = "/transfer")
|
||||
@PreAuthorize(value = "authenticated and @sae.hasAuthority(T(cn.topiam.employee.support.security.userdetails.UserType).ADMIN)")
|
||||
public ApiRestResult<Boolean> userTransfer(@Parameter(description = "用户ID") @NotBlank(message = "用户ID不能为空") String userId,
|
||||
@Parameter(description = "组织ID") @NotBlank(message = "组织ID不能为空") String orgId) {
|
||||
return ApiRestResult.<Boolean> builder().result(userService.userTransfer(userId, orgId))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户离职
|
||||
*
|
||||
|
@ -304,7 +286,7 @@ public class UserController {
|
|||
@Operation(description = "查询用户登录审计列表")
|
||||
@GetMapping(value = "/login_audit/list")
|
||||
@PreAuthorize(value = "authenticated and @sae.hasAuthority(T(cn.topiam.employee.support.security.userdetails.UserType).ADMIN)")
|
||||
public ApiRestResult<Page<UserLoginAuditListResult>> getUserLoginAuditList(@Parameter(description = "ID") Long id,
|
||||
public ApiRestResult<Page<UserLoginAuditListResult>> getUserLoginAuditList(@Parameter(description = "ID") @RequestParam(value = "userId", required = false) @NotNull(message = "用户ID不能为空") Long id,
|
||||
PageModel pageModel) {
|
||||
Page<UserLoginAuditListResult> list = userService.findUserLoginAuditList(id, pageModel);
|
||||
return ApiRestResult.ok(list);
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.console.controller.user;
|
||||
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import cn.topiam.employee.audit.annotation.Audit;
|
||||
import cn.topiam.employee.audit.event.type.EventType;
|
||||
import cn.topiam.employee.console.pojo.update.user.*;
|
||||
import cn.topiam.employee.console.service.user.UserProfileService;
|
||||
import cn.topiam.employee.support.result.ApiRestResult;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import static cn.topiam.employee.common.constant.UserConstants.*;
|
||||
import static cn.topiam.employee.support.constant.EiamConstants.V1_API_PATH;
|
||||
|
||||
/**
|
||||
* 账户管理
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2021/9/12 21:39
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = V1_API_PATH + "/user/profile")
|
||||
public class UserProfileController {
|
||||
|
||||
/**
|
||||
* 修改账户信息
|
||||
*
|
||||
* @return {@link ApiRestResult}
|
||||
*/
|
||||
@Audit(type = EventType.MODIFY_ACCOUNT_INFO_PORTAL)
|
||||
@Operation(summary = "修改账户信息")
|
||||
@PutMapping("/change_info")
|
||||
public ApiRestResult<Boolean> changeInfo(@RequestBody @Validated UpdateUserInfoRequest param) {
|
||||
Boolean result = userProfileService.changeInfo(param);
|
||||
return ApiRestResult.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*
|
||||
* @return {@link ApiRestResult}
|
||||
*/
|
||||
@Audit(type = EventType.MODIFY_USER_PASSWORD_PORTAL)
|
||||
@Operation(summary = "修改账户密码")
|
||||
@PutMapping("/change_password")
|
||||
public ApiRestResult<Boolean> changePassword(@RequestBody @Validated ChangePasswordRequest param) {
|
||||
return ApiRestResult.ok(userProfileService.changePassword(param));
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备修改手机
|
||||
*
|
||||
* @return {@link ApiRestResult}
|
||||
*/
|
||||
@Audit(type = EventType.PREPARE_MODIFY_PHONE)
|
||||
@Operation(summary = "准备修改手机")
|
||||
@PostMapping("/prepare_change_phone")
|
||||
public ApiRestResult<Boolean> prepareChangePhone(@RequestBody @Validated PrepareChangePhoneRequest param) {
|
||||
return ApiRestResult.ok(userProfileService.prepareChangePhone(param));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改手机
|
||||
*
|
||||
* @return {@link ApiRestResult}
|
||||
*/
|
||||
@Audit(type = EventType.MODIFY_USER_PHONE_PORTAL)
|
||||
@Operation(summary = "修改手机")
|
||||
@PutMapping("/change_phone")
|
||||
public ApiRestResult<Boolean> changePhone(@RequestBody @Validated ChangePhoneRequest param) {
|
||||
return ApiRestResult.ok(userProfileService.changePhone(param));
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备修改邮箱
|
||||
*
|
||||
* @return {@link ApiRestResult}
|
||||
*/
|
||||
@Audit(type = EventType.PREPARE_MODIFY_EMAIL)
|
||||
@Operation(summary = "准备修改邮箱")
|
||||
@PostMapping("/prepare_change_email")
|
||||
public ApiRestResult<Boolean> prepareChangeEmail(@RequestBody @Validated PrepareChangeEmailRequest param) {
|
||||
return ApiRestResult.ok(userProfileService.prepareChangeEmail(param));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改邮箱
|
||||
*
|
||||
* @return {@link ApiRestResult}
|
||||
*/
|
||||
@Audit(type = EventType.MODIFY_USER_EMAIL_PORTAL)
|
||||
@Operation(summary = "修改邮箱")
|
||||
@PutMapping("/change_email")
|
||||
public ApiRestResult<Boolean> changeEmail(@RequestBody @Validated ChangeEmailRequest param) {
|
||||
return ApiRestResult.ok(userProfileService.changeEmail(param));
|
||||
}
|
||||
|
||||
/**
|
||||
* 忘记密码发送验证码
|
||||
*
|
||||
* @return {@link ApiRestResult}
|
||||
*/
|
||||
@Operation(summary = "忘记密码发送验证码")
|
||||
@GetMapping(FORGET_PASSWORD_CODE)
|
||||
public ApiRestResult<Boolean> forgetPasswordCode(@Parameter(description = "验证码接收者(邮箱/手机号)") @RequestParam String recipient) {
|
||||
return ApiRestResult.ok(userProfileService.forgetPasswordCode(recipient));
|
||||
}
|
||||
|
||||
/**
|
||||
* 忘记密码预认证
|
||||
*
|
||||
* @return {@link ApiRestResult}
|
||||
*/
|
||||
@Operation(summary = "忘记密码预认证")
|
||||
@PostMapping(PREPARE_FORGET_PASSWORD)
|
||||
public ApiRestResult<Boolean> prepareForgetPassword(@RequestBody @Validated PrepareForgetPasswordRequest param) {
|
||||
return ApiRestResult
|
||||
.ok(userProfileService.prepareForgetPassword(param.getRecipient(), param.getCode()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 忘记密码
|
||||
*
|
||||
* @return {@link ApiRestResult}
|
||||
*/
|
||||
@Operation(summary = "忘记密码")
|
||||
@PutMapping(FORGET_PASSWORD)
|
||||
public ApiRestResult<Boolean> forgetPassword(@RequestBody @Validated ForgetPasswordRequest forgetPasswordRequest) {
|
||||
return ApiRestResult.ok(userProfileService.forgetPassword(forgetPasswordRequest));
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户资料service
|
||||
*/
|
||||
private final UserProfileService userProfileService;
|
||||
|
||||
public UserProfileController(UserProfileService userProfileService) {
|
||||
this.userProfileService = userProfileService;
|
||||
}
|
||||
}
|
|
@ -23,19 +23,17 @@ import java.util.List;
|
|||
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.elasticsearch.client.elc.NativeQuery;
|
||||
import org.springframework.data.elasticsearch.client.elc.NativeQueryBuilder;
|
||||
import org.springframework.data.elasticsearch.client.elc.Queries;
|
||||
import org.springframework.data.elasticsearch.core.SearchHits;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.querydsl.core.types.ExpressionUtils;
|
||||
import com.querydsl.core.types.Predicate;
|
||||
|
||||
import cn.topiam.employee.audit.entity.AuditElasticSearchEntity;
|
||||
import cn.topiam.employee.audit.entity.Event;
|
||||
import cn.topiam.employee.audit.event.type.EventType;
|
||||
import cn.topiam.employee.audit.entity.AuditEntity;
|
||||
import cn.topiam.employee.audit.entity.GeoLocation;
|
||||
import cn.topiam.employee.audit.entity.QAuditEntity;
|
||||
import cn.topiam.employee.audit.entity.UserAgent;
|
||||
import cn.topiam.employee.audit.event.type.PortalEventType;
|
||||
import cn.topiam.employee.common.constant.CommonConstants;
|
||||
import cn.topiam.employee.common.entity.account.UserDetailEntity;
|
||||
|
@ -52,20 +50,9 @@ import cn.topiam.employee.console.pojo.update.account.UserUpdateParam;
|
|||
import cn.topiam.employee.support.context.ApplicationContextHelp;
|
||||
import cn.topiam.employee.support.repository.page.domain.Page;
|
||||
import cn.topiam.employee.support.repository.page.domain.PageModel;
|
||||
|
||||
import co.elastic.clients.elasticsearch._types.FieldSort;
|
||||
import co.elastic.clients.elasticsearch._types.FieldValue;
|
||||
import co.elastic.clients.elasticsearch._types.SortOptions;
|
||||
import co.elastic.clients.elasticsearch._types.SortOrder;
|
||||
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
|
||||
import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders;
|
||||
import co.elastic.clients.elasticsearch._types.query_dsl.TermsQueryField;
|
||||
import static cn.topiam.employee.audit.entity.Actor.ACTOR_ID;
|
||||
import static cn.topiam.employee.audit.entity.Event.EVENT_TIME;
|
||||
import static cn.topiam.employee.audit.entity.Event.EVENT_TYPE;
|
||||
import static cn.topiam.employee.audit.enums.TargetType.PORTAL;
|
||||
import static cn.topiam.employee.audit.event.type.EventType.APP_SSO;
|
||||
import static cn.topiam.employee.audit.event.type.EventType.LOGIN_PORTAL;
|
||||
import static cn.topiam.employee.audit.service.converter.AuditDataConverter.SORT_EVENT_TIME;
|
||||
import static cn.topiam.employee.common.util.ImageAvatarUtils.bufferedImageToBase64;
|
||||
import static cn.topiam.employee.common.util.ImageAvatarUtils.generateAvatarImg;
|
||||
import static cn.topiam.employee.support.util.PhoneNumberUtils.getPhoneAreaCode;
|
||||
|
@ -259,90 +246,56 @@ public interface UserConverter {
|
|||
* 审计列表请求到本机搜索查询
|
||||
*
|
||||
* @param id {@link Long}
|
||||
* @param page {@link PageModel}
|
||||
* @return {@link NativeQuery}
|
||||
*/
|
||||
default NativeQuery auditListRequestConvertToNativeQuery(Long id, PageModel page) {
|
||||
//构建查询 builder下有 must、should 以及 mustNot 相当于 sql 中的 and、or 以及 not
|
||||
BoolQuery.Builder queryBuilder = QueryBuilders.bool();
|
||||
List<SortOptions> fieldSortBuilders = Lists.newArrayList();
|
||||
//事件类型
|
||||
List<FieldValue> set = new ArrayList<>();
|
||||
set.add(FieldValue.of(LOGIN_PORTAL.getCode()));
|
||||
set.add(FieldValue.of(EventType.APP_SSO.getCode()));
|
||||
queryBuilder.must(QueryBuilders.terms(builder -> {
|
||||
builder.terms(new TermsQueryField.Builder().value(set).build());
|
||||
builder.field(EVENT_TYPE);
|
||||
return builder;
|
||||
}));
|
||||
//用户id
|
||||
queryBuilder.must(Queries.termQueryAsQuery(ACTOR_ID, id.toString()));
|
||||
//字段排序
|
||||
page.getSorts().forEach(sort -> {
|
||||
co.elastic.clients.elasticsearch._types.SortOrder sortOrder;
|
||||
if (org.apache.commons.lang3.StringUtils.equals(sort.getSorter(), SORT_EVENT_TIME)) {
|
||||
if (sort.getAsc()) {
|
||||
sortOrder = co.elastic.clients.elasticsearch._types.SortOrder.Asc;
|
||||
} else {
|
||||
sortOrder = SortOrder.Desc;
|
||||
}
|
||||
} else {
|
||||
sortOrder = SortOrder.Desc;
|
||||
}
|
||||
SortOptions eventTimeSortBuilder = SortOptions
|
||||
.of(s -> s.field(FieldSort.of(f -> f.field(EVENT_TIME).order(sortOrder))));
|
||||
fieldSortBuilders.add(eventTimeSortBuilder);
|
||||
});
|
||||
NativeQueryBuilder nativeQueryBuilder = new NativeQueryBuilder()
|
||||
.withQuery(queryBuilder.build()._toQuery())
|
||||
//分页参数
|
||||
.withPageable(PageRequest.of(page.getCurrent(), page.getPageSize()));
|
||||
if (!CollectionUtils.isEmpty(fieldSortBuilders)) {
|
||||
//排序
|
||||
nativeQueryBuilder.withSort(fieldSortBuilders);
|
||||
}
|
||||
return nativeQueryBuilder.build();
|
||||
default Predicate auditListRequestConvertToNativeQuery(Long id) {
|
||||
QAuditEntity auditEntity = QAuditEntity.auditEntity;
|
||||
return ExpressionUtils.and(auditEntity.isNotNull(),
|
||||
auditEntity.deleted.eq(Boolean.FALSE).and(auditEntity.actorId.eq(id.toString()))
|
||||
.and(auditEntity.eventType.in(LOGIN_PORTAL, APP_SSO)));
|
||||
}
|
||||
|
||||
/**
|
||||
* searchHits 转用户登录日志列表
|
||||
*
|
||||
* @param search {@link SearchHits}
|
||||
* @param auditEntityPage {@link Page}
|
||||
* @param page {@link PageModel}
|
||||
* @return {@link Page}
|
||||
*/
|
||||
default Page<UserLoginAuditListResult> searchHitsConvertToAuditListResult(SearchHits<AuditElasticSearchEntity> search,
|
||||
PageModel page) {
|
||||
default Page<UserLoginAuditListResult> entityConvertToAuditListResult(org.springframework.data.domain.Page<AuditEntity> auditEntityPage,
|
||||
PageModel page) {
|
||||
List<UserLoginAuditListResult> list = new ArrayList<>();
|
||||
//总记录数
|
||||
search.forEach(hit -> {
|
||||
AuditElasticSearchEntity content = hit.getContent();
|
||||
Event event = content.getEvent();
|
||||
auditEntityPage.forEach(audit -> {
|
||||
UserLoginAuditListResult result = new UserLoginAuditListResult();
|
||||
result.setId(audit.getId().toString());
|
||||
//单点登录
|
||||
if (event.getType().getCode().equals(PortalEventType.APP_SSO.getCode())) {
|
||||
result.setAppName(getAppName(content.getTargets().get(0).getId()));
|
||||
if (audit.getEventType().getCode().equals(PortalEventType.APP_SSO.getCode())) {
|
||||
result.setAppName(getAppName(audit.getTargets().get(0).getId()));
|
||||
}
|
||||
//登录门户
|
||||
if (event.getType().getCode().equals(PortalEventType.LOGIN_PORTAL.getCode())) {
|
||||
if (audit.getEventType().getCode().equals(PortalEventType.LOGIN_PORTAL.getCode())) {
|
||||
result.setAppName(PORTAL.getDesc());
|
||||
}
|
||||
result.setEventTime(event.getTime());
|
||||
result.setClientIp(content.getGeoLocation().getIp());
|
||||
result.setBrowser(content.getUserAgent().getBrowser());
|
||||
result.setLocation(content.getGeoLocation().getCityName());
|
||||
result.setEventStatus(event.getStatus());
|
||||
UserAgent userAgent = audit.getUserAgent();
|
||||
GeoLocation geoLocation = audit.getGeoLocation();
|
||||
result.setEventTime(audit.getEventTime());
|
||||
result.setClientIp(geoLocation.getIp());
|
||||
result.setLocation(geoLocation.getCityName());
|
||||
result.setBrowser(userAgent.getBrowser());
|
||||
result.setPlatform(userAgent.getPlatform() + " " + userAgent.getPlatformVersion());
|
||||
result.setEventStatus(audit.getEventStatus());
|
||||
list.add(result);
|
||||
});
|
||||
//@formatter:off
|
||||
Page<UserLoginAuditListResult> result = new Page<>();
|
||||
result.setPagination(Page.Pagination.builder()
|
||||
.total(search.getTotalHits())
|
||||
.totalPages(Math.toIntExact(search.getTotalHits() / page.getPageSize()))
|
||||
.current(page.getCurrent() + 1)
|
||||
.build());
|
||||
result.setList(list);
|
||||
//@formatter:on
|
||||
Page<UserLoginAuditListResult> result = new Page<>();
|
||||
result.setPagination(Page.Pagination.builder()
|
||||
.total(auditEntityPage.getTotalElements())
|
||||
.totalPages(auditEntityPage.getTotalPages())
|
||||
.current(page.getCurrent() + 1)
|
||||
.build());
|
||||
result.setList(list);
|
||||
//@formatter:on
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.console.converter.user;
|
||||
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
|
||||
import cn.topiam.employee.common.entity.account.UserEntity;
|
||||
import cn.topiam.employee.common.entity.setting.AdministratorEntity;
|
||||
import cn.topiam.employee.console.pojo.update.user.UpdateUserInfoRequest;
|
||||
|
||||
/**
|
||||
* AccountConverter
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/3/25 21:52
|
||||
*/
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface UserProfileConverter {
|
||||
|
||||
/**
|
||||
* 用户更新参数转换为用户实体类
|
||||
*
|
||||
* @param param {@link UpdateUserInfoRequest} 更新参数
|
||||
* @return {@link UserEntity} 用户实体
|
||||
*/
|
||||
@Mapping(target = "deleted", ignore = true)
|
||||
@Mapping(target = "phoneVerified", ignore = true)
|
||||
@Mapping(target = "phoneAreaCode", ignore = true)
|
||||
@Mapping(target = "username", ignore = true)
|
||||
@Mapping(target = "updateTime", ignore = true)
|
||||
@Mapping(target = "updateBy", ignore = true)
|
||||
@Mapping(target = "status", ignore = true)
|
||||
@Mapping(target = "remark", ignore = true)
|
||||
@Mapping(target = "phone", ignore = true)
|
||||
@Mapping(target = "password", ignore = true)
|
||||
@Mapping(target = "lastUpdatePasswordTime", ignore = true)
|
||||
@Mapping(target = "lastAuthTime", ignore = true)
|
||||
@Mapping(target = "lastAuthIp", ignore = true)
|
||||
@Mapping(target = "id", ignore = true)
|
||||
@Mapping(target = "expand", ignore = true)
|
||||
@Mapping(target = "emailVerified", ignore = true)
|
||||
@Mapping(target = "email", ignore = true)
|
||||
@Mapping(target = "createTime", ignore = true)
|
||||
@Mapping(target = "createBy", ignore = true)
|
||||
@Mapping(target = "authTotal", ignore = true)
|
||||
AdministratorEntity userUpdateParamConvertToAdministratorEntity(UpdateUserInfoRequest param);
|
||||
}
|
|
@ -117,11 +117,17 @@ public class UserListResult implements Serializable {
|
|||
private LocalDateTime lastAuthTime;
|
||||
|
||||
/**
|
||||
* 目录
|
||||
* 从组织机构目录
|
||||
*/
|
||||
@Parameter(description = "组织机构目录")
|
||||
@Parameter(description = "从组织机构目录")
|
||||
private String orgDisplayPath;
|
||||
|
||||
/**
|
||||
* 主组织目录
|
||||
*/
|
||||
@Parameter(description = "主组织机构目录")
|
||||
private String primaryOrgDisplayPath;
|
||||
|
||||
/**
|
||||
* 最后修改密码时间
|
||||
*/
|
||||
|
|
|
@ -35,6 +35,11 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
|||
@Data
|
||||
@Schema(description = "用户登录日志返回响应")
|
||||
public class UserLoginAuditListResult {
|
||||
/**
|
||||
* ID
|
||||
*/
|
||||
@Parameter(description = "ID")
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 应用名称
|
||||
|
@ -48,6 +53,12 @@ public class UserLoginAuditListResult {
|
|||
@Parameter(description = "客户端IP")
|
||||
private String clientIp;
|
||||
|
||||
/**
|
||||
* 操作系统
|
||||
*/
|
||||
@Parameter(description = "操作系统")
|
||||
private String platform;
|
||||
|
||||
/**
|
||||
* 登录结果
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.console.pojo.update.user;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
|
||||
/**
|
||||
* 更改电子邮件入参
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/8/8 21:15
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "更改电子邮件入参")
|
||||
public class ChangeEmailRequest implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 5681761697876754485L;
|
||||
|
||||
/**
|
||||
* OTP
|
||||
*/
|
||||
@NotEmpty(message = "OTP验证码不能为空")
|
||||
@Parameter(description = "OTP")
|
||||
private String otp;
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
@NotEmpty(message = "邮箱不能为空")
|
||||
@Parameter(description = "邮箱")
|
||||
private String email;
|
||||
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.console.pojo.update.user;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
|
||||
/**
|
||||
* 更改密码入参
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/8/8 21:15
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "更改密码入参")
|
||||
public class ChangePasswordRequest implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 5681761697876754485L;
|
||||
|
||||
/**
|
||||
* 新密码
|
||||
*/
|
||||
@NotEmpty(message = "新密码不能为空")
|
||||
@Parameter(description = "新密码")
|
||||
private String newPassword;
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
@NotEmpty(message = "旧密码不能为空")
|
||||
@Parameter(description = "旧密码")
|
||||
private String oldPassword;
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.console.pojo.update.user;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
|
||||
/**
|
||||
* 更改手机号入参
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/8/8 21:15
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "更改手机号入参")
|
||||
public class ChangePhoneRequest implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 5681761697876754485L;
|
||||
|
||||
/**
|
||||
* OTP
|
||||
*/
|
||||
@NotEmpty(message = "OTP验证码不能为空")
|
||||
@Parameter(description = "OTP")
|
||||
private String otp;
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
@NotEmpty(message = "手机号不能为空")
|
||||
@Parameter(description = "手机号")
|
||||
private String phone;
|
||||
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.console.pojo.update.user;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
|
||||
/**
|
||||
* 忘记密码入参
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2023/02/27 21:15
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "忘记密码入参")
|
||||
public class ForgetPasswordRequest implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 5681761697876754485L;
|
||||
|
||||
/**
|
||||
* 新密码
|
||||
*/
|
||||
@NotEmpty(message = "新密码不能为空")
|
||||
@Parameter(description = "新密码")
|
||||
private String newPassword;
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.console.pojo.update.user;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/8/8 21:15
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "准备更改电子邮件入参")
|
||||
public class PrepareChangeEmailRequest implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 5681761697876754485L;
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
@NotEmpty(message = "邮箱不能为空")
|
||||
@Parameter(description = "邮箱")
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@NotEmpty(message = "密码不能为空")
|
||||
@Parameter(description = "密码")
|
||||
private String password;
|
||||
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.console.pojo.update.user;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
import cn.topiam.employee.common.enums.MessageNoticeChannel;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
*准备更改密码入参
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/11/13 21:15
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "准备更改密码入参")
|
||||
public class PrepareChangePasswordRequest implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 5681761697876754485L;
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
*/
|
||||
@NotNull(message = "消息类型不能为空")
|
||||
@Parameter(description = "消息类型")
|
||||
private MessageNoticeChannel channel;
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.console.pojo.update.user;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
|
||||
/**
|
||||
*准备更改手机号入参
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/8/8 21:15
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "准备更改手机号入参")
|
||||
public class PrepareChangePhoneRequest implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 5681761697876754485L;
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
@NotEmpty(message = "手机号不能为空")
|
||||
@Parameter(description = "手机号")
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 手机号区域
|
||||
*/
|
||||
@NotEmpty(message = "手机号区域不能为空")
|
||||
@Parameter(description = "手机号区域")
|
||||
private String phoneRegion;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@NotEmpty(message = "密码不能为空")
|
||||
@Parameter(description = "密码")
|
||||
private String password;
|
||||
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.console.pojo.update.user;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
|
||||
/**
|
||||
* 忘记密码预认证
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2023/02/27 21:15
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "忘记密码预认证")
|
||||
public class PrepareForgetPasswordRequest implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 5681761697876754482L;
|
||||
|
||||
/**
|
||||
* 验证码接收者
|
||||
*/
|
||||
@NotEmpty(message = "邮箱/手机号不能为空")
|
||||
@Parameter(description = "验证码接收者(邮箱/手机号)")
|
||||
private String recipient;
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
@NotEmpty(message = "验证码不能为空")
|
||||
@Parameter(description = "验证码")
|
||||
private String code;
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.console.pojo.update.user;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
/**
|
||||
* 编辑用户入参
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2020/8/11 23:16
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "修改用户入参")
|
||||
public class UpdateUserInfoRequest implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = -6616249172773611157L;
|
||||
|
||||
/**
|
||||
* 姓名
|
||||
*/
|
||||
@Schema(description = "姓名")
|
||||
private String fullName;
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
@Schema(description = "昵称")
|
||||
private String nickName;
|
||||
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
@Schema(description = "头像")
|
||||
private String avatar;
|
||||
}
|
|
@ -120,15 +120,6 @@ public interface UserService {
|
|||
*/
|
||||
boolean deleteUser(String id);
|
||||
|
||||
/**
|
||||
* 用户转岗
|
||||
*
|
||||
* @param userId {@link String}
|
||||
* @param orgId {@link String}
|
||||
* @return {@link Boolean}
|
||||
*/
|
||||
Boolean userTransfer(String userId, String orgId);
|
||||
|
||||
/**
|
||||
* 批量删除用户
|
||||
*
|
||||
|
|
|
@ -36,6 +36,9 @@ import cn.topiam.employee.audit.context.AuditContext;
|
|||
import cn.topiam.employee.audit.entity.Target;
|
||||
import cn.topiam.employee.audit.enums.TargetType;
|
||||
import cn.topiam.employee.common.entity.account.*;
|
||||
import cn.topiam.employee.common.entity.account.QUserEntity;
|
||||
import cn.topiam.employee.common.entity.account.QUserGroupEntity;
|
||||
import cn.topiam.employee.common.entity.account.QUserGroupMemberEntity;
|
||||
import cn.topiam.employee.common.entity.account.po.UserPO;
|
||||
import cn.topiam.employee.common.entity.account.query.UserGroupMemberListQuery;
|
||||
import cn.topiam.employee.common.repository.account.UserGroupMemberRepository;
|
||||
|
|
|
@ -25,10 +25,7 @@ import java.util.*;
|
|||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate;
|
||||
import org.springframework.data.elasticsearch.client.elc.NativeQuery;
|
||||
import org.springframework.data.elasticsearch.core.SearchHits;
|
||||
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
|
||||
import org.springframework.data.querydsl.QPageRequest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
@ -37,13 +34,17 @@ import org.springframework.transaction.annotation.Transactional;
|
|||
import com.google.i18n.phonenumbers.NumberParseException;
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
import com.google.i18n.phonenumbers.Phonenumber;
|
||||
import com.querydsl.core.types.OrderSpecifier;
|
||||
import com.querydsl.core.types.Predicate;
|
||||
import com.querydsl.core.types.dsl.BooleanExpression;
|
||||
|
||||
import cn.topiam.employee.audit.context.AuditContext;
|
||||
import cn.topiam.employee.audit.entity.AuditElasticSearchEntity;
|
||||
import cn.topiam.employee.audit.entity.QAuditEntity;
|
||||
import cn.topiam.employee.audit.entity.Target;
|
||||
import cn.topiam.employee.audit.enums.TargetType;
|
||||
import cn.topiam.employee.audit.repository.AuditRepository;
|
||||
import cn.topiam.employee.common.entity.account.*;
|
||||
import cn.topiam.employee.common.entity.account.QUserEntity;
|
||||
import cn.topiam.employee.common.entity.account.po.UserPO;
|
||||
import cn.topiam.employee.common.entity.account.query.UserListNotInGroupQuery;
|
||||
import cn.topiam.employee.common.entity.account.query.UserListQuery;
|
||||
|
@ -61,7 +62,6 @@ import cn.topiam.employee.console.service.account.UserService;
|
|||
import cn.topiam.employee.core.message.MsgVariable;
|
||||
import cn.topiam.employee.core.message.mail.MailMsgEventPublish;
|
||||
import cn.topiam.employee.core.message.sms.SmsMsgEventPublish;
|
||||
import cn.topiam.employee.support.autoconfiguration.SupportProperties;
|
||||
import cn.topiam.employee.support.exception.BadParamsException;
|
||||
import cn.topiam.employee.support.exception.InfoValidityFailException;
|
||||
import cn.topiam.employee.support.exception.TopIamException;
|
||||
|
@ -76,7 +76,7 @@ import lombok.RequiredArgsConstructor;
|
|||
import lombok.extern.slf4j.Slf4j;
|
||||
import static cn.topiam.employee.audit.enums.TargetType.USER;
|
||||
import static cn.topiam.employee.audit.enums.TargetType.USER_DETAIL;
|
||||
import static cn.topiam.employee.common.constant.AuditConstants.getAuditIndexPrefix;
|
||||
import static cn.topiam.employee.audit.service.converter.AuditDataConverter.SORT_EVENT_TIME;
|
||||
import static cn.topiam.employee.core.message.sms.SmsMsgEventPublish.USERNAME;
|
||||
import static cn.topiam.employee.support.repository.domain.BaseEntity.LAST_MODIFIED_BY;
|
||||
import static cn.topiam.employee.support.repository.domain.BaseEntity.LAST_MODIFIED_TIME;
|
||||
|
@ -390,28 +390,6 @@ public class UserServiceImpl implements UserService {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户转岗
|
||||
*
|
||||
* @param userId {@link String}
|
||||
* @param orgId {@link String}
|
||||
* @return {@link Boolean}
|
||||
*/
|
||||
@Override
|
||||
public Boolean userTransfer(String userId, String orgId) {
|
||||
Optional<OrganizationEntity> entity = organizationRepository.findById(orgId);
|
||||
//additionalContent
|
||||
if (entity.isEmpty()) {
|
||||
AuditContext.setContent("操作失败,组织不存在");
|
||||
log.warn(AuditContext.getContent());
|
||||
throw new TopIamException(AuditContext.getContent());
|
||||
}
|
||||
organizationMemberRepository.deleteByOrgIdAndUserId(orgId, Long.valueOf(userId));
|
||||
userRepository.save(null);
|
||||
AuditContext.setTarget(Target.builder().id(userId).type(TargetType.USER).build());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除用户
|
||||
*
|
||||
|
@ -501,13 +479,21 @@ public class UserServiceImpl implements UserService {
|
|||
@Override
|
||||
public Page<UserLoginAuditListResult> findUserLoginAuditList(Long id, PageModel pageModel) {
|
||||
//查询入参转查询条件
|
||||
NativeQuery nsq = userConverter.auditListRequestConvertToNativeQuery(id, pageModel);
|
||||
//查询列表
|
||||
SearchHits<AuditElasticSearchEntity> search = elasticsearchTemplate.search(nsq,
|
||||
AuditElasticSearchEntity.class, IndexCoordinates
|
||||
.of(getAuditIndexPrefix(supportProperties.getAudit().getIndexPrefix() + "*")));
|
||||
//结果转返回结果
|
||||
return userConverter.searchHitsConvertToAuditListResult(search, pageModel);
|
||||
Predicate predicate = userConverter.auditListRequestConvertToNativeQuery(id);
|
||||
// 字段排序
|
||||
OrderSpecifier<LocalDateTime> order = QAuditEntity.auditEntity.eventTime.desc();
|
||||
for (PageModel.Sort sort : pageModel.getSorts()) {
|
||||
if (StringUtils.equals(sort.getSorter(), SORT_EVENT_TIME)) {
|
||||
if (sort.getAsc()) {
|
||||
order = QAuditEntity.auditEntity.eventTime.asc();
|
||||
}
|
||||
}
|
||||
}
|
||||
//分页条件
|
||||
QPageRequest request = QPageRequest.of(pageModel.getCurrent(), pageModel.getPageSize(),
|
||||
order);
|
||||
return userConverter
|
||||
.entityConvertToAuditListResult(auditRepository.findAll(predicate, request), pageModel);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -572,11 +558,6 @@ public class UserServiceImpl implements UserService {
|
|||
*/
|
||||
private final UserHistoryPasswordRepository userHistoryPasswordRepository;
|
||||
|
||||
/**
|
||||
* ElasticsearchTemplate
|
||||
*/
|
||||
private final ElasticsearchTemplate elasticsearchTemplate;
|
||||
|
||||
/**
|
||||
* 邮件消息发布
|
||||
*/
|
||||
|
@ -587,14 +568,13 @@ public class UserServiceImpl implements UserService {
|
|||
*/
|
||||
private final SmsMsgEventPublish smsMsgEventPublish;
|
||||
|
||||
/**
|
||||
* EiamSupportProperties
|
||||
*/
|
||||
private final SupportProperties supportProperties;
|
||||
|
||||
/**
|
||||
* PasswordPolicyManager
|
||||
*/
|
||||
private final PasswordPolicyManager<UserEntity> passwordPolicyManager;
|
||||
|
||||
/**
|
||||
* AuditRepository
|
||||
*/
|
||||
private final AuditRepository auditRepository;
|
||||
}
|
||||
|
|
|
@ -23,11 +23,9 @@ import java.util.*;
|
|||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.data.elasticsearch.client.elc.*;
|
||||
import org.springframework.data.elasticsearch.core.SearchHits;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import cn.topiam.employee.audit.entity.AuditElasticSearchEntity;
|
||||
import cn.topiam.employee.audit.event.type.EventType;
|
||||
import cn.topiam.employee.audit.repository.AuditRepository;
|
||||
import cn.topiam.employee.audit.repository.result.AuditStatisticsResult;
|
||||
|
@ -45,7 +43,6 @@ import cn.topiam.employee.support.autoconfiguration.SupportProperties;
|
|||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import co.elastic.clients.elasticsearch._types.aggregations.Aggregation;
|
||||
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
|
||||
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
|
||||
import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders;
|
||||
|
@ -156,7 +153,7 @@ public class AnalysisServiceImpl implements AnalysisService {
|
|||
* 登录区域统计
|
||||
*
|
||||
* @param params {@link AnalysisQuery}
|
||||
* @return {@link List< AuthnZoneResult >}
|
||||
* @return {@link AuthnZoneResult}
|
||||
*/
|
||||
@Override
|
||||
public List<AuthnZoneResult> authnZone(AnalysisQuery params) {
|
||||
|
@ -182,24 +179,6 @@ public class AnalysisServiceImpl implements AnalysisService {
|
|||
return app.getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* ES聚合查询
|
||||
*
|
||||
* @param searchHits {@link SearchHits<AuditElasticSearchEntity>}
|
||||
* @return {@link Aggregation}
|
||||
*/
|
||||
private ElasticsearchAggregation getCountAggregation(SearchHits<AuditElasticSearchEntity> searchHits) {
|
||||
ElasticsearchAggregations elasticsearchAggregations = (ElasticsearchAggregations) searchHits
|
||||
.getAggregations();
|
||||
if (elasticsearchAggregations == null) {
|
||||
return null;
|
||||
}
|
||||
List<ElasticsearchAggregation> aggregations = elasticsearchAggregations.aggregations();
|
||||
return aggregations.stream()
|
||||
.filter(aggregation -> aggregation.aggregation().getName().equals(COUNT)).findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼装查询条件
|
||||
*
|
||||
|
|
|
@ -208,7 +208,6 @@ public class IdentitySourceServiceImpl implements IdentitySourceService {
|
|||
@Override
|
||||
public Boolean saveIdentitySourceConfig(IdentitySourceConfigSaveParam param) {
|
||||
IdentitySourceEntity entity = getIdentitySource(param.getId());
|
||||
param.getBasicConfig().putAll(JSONObject.parseObject(entity.getBasicConfig()));
|
||||
//转换
|
||||
IdentitySourceEntity source = identitySourceConverter
|
||||
.saveConfigParamConverterToEntity(param, entity.getProvider());
|
||||
|
|
|
@ -233,7 +233,7 @@ public class AdministratorServiceImpl implements AdministratorService {
|
|||
Base64.getUrlDecoder().decode(password.getBytes(StandardCharsets.UTF_8)),
|
||||
StandardCharsets.UTF_8);
|
||||
password = passwordEncoder.encode(password);
|
||||
administratorRepository.updatePassword(id, password);
|
||||
administratorRepository.updatePassword(Long.valueOf(id), password, LocalDateTime.now());
|
||||
AuditContext.setTarget(Target.builder().id(id).type(TargetType.ADMINISTRATOR).build());
|
||||
// 下线登录中已重置密码的管理员
|
||||
removeSession(entity.getUsername());
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.console.service.user;
|
||||
|
||||
import cn.topiam.employee.console.pojo.update.user.*;
|
||||
|
||||
/**
|
||||
* 账户服务
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/4/3 22:20
|
||||
*/
|
||||
public interface UserProfileService {
|
||||
|
||||
/**
|
||||
* 更新用户
|
||||
*
|
||||
* @param param {@link UpdateUserInfoRequest}
|
||||
* @return {@link Boolean}
|
||||
*/
|
||||
Boolean changeInfo(UpdateUserInfoRequest param);
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*
|
||||
* @param param {@link ChangePasswordRequest}
|
||||
* @return Boolean
|
||||
*/
|
||||
Boolean changePassword(ChangePasswordRequest param);
|
||||
|
||||
/**
|
||||
* 修改手机
|
||||
*
|
||||
* @param param {@link PrepareChangePhoneRequest}
|
||||
* @return {@link Boolean}
|
||||
*/
|
||||
Boolean prepareChangePhone(PrepareChangePhoneRequest param);
|
||||
|
||||
/**
|
||||
* 修改手机
|
||||
*
|
||||
* @param param {@link ChangePhoneRequest}
|
||||
* @return {@link Boolean}
|
||||
*/
|
||||
Boolean changePhone(ChangePhoneRequest param);
|
||||
|
||||
/**
|
||||
* 准备修改邮箱
|
||||
*
|
||||
* @param param {@link PrepareChangeEmailRequest}
|
||||
* @return {@link Boolean}
|
||||
*/
|
||||
Boolean prepareChangeEmail(PrepareChangeEmailRequest param);
|
||||
|
||||
/**
|
||||
* 修改邮箱
|
||||
*
|
||||
* @param param {@link ChangeEmailRequest}
|
||||
* @return {@link Boolean}
|
||||
*/
|
||||
Boolean changeEmail(ChangeEmailRequest param);
|
||||
|
||||
/**
|
||||
* 忘记密码发送验证码
|
||||
*
|
||||
* @param recipient {@link String} 验证码接收者(邮箱/手机号)
|
||||
* @return {@link Boolean}
|
||||
*/
|
||||
Boolean forgetPasswordCode(String recipient);
|
||||
|
||||
/**
|
||||
* 忘记密码预认证
|
||||
*
|
||||
* @param recipient {@link String} 验证码接收者(邮箱/手机号)
|
||||
* @param code {@link String} 验证码
|
||||
* @return {@link Boolean} 忘记密码 Token
|
||||
*/
|
||||
Boolean prepareForgetPassword(String recipient, String code);
|
||||
|
||||
/**
|
||||
* 忘记密码
|
||||
*
|
||||
* @param forgetPasswordRequest {@link ForgetPasswordRequest}
|
||||
* @return {@link Boolean}
|
||||
*/
|
||||
Boolean forgetPassword(ForgetPasswordRequest forgetPasswordRequest);
|
||||
|
||||
}
|
|
@ -0,0 +1,408 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.console.service.user.impl;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.scheduling.annotation.AsyncConfigurer;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.session.SessionInformation;
|
||||
import org.springframework.security.core.session.SessionRegistry;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import cn.topiam.employee.common.entity.account.UserEntity;
|
||||
import cn.topiam.employee.common.entity.setting.AdministratorEntity;
|
||||
import cn.topiam.employee.common.enums.MailType;
|
||||
import cn.topiam.employee.common.enums.MessageNoticeChannel;
|
||||
import cn.topiam.employee.common.enums.SmsType;
|
||||
import cn.topiam.employee.common.exception.PasswordValidatedFailException;
|
||||
import cn.topiam.employee.common.exception.UserNotFoundException;
|
||||
import cn.topiam.employee.common.repository.setting.AdministratorRepository;
|
||||
import cn.topiam.employee.console.converter.user.UserProfileConverter;
|
||||
import cn.topiam.employee.console.pojo.update.user.*;
|
||||
import cn.topiam.employee.console.service.user.UserProfileService;
|
||||
import cn.topiam.employee.core.message.sms.SmsMsgEventPublish;
|
||||
import cn.topiam.employee.core.security.otp.OtpContextHelp;
|
||||
import cn.topiam.employee.support.context.ServletContextHelp;
|
||||
import cn.topiam.employee.support.exception.BadParamsException;
|
||||
import cn.topiam.employee.support.exception.InfoValidityFailException;
|
||||
import cn.topiam.employee.support.exception.TopIamException;
|
||||
import cn.topiam.employee.support.security.util.SecurityUtils;
|
||||
import cn.topiam.employee.support.util.BeanUtils;
|
||||
import cn.topiam.employee.support.util.PhoneNumberUtils;
|
||||
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import static cn.topiam.employee.core.message.sms.SmsMsgEventPublish.USERNAME;
|
||||
import static cn.topiam.employee.support.constant.EiamConstants.FORGET_PASSWORD_TOKEN_ID;
|
||||
import static cn.topiam.employee.support.exception.enums.ExceptionStatus.EX000102;
|
||||
import static cn.topiam.employee.support.repository.domain.BaseEntity.LAST_MODIFIED_BY;
|
||||
import static cn.topiam.employee.support.repository.domain.BaseEntity.LAST_MODIFIED_TIME;
|
||||
import static cn.topiam.employee.support.util.EmailUtils.isEmailValidate;
|
||||
import static cn.topiam.employee.support.util.PhoneNumberUtils.isPhoneValidate;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/10/3 22:20
|
||||
*/
|
||||
@Service
|
||||
public class UserProfileServiceImpl implements UserProfileService {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(UserProfileServiceImpl.class);
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean changeInfo(UpdateUserInfoRequest param) {
|
||||
//用户信息
|
||||
AdministratorEntity administrator = userProfileConverter
|
||||
.userUpdateParamConvertToAdministratorEntity(param);
|
||||
AdministratorEntity user = administratorRepository
|
||||
.findById(Long.valueOf(SecurityUtils.getCurrentUser().getId())).orElseThrow();
|
||||
BeanUtils.merge(administrator, user, LAST_MODIFIED_BY, LAST_MODIFIED_TIME);
|
||||
administratorRepository.save(user);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean changePassword(ChangePasswordRequest param) {
|
||||
//获取用户
|
||||
AdministratorEntity administrator = getCurrentUser();
|
||||
//校验旧密码
|
||||
if (!passwordEncoder.matches(param.getOldPassword(), administrator.getPassword())) {
|
||||
logger.error("用户ID: [{}] 用户名: [{}] 修改密码失败,原密码错误", administrator.getId(),
|
||||
administrator.getUsername());
|
||||
throw new PasswordValidatedFailException("旧密码错误");
|
||||
}
|
||||
//修改密码
|
||||
administratorRepository.updatePassword(Long.valueOf(SecurityUtils.getCurrentUser().getId()),
|
||||
passwordEncoder.encode(param.getNewPassword()), LocalDateTime.now());
|
||||
logger.info("用户ID: [{}] 用户名: [{}] 修改密码成功", administrator.getId(),
|
||||
administrator.getUsername());
|
||||
//异步下线所有用户
|
||||
removeSession(SecurityUtils.getCurrentUserName());
|
||||
//@formatter:on
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean prepareChangePhone(PrepareChangePhoneRequest param) {
|
||||
AdministratorEntity user = validatedPassword(param.getPassword());
|
||||
// 发送短信验证码
|
||||
if (StringUtils.isNotBlank(user.getPhone())) {
|
||||
otpContextHelp.sendOtp(param.getPhone(), SmsType.UPDATE_PHONE.getCode(),
|
||||
MessageNoticeChannel.SMS);
|
||||
} else {
|
||||
otpContextHelp.sendOtp(param.getPhone(), SmsType.BIND_PHONE.getCode(),
|
||||
MessageNoticeChannel.SMS);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改手机
|
||||
*
|
||||
* @param param {@link ChangePhoneRequest}
|
||||
* @return Boolean
|
||||
*/
|
||||
@Override
|
||||
public Boolean changePhone(ChangePhoneRequest param) {
|
||||
AdministratorEntity user = getCurrentUser();
|
||||
Boolean checkOtp;
|
||||
if (StringUtils.isNotBlank(user.getPhone())) {
|
||||
checkOtp = otpContextHelp.checkOtp(SmsType.UPDATE_PHONE.getCode(),
|
||||
MessageNoticeChannel.SMS, param.getPhone(), param.getOtp());
|
||||
} else {
|
||||
checkOtp = otpContextHelp.checkOtp(SmsType.BIND_PHONE.getCode(),
|
||||
MessageNoticeChannel.SMS, param.getPhone(), param.getOtp());
|
||||
}
|
||||
if (!checkOtp) {
|
||||
throw new InfoValidityFailException(EX000102.getMessage());
|
||||
}
|
||||
// 校验是否已经存在
|
||||
Optional<AdministratorEntity> optionalAdministrator = administratorRepository
|
||||
.findByPhone(param.getPhone());
|
||||
if (optionalAdministrator.isPresent()
|
||||
&& !user.getId().equals(optionalAdministrator.get().getId())) {
|
||||
throw new TopIamException("系统中已存在[" + param.getPhone() + "]手机号, 请先解绑");
|
||||
}
|
||||
Long id = Long.valueOf(SecurityUtils.getCurrentUser().getId());
|
||||
administratorRepository.updatePhone(id, param.getPhone());
|
||||
// 修改手机号成功发送短信
|
||||
LinkedHashMap<String, String> parameter = Maps.newLinkedHashMap();
|
||||
parameter.put(USERNAME, user.getUsername());
|
||||
smsMsgEventPublish.publish(SmsType.BIND_PHONE_SUCCESS, param.getPhone(), parameter);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备修改邮箱
|
||||
*
|
||||
* @param param {@link PrepareChangeEmailRequest}
|
||||
* @return {@link Boolean}
|
||||
*/
|
||||
@Override
|
||||
public Boolean prepareChangeEmail(PrepareChangeEmailRequest param) {
|
||||
AdministratorEntity user = validatedPassword(param.getPassword());
|
||||
// 发送邮箱验证码
|
||||
if (StringUtils.isNotBlank(user.getEmail())) {
|
||||
otpContextHelp.sendOtp(param.getEmail(), MailType.UPDATE_BIND_MAIL.getCode(),
|
||||
MessageNoticeChannel.MAIL);
|
||||
} else {
|
||||
otpContextHelp.sendOtp(param.getEmail(), MailType.BIND_EMAIL.getCode(),
|
||||
MessageNoticeChannel.MAIL);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更改邮箱
|
||||
*
|
||||
* @param param {@link ChangeEmailRequest}
|
||||
* @return {@link Boolean}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean changeEmail(ChangeEmailRequest param) {
|
||||
AdministratorEntity administrator = getCurrentUser();
|
||||
Boolean checkOtp;
|
||||
if (StringUtils.isNotBlank(administrator.getEmail())) {
|
||||
checkOtp = otpContextHelp.checkOtp(MailType.UPDATE_BIND_MAIL.getCode(),
|
||||
MessageNoticeChannel.MAIL, param.getEmail(), param.getOtp());
|
||||
} else {
|
||||
checkOtp = otpContextHelp.checkOtp(MailType.BIND_EMAIL.getCode(),
|
||||
MessageNoticeChannel.MAIL, param.getEmail(), param.getOtp());
|
||||
}
|
||||
if (!checkOtp) {
|
||||
throw new InfoValidityFailException(EX000102.getMessage());
|
||||
}
|
||||
// 校验是否已经存在
|
||||
Optional<AdministratorEntity> optionalAdministrator = administratorRepository
|
||||
.findByEmail(param.getEmail());
|
||||
if (optionalAdministrator.isPresent()
|
||||
&& !administrator.getId().equals(optionalAdministrator.get().getId())) {
|
||||
throw new TopIamException("系统中已存在[" + param.getEmail() + "]邮箱, 请先解绑");
|
||||
}
|
||||
administratorRepository.updateEmail(Long.valueOf(SecurityUtils.getCurrentUser().getId()),
|
||||
param.getEmail());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean forgetPasswordCode(String recipient) {
|
||||
if (isEmailValidate(recipient)) {
|
||||
// 验证在库中是否有邮箱
|
||||
Optional<AdministratorEntity> byEmail = administratorRepository.findByEmail(recipient);
|
||||
if (byEmail.isPresent()) {
|
||||
otpContextHelp.sendOtp(recipient, MailType.FORGET_PASSWORD.getCode(),
|
||||
MessageNoticeChannel.MAIL);
|
||||
return true;
|
||||
}
|
||||
logger.warn("忘记密码: 邮箱: [{}] 不存在", recipient);
|
||||
} else if (isPhoneValidate(recipient)) {
|
||||
// 验证在库中是否有手机号
|
||||
Optional<AdministratorEntity> byPhone = administratorRepository
|
||||
.findByPhone(PhoneNumberUtils.getPhoneNumber(recipient));
|
||||
if (byPhone.isPresent()) {
|
||||
otpContextHelp.sendOtp(recipient, SmsType.FORGET_PASSWORD.getCode(),
|
||||
MessageNoticeChannel.SMS);
|
||||
return true;
|
||||
}
|
||||
logger.warn("忘记密码: 手机号: [{}] 不存在", recipient);
|
||||
}
|
||||
logger.error("忘记密码: 接受者: [{}] 格式错误", recipient);
|
||||
throw new BadParamsException("请输入正确的手机号或邮箱");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean prepareForgetPassword(String recipient, String code) {
|
||||
// 校验验证码
|
||||
Boolean checkOtp = false;
|
||||
Optional<AdministratorEntity> user = Optional.empty();
|
||||
if (isEmailValidate(recipient)) {
|
||||
user = administratorRepository.findByEmail(recipient);
|
||||
if (user.isPresent()) {
|
||||
checkOtp = otpContextHelp.checkOtp(MailType.FORGET_PASSWORD.getCode(),
|
||||
MessageNoticeChannel.MAIL, recipient, code);
|
||||
}
|
||||
} else if (isPhoneValidate(recipient)) {
|
||||
user = administratorRepository.findByPhone(PhoneNumberUtils.getPhoneNumber(recipient));
|
||||
if (user.isPresent()) {
|
||||
checkOtp = otpContextHelp.checkOtp(SmsType.FORGET_PASSWORD.getCode(),
|
||||
MessageNoticeChannel.SMS, recipient, code);
|
||||
}
|
||||
}
|
||||
if (!checkOtp) {
|
||||
throw new InfoValidityFailException(EX000102.getMessage());
|
||||
}
|
||||
|
||||
// 生成忘记密码TOKEN ID
|
||||
String tokenId = UUID.randomUUID().toString();
|
||||
HttpSession session = ServletContextHelp.getSession();
|
||||
// 保存用户ID到Redis, 有效期10分钟
|
||||
stringRedisTemplate.opsForValue().set(session.getId() + tokenId,
|
||||
String.valueOf(user.get().getId()), 10, TimeUnit.MINUTES);
|
||||
// 保存TOKEN ID到会话
|
||||
session.setAttribute(FORGET_PASSWORD_TOKEN_ID, tokenId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean forgetPassword(ForgetPasswordRequest forgetPasswordRequest) {
|
||||
// 验证TOKEN
|
||||
HttpSession session = ServletContextHelp.getSession();
|
||||
String redisTokenId = session.getId() + session.getAttribute(FORGET_PASSWORD_TOKEN_ID);
|
||||
String userId = stringRedisTemplate.opsForValue().get(redisTokenId);
|
||||
if (Objects.isNull(userId)) {
|
||||
// 清除tokenId
|
||||
session.removeAttribute(FORGET_PASSWORD_TOKEN_ID);
|
||||
return false;
|
||||
}
|
||||
//修改密码
|
||||
Optional<AdministratorEntity> user = administratorRepository.findById(Long.valueOf(userId));
|
||||
if (user.isPresent()) {
|
||||
AdministratorEntity administratorEntity = user.get();
|
||||
administratorRepository.updatePassword(administratorEntity.getId(),
|
||||
passwordEncoder.encode(forgetPasswordRequest.getNewPassword()),
|
||||
LocalDateTime.now());
|
||||
logger.info("忘记密码: 用户ID: [{}] 用户名: [{}] 修改密码成功", administratorEntity.getId(),
|
||||
administratorEntity.getUsername());
|
||||
removeSession(administratorEntity.getUsername());
|
||||
stringRedisTemplate.delete(redisTokenId);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步下线所有用户
|
||||
*
|
||||
* @param username {@link String}
|
||||
*/
|
||||
private void removeSession(String username) {
|
||||
executor.execute(() -> {
|
||||
List<SessionInformation> sessions = sessionRegistry.getAllSessions(username, false);
|
||||
sessions.forEach(SessionInformation::expireNow);
|
||||
//@formatter:on
|
||||
});
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* 为保证安全,从数据库获取最新信息使用,并更新到当前上下文中
|
||||
*
|
||||
* @return {@link UserEntity}
|
||||
*/
|
||||
public AdministratorEntity getCurrentUser() {
|
||||
String userId = SecurityUtils.getCurrentUserId();
|
||||
Optional<AdministratorEntity> optional = administratorRepository
|
||||
.findById(Long.valueOf(userId));
|
||||
if (optional.isPresent()) {
|
||||
return optional.get();
|
||||
}
|
||||
SecurityContextHolder.clearContext();
|
||||
logger.error("根据用户ID: [{}] 未查询到用户信息", userId);
|
||||
throw new UserNotFoundException();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码
|
||||
*
|
||||
* @param password {@link String}
|
||||
* @return {@link UserEntity}
|
||||
*/
|
||||
public AdministratorEntity validatedPassword(String password) {
|
||||
AdministratorEntity user = getCurrentUser();
|
||||
boolean matches = passwordEncoder.matches(password, user.getPassword());
|
||||
if (!matches) {
|
||||
logger.error("用户ID: [{}] 用户名: [{}] 密码匹配失败", user.getId(), user.getUsername());
|
||||
throw new PasswordValidatedFailException();
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executor
|
||||
*/
|
||||
private final Executor executor;
|
||||
|
||||
/**
|
||||
* AccountConverter
|
||||
*/
|
||||
private final UserProfileConverter userProfileConverter;
|
||||
|
||||
/**
|
||||
* PasswordEncoder
|
||||
*/
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
/**
|
||||
* AdministratorRepository
|
||||
*/
|
||||
private final AdministratorRepository administratorRepository;
|
||||
|
||||
/**
|
||||
* SessionRegistry
|
||||
*/
|
||||
private final SessionRegistry sessionRegistry;
|
||||
|
||||
/**
|
||||
* OtpContextHelp
|
||||
*/
|
||||
private final OtpContextHelp otpContextHelp;
|
||||
|
||||
/**
|
||||
* SmsMsgEventPublish
|
||||
*/
|
||||
private final SmsMsgEventPublish smsMsgEventPublish;
|
||||
|
||||
/**
|
||||
* StringRedisTemplate
|
||||
*/
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
public UserProfileServiceImpl(AsyncConfigurer asyncConfigurer,
|
||||
UserProfileConverter userProfileConverter,
|
||||
PasswordEncoder passwordEncoder,
|
||||
AdministratorRepository administratorRepository,
|
||||
SessionRegistry sessionRegistry, OtpContextHelp otpContextHelp,
|
||||
SmsMsgEventPublish smsMsgEventPublish,
|
||||
StringRedisTemplate stringRedisTemplate) {
|
||||
this.executor = asyncConfigurer.getAsyncExecutor();
|
||||
this.userProfileConverter = userProfileConverter;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.administratorRepository = administratorRepository;
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
this.otpContextHelp = otpContextHelp;
|
||||
this.smsMsgEventPublish = smsMsgEventPublish;
|
||||
this.stringRedisTemplate = stringRedisTemplate;
|
||||
}
|
||||
}
|
|
@ -20,12 +20,13 @@ package cn.topiam.employee.console.synchronizer.configuration;
|
|||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import cn.topiam.employee.support.context.ApplicationContextHelp;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.config.BeanDefinitionHolder;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
|
||||
|
@ -69,6 +70,7 @@ import cn.topiam.employee.support.scheduler.SpringSchedulerRegister;
|
|||
import cn.topiam.employee.support.trace.TraceUtils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static cn.topiam.employee.common.enums.identitysource.IdentitySourceProvider.DINGTALK;
|
||||
import static cn.topiam.employee.support.lock.LockAspect.getTopiamLockKeyPrefix;
|
||||
|
||||
|
@ -123,12 +125,9 @@ public class IdentitySourceBeanRegistry implements IdentitySourceEventListener {
|
|||
.getBeanFactory();
|
||||
//如果已经存在,销毁
|
||||
try {
|
||||
if (ObjectUtils
|
||||
.isNotEmpty(beanFactory.getBean(IdentitySourceBeanUtils.getSourceBeanName(id)))) {
|
||||
if (beanFactory.containsBean(IdentitySourceBeanUtils.getSourceBeanName(id))) {
|
||||
destroyIdentitySourceBean(id, applicationContext);
|
||||
}
|
||||
} catch (NoSuchBeanDefinitionException ignored) {
|
||||
|
||||
} finally {
|
||||
BeanDefinitionHolder definitionHolder = getBeanDefinitionHolder(entity,
|
||||
applicationContext);
|
||||
|
@ -197,8 +196,9 @@ public class IdentitySourceBeanRegistry implements IdentitySourceEventListener {
|
|||
definitionBuilder.addConstructorArgValue(identitySourceSyncUserPostProcessor);
|
||||
definitionBuilder.addConstructorArgValue(identitySourceSyncDeptPostProcessor);
|
||||
definitionBuilder.addConstructorArgValue(identitySourceEventPostProcessor);
|
||||
//设置为 RefreshScope
|
||||
definitionBuilder.setScope("refresh");
|
||||
// 设置作用域为prototype
|
||||
definitionBuilder.setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE);
|
||||
|
||||
return new BeanDefinitionHolder(definitionBuilder.getBeanDefinition(),
|
||||
IdentitySourceBeanUtils.getSourceBeanName(entity.getId().toString()));
|
||||
}
|
||||
|
@ -211,7 +211,8 @@ public class IdentitySourceBeanRegistry implements IdentitySourceEventListener {
|
|||
*/
|
||||
private static void destroyIdentitySourceBean(String id,
|
||||
ApplicationContext applicationContext) {
|
||||
BeanDefinitionRegistry beanDefinitionRegistry = (BeanDefinitionRegistry) ((ConfigurableApplicationContext) applicationContext)
|
||||
ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext;
|
||||
BeanDefinitionRegistry beanDefinitionRegistry = (BeanDefinitionRegistry) configurableApplicationContext
|
||||
.getBeanFactory();
|
||||
String beanName = IdentitySourceBeanUtils.getSourceBeanName(id);
|
||||
try {
|
||||
|
@ -279,6 +280,8 @@ public class IdentitySourceBeanRegistry implements IdentitySourceEventListener {
|
|||
registerIdentitySourceBean(entity, applicationContext);
|
||||
log.info("注册身份源: {} 同步任务", id);
|
||||
registerIdentitySourceSyncTask(entity, applicationContext);
|
||||
// 刷新
|
||||
ApplicationContextHelp.refresh(IdentitySourceBeanUtils.getSourceBeanName(id));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ public class IdentitySourceEventReceiveEndpoint {
|
|||
* 事件通知处理
|
||||
*
|
||||
* @param request {@link HttpServletRequest}
|
||||
* @param response {@link HttpServletRequest}
|
||||
* @param code {@link String}
|
||||
* @return {@link ResponseEntity}
|
||||
*/
|
||||
@Trace
|
||||
|
@ -70,6 +70,7 @@ public class IdentitySourceEventReceiveEndpoint {
|
|||
Object event = identitySource.event(request, body);
|
||||
return ResponseEntity.ok(event);
|
||||
}
|
||||
log.error("身份源信息不存在:[{}]", code);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
|
|
|
@ -178,7 +178,7 @@ public class AbstractIdentitySourcePostProcessor {
|
|||
entity.setDataOrigin(dataOrigin);
|
||||
entity.setIdentitySourceId(identitySource.getId());
|
||||
entity.setPassword(passwordEncoder.encode(defaultPassword));
|
||||
entity.setPlaintext(defaultPassword);
|
||||
entity.setPasswordPlainText(defaultPassword);
|
||||
|
||||
//必须字段
|
||||
entity.setCreateBy(SYSTEM_DEFAULT_USER_NAME);
|
||||
|
@ -302,14 +302,14 @@ public class AbstractIdentitySourcePostProcessor {
|
|||
thirdPartyUser.getEmail(), thirdPartyUser.getPhone());
|
||||
if (StringUtils.isNotEmpty(thirdPartyUser.getEmail())) {
|
||||
Map<String, Object> parameter = new HashMap<>(16);
|
||||
parameter.put(MsgVariable.PASSWORD, thirdPartyUser.getPlaintext());
|
||||
parameter.put(MsgVariable.PASSWORD, thirdPartyUser.getPasswordPlainText());
|
||||
mailMsgEventPublish.publish(MailType.RESET_PASSWORD_CONFIRM,
|
||||
thirdPartyUser.getEmail(), parameter);
|
||||
}
|
||||
if (StringUtils.isNotEmpty(thirdPartyUser.getPhone())) {
|
||||
LinkedHashMap<String, String> parameter = new LinkedHashMap<>();
|
||||
parameter.put(USERNAME, thirdPartyUser.getUsername());
|
||||
parameter.put(MsgVariable.PASSWORD, thirdPartyUser.getPlaintext());
|
||||
parameter.put(MsgVariable.PASSWORD, thirdPartyUser.getPasswordPlainText());
|
||||
smsMsgEventPublish.publish(SmsType.RESET_PASSWORD_SUCCESS,
|
||||
thirdPartyUser.getPhone(), parameter);
|
||||
}
|
||||
|
|
|
@ -414,7 +414,7 @@ public class DefaultIdentitySourceUserPostProcessor extends AbstractIdentitySour
|
|||
log.info("上游用户:[{}]对应系统用户:[{}]({})存在,用户信息不一致,修改用户信息:{}", thirdPartyUser.getUserId(), currentUser.getUsername(), currentUser.getId(), JSONObject.toJSONString(currentUser));
|
||||
updateUsers.add(currentUser);
|
||||
} else {
|
||||
skipUsers.add(SkipUser.builder().user(currentUser).actionType(IdentitySourceActionType.UPDATE).status(SyncStatus.SKIP).reason( "用户信息一致").build());
|
||||
skipUsers.add(SkipUser.builder().user(currentUser).actionType(IdentitySourceActionType.UPDATE).status(SyncStatus.SKIP).reason("用户信息一致").build());
|
||||
log.info("上游用户:[{}]对应系统用户:[{}]({})存在,用户信息一致", thirdPartyUser.getUserId(), currentUser.getUsername(), currentUser.getId());
|
||||
}
|
||||
//处理组织机构关系
|
||||
|
|
|
@ -51,7 +51,7 @@ public interface AccountConverter {
|
|||
* @param param {@link UpdateUserInfoRequest} 更新参数
|
||||
* @return {@link UserEntity} 用户实体
|
||||
*/
|
||||
@Mapping(target = "plaintext", ignore = true)
|
||||
@Mapping(target = "passwordPlainText", ignore = true)
|
||||
@Mapping(target = "deleted", ignore = true)
|
||||
@Mapping(target = "identitySourceId", ignore = true)
|
||||
@Mapping(target = "phoneVerified", ignore = true)
|
||||
|
@ -124,8 +124,8 @@ public interface AccountConverter {
|
|||
* 账号绑定entity转result
|
||||
*
|
||||
* @param identityProviderList {@link List<IdentityProviderEntity>}
|
||||
* @param userIdpBindList {@link Iterable< UserIdpBindPO >}
|
||||
* @return {@link List< BoundIdpListResult >}
|
||||
* @param userIdpBindList {@link Iterable<UserIdpBindPO>}
|
||||
* @return {@link List<BoundIdpListResult>}
|
||||
*/
|
||||
default List<BoundIdpListResult> entityConverterToBoundIdpListResult(List<IdentityProviderEntity> identityProviderList,
|
||||
Iterable<UserIdpBindPO> userIdpBindList) {
|
||||
|
|
|
@ -48,12 +48,6 @@ public class UpdateUserInfoRequest implements Serializable {
|
|||
@Schema(description = "昵称")
|
||||
private String nickName;
|
||||
|
||||
/**
|
||||
* 个人简介
|
||||
*/
|
||||
@Schema(description = "个人简介")
|
||||
private String personalProfile;
|
||||
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
|
|
|
@ -88,13 +88,13 @@
|
|||
"@types/numeral": "^2.0.3",
|
||||
"@types/qs": "^6.9.8",
|
||||
"@types/react": "^18.2.25",
|
||||
"@types/react-dom": "^18.2.10",
|
||||
"@types/react-dom": "^18.2.11",
|
||||
"@types/react-helmet": "^6.1.7",
|
||||
"@umijs/lint": "^4.0.83",
|
||||
"@umijs/max": "^4.0.83",
|
||||
"cross-env": "^7.0.3",
|
||||
"cross-port-killer": "^1.4.0",
|
||||
"eslint": "^8.50.0",
|
||||
"eslint": "^8.51.0",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^14.0.1",
|
||||
"prettier": "^3.0.3",
|
||||
|
|
|
@ -117,7 +117,7 @@ export const layout: RunTimeLayoutConfig = ({ initialState, loading }) => {
|
|||
heightLayoutHeader: showBanner ? 78 : 56,
|
||||
},
|
||||
pageContainer: {
|
||||
paddingBlockPageContainerContent: 6,
|
||||
paddingBlockPageContainerContent: 12,
|
||||
paddingInlinePageContainerContent: 24,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -16,12 +16,7 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
ProForm,
|
||||
ProFormText,
|
||||
ProFormTextArea,
|
||||
useStyle as useAntdStyle,
|
||||
} from '@ant-design/pro-components';
|
||||
import { ProForm, ProFormText, useStyle as useAntdStyle } from '@ant-design/pro-components';
|
||||
import { App, Avatar, Button, Form, Skeleton, Upload } from 'antd';
|
||||
import { useState } from 'react';
|
||||
|
||||
|
@ -103,10 +98,10 @@ function useStyle() {
|
|||
}
|
||||
export const FORM_ITEM_LAYOUT = {
|
||||
labelCol: {
|
||||
span: 4,
|
||||
span: 5,
|
||||
},
|
||||
wrapperCol: {
|
||||
span: 20,
|
||||
span: 19,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -117,7 +112,8 @@ const BaseView = () => {
|
|||
const [loading, setLoading] = useState<boolean>();
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const [avatarURL, setAvatarURL] = useState<string | undefined>(initialState?.currentUser?.avatar);
|
||||
const [name, setName] = useState<string>('');
|
||||
const [avatarUploaded, setAvatarUploaded] = useState<boolean>(false);
|
||||
const [name, setName] = useState<string>();
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
setLoading(true);
|
||||
|
@ -138,7 +134,7 @@ const BaseView = () => {
|
|||
fullName: values.fullName,
|
||||
nickName: values.nickName,
|
||||
personalProfile: values.personalProfile,
|
||||
avatar: avatarURL,
|
||||
avatar: avatarUploaded ? avatarURL : undefined,
|
||||
}),
|
||||
publicSecret,
|
||||
),
|
||||
|
@ -163,7 +159,7 @@ const BaseView = () => {
|
|||
callBack,
|
||||
}: {
|
||||
avatar: string | undefined;
|
||||
name: string;
|
||||
name?: string;
|
||||
callBack: any;
|
||||
}) => (
|
||||
<>
|
||||
|
@ -179,12 +175,13 @@ const BaseView = () => {
|
|||
className={classnames(`${prefixCls}-avatar-name`, hashId)}
|
||||
size={144}
|
||||
>
|
||||
{name.substring(0, 1)}
|
||||
{name?.substring(0, 1)}
|
||||
</Avatar>
|
||||
)}
|
||||
</div>
|
||||
<ImgCrop
|
||||
rotationSlider
|
||||
aspectSlider
|
||||
modalOk={intl.formatMessage({ id: 'app.confirm' })}
|
||||
modalCancel={intl.formatMessage({ id: 'app.cancel' })}
|
||||
>
|
||||
|
@ -229,7 +226,7 @@ const BaseView = () => {
|
|||
onFinish={handleFinish}
|
||||
submitter={{
|
||||
render: (p, dom) => {
|
||||
return <Form.Item wrapperCol={{ span: 20, offset: 4 }}>{dom}</Form.Item>;
|
||||
return <Form.Item wrapperCol={{ span: 19, offset: 5 }}>{dom}</Form.Item>;
|
||||
},
|
||||
searchConfig: {
|
||||
submitText: intl.formatMessage({ id: 'app.save' }),
|
||||
|
@ -284,15 +281,17 @@ const BaseView = () => {
|
|||
},
|
||||
]}
|
||||
/>
|
||||
<ProFormTextArea
|
||||
width="md"
|
||||
name="personalProfile"
|
||||
label={intl.formatMessage({ id: 'page.account.base.form.personal_profile' })}
|
||||
/>
|
||||
</ProForm>
|
||||
</div>
|
||||
<div className={classnames(`${prefixCls}-right`, hashId)}>
|
||||
<AvatarView avatar={avatarURL} callBack={setAvatarURL} name={name} />
|
||||
<AvatarView
|
||||
avatar={avatarURL}
|
||||
callBack={(avatarUrl: string) => {
|
||||
setAvatarURL(avatarUrl);
|
||||
setAvatarUploaded(true);
|
||||
}}
|
||||
name={name}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -139,11 +139,7 @@ export default (props: {
|
|||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: (
|
||||
<FormattedMessage
|
||||
id={intl.formatMessage({ id: 'page.account.common.form.phone.rule.0' })}
|
||||
/>
|
||||
),
|
||||
message: <FormattedMessage id={'page.account.common.form.phone.rule.0'} />,
|
||||
},
|
||||
{
|
||||
validator: async (rule, value) => {
|
||||
|
@ -196,9 +192,6 @@ export default (props: {
|
|||
formRef.current?.setFields([{ name: FieldNames.PASSWORD, errors: [`${message}`] }]);
|
||||
return Promise.reject();
|
||||
}
|
||||
if (!success) {
|
||||
return Promise.reject();
|
||||
}
|
||||
if (success && result) {
|
||||
setHasSendCaptcha(true);
|
||||
useApp.message.success(intl.formatMessage({ id: 'app.send_successfully' }));
|
||||
|
@ -206,6 +199,7 @@ export default (props: {
|
|||
}
|
||||
useApp.message.error(message);
|
||||
captchaRef.current?.endTiming();
|
||||
return Promise.reject();
|
||||
}
|
||||
return Promise.reject();
|
||||
}}
|
||||
|
|
|
@ -78,21 +78,10 @@ const SecurityView = () => {
|
|||
}
|
||||
}, [refresh]);
|
||||
|
||||
const passwordStrength = {
|
||||
strong: <span className={classnames(`${prefixCls}-strong`, hashId)}>强</span>,
|
||||
medium: <span className={classnames(`${prefixCls}-medium`, hashId)}>中</span>,
|
||||
weak: <span className={classnames(`${prefixCls}-weak`, hashId)}>弱</span>,
|
||||
};
|
||||
|
||||
const getData = () => [
|
||||
{
|
||||
title: '账户密码',
|
||||
description: (
|
||||
<>
|
||||
当前密码强度:
|
||||
{passwordStrength.strong}
|
||||
</>
|
||||
),
|
||||
actions: [
|
||||
<a
|
||||
key="Modify"
|
||||
|
|
|
@ -27,7 +27,6 @@ export default {
|
|||
'page.account.base.form.full_name': '姓名',
|
||||
'page.account.base.form.nick_name': '昵称',
|
||||
'page.account.base.form.nick_name.rule.0': '请输入您的昵称',
|
||||
'page.account.base.form.personal_profile': '个人简介',
|
||||
'page.account.common.form.password': '密码',
|
||||
'page.account.common.form.password.placeholder': '请输入密码',
|
||||
'page.account.common.form.password.rule.0': '请输入密码',
|
||||
|
@ -63,7 +62,7 @@ export default {
|
|||
'page.account.modify_password.success': '修改成功,请重新登录',
|
||||
'page.account.modify_password.form.new_password': '新密码',
|
||||
'page.account.modify_password.form.new_password.placeholder': '请输入新密码',
|
||||
'page.account.modify_password.form.new_password.rule.0': '请输入请输入新密码',
|
||||
'page.account.modify_password.form.new_password.rule.0': '请输入新密码',
|
||||
'page.account.modify_password.form.verify-code': '验证码',
|
||||
'page.account.modify_password.form.verify-code-type.label': '验证方式',
|
||||
'page.account.modify_password.form.verify-code-type.rule.0': '请选择验证方式',
|
||||
|
|
|
@ -53,9 +53,9 @@ public class JwtUtils {
|
|||
public static Claims parserToken(String token, String publicKey) {
|
||||
try {
|
||||
PublicKey readPublicKey = X509Utils.readPublicKey(publicKey, "");
|
||||
JwtParser jwtParser = Jwts.parserBuilder().setSigningKey(readPublicKey).build();
|
||||
JwtParser jwtParser = Jwts.parser().verifyWith(readPublicKey).build();
|
||||
// 解析 JWT
|
||||
return jwtParser.parseClaimsJws(token).getBody();
|
||||
return jwtParser.parseSignedClaims(token).getPayload();
|
||||
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
|
||||
logger.info("Invalid JWT signature.");
|
||||
logger.trace("Invalid JWT signature trace: {}", e.getMessage());
|
||||
|
|
Loading…
Reference in New Issue