MemberService.java

package io.featureprobe.api.service;

import io.featureprobe.api.auth.TokenHelper;
import io.featureprobe.api.base.component.SpringBeanManager;
import io.featureprobe.api.base.constants.MessageKey;
import io.featureprobe.api.base.db.ExcludeTenant;
import io.featureprobe.api.base.enums.MemberSourceEnum;
import io.featureprobe.api.base.enums.MemberStatusEnum;
import io.featureprobe.api.base.enums.OrganizationRoleEnum;
import io.featureprobe.api.base.enums.ResourceType;
import io.featureprobe.api.base.exception.ForbiddenException;
import io.featureprobe.api.base.security.IEncryptionService;
import io.featureprobe.api.base.tenant.TenantContext;
import io.featureprobe.api.dao.entity.Member;
import io.featureprobe.api.dao.entity.Organization;
import io.featureprobe.api.dao.entity.OrganizationMember;
import io.featureprobe.api.dao.exception.ResourceNotFoundException;
import io.featureprobe.api.dao.repository.MemberRepository;
import io.featureprobe.api.dao.repository.OrganizationMemberRepository;
import io.featureprobe.api.dao.repository.OrganizationRepository;
import io.featureprobe.api.dao.utils.PageRequestUtil;
import io.featureprobe.api.dto.MemberCreateRequest;
import io.featureprobe.api.dto.MemberModifyPasswordRequest;
import io.featureprobe.api.dto.MemberItemResponse;
import io.featureprobe.api.dto.MemberResponse;
import io.featureprobe.api.dto.MemberSearchRequest;
import io.featureprobe.api.dto.MemberUpdateRequest;
import io.featureprobe.api.mapper.MemberMapper;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.criteria.Predicate;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

@ExcludeTenant
@Slf4j
@Service
@RequiredArgsConstructor
@Data
public class MemberService {

    private final MemberRepository memberRepository;

    private final MemberIncludeDeletedService memberIncludeDeletedService;

    private final OrganizationRepository organizationRepository;

    private final OrganizationMemberRepository organizationMemberRepository;

    @PersistenceContext
    public final EntityManager entityManager;

    private static final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    @Value("${app.security.encryption.impl:plaintext}")
    private String encryptionName;

    @Transactional(rollbackFor = Exception.class)
    public List<MemberResponse> createUserInCurrentOrganization(MemberCreateRequest createRequest) {
        return this.create(TenantContext.getCurrentOrganization().getOrganizationId(), createRequest,
                MemberStatusEnum.ACTIVE);
    }

    @Transactional(rollbackFor = Exception.class)
    public List<MemberResponse> create(Long organizationId,
                                       MemberCreateRequest createRequest,
                                       MemberStatusEnum memberStatusEnum) {
        List<Member> savedMembers = memberRepository.saveAll(
                newMembers(organizationId, createRequest, memberStatusEnum));
        return savedMembers.stream().map(item -> translateResponse(item)).collect(Collectors.toList());
    }

    @Transactional(rollbackFor = Exception.class)
    public MemberResponse update(MemberUpdateRequest updateRequest) {
        Member member = findMemberByAccount(updateRequest.getAccount());
        MemberMapper.INSTANCE.mapEntity(updateRequest, member);
        if (StringUtils.isNotBlank(updateRequest.getPassword()) ||
                Objects.nonNull(updateRequest.getRole())) {
            verifyAdminPrivileges();
            OrganizationMember organizationMember = member.getOrganizationMembers()
                    .stream()
                    .filter(it -> it.getOrganization().getId().equals(TenantContext.getCurrentOrganization()
                            .getOrganizationId())).findFirst()
                    .orElseThrow(() -> new ResourceNotFoundException(ResourceType.ORGANIZATION_MEMBER,
                            member.getAccount()));
            organizationMember.setRole(updateRequest.getRole());
        }
        return translateResponse(save(member));
    }

