MyBatisIdentityStore.java

package com.ecommerce.identity.infrastructure.persistence;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.ecommerce.identity.application.port.IdentityStore;
import com.ecommerce.identity.domain.model.AccountStatus;
import com.ecommerce.identity.domain.model.StoredRefreshToken;
import com.ecommerce.identity.domain.model.UserAccount;
import com.ecommerce.identity.infrastructure.persistence.entity.LoginRecordEntity;
import com.ecommerce.identity.infrastructure.persistence.entity.RefreshTokenEntity;
import com.ecommerce.identity.infrastructure.persistence.entity.RoleEntity;
import com.ecommerce.identity.infrastructure.persistence.entity.UserAccountEntity;
import com.ecommerce.identity.infrastructure.persistence.entity.UserRoleEntity;
import com.ecommerce.identity.infrastructure.persistence.mapper.LoginRecordMapper;
import com.ecommerce.identity.infrastructure.persistence.mapper.RefreshTokenMapper;
import com.ecommerce.identity.infrastructure.persistence.mapper.RoleMapper;
import com.ecommerce.identity.infrastructure.persistence.mapper.UserAccountMapper;
import com.ecommerce.identity.infrastructure.persistence.mapper.UserRoleMapper;
import org.springframework.stereotype.Repository;

import java.time.Instant;
import java.util.List;
import java.util.Optional;

@Repository
public class MyBatisIdentityStore implements IdentityStore {

    private final UserAccountMapper accountMapper;
    private final RoleMapper roleMapper;
    private final UserRoleMapper userRoleMapper;
    private final RefreshTokenMapper refreshTokenMapper;
    private final LoginRecordMapper loginRecordMapper;

    public MyBatisIdentityStore(
            UserAccountMapper accountMapper,
            RoleMapper roleMapper,
            UserRoleMapper userRoleMapper,
            RefreshTokenMapper refreshTokenMapper,
            LoginRecordMapper loginRecordMapper) {
        this.accountMapper = accountMapper;
        this.roleMapper = roleMapper;
        this.userRoleMapper = userRoleMapper;
        this.refreshTokenMapper = refreshTokenMapper;
        this.loginRecordMapper = loginRecordMapper;
    }

    @Override
    public Instant currentTime() {
        return accountMapper.currentTime();
    }

    @Override
    public boolean accountExistsByEmail(String normalizedEmail) {
        return accountMapper.exists(new LambdaQueryWrapper<UserAccountEntity>()
                .eq(UserAccountEntity::getEmail, normalizedEmail));
    }

    @Override
    public Optional<UserAccount> findAccountByEmail(String normalizedEmail) {
        return Optional.ofNullable(accountMapper.selectOne(
                        new LambdaQueryWrapper<UserAccountEntity>()
                                .eq(UserAccountEntity::getEmail, normalizedEmail)))
                .map(this::toDomain);
    }

    @Override
    public Optional<UserAccount> findAccountById(Long userId) {
        return Optional.ofNullable(accountMapper.selectById(userId)).map(this::toDomain);
    }

    @Override
    public UserAccount createAccount(
            String normalizedEmail,
            String passwordHash,
            String displayName,
            Instant now) {
        UserAccountEntity entity = new UserAccountEntity();
        entity.setEmail(normalizedEmail);
        entity.setPasswordHash(passwordHash);
        entity.setDisplayName(displayName);
        entity.setStatus(AccountStatus.ACTIVE.name());
        entity.setVersion(0);
        entity.setCreatedAt(now);
        entity.setUpdatedAt(now);
        accountMapper.insert(entity);
        return toDomain(entity);
    }

    @Override
    public void assignRole(Long userId, String roleCode, Instant now) {
        RoleEntity role = roleMapper.selectOne(new LambdaQueryWrapper<RoleEntity>()
                .eq(RoleEntity::getCode, roleCode));
        if (role == null) {
            throw new IllegalStateException("Role is not initialized: " + roleCode);
        }

        UserRoleEntity assignment = new UserRoleEntity();
        assignment.setUserId(userId);
        assignment.setRoleId(role.getId());
        assignment.setCreatedAt(now);
        userRoleMapper.insertAssignment(assignment);
    }

    @Override
    public List<String> findRoleCodes(Long userId) {
        return roleMapper.selectCodesByUserId(userId);
    }

    @Override
    public void saveRefreshToken(Long userId, String tokenHash, Instant expiresAt, Instant now) {
        RefreshTokenEntity entity = new RefreshTokenEntity();
        entity.setUserId(userId);
        entity.setTokenHash(tokenHash);
        entity.setExpiresAt(expiresAt);
        entity.setCreatedAt(now);
        refreshTokenMapper.insert(entity);
    }

    @Override
    public Optional<StoredRefreshToken> findRefreshTokenByHash(String tokenHash) {
        return Optional.ofNullable(refreshTokenMapper.selectOne(
                        new LambdaQueryWrapper<RefreshTokenEntity>()
                                .eq(RefreshTokenEntity::getTokenHash, tokenHash)))
                .map(this::toDomain);
    }

    @Override
    public boolean revokeRefreshToken(Long tokenId, Instant now) {
        int updated = refreshTokenMapper.update(null,
                new LambdaUpdateWrapper<RefreshTokenEntity>()
                        .eq(RefreshTokenEntity::getId, tokenId)
                        .isNull(RefreshTokenEntity::getRevokedAt)
                        .set(RefreshTokenEntity::getRevokedAt, now)
                        .set(RefreshTokenEntity::getLastUsedAt, now));
        return updated == 1;
    }

    @Override
    public void saveLoginRecord(
            Long userId,
            String normalizedEmail,
            boolean successful,
            String failureCode,
            String clientIp,
            String userAgent,
            Instant now) {
        LoginRecordEntity entity = new LoginRecordEntity();
        entity.setUserId(userId);
        entity.setNormalizedEmail(normalizedEmail);
        entity.setSuccessful(successful);
        entity.setFailureCode(failureCode);
        entity.setClientIp(clientIp);
        entity.setUserAgent(userAgent);
        entity.setCreatedAt(now);
        loginRecordMapper.insert(entity);
    }

    private UserAccount toDomain(UserAccountEntity entity) {
        return new UserAccount(
                entity.getId(),
                entity.getEmail(),
                entity.getPasswordHash(),
                entity.getDisplayName(),
                AccountStatus.valueOf(entity.getStatus()),
                entity.getCreatedAt(),
                entity.getUpdatedAt()
        );
    }

    private StoredRefreshToken toDomain(RefreshTokenEntity entity) {
        return new StoredRefreshToken(
                entity.getId(),
                entity.getUserId(),
                entity.getTokenHash(),
                entity.getExpiresAt(),
                entity.getRevokedAt()
        );
    }
}