package com.fontana.backend.session.service;
import com.fontana.backend.exception.customExceptions.NotFoundException;
import com.fontana.backend.exception.customExceptions.RoleNotAllowedException;
import com.fontana.backend.exception.customExceptions.SessionNotModifiedException;
import com.fontana.backend.role.entity.RoleType;
import com.fontana.backend.session.dto.*;
import com.fontana.backend.session.entity.Session;
import com.fontana.backend.session.mapper.SessionMapper;
import com.fontana.backend.session.repository.SessionRepository;
import com.fontana.backend.user.entity.User;
import com.fontana.backend.user.repository.UserRepository;
import com.fontana.backend.utils.AuthUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static com.fontana.backend.config.RestEndpoints.SESSION;
@Service
@RequiredArgsConstructor
@Slf4j
public class SessionServiceImpl implements SessionService {
@Value("${session.not-found-msg}")
private String notFoundMsg;
@Value("${session.busy-msg}")
private String sessionBusyMsg;
@Value("${session.not-allowed-to-close-msg}")
private String notAllowedToCloseMsg;
@Value("${session.already-closed}")
private String sessionAlreadyClosedMsg;
@Value("${session.role-not-allowed-msg}")
private String roleNotAllowedMsg;
@Value("${user.not-found-msg}")
private String userNotFoundMsg;
@Value("${cache.active-session}")
private String activeSessionLabel;
@Value("${session.expiration-delay}")
private String expirationDelay;
private final SessionRepository sessionRepository;
private final UserRepository userRepository;
private final SessionMapper sessionMapper;
private final AuthUtils authUtils;
private final CacheManager cacheManager;
@Scheduled(fixedRate = 15000)
public void autoCloseSession() {
Session session = getActiveSession();
if (session != null && session.getExpirationTime().isBefore(LocalDateTime.now())) {
Session updated = buildUpdatedSession(session, null, false, true);
sessionRepository.save(updated);
Objects.requireNonNull(cacheManager.getCache(activeSessionLabel)).clear();
log.info("(SESSION SCHEDULER) Auto closed session due to no activity: " + session);
return;
}
log.info("(SESSION SCHEDULER) AutoCloseSession scheduler invoked with no action.");
}
@Override
public List<SessionResponseDTO> findAll(String watcher) {
if (watcher != null) {
User user = userRepository.findByUsername(watcher).orElseThrow(
() -> new NotFoundException(userNotFoundMsg));
if (!user.getRole().getName().equals(RoleType.ADMIN.name())) {
throw new RoleNotAllowedException(roleNotAllowedMsg);
}
log.info("Filtered sessions: " + filterSessionsInReversedOrder(user).size());
return filterSessionsInReversedOrder(user).stream()
.map(sessionMapper::map)
.toList();
}
return sessionRepository.findAll().stream()
.map(sessionMapper::map)
.toList();
}
@Override
public SessionResponseDTO findById(Integer id) {
Session session = sessionRepository.findById(id).orElseThrow(
() -> new NotFoundException(notFoundMsg.concat(" " + id)));
return sessionMapper.map(session);
}
@Override
public ResponseEntity<?> add(SessionRequestDTO sessionRequestDTO) {
Session activeSession = getActiveSession();
String authority = authUtils.extractAuthenticatedAuthority();
if (authority.equals(RoleType.VIEWER.name())) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(buildRoleNotAllowedResponse());
}
if (activeSession != null && authority.equals(RoleType.ADMIN.name())) {
Session updated = buildUpdatedSession(activeSession, null, true, false);
log.info("Session force-closed by admin: " + updated);
sessionRepository.save(updated);
}
if (activeSession != null && !authority.equals(RoleType.ADMIN.name())) {
SessionBusyResponse response = buildSessionBusyResponse(sessionBusyMsg, activeSession);
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response);
}
Session saved = sessionRepository.save(sessionMapper.map(sessionRequestDTO));
Objects.requireNonNull(cacheManager.getCache(activeSessionLabel)).put("id", saved.getId());
log.info("(OPEN) activeSession: " + saved);
return buildAddSessionResponse(saved);
}
@Override
public ResponseEntity<?> updateCloseSession(SessionCloseRequest sessionCloseRequest) {
Session activeSession = getActiveSession();
log.info("(CLOSE) active session: " + activeSession);
String authority = authUtils.extractAuthenticatedAuthority();
log.info("Request:" + sessionCloseRequest);
if (activeSession == null) {
throw new SessionNotModifiedException(sessionAlreadyClosedMsg);
}
String currentPrincipalName = authUtils.getAuthentication().getPrincipal().toString();
if (activeSession.getUsername().equals(currentPrincipalName) || authority.equals(RoleType.ADMIN.name())) {
Session updated = buildUpdatedSession(
activeSession, sessionCloseRequest, false, false);
log.info("Updated:" + updated);
sessionRepository.save(updated);
Objects.requireNonNull(cacheManager.getCache(activeSessionLabel)).clear();
return ResponseEntity.ok().build();
} else {
throw new SessionNotModifiedException(notAllowedToCloseMsg);
}
}
@Override
public boolean checkIsActive(String username) {
Session session = getActiveSession();
log.info(String.valueOf(session));
if (session == null) {
throw new NotFoundException(notFoundMsg);
}
return session.getClosedTime() == null;
}
public void updateExpirationTime() {
Session session = getActiveSession();
if (session != null) {
session.setExpirationTime(LocalDateTime.now().plusMinutes(Integer.parseInt(expirationDelay)));
sessionRepository.save(session);
}
}
public Session getActiveSession() {
Cache cache = cacheManager.getCache(activeSessionLabel);
Cache.ValueWrapper valueWrapper = cache.get("id");
if (valueWrapper != null && valueWrapper.get() != null) {
log.info("Active session id: " + valueWrapper.get());
return sessionRepository.findById((int) valueWrapper.get()).orElse(null);
}
return null;
}
private ResponseEntity<?> buildAddSessionResponse(Session saved) {
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.LOCATION, SESSION.concat("/").concat(saved.getId().toString()));
Map<String, LocalDateTime> response = new HashMap<>();
response.put("expirationTime", saved.getExpirationTime());
return ResponseEntity.status(HttpStatus.OK).headers(headers).body(response);
}
/**
* Filters and retrieves a list of closed sessions in reversed order of opening time,
* where the session's opening time is after the user's last role change.
*
* @param user for whom to filter sessions.
* @return A list of SessionResponseDTO objects representing the filtered sessions.
*/
public List<Session> filterSessionsInReversedOrder(User user) {
Pageable pageable = PageRequest.of(0, 21, Sort.by(Sort.Direction.DESC, "id"));
return sessionRepository.findAllInReversedOrderAfterDate(user.getLastRoleChange(), pageable).stream()
.filter(session -> !session.getUsername().equals(authUtils.getAuthentication().getPrincipal()))
.filter(session -> session.getClosedTime() != null)
.filter(session -> session.getWatchers().stream()
.noneMatch(watcher -> watcher.getWatcher().equals(user.getUsername())))
.toList();
}
private Session buildUpdatedSession(
Session activeSession,
SessionCloseRequest request,
boolean isForcedToClose,
boolean isAutoClosed) {
return Session.builder()
.id(activeSession.getId())
.username(activeSession.getUsername())
.openedTime(activeSession.getOpenedTime())
.closedTime(request != null ? request.getClosedTime() : LocalDateTime.now())
.expirationTime(request != null ? request.getClosedTime() : LocalDateTime.now())
.logs(activeSession.getLogs())
.isForcedToClose(isForcedToClose)
.isAutoClosed(isAutoClosed)
.build();
}
private SessionBusyResponse buildSessionBusyResponse(String message, Session activeSession) {
return SessionBusyResponse.builder()
.message(message)
.activeUserName(activeSession.getUsername())
.activeSessionStartTime(activeSession.getOpenedTime())
.intercepted(LocalDateTime.now())
.build();
}
private SessionRoleNotAllowedResponse buildRoleNotAllowedResponse() {
return SessionRoleNotAllowedResponse.builder()
.message(roleNotAllowedMsg)
.intercepted(LocalDateTime.now())
.build();
}
}