Skip to content

Commit 1851b38

Browse files
authored
Merge pull request #67 from thughari/dev
account deletion added
2 parents ee51157 + 3b934cb commit 1851b38

File tree

17 files changed

+717
-35
lines changed

17 files changed

+717
-35
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.thughari.jobtrackerpro.controller;
2+
3+
import com.thughari.jobtrackerpro.dto.DeletionWarning;
4+
import com.thughari.jobtrackerpro.service.UserDeletionService;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.http.ResponseEntity;
7+
import org.springframework.security.access.prepost.PreAuthorize;
8+
import org.springframework.security.core.context.SecurityContextHolder;
9+
import org.springframework.web.bind.annotation.*;
10+
11+
@Slf4j
12+
@RestController
13+
@RequestMapping("/api/users")
14+
public class UserDeletionController {
15+
16+
private final UserDeletionService userDeletionService;
17+
18+
public UserDeletionController(UserDeletionService userDeletionService) {
19+
this.userDeletionService = userDeletionService;
20+
}
21+
22+
/**
23+
* Request account deletion
24+
* Account will be permanently deleted after 3 days unless cancelled
25+
*/
26+
@PostMapping("/request-deletion")
27+
@PreAuthorize("isAuthenticated()")
28+
public ResponseEntity<?> requestDeletion() {
29+
String email = getAuthenticatedEmail();
30+
userDeletionService.requestDeletion(email);
31+
return ResponseEntity.ok().body(
32+
java.util.Map.of(
33+
"message", "Deletion request submitted. Your account will be permanently deleted in 3 days.",
34+
"daysRemaining", 3
35+
)
36+
);
37+
}
38+
39+
/**
40+
* Check if user has a pending deletion request
41+
*/
42+
@GetMapping("/deletion-status")
43+
@PreAuthorize("isAuthenticated()")
44+
public ResponseEntity<DeletionWarning> checkDeletionStatus() {
45+
String email = getAuthenticatedEmail();
46+
DeletionWarning warning = userDeletionService.checkPendingDeletion(email);
47+
return ResponseEntity.ok(warning);
48+
}
49+
50+
/**
51+
* Cancel pending deletion
52+
*/
53+
@PostMapping("/cancel-deletion")
54+
@PreAuthorize("isAuthenticated()")
55+
public ResponseEntity<?> cancelDeletion() {
56+
String email = getAuthenticatedEmail();
57+
userDeletionService.cancelDeletion(email);
58+
return ResponseEntity.ok().body(
59+
java.util.Map.of(
60+
"message", "Deletion request cancelled successfully."
61+
)
62+
);
63+
}
64+
65+
private String getAuthenticatedEmail() {
66+
return ((String) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).toLowerCase();
67+
}
68+
}
69+
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.thughari.jobtrackerpro.dto;
2+
3+
public class DeletionWarning {
4+
public final boolean hasPendingDeletion;
5+
public final long daysRemaining;
6+
7+
public DeletionWarning(boolean hasPendingDeletion, long daysRemaining) {
8+
this.hasPendingDeletion = hasPendingDeletion;
9+
this.daysRemaining = daysRemaining;
10+
}
11+
}

backend/src/main/java/com/thughari/jobtrackerpro/dto/UserProfileResponse.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@ public class UserProfileResponse {
1414
private boolean gmailConnected;
1515
private boolean gmailSyncInProgress;
1616
private boolean enabled;
17+
private boolean pendingDeletion;
18+
private long daysUntilDeletion;
1719
}

backend/src/main/java/com/thughari/jobtrackerpro/entity/User.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,10 @@ public class User {
6161

6262
@Column(nullable = false)
6363
private Boolean enabled = false;
64+
65+
@Column(name = "deletion_requested_at")
66+
private LocalDateTime deletionRequestedAt;
67+
68+
@Column(name = "pending_deletion")
69+
private Boolean pendingDeletion = false;
6470
}

