Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.thughari.jobtrackerpro.controller;

import com.thughari.jobtrackerpro.dto.DeletionWarning;
import com.thughari.jobtrackerpro.service.UserDeletionService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
@RequestMapping("/api/users")
public class UserDeletionController {

private final UserDeletionService userDeletionService;

public UserDeletionController(UserDeletionService userDeletionService) {
this.userDeletionService = userDeletionService;
}

/**
* Request account deletion
* Account will be permanently deleted after 3 days unless cancelled
*/
@PostMapping("/request-deletion")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<?> requestDeletion() {
String email = getAuthenticatedEmail();
userDeletionService.requestDeletion(email);
return ResponseEntity.ok().body(
java.util.Map.of(
"message", "Deletion request submitted. Your account will be permanently deleted in 3 days.",
"daysRemaining", 3
)
);
}

/**
* Check if user has a pending deletion request
*/
@GetMapping("/deletion-status")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<DeletionWarning> checkDeletionStatus() {
String email = getAuthenticatedEmail();
DeletionWarning warning = userDeletionService.checkPendingDeletion(email);
return ResponseEntity.ok(warning);
}

/**
* Cancel pending deletion
*/
@PostMapping("/cancel-deletion")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<?> cancelDeletion() {
String email = getAuthenticatedEmail();
userDeletionService.cancelDeletion(email);
return ResponseEntity.ok().body(
java.util.Map.of(
"message", "Deletion request cancelled successfully."
)
);
}

private String getAuthenticatedEmail() {
return ((String) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).toLowerCase();
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.thughari.jobtrackerpro.dto;

public class DeletionWarning {
public final boolean hasPendingDeletion;
public final long daysRemaining;

public DeletionWarning(boolean hasPendingDeletion, long daysRemaining) {
this.hasPendingDeletion = hasPendingDeletion;
this.daysRemaining = daysRemaining;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ public class UserProfileResponse {
private boolean gmailConnected;
private boolean gmailSyncInProgress;
private boolean enabled;
private boolean pendingDeletion;
private long daysUntilDeletion;
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,10 @@ public class User {

@Column(nullable = false)
private Boolean enabled = false;

@Column(name = "deletion_requested_at")
private LocalDateTime deletionRequestedAt;

@Column(name = "pending_deletion")
private Boolean pendingDeletion = false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,9 @@ void markStaleJobsAsRejected(
@Param("now") LocalDateTime now,
@Param("note") String note
);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("DELETE FROM Job j WHERE j.userEmail = :email")
void deleteByUserEmail(@Param("email") String email);

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,7 @@ public interface UserRepository extends JpaRepository<User, UUID> {
@Modifying
@Query("DELETE FROM User u WHERE u.enabled = false AND u.provider = 'LOCAL' AND u.createdAt < :cutoff")
void deleteUnverifiedUsers(@Param("cutoff") LocalDateTime cutoff);

@Query("SELECT u FROM User u WHERE u.pendingDeletion = true AND u.deletionRequestedAt < :cutoffDate")
List<User> findAllByPendingDeletionTrueAndDeletionRequestedAtBefore(@Param("cutoffDate") LocalDateTime cutoffDate);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.thughari.jobtrackerpro.repo.VerificationTokenRepository;
import com.thughari.jobtrackerpro.service.GmailIntegrationService;
import com.thughari.jobtrackerpro.service.JobService;
import com.thughari.jobtrackerpro.service.UserDeletionService;

import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -23,19 +24,23 @@ public class JobScheduler {
private final UserRepository userRepository;
private final GmailIntegrationService gmailIntegrationService;

private final UserDeletionService userDeletionService;

private final PasswordResetTokenRepository passwordTokenRepo;
private final VerificationTokenRepository verificationTokenRepo;

public JobScheduler(JobService jobService,
UserRepository userRepository,
GmailIntegrationService gmailIntegrationService,
PasswordResetTokenRepository passwordTokenRepo,
VerificationTokenRepository verificationTokenRepo) {
VerificationTokenRepository verificationTokenRepo,
UserDeletionService userDeletionService) {
this.jobService = jobService;
this.userRepository = userRepository;
this.gmailIntegrationService = gmailIntegrationService;
this.passwordTokenRepo = passwordTokenRepo;
this.verificationTokenRepo = verificationTokenRepo;
this.userDeletionService = userDeletionService;
}

/**
Expand All @@ -56,7 +61,7 @@ public void runStaleJobCleanup() {
/**
* Gmail Security: Renews the 7-day watch lease every 5 days.
*/
@Scheduled(cron = "0 0 0 */5 * *")
@Scheduled(cron = "0 30 0 */5 * *")
public void renewGmailWatches() {
log.info("Gmail Sync: Starting bulk watch renewal...");

Expand All @@ -78,7 +83,7 @@ public void renewGmailWatches() {
log.info("Gmail Sync: Finished bulk watch renewal for {} users.", users.size());
}

@Scheduled(cron = "0 0 2 * * *")
@Scheduled(cron = "0 0 1 * * *")
@Transactional
public void runSystemCleanup() {
log.info("Starting system-wide security cleanup...");
Expand All @@ -92,4 +97,26 @@ public void runSystemCleanup() {

log.info("System cleanup completed. Database pruned of expired security entries.");
}

/*
* Scheduled task to process user deletions after the 3-day grace period.
*/
@Scheduled(cron = "0 30 1 * * *")
public void processScheduledDeletions() {
log.info("Starting scheduled user deletion cleanup...");

List<User> usersToDelete = userRepository.findAllByPendingDeletionTrueAndDeletionRequestedAtBefore(
LocalDateTime.now().minusDays(3)
);

for (User user : usersToDelete) {
try {
userDeletionService.deleteUserCompletely(user.getEmail());
} catch (Exception e) {
log.error("Failed to delete user: {}", user.getEmail(), e);
}
}

log.info("Scheduled deletion cleanup completed. Deleted {} users.", usersToDelete.size());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public class AuthService {
private final JwtUtils jwtUtils;
private final StorageService storageService;
private final CacheManager cacheManager;
private final UserDeletionService userDeletionService;

@Value("${app.base-url}")
private String baseUrl;
Expand All @@ -55,7 +56,7 @@ public class AuthService {

public AuthService(UserRepository userRepository, PasswordEncoder passwordEncoder,
JwtUtils jwtUtils, StorageService storageService,
PasswordResetTokenRepository passwordResetTokenRepository, EmailService emailService, CacheManager cacheManager, VerificationTokenRepository verificationTokenRepository) {
PasswordResetTokenRepository passwordResetTokenRepository, EmailService emailService, CacheManager cacheManager, VerificationTokenRepository verificationTokenRepository, UserDeletionService userDeletionService) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.jwtUtils = jwtUtils;
Expand All @@ -64,6 +65,7 @@ public AuthService(UserRepository userRepository, PasswordEncoder passwordEncode
this.emailService = emailService;
this.cacheManager = cacheManager;
this.verificationTokenRepository = verificationTokenRepository;
this.userDeletionService = userDeletionService;
}

public void registerUser(AuthRequest request) {
Expand Down Expand Up @@ -350,6 +352,17 @@ private UserProfileResponse mapToProfileResponse(User user) {
response.setGmailConnected(Boolean.TRUE.equals(user.getGmailConnected()));
response.setGmailSyncInProgress(Boolean.TRUE.equals(user.getGmailSyncInProgress()));
response.setEnabled(Boolean.TRUE.equals(user.getEnabled()));

// Set deletion warning info
if (Boolean.TRUE.equals(user.getPendingDeletion())) {
DeletionWarning warning = userDeletionService.checkPendingDeletion(user.getEmail());
response.setPendingDeletion(true);
response.setDaysUntilDeletion(warning.daysRemaining);
} else {
response.setPendingDeletion(false);
response.setDaysUntilDeletion(0);
}

return response;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public JobService(JobRepository jobRepository, CacheManager cacheManager,UserRep
@Cacheable(value = "jobList", key = "#email")
public List<JobDTO> getAllJobs(String email) {
return jobRepository.findByUserEmailOrderByUpdatedAtDesc(email)
.stream()
.parallelStream()
.map(this::convertToDto)
.collect(Collectors.toList());
}
Expand All @@ -76,32 +76,48 @@ public DashboardResponse getDashboardData(String email) {
DashboardResponse response = new DashboardResponse();

long total = jobs.size();
long active = jobs.stream().filter(j -> j.getStatus() != null &&
long active = jobs.parallelStream().filter(j -> j.getStatus() != null &&
!j.getStatus().equals("Rejected") && !j.getStatus().equals("Offer Received")).count();
long interviews = jobs.stream()
long interviews = jobs.parallelStream()
.filter(j -> "Interview Scheduled".equals(j.getStatus()) ||
(j.getStage() != null && j.getStage() >= 3))
.count();
long offers = jobs.stream().filter(j -> "Offer Received".equals(j.getStatus())).count();
long activeInterviews = jobs.stream().filter(j -> "Interview Scheduled".equals(j.getStatus())).count();
long offers = jobs.parallelStream().filter(j -> "Offer Received".equals(j.getStatus())).count();
long activeInterviews = jobs.parallelStream().filter(j -> "Interview Scheduled".equals(j.getStatus())).count();

response.setStats(new DashboardStatsDTO(total, active, interviews, activeInterviews, offers));

Map<String, Long> statusMap = jobs.stream()
Map<String, Long> statusMap = jobs.parallelStream()
.collect(Collectors.groupingBy(Job::getStatus, Collectors.counting()));
response.setStatusChart(mapToChartData(statusMap));

LocalDateTime sixMonthsAgo = LocalDateTime.now().minusMonths(6);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM yy");
Map<String, Long> monthMap = jobs.stream()

List<Job> jobsForMonthly = jobs.parallelStream()
.filter(job -> job.getAppliedDate() != null)
.sorted(Comparator.comparing(Job::getAppliedDate))
.toList();

List<Job> last6Months = jobsForMonthly.parallelStream()
.filter(job -> job.getAppliedDate().isAfter(sixMonthsAgo))
.toList();

List<Job> jobsToChart = last6Months.parallelStream()
.collect(Collectors.groupingBy(
job -> job.getAppliedDate().format(formatter),
Collectors.toList()
)).size() >= 3 ? last6Months : jobsForMonthly;

Map<String, Long> monthMap = jobsToChart.parallelStream()
.collect(Collectors.groupingBy(
job -> job.getAppliedDate().format(formatter),
LinkedHashMap::new,
Collectors.counting()
));
response.setMonthlyChart(mapToChartData(monthMap));

long interviewCount = jobs.stream()
long interviewCount = jobs.parallelStream()
.filter(j -> j.getStage() != null && j.getStage() >= 3)
.count(); response.setInterviewChart(List.of(
new ChartData("Interviewed", interviewCount),
Expand Down Expand Up @@ -157,7 +173,7 @@ public void deleteJob(UUID id, String email) {
@Transactional
public void saveBatchResults(String email, List<EmailBatchItem> batchItems, List<JobDTO> extractedJobs) {

List<List<String>> batchUrlLists = batchItems.stream()
List<List<String>> batchUrlLists = batchItems.parallelStream()
.map(item -> UrlParser.extractAndCleanUrls(item.body()))
.toList();
for (JobDTO job : extractedJobs) {
Expand All @@ -170,7 +186,7 @@ public void saveBatchResults(String email, List<EmailBatchItem> batchItems, List
job.setUrl(originalUrls.get(job.getUrlIndex()));
}
else if (job.getUrl() == null || job.getUrl().isEmpty()) {
job.setUrl(originalUrls.stream()
job.setUrl(originalUrls.parallelStream()
.filter(u -> {
String lower = u.toLowerCase();
return lower.contains("career") ||
Expand Down Expand Up @@ -255,7 +271,7 @@ private Job findBestMatch(List<Job> existingJobs, JobDTO incoming) {
String incomingCompany = incoming.getCompany().toLowerCase().trim();
String incomingRole = (incoming.getRole() != null) ? incoming.getRole().toLowerCase().trim() : "";

List<Job> companyMatches = existingJobs.stream()
List<Job> companyMatches = existingJobs.parallelStream()
.filter(job -> {
if (job.getCompany() == null) return false;
String dbCompany = job.getCompany().toLowerCase().trim();
Expand All @@ -265,15 +281,15 @@ private Job findBestMatch(List<Job> existingJobs, JobDTO incoming) {

if (companyMatches.isEmpty()) return null;

List<Job> activeMatches = companyMatches.stream()
List<Job> activeMatches = companyMatches.parallelStream()
.filter(j -> j.getStatus() != null &&
!j.getStatus().equalsIgnoreCase("Rejected") &&
!j.getStatus().equalsIgnoreCase("Offer Received"))
.collect(Collectors.toList());

if (activeMatches.isEmpty()) return null;

return activeMatches.stream()
return activeMatches.parallelStream()
.max((j1, j2) -> {
double sim1 = calculateSimilarity(j1.getRole(), incomingRole);
double sim2 = calculateSimilarity(j2.getRole(), incomingRole);
Expand Down Expand Up @@ -380,7 +396,7 @@ private Job convertToEntity(JobDTO dto) {
}

private List<ChartData> mapToChartData(Map<String, Long> map) {
return map.entrySet().stream()
return map.entrySet().parallelStream()
.map(e -> new ChartData(e.getKey(), e.getValue()))
.collect(Collectors.toList());
}
Expand Down
Loading
Loading