initial commit

This commit is contained in:
2026-04-21 06:25:33 +07:00
commit 85efdb7714
214 changed files with 6821 additions and 0 deletions

View File

@ -0,0 +1,187 @@
package id.iptek.utms.preference.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import id.iptek.utms.auth.security.UserPrincipal;
import id.iptek.utms.core.exception.AppException;
import id.iptek.utms.core.i18n.MessageResolver;
import id.iptek.utms.preference.domain.UserUiPreference;
import id.iptek.utms.preference.dto.TablePreferenceProfile;
import id.iptek.utms.preference.dto.TablePreferenceRequest;
import id.iptek.utms.preference.dto.TablePreferenceSavedProfile;
import id.iptek.utms.preference.dto.UserUiPreferencesResponse;
import id.iptek.utms.preference.repository.UserUiPreferenceRepository;
import id.iptek.utms.tenant.TenantContext;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.regex.Pattern;
@Service
public class UserPreferenceService {
private static final String VALUE_JSON_FIELD = "visibleColumns";
private static final Pattern PREFERENCE_KEY_PATTERN =
Pattern.compile("^(users|roles|workflow|audit|modules):[A-Za-z0-9_./-]+$");
private static final Map<String, List<String>> DEFAULT_COLUMNS_BY_KEY = Map.of(
"users:workflow-requests", List.of("id", "resourceType", "resourceId", "makerUsername", "status", "requiredSteps", "currentStep", "createdAt", "updatedAt", "actions"),
"workflow:requests", List.of("id", "resourceType", "resourceId", "makerUsername", "status", "updatedAt")
);
private final UserUiPreferenceRepository repository;
private final ObjectMapper objectMapper;
private final MessageResolver messageResolver;
public UserPreferenceService(UserUiPreferenceRepository repository,
ObjectMapper objectMapper,
MessageResolver messageResolver) {
this.repository = repository;
this.objectMapper = objectMapper;
this.messageResolver = messageResolver;
}
public UserUiPreferencesResponse getAll(Authentication authentication) {
UUID userId = getUserId(authentication);
TenantContext.getRequiredTenantId();
List<UserUiPreference> preferences = repository.findByUserId(userId);
preferences.sort(Comparator.comparing(UserUiPreference::getUpdatedAt, Comparator.nullsLast(Comparator.reverseOrder())));
List<TablePreferenceProfile> columns = preferences.stream()
.map(this::toProfile)
.toList();
Instant latestUpdatedAt = preferences.stream()
.map(UserUiPreference::getUpdatedAt)
.filter(Objects::nonNull)
.max(Instant::compareTo)
.orElse(null);
return new UserUiPreferencesResponse(columns, latestUpdatedAt);
}
@Transactional
public TablePreferenceSavedProfile upsert(Authentication authentication, TablePreferenceRequest request) {
UUID userId = getUserId(authentication);
String tenantId = TenantContext.getRequiredTenantId();
String normalizedKey = normalizePreferenceKey(request.preferenceKey());
List<String> normalizedColumns = normalizeVisibleColumns(request.visibleColumns());
UserUiPreference preference = repository.findByUserIdAndPreferenceKey(userId, normalizedKey)
.orElseGet(() -> {
UserUiPreference created = new UserUiPreference();
created.setUserId(userId);
created.setTenantId(tenantId);
created.setPreferenceKey(normalizedKey);
return created;
});
preference.setValueJson(serializePreferenceValue(normalizedColumns));
UserUiPreference saved = repository.save(preference);
return new TablePreferenceSavedProfile(
saved.getPreferenceKey(),
normalizedColumns,
saved.getUpdatedAt()
);
}
@Transactional
public TablePreferenceProfile resetTablePreference(Authentication authentication, String preferenceKey) {
UUID userId = getUserId(authentication);
String normalizedKey = normalizePreferenceKey(preferenceKey);
repository.deleteByUserIdAndPreferenceKey(userId, normalizedKey);
return new TablePreferenceProfile(normalizedKey, getDefaultColumns(normalizedKey));
}
@Transactional
public void resetAll(Authentication authentication) {
UUID userId = getUserId(authentication);
repository.deleteByUserId(userId);
}
private TablePreferenceProfile toProfile(UserUiPreference preference) {
return new TablePreferenceProfile(
preference.getPreferenceKey(),
parseVisibleColumns(preference.getValueJson())
);
}
private UUID getUserId(Authentication authentication) {
Object principal = authentication != null ? authentication.getPrincipal() : null;
if (principal instanceof UserPrincipal userPrincipal) {
return userPrincipal.getId();
}
throw new AppException(messageResolver.get("auth.invalid.credentials"));
}
private String normalizePreferenceKey(String preferenceKey) {
if (preferenceKey == null || preferenceKey.isBlank()) {
throw new AppException(messageResolver.get("user.preferences.invalid.key"));
}
String normalized = preferenceKey.trim();
if (!PREFERENCE_KEY_PATTERN.matcher(normalized).matches()) {
throw new AppException(messageResolver.get("user.preferences.invalid.key"));
}
return normalized;
}
private List<String> normalizeVisibleColumns(List<String> visibleColumns) {
if (visibleColumns == null || visibleColumns.isEmpty()) {
throw new AppException(messageResolver.get("user.preferences.invalid.columns"));
}
List<String> normalized = visibleColumns.stream()
.map(column -> column == null ? null : column.trim())
.filter(Objects::nonNull)
.toList();
if (normalized.isEmpty() || normalized.stream().anyMatch(String::isBlank)) {
throw new AppException(messageResolver.get("user.preferences.invalid.columns"));
}
return List.copyOf(normalized);
}
private String serializePreferenceValue(List<String> visibleColumns) {
try {
Map<String, List<String>> value = new LinkedHashMap<>();
value.put(VALUE_JSON_FIELD, visibleColumns);
return objectMapper.writeValueAsString(value);
} catch (Exception ex) {
throw new AppException(messageResolver.get("user.preferences.serialize.failed"));
}
}
private List<String> parseVisibleColumns(String valueJson) {
try {
JsonNode root = objectMapper.readTree(valueJson);
JsonNode columns = root.get(VALUE_JSON_FIELD);
if (columns == null || !columns.isArray()) {
throw new AppException(messageResolver.get("user.preferences.invalid.value"));
}
List<String> visibleColumns = new ArrayList<>();
for (JsonNode value : columns) {
String column = value != null ? value.asText() : null;
if (column == null || column.isBlank()) {
throw new AppException(messageResolver.get("user.preferences.invalid.value"));
}
visibleColumns.add(column);
}
return visibleColumns;
} catch (AppException ex) {
throw ex;
} catch (Exception ex) {
throw new AppException(messageResolver.get("user.preferences.invalid.value"));
}
}
private List<String> getDefaultColumns(String preferenceKey) {
return DEFAULT_COLUMNS_BY_KEY.getOrDefault(preferenceKey, List.of("id", "createdAt", "updatedAt", "actions"));
}
}