backend/src/main/java/com/thughari/jobtrackerpro/repo/JobRepository.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,9 @@ void markStaleJobsAsRejected(
6161
@Param("now") LocalDateTime now,
6262
@Param("note") String note
6363
);
64+
65+
@Modifying(clearAutomatically = true, flushAutomatically = true)
66+
@Query("DELETE FROM Job j WHERE j.userEmail = :email")
67+
void deleteByUserEmail(@Param("email") String email);
6468

6569
}

backend/src/main/java/com/thughari/jobtrackerpro/repo/UserRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,7 @@ public interface UserRepository extends JpaRepository<User, UUID> {
3535
@Modifying
3636
@Query("DELETE FROM User u WHERE u.enabled = false AND u.provider = 'LOCAL' AND u.createdAt < :cutoff")
3737
void deleteUnverifiedUsers(@Param("cutoff") LocalDateTime cutoff);
38+
39+
@Query("SELECT u FROM User u WHERE u.pendingDeletion = true AND u.deletionRequestedAt < :cutoffDate")
40+
List<User> findAllByPendingDeletionTrueAndDeletionRequestedAtBefore(@Param("cutoffDate") LocalDateTime cutoffDate);
3841
}

backend/src/main/java/com/thughari/jobtrackerpro/scheduler/JobScheduler.java

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.thughari.jobtrackerpro.repo.VerificationTokenRepository;
77
import com.thughari.jobtrackerpro.service.GmailIntegrationService;
88
import com.thughari.jobtrackerpro.service.JobService;
9+
import com.thughari.jobtrackerpro.service.UserDeletionService;
910

1011
import jakarta.transaction.Transactional;
1112
import lombok.extern.slf4j.Slf4j;
@@ -23,19 +24,23 @@ public class JobScheduler {
2324
private final UserRepository userRepository;
2425
private final GmailIntegrationService gmailIntegrationService;
2526

27+
private final UserDeletionService userDeletionService;
28+
2629
private final PasswordResetTokenRepository passwordTokenRepo;
2730
private final VerificationTokenRepository verificationTokenRepo;
2831

2932
public JobScheduler(JobService jobService,
3033
UserRepository userRepository,
3134
GmailIntegrationService gmailIntegrationService,
3235
PasswordResetTokenRepository passwordTokenRepo,
33-
VerificationTokenRepository verificationTokenRepo) {
36+
VerificationTokenRepository verificationTokenRepo,
37+
UserDeletionService userDeletionService) {
3438
this.jobService = jobService;
3539
this.userRepository = userRepository;
3640
this.gmailIntegrationService = gmailIntegrationService;
3741
this.passwordTokenRepo = passwordTokenRepo;
3842
this.verificationTokenRepo = verificationTokenRepo;
43+
this.userDeletionService = userDeletionService;
3944
}
4045

4146
/**
@@ -56,7 +61,7 @@ public void runStaleJobCleanup() {
5661
/**
5762
* Gmail Security: Renews the 7-day watch lease every 5 days.
5863
*/
59-
@Scheduled(cron = "0 0 0 */5 * *")
64+
@Scheduled(cron = "0 30 0 */5 * *")
6065
public void renewGmailWatches() {
6166
log.info("Gmail Sync: Starting bulk watch renewal...");
6267

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

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

9398
log.info("System cleanup completed. Database pruned of expired security entries.");
9499
}
100+
101+
/*
102+
* Scheduled task to process user deletions after the 3-day grace period.
103+
*/
104+
@Scheduled(cron = "0 30 1 * * *")
105+
public void processScheduledDeletions() {
106+
log.info("Starting scheduled user deletion cleanup...");
107+
108+
List<User> usersToDelete = userRepository.findAllByPendingDeletionTrueAndDeletionRequestedAtBefore(
109+
LocalDateTime.now().minusDays(3)
110+
);
111+
112+
for (User user : usersToDelete) {
113+
try {
114+
userDeletionService.deleteUserCompletely(user.getEmail());
115+
} catch (Exception e) {
116+
log.error("Failed to delete user: {}", user.getEmail(), e);
117+
}
118+
}
119+
120+
log.info("Scheduled deletion cleanup completed. Deleted {} users.", usersToDelete.size());
121+
}
95122
}