    @Transactional(rollbackFor = Exception.class)
    public MemberItemResponse modifyPassword(MemberModifyPasswordRequest modifyPasswordRequest) {
        Member member = findLoggedInMember();
        verifyPassword(modifyPasswordRequest.getOldPassword(), member.getPassword());
        member.setPassword(passwordEncoder.encode(modifyPasswordRequest.getNewPassword()));
        return MemberMapper.INSTANCE.entityToItemResponse(save(member));
    }

    @Transactional(rollbackFor = Exception.class)
    public MemberItemResponse modifyPasswordAndActive(Member member, String password) {
        if (member == null || StringUtils.isEmpty(password)) {
            throw new IllegalArgumentException("member or password invalid");
        }
        member.setPassword(passwordEncoder.encode(password));
        member.setStatus(MemberStatusEnum.ACTIVE);
        return MemberMapper.INSTANCE.entityToItemResponse(save(member));
    }

    @Transactional(rollbackFor = Exception.class)
    public void updateVisitedTime(String account) {
        Member member = findMemberByAccount(account);
        member.setVisitedTime(new Date());
        save(member);
    }

    @Transactional(rollbackFor = Exception.class)
    public MemberResponse delete(String account) {
        verifyAdminPrivileges();
        Member member = findMemberByAccount(account);
        member.setDeleted(true);
        member.deleteOrganization(Long.parseLong(TenantContext.getCurrentTenant()));
        save(member);
        return translateResponse(member);
    }

    public Member save(Member member) {
        return memberRepository.save(member);
    }

    private List<Member> saveAll(Iterable<Member> members) {
        return memberRepository.saveAll(members);
    }

    private MemberResponse translateResponse(Member member) {
        MemberResponse memberResponse = MemberMapper.INSTANCE.entityToResponse(member);
        OrganizationRoleEnum role = member.getRole(Long.parseLong(TenantContext.getCurrentTenant()));
        memberResponse.setRole(role == null ? null : role.name());
        return memberResponse;
    }

    private List<Member> newMembers(Long organizationId, MemberCreateRequest createRequest,
                                    MemberStatusEnum memberStatusEnum) {
        return createRequest.getAccounts()
                .stream()
                .filter(account -> memberIncludeDeletedService.validateAccountIncludeDeleted(account))
                .map(account -> newMember(organizationId, account, createRequest, memberStatusEnum))
                .collect(Collectors.toList());
    }

    private Member newMember(Long organizationId, String account, MemberCreateRequest createRequest,
                             MemberStatusEnum memberStatusEnum) {
        Member member = new Member();
        member.setAccount(account);
        member.setStatus(memberStatusEnum);
        member.setSource(createRequest.getSource());
        member.setPassword(new BCryptPasswordEncoder().encode(createRequest.getPassword()));

        Organization organization = organizationRepository.findById(organizationId).get();
        boolean valid = memberStatusEnum == MemberStatusEnum.ACTIVE;
        member.addOrganization(organization, createRequest.getRole(), valid);

        return member;
    }

    private void verifyPassword(String oldPassword, String newPassword) {
        if (!passwordEncoder.matches(oldPassword, newPassword)) {
            throw new IllegalArgumentException(MessageKey.INVALID_OLD_PASSWORD);
        }
    }

    public Optional<Member> findByAccount(String account) {
        IEncryptionService encryptionService = SpringBeanManager.getBeanByName(encryptionName);
        account = encryptionService.encrypt(account);
        return memberRepository.findByAccount(account);
    }

    public Optional<Member> findById(Long memberId) {
        return memberRepository.findById(memberId);
    }

    public boolean existsByAccount(String account) {
        IEncryptionService encryptionService = SpringBeanManager.getBeanByName(encryptionName);
        account = encryptionService.encrypt(account);
        return memberRepository.existsByAccount(account);
    }

