Skip to content

Commit 7768882

Browse files
authored
Merge pull request #48 from thughari/feature/gmail-integration
Feature/gmail integration
2 parents 631b50c + da2bcc6 commit 7768882

File tree

72 files changed

+3415
-845
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+3415
-845
lines changed

backend/LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2025 Hari Thatikonda
3+
Copyright (c) 2026 Hari Thatikonda
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

backend/pom.xml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@
3030
<java.version>21</java.version>
3131
<aws.sdk.version>2.25.27</aws.sdk.version>
3232
<jjwt.version>0.11.5</jjwt.version>
33+
<google.apis.version>v1-rev20220404-2.0.0</google.apis.version>
3334
</properties>
3435

3536
<dependencies>
36-
37+
3738
<dependency>
3839
<groupId>org.springframework.boot</groupId>
3940
<artifactId>spring-boot-starter-web</artifactId>
@@ -72,6 +73,12 @@
7273
<artifactId>s3</artifactId>
7374
<version>${aws.sdk.version}</version>
7475
</dependency>
76+
77+
<dependency>
78+
<groupId>com.google.apis</groupId>
79+
<artifactId>google-api-services-gmail</artifactId>
80+
<version>${google.apis.version}</version>
81+
</dependency>
7582

7683
<dependency>
7784
<groupId>org.springframework.boot</groupId>

backend/service.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,4 +140,14 @@ spec:
140140
valueFrom:
141141
secretKeyRef:
142142
name: email-sender-address
143+
key: latest
144+
- name: GOOGLE_PUBSUB_TOPIC
145+
valueFrom:
146+
secretKeyRef:
147+
name: google-pubsub-topic
148+
key: latest
149+
- name: GOOGLE_PUBSUB_SERVICE_ACCOUNT
150+
valueFrom:
151+
secretKeyRef:
152+
name: google-pubsub-service-account
143153
key: latest

backend/src/main/java/com/thughari/jobtrackerpro/JobTrackerProApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
import org.springframework.cache.annotation.EnableCaching;
88
import org.springframework.data.web.config.EnableSpringDataWebSupport;
99
import org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode;
10+
import org.springframework.scheduling.annotation.EnableAsync;
1011
import org.springframework.scheduling.annotation.EnableScheduling;
1112

