Merge remote-tracking branch 'origin/master'

pull/66/head
shao1121353141 2023-10-09 17:08:43 +08:00
commit 726d928527
84 changed files with 3207 additions and 901 deletions

View File

@ -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));
}
}

View File

@ -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<>() {
});
}
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}
/**

View File

@ -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;
}

View File

@ -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";
}

View File

@ -187,7 +187,7 @@ public class UserEntity extends LogicDeleteEntity<Long> {
*/
@Transient
@JsonIgnore
private String plaintext;
private String passwordPlainText;
@Override
public boolean equals(Object o) {

View File

@ -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;
/**
*
*/

View File

@ -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("%'");

View File

@ -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;
}

View File

@ -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);
/**

View File

@ -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);

View File

@ -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);
}

View File

@ -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("暂未配置存储提供商");
}
}

View File

@ -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"/>

View File

@ -40,6 +40,12 @@ export default [
hideInMenu: true,
component: './user/SessionExpired',
},
{
name: 'account.profile',
path: '/user/profile',
hideInMenu: true,
component: './user/Profile',
},
/*欢迎页*/
{
name: 'welcome',

View File

@ -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",

View File

@ -113,7 +113,7 @@ export const layout: RunTimeLayoutConfig = ({ initialState, loading }) => {
heightLayoutHeader: showBanner ? 78 : 56,
},
pageContainer: {
paddingBlockPageContainerContent: 6,
paddingBlockPageContainerContent: 12,
paddingInlinePageContainerContent: 24,
},
},

View File

@ -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',

View File

@ -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': '退出登录',

View File

@ -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': '用户绑定',

View File

@ -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 }}

View File

@ -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

View File

@ -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',
}

View File

@ -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': '登录结果',

View File

@ -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',
}

View File

@ -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' }),

View File

@ -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': '过期锁定',

View File

@ -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(() => {

View File

@ -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;

View File

@ -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;

View File

@ -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>
</>
);
};

View File

@ -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;

View File

@ -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>,
);
};

View File

@ -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;

View File

@ -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',
}

View File

@ -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',
}

View File

@ -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;

View File

@ -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': '修改手机号',
};

View File

@ -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',
});
}

View File

@ -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)];
});
}

View File

@ -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;

View File

@ -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');
}

View File

@ -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();

View File

@ -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;
/**
*
*/

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -117,11 +117,17 @@ public class UserListResult implements Serializable {
private LocalDateTime lastAuthTime;
/**
*
*
*/
@Parameter(description = "组织机构目录")
@Parameter(description = "组织机构目录")
private String orgDisplayPath;
/**
*
*/
@Parameter(description = "主组织机构目录")
private String primaryOrgDisplayPath;
/**
*
*/

View File

@ -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;
/**
*
*/

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
/**
*
*

View File

@ -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;

View File

@ -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;
}

View File

@ -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);
}
/**
*
*

View File

@ -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());

View File

@ -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());

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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());
}
//处理组织机构关系

View File

@ -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 {
* entityresult
*
* @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) {

View File

@ -48,12 +48,6 @@ public class UpdateUserInfoRequest implements Serializable {
@Schema(description = "昵称")
private String nickName;
/**
*
*/
@Schema(description = "个人简介")
private String personalProfile;
/**
*
*/

View File

@ -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",

View File

@ -117,7 +117,7 @@ export const layout: RunTimeLayoutConfig = ({ initialState, loading }) => {
heightLayoutHeader: showBanner ? 78 : 56,
},
pageContainer: {
paddingBlockPageContainerContent: 6,
paddingBlockPageContainerContent: 12,
paddingInlinePageContainerContent: 24,
},
},

View File

@ -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>
</>
)}

View File

@ -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();
}}

View File

@ -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"

View File

@ -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': '请选择验证方式',

View File

@ -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());