    private void verifyAdminPrivileges() {
        if (!TokenHelper.isOwner()) {
            throw new ForbiddenException();
        }
    }

    public Page<MemberItemResponse> list(MemberSearchRequest searchRequest) {
        Pageable pageable = PageRequestUtil.toPageable(searchRequest, Sort.Direction.DESC, "createdTime");
        Specification<OrganizationMember> spec = (root, query, cb) -> {
            Predicate p1 = cb.equal(root.get("organization").get("id"), TenantContext.getCurrentOrganization()
                    .getOrganizationId());
            Predicate p2 = cb.notEqual(root.get("member").get("source"), MemberSourceEnum.ACCESS_TOKEN.name());
            return query.where(cb.and(p1, p2)).getRestriction();
        };
        Page<OrganizationMember> organizationMembers = organizationMemberRepository.findAll(spec, pageable);
        List<Long> memberIds = organizationMembers.getContent()
                .stream()
                .map(organizationMember -> organizationMember.getMember().getId())
                .collect(Collectors.toList());
        Map<Long, Member> idToMember = memberRepository.findAllById(memberIds).stream()
                .collect(Collectors.toMap(Member::getId, Function.identity()));
        return convertToResponse(getOwnerTotalCount(),
                TokenHelper.isOwner(),
                organizationMembers, idToMember);
    }

    private long getOwnerTotalCount() {
        Specification<OrganizationMember> spec = (root, query, cb) -> {
            Predicate p1 = cb.equal(root.get("organization").get("id"), TenantContext.getCurrentOrganization()
                    .getOrganizationId());
            Predicate p2 = cb.equal(root.get("role"), OrganizationRoleEnum.OWNER);
            return query.where(cb.and(p1, p2)).getRestriction();
        };
        return organizationMemberRepository.count(spec);

    }

    private Page<MemberItemResponse> convertToResponse(long ownerCount,
                                                       boolean currentIsOwner,
                                                       Page<OrganizationMember> organizationMembers,
                                                       Map<Long, Member> idToMember) {
        return organizationMembers.map(item -> {
            MemberItemResponse response = MemberMapper.INSTANCE
                    .entityToItemResponse(idToMember.get(item.getMember().getId()));
            response.setVisitedTime(item.getLoginTime());
            if (item.getRole() == null) {
                response.setAllowEdit(false);
                return response;
            }
            response.setRole(item.getRole().name());
            boolean allowEdit = currentIsOwner;

            if (allowEdit && item.getRole().isOwner() && ownerCount == 1) {
                allowEdit = false;
            }
            response.setAllowEdit(allowEdit);
            response.setValid(item.getValid());
            response.setOrganizationMemberCreateBy(item.getCreatedBy().getAccount());
            return response;
        });
    }

    public MemberItemResponse queryByAccount(String account) {
        Member member = findMemberByAccount(account, true);
        return MemberMapper.INSTANCE.entityToItemResponse(member);
    }

    private Member findLoggedInMember() {
        return findMemberByAccount(TokenHelper.getAccount());
    }

    private Member findMemberByAccount(String account) {
        return findMemberByAccount(account, false);
    }

    private Member findMemberByAccount(String account, boolean includeDeleted) {
        IEncryptionService encryptionService = SpringBeanManager.getBeanByName(encryptionName);
        account = encryptionService.encrypt(account);
        Optional<Member> member = includeDeleted ? memberIncludeDeletedService
                .queryMemberByAccountIncludeDeleted(account) : memberRepository.findByAccount(account);
        String finalAccount = account;
        return member.orElseThrow(() -> new ResourceNotFoundException(ResourceType.MEMBER, finalAccount));
    }

    public void updateLoginTime(Member member, Long organizationId) {
        member.getOrganizationMembers().forEach(organizationMember -> {
            if (organizationId == organizationMember.getOrganization().getId()) {
                organizationMember.setLoginTime(new Date());
            }
        });
        memberRepository.save(member);
    }

}