1213
@SpringBootApplication
1314
@EnableCaching
1415
@EnableSpringDataWebSupport(pageSerializationMode = PageSerializationMode.VIA_DTO)
1516
@EnableScheduling
17+
@EnableAsync
1618
public class JobTrackerProApplication {
1719

1820
public static void main(String[] args) {

backend/src/main/java/com/thughari/jobtrackerpro/config/AsyncConfig.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import org.springframework.context.annotation.Bean;
44
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.context.annotation.Primary;
56
import org.springframework.scheduling.annotation.EnableAsync;
67
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
78
import java.util.concurrent.Executor;
@@ -20,4 +21,16 @@ public Executor dashboardExecutor() {
2021
executor.initialize();
2122
return executor;
2223
}
24+
25+
@Primary
26+
@Bean(name = "taskExecutor")
27+
public Executor taskExecutor() {
28+
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
29+
executor.setCorePoolSize(5);
30+
executor.setMaxPoolSize(15);
31+
executor.setQueueCapacity(100);
32+
executor.setThreadNamePrefix("GmailSync-");
33+
executor.initialize();
34+
return executor;
35+
}
2336
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.thughari.jobtrackerpro.config;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
6+
import org.springframework.security.crypto.password.PasswordEncoder;
7+
8+
@Configuration
9+
public class PasswordConfig {
10+
11+
@Bean
12+
public PasswordEncoder passwordEncoder() {
13+
return new BCryptPasswordEncoder();
14+
}
15+
}

backend/src/main/java/com/thughari/jobtrackerpro/config/SecurityConfig.java

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
1515
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
1616
import org.springframework.security.config.http.SessionCreationPolicy;
17-
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
18-
import org.springframework.security.crypto.password.PasswordEncoder;
1917

2018
import org.springframework.security.web.SecurityFilterChain;
2119
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@@ -73,12 +71,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
7371

7472
return http.build();
7573
}
76-
77-
@Bean
78-
public PasswordEncoder passwordEncoder() {
79-
return new BCryptPasswordEncoder();
80-
}
81-
74+
8275
@Bean
8376
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
8477
return config.getAuthenticationManager();

backend/src/main/java/com/thughari/jobtrackerpro/controller/AuthController.java

Lines changed: 52 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
import com.thughari.jobtrackerpro.dto.UserProfileResponse;
99
import com.thughari.jobtrackerpro.service.AuthService;
1010
import jakarta.servlet.http.HttpServletResponse;
11+
import lombok.extern.slf4j.Slf4j;
12+
13+
import java.util.Map;
14+
1115
import org.springframework.beans.factory.annotation.Value;
1216
import org.springframework.http.MediaType;
1317
import org.springframework.http.ResponseCookie;
@@ -16,6 +20,7 @@
1620
import org.springframework.web.bind.annotation.*;
1721
import org.springframework.web.multipart.MultipartFile;
1822

23+
@Slf4j
1924
@RestController
2025
@RequestMapping("/api/auth")
2126
public class AuthController {
@@ -36,43 +41,43 @@ public AuthController(AuthService authService) {
3641
}
3742

3843
@PostMapping("/signup")
39-
public ResponseEntity<?> registerUser(@RequestBody AuthRequest request, HttpServletResponse response) {
40-
try {
41-
AuthTokens tokens = authService.registerUser(request);
42-
attachRefreshCookie(response, tokens.refreshToken());
43-
return ResponseEntity.ok(new AuthResponse(tokens.accessToken()));
44-
} catch (IllegalArgumentException e) {
45-
return ResponseEntity.badRequest().body(e.getMessage());
46-
}
47-
}
48-
49-
@PostMapping("/login")
50-
public ResponseEntity<?> loginUser(@RequestBody AuthRequest request, HttpServletResponse response) {
51-
try {
52-
AuthTokens tokens = authService.loginUser(request);
53-
attachRefreshCookie(response, tokens.refreshToken());
54-
return ResponseEntity.ok(new AuthResponse(tokens.accessToken()));
55-
} catch (IllegalArgumentException e) {
56-
return ResponseEntity.badRequest().body(e.getMessage());
57-
}
58-
}
59-
60-
@PostMapping("/refresh")
61-
public ResponseEntity<?> refreshToken(@CookieValue(name = "refresh_token", required = false) String refreshToken,
62-
HttpServletResponse response) {
63-
if (refreshToken == null || refreshToken.isBlank()) {
64-
return ResponseEntity.status(401).body("Missing refresh token");
65-
}
66-
67-
try {
68-
AuthTokens tokens = authService.refreshAccessToken(refreshToken);
69-
attachRefreshCookie(response, tokens.refreshToken());
70-
return ResponseEntity.ok(new AuthResponse(tokens.accessToken()));
71-
} catch (IllegalArgumentException e) {
72-
clearRefreshCookie(response);
73-
return ResponseEntity.status(401).body("Invalid refresh token");
74-
}
75-
}
44+
public ResponseEntity<?> registerUser(@RequestBody AuthRequest request) {
45+
authService.registerUser(request);
46+
return ResponseEntity.ok(Map.of("message", "Registration successful. Please check your email to verify your account."));
47+
}
48+
49+
@PostMapping("/login")
50+
public ResponseEntity<?> loginUser(@RequestBody AuthRequest request, HttpServletResponse response) {
51+
AuthTokens tokens = authService.loginUser(request);
52+
attachRefreshCookie(response, tokens.refreshToken());
53+
return ResponseEntity.ok(new AuthResponse(tokens.accessToken()));
54+
}
55+
56+
@GetMapping("/verify-email")
57+
public ResponseEntity<?> verifyEmail(@RequestParam String token, HttpServletResponse response) {
58+
AuthTokens tokens = authService.verifyUser(token);
59+
attachRefreshCookie(response, tokens.refreshToken());
60+
return ResponseEntity.ok(new AuthResponse(tokens.accessToken()));
61+
}
62+
63+
@PostMapping("/resend-verification")
64+
public ResponseEntity<?> resendVerification(@RequestParam String email) {
65+
authService.resendVerificationEmail(email);
66+
return ResponseEntity.ok(Map.of("message", "A new verification link has been sent."));
67+
}
68+
69+
70+
@PostMapping("/refresh")
71+
public ResponseEntity<?> refreshToken(@CookieValue(name = "refresh_token", required = false) String refreshToken,
72+
HttpServletResponse response) {
73+
if (refreshToken == null || refreshToken.isBlank()) {
74+
return ResponseEntity.status(401).body("Missing refresh token");
75+
}
76+
77+
AuthTokens tokens = authService.refreshAccessToken(refreshToken);
78+
attachRefreshCookie(response, tokens.refreshToken());
79+
return ResponseEntity.ok(new AuthResponse(tokens.accessToken()));
80+
}
7681

7782
@PostMapping("/logout")
7883
public ResponseEntity<?> logout(HttpServletResponse response) {
@@ -97,43 +102,29 @@ public ResponseEntity<?> updateProfile(
97102
}
98103

99104
@PutMapping("/password")
100-
public ResponseEntity<?> changePassword(@RequestBody ChangePasswordRequest request) {
101-
try {
102-
String email = getAuthenticatedEmail();
103-
authService.changePassword(email, request);
104-
return ResponseEntity.ok().body("Password set successfully.");
105-
} catch (IllegalArgumentException e) {
106-
return ResponseEntity.badRequest().body(e.getMessage());
107-
}
108-
}
105+
public ResponseEntity<?> changePassword(@RequestBody ChangePasswordRequest request) {
106+
authService.changePassword(getAuthenticatedEmail(), request);
107+
return ResponseEntity.ok().body(Map.of("message", "Password set successfully."));
108+
}
109109

110110
@PostMapping("/forgot-password")
111111
public ResponseEntity<?> forgotPassword(@RequestParam String email) {
112-
try {
113-
authService.forgotPassword(email);
114-
return ResponseEntity.ok("If that email exists, a reset link has been sent.");
115-
} catch (Exception e) {
116-
return ResponseEntity.ok("If that email exists, a reset link has been sent.");
117-
}
112+
authService.forgotPassword(email);
113+
return ResponseEntity.ok("If that email exists, a reset link has been sent.");
118114
}
119115

120116
@PostMapping("/reset-password")
121-
public ResponseEntity<?> resetPassword(@RequestBody ResetPasswordRequest request) {
122-
try {
123-
authService.resetPassword(request.getToken(), request.getNewPassword());
124-
return ResponseEntity.ok("Password reset successfully. Please login.");
125-
} catch (Exception e) {
126-
return ResponseEntity.badRequest().body(e.getMessage());
127-
}
128-
}
117+
public ResponseEntity<?> resetPassword(@RequestBody ResetPasswordRequest request) {
118+
authService.resetPassword(request.getToken(), request.getNewPassword());
119+
return ResponseEntity.ok(Map.of("message", "Password reset successfully."));
120+
}
129121

130122
private String getAuthenticatedEmail() {
131123
return ((String) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).toLowerCase();
132124
}
133125

134126
private void attachRefreshCookie(HttpServletResponse response, String refreshToken) {
135127
response.addHeader("Set-Cookie", buildRefreshCookie(refreshToken, "/", refreshExpirationMs / 1000).toString());
136-
// Clear legacy cookie written by older builds to prevent duplicate refresh_token cookies.
137128
response.addHeader("Set-Cookie", buildRefreshCookie("", "/api/auth", 0).toString());
138129
}
139130

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.thughari.jobtrackerpro.controller;
2+
3+
import com.github.benmanes.caffeine.cache.Cache;
4+
import com.github.benmanes.caffeine.cache.Caffeine;
5+
import com.thughari.jobtrackerpro.service.GmailIntegrationService;
6+
import lombok.extern.slf4j.Slf4j;
7+
8+
import org.springframework.http.HttpStatus;
9+
import org.springframework.http.ResponseEntity;
10+
import org.springframework.security.core.context.SecurityContextHolder;
11+
import org.springframework.web.bind.annotation.*;
12+
13+
import java.util.Map;
14+
import java.util.concurrent.TimeUnit;
15+
16+
@RestController
17+
@RequestMapping("/api/integrations")
18+
@Slf4j
19+
public class GmailIntegrationController {
20+
21+
private final GmailIntegrationService gmailAutomationService;
22+
23+
private final Cache<String, Boolean> syncThrottler = Caffeine.newBuilder()
24+
.expireAfterWrite(10, TimeUnit.SECONDS) // Block duplicate clicks for 10s
25+
.build();
26+
27+
public GmailIntegrationController(GmailIntegrationService gmailAutomationService) {
28+
this.gmailAutomationService = gmailAutomationService;
29+
}
30+
31+
@PostMapping("/gmail/connect")
32+
public ResponseEntity<String> connectGmail(@RequestBody Map<String, String> body) {
33+
String authCode = body.get("code");
34+
String email = getAuthenticatedEmail();
35+
36+
try {
37+
gmailAutomationService.connectAndSetupPush(authCode, email);
38+
return ResponseEntity.ok("Gmail Automation enabled successfully.");
39+
} catch (Exception e) {
40+
log.error("Failed to setup Gmail for user {}: {}", email, e.getMessage());
41+
return ResponseEntity.status(500).body("Failed to setup Gmail: " + e.getMessage());
42+
}
43+
}
44+
45+
@PostMapping("/gmail/disconnect")
46+
public ResponseEntity<Void> disconnectGmail() {
47+
String email = SecurityContextHolder.getContext().getAuthentication().getName().toLowerCase();
48+
gmailAutomationService.disconnectGmail(email);
49+
return ResponseEntity.noContent().build();
50+
}
51+
52+
@PostMapping("/gmail/sync")
53+
public ResponseEntity<String> syncGmail() {
54+
String email = getAuthenticatedEmail();
55+
56+
if (syncThrottler.getIfPresent(email) != null) {
57+
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
58+
.body("Sync already requested. Please wait.");
59+
}
60+
syncThrottler.put(email, true);
61+
gmailAutomationService.initiateManualSync(email);
62+
return ResponseEntity.ok("Sync started in background. Your dashboard will update shortly.");
63+
}
64+
65+
private String getAuthenticatedEmail() {
66+
return SecurityContextHolder.getContext().getAuthentication().getName().toLowerCase();
67+
}
68+
}

0 commit comments

Comments
 (0)