backend/src/main/java/com/thughari/jobtrackerpro/service/AuthService.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public class AuthService {
4040
private final JwtUtils jwtUtils;
4141
private final StorageService storageService;
4242
private final CacheManager cacheManager;
43+
private final UserDeletionService userDeletionService;
4344

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

5657
public AuthService(UserRepository userRepository, PasswordEncoder passwordEncoder,
5758
JwtUtils jwtUtils, StorageService storageService,
58-
PasswordResetTokenRepository passwordResetTokenRepository, EmailService emailService, CacheManager cacheManager, VerificationTokenRepository verificationTokenRepository) {
59+
PasswordResetTokenRepository passwordResetTokenRepository, EmailService emailService, CacheManager cacheManager, VerificationTokenRepository verificationTokenRepository, UserDeletionService userDeletionService) {
5960
this.userRepository = userRepository;
6061
this.passwordEncoder = passwordEncoder;
6162
this.jwtUtils = jwtUtils;
@@ -64,6 +65,7 @@ public AuthService(UserRepository userRepository, PasswordEncoder passwordEncode
6465
this.emailService = emailService;
6566
this.cacheManager = cacheManager;
6667
this.verificationTokenRepository = verificationTokenRepository;
68+
this.userDeletionService = userDeletionService;
6769
}
6870

6971
public void registerUser(AuthRequest request) {
@@ -350,6 +352,17 @@ private UserProfileResponse mapToProfileResponse(User user) {
350352
response.setGmailConnected(Boolean.TRUE.equals(user.getGmailConnected()));
351353
response.setGmailSyncInProgress(Boolean.TRUE.equals(user.getGmailSyncInProgress()));
352354
response.setEnabled(Boolean.TRUE.equals(user.getEnabled()));
355+
356+
// Set deletion warning info
357+
if (Boolean.TRUE.equals(user.getPendingDeletion())) {
358+
DeletionWarning warning = userDeletionService.checkPendingDeletion(user.getEmail());
359+
response.setPendingDeletion(true);
360+
response.setDaysUntilDeletion(warning.daysRemaining);
361+
} else {
362+
response.setPendingDeletion(false);
363+
response.setDaysUntilDeletion(0);
364+
}
365+
353366
return response;
354367
}
355368

backend/src/main/java/com/thughari/jobtrackerpro/service/JobService.java

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public JobService(JobRepository jobRepository, CacheManager cacheManager,UserRep
5151
@Cacheable(value = "jobList", key = "#email")
5252
public List<JobDTO> getAllJobs(String email) {
5353
return jobRepository.findByUserEmailOrderByUpdatedAtDesc(email)
54-
.stream()
54+
.parallelStream()
5555
.map(this::convertToDto)
5656
.collect(Collectors.toList());
5757
}
@@ -76,32 +76,48 @@ public DashboardResponse getDashboardData(String email) {
7676
DashboardResponse response = new DashboardResponse();
7777

7878
long total = jobs.size();
79-
long active = jobs.stream().filter(j -> j.getStatus() != null &&
79+
long active = jobs.parallelStream().filter(j -> j.getStatus() != null &&
8080
!j.getStatus().equals("Rejected") && !j.getStatus().equals("Offer Received")).count();
81-
long interviews = jobs.stream()
81+
long interviews = jobs.parallelStream()
8282
.filter(j -> "Interview Scheduled".equals(j.getStatus()) ||
8383
(j.getStage() != null && j.getStage() >= 3))
8484
.count();
85-
long offers = jobs.stream().filter(j -> "Offer Received".equals(j.getStatus())).count();
86-
long activeInterviews = jobs.stream().filter(j -> "Interview Scheduled".equals(j.getStatus())).count();
85+
long offers = jobs.parallelStream().filter(j -> "Offer Received".equals(j.getStatus())).count();
86+
long activeInterviews = jobs.parallelStream().filter(j -> "Interview Scheduled".equals(j.getStatus())).count();
8787

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

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

94+
LocalDateTime sixMonthsAgo = LocalDateTime.now().minusMonths(6);
9495
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM yy");
95-
Map<String, Long> monthMap = jobs.stream()
96+
97+
List<Job> jobsForMonthly = jobs.parallelStream()
98+
.filter(job -> job.getAppliedDate() != null)
9699
.sorted(Comparator.comparing(Job::getAppliedDate))
100+
.toList();
101+
102+
List<Job> last6Months = jobsForMonthly.parallelStream()
103+
.filter(job -> job.getAppliedDate().isAfter(sixMonthsAgo))
104+
.toList();
105+
106+
List<Job> jobsToChart = last6Months.parallelStream()
107+
.collect(Collectors.groupingBy(
108+
job -> job.getAppliedDate().format(formatter),
109+
Collectors.toList()
110+
)).size() >= 3 ? last6Months : jobsForMonthly;
111+
112+
Map<String, Long> monthMap = jobsToChart.parallelStream()
97113
.collect(Collectors.groupingBy(
98114
job -> job.getAppliedDate().format(formatter),
99115
LinkedHashMap::new,
100116
Collectors.counting()
101117
));
102118
response.setMonthlyChart(mapToChartData(monthMap));
103119

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

160-
List<List<String>> batchUrlLists = batchItems.stream()
176+
List<List<String>> batchUrlLists = batchItems.parallelStream()
161177
.map(item -> UrlParser.extractAndCleanUrls(item.body()))
162178
.toList();
163179
for (JobDTO job : extractedJobs) {
@@ -170,7 +186,7 @@ public void saveBatchResults(String email, List<EmailBatchItem> batchItems, List
170186
job.setUrl(originalUrls.get(job.getUrlIndex()));
171187
}
172188
else if (job.getUrl() == null || job.getUrl().isEmpty()) {
173-
job.setUrl(originalUrls.stream()
189+
job.setUrl(originalUrls.parallelStream()
174190
.filter(u -> {
175191
String lower = u.toLowerCase();
176192
return lower.contains("career") ||
@@ -255,7 +271,7 @@ private Job findBestMatch(List<Job> existingJobs, JobDTO incoming) {
255271
String incomingCompany = incoming.getCompany().toLowerCase().trim();
256272
String incomingRole = (incoming.getRole() != null) ? incoming.getRole().toLowerCase().trim() : "";
257273

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

266282
if (companyMatches.isEmpty()) return null;
267283

268-
List<Job> activeMatches = companyMatches.stream()
284+
List<Job> activeMatches = companyMatches.parallelStream()
269285
.filter(j -> j.getStatus() != null &&
270286
!j.getStatus().equalsIgnoreCase("Rejected") &&
271287
!j.getStatus().equalsIgnoreCase("Offer Received"))
272288
.collect(Collectors.toList());
273289

274290
if (activeMatches.isEmpty()) return null;
275291

276-
return activeMatches.stream()
292+
return activeMatches.parallelStream()
277293
.max((j1, j2) -> {
278294
double sim1 = calculateSimilarity(j1.getRole(), incomingRole);
279295
double sim2 = calculateSimilarity(j2.getRole(), incomingRole);
@@ -380,7 +396,7 @@ private Job convertToEntity(JobDTO dto) {
380396
}
381397

382398
private List<ChartData> mapToChartData(Map<String, Long> map) {
383-
return map.entrySet().stream()
399+
return map.entrySet().parallelStream()
384400
.map(e -> new ChartData(e.getKey(), e.getValue()))
385401
.collect(Collectors.toList());
386402
}

0 commit comments

Comments
 (0)