Skip to content

Commit e77da6c

Browse files
feat: add google drive resume integration
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent aef81ce commit e77da6c

28 files changed

Lines changed: 2091 additions & 16 deletions

.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ MAIL_USERNAME=no-reply@example.dev
2525
MAIL_SMTP_AUTH=true
2626
MAIL_SMTP_SSL=true
2727

28+
# Google Drive OAuth
29+
# Redirect URI must exactly match the OAuth client config in Google Cloud Console.
30+
GOOGLE_DRIVE_CLIENT_ID=
31+
GOOGLE_DRIVE_CLIENT_SECRET=
32+
GOOGLE_DRIVE_REDIRECT_URI=http://localhost:8080/api/v1/google-drive/oauth/callback
33+
GOOGLE_DRIVE_OAUTH_COMPLETE_URL=http://localhost:5173/settings/google-drive/callback
34+
2835
# ============================================
2936
# Synkra AIOX Environment Configuration
3037
# ============================================

README.md

Lines changed: 180 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,27 +52,27 @@ A production-ready Spring Boot REST API for tracking job applications, built wit
5252

5353
| Method | Endpoint | Description |
5454
|--------|----------|-------------|
55-
| POST | `/api/auth/register` | Register a new user |
56-
| POST | `/api/auth/login` | Login and receive tokens |
57-
| POST | `/api/auth/refresh` | Refresh access token |
58-
| POST | `/api/auth/logout` | Logout and revoke refresh token |
59-
| POST | `/api/auth/forgot-password` | Request password reset |
60-
| POST | `/api/auth/reset-password` | Reset password with token |
61-
| GET | `/api/auth/me` | Get current user info |
55+
| POST | `/api/v1/auth/register` | Register a new user |
56+
| POST | `/api/v1/auth/login` | Login and receive tokens |
57+
| POST | `/api/v1/auth/refresh` | Refresh access token |
58+
| POST | `/api/v1/auth/logout` | Logout and revoke refresh token |
59+
| POST | `/api/v1/auth/forgot-password` | Request password reset |
60+
| POST | `/api/v1/auth/reset-password` | Reset password with token |
61+
| GET | `/api/v1/auth/me` | Get current user info |
6262

6363
### Applications
6464

6565
| Method | Endpoint | Description |
6666
|--------|----------|-------------|
67-
| POST | `/api/applications` | Create application |
68-
| GET | `/api/applications` | List all (paginated + filterable) |
69-
| GET | `/api/applications/{id}` | Get by ID |
70-
| PUT | `/api/applications/{id}` | Full update |
71-
| PATCH | `/api/applications/{id}/status` | Update status |
72-
| PATCH | `/api/applications/{id}/reminder` | Toggle reminder |
73-
| DELETE | `/api/applications/{id}` | Delete |
74-
| GET | `/api/applications/upcoming` | Upcoming next steps |
75-
| GET | `/api/applications/overdue` | Overdue next steps |
67+
| POST | `/api/v1/applications` | Create application |
68+
| GET | `/api/v1/applications` | List all (paginated + filterable) |
69+
| GET | `/api/v1/applications/{id}` | Get by ID |
70+
| PUT | `/api/v1/applications/{id}` | Full update |
71+
| PATCH | `/api/v1/applications/{id}/status` | Update status |
72+
| PATCH | `/api/v1/applications/{id}/reminder` | Toggle reminder |
73+
| DELETE | `/api/v1/applications/{id}` | Delete |
74+
| GET | `/api/v1/applications/upcoming` | Upcoming next steps |
75+
| GET | `/api/v1/applications/overdue` | Overdue next steps |
7676

7777
### Gamification
7878

@@ -82,6 +82,19 @@ A production-ready Spring Boot REST API for tracking job applications, built wit
8282
| GET | `/api/v1/gamification/achievements` | List achievement catalog with unlocked state |
8383
| POST | `/api/v1/gamification/events` | Apply a tracked XP event and return updated profile |
8484

85+
### Google Drive
86+
87+
| Method | Endpoint | Description |
88+
|--------|----------|-------------|
89+
| POST | `/api/v1/google-drive/oauth/start` | Generate the Google OAuth authorization URL for the authenticated user |
90+
| GET | `/api/v1/google-drive/oauth/callback` | Google OAuth callback endpoint used by Google Cloud |
91+
| GET | `/api/v1/google-drive/status` | Return current Google Drive connection status, configured root folder, and base resumes |
92+
| DELETE | `/api/v1/google-drive/connection` | Disconnect the current user's Google account and remove stored Drive preferences |
93+
| PUT | `/api/v1/google-drive/root-folder` | Validate and save the user's base Drive folder |
94+
| POST | `/api/v1/google-drive/base-resumes` | Register a Google Docs base resume by Google Docs URL or file ID |
95+
| DELETE | `/api/v1/google-drive/base-resumes/{baseResumeId}` | Remove a configured base resume |
96+
| POST | `/api/v1/google-drive/applications/{applicationId}/resume-copies` | Copy a configured base resume into the application's Drive subfolder |
97+
8598
## Application Status Values
8699

87100
- `RH`
@@ -192,9 +205,156 @@ export DB_URL=jdbc:mariadb://localhost:3306/jobtracker?createDatabaseIfNotExist=
192205
export DB_USERNAME=jobtracker
193206
export DB_PASSWORD=jobtracker
194207
export JWT_SECRET=your-secret-key-at-least-256-bits-long
208+
export GOOGLE_DRIVE_CLIENT_ID=your-google-client-id
209+
export GOOGLE_DRIVE_CLIENT_SECRET=your-google-client-secret
210+
export GOOGLE_DRIVE_REDIRECT_URI=http://localhost:8080/api/v1/google-drive/oauth/callback
211+
export GOOGLE_DRIVE_OAUTH_COMPLETE_URL=http://localhost:5173/settings/google-drive/callback
195212
mvn spring-boot:run
196213
```
197214

215+
## Google Drive integration
216+
217+
This backend supports per-user Google Drive OAuth2 for resume-copy automation. It does **not** replace the app's JWT auth flow; users stay authenticated with the existing bearer token, and Google is connected separately with `POST /api/v1/google-drive/oauth/start`.
218+
219+
### Required Google Cloud setup
220+
221+
1. Create a Google Cloud OAuth client for a web application.
222+
2. Enable the **Google Drive API**.
223+
3. Add the backend callback URL as an authorized redirect URI. Example local value:
224+
- `http://localhost:8080/api/v1/google-drive/oauth/callback`
225+
4. Configure these environment variables:
226+
227+
| Variable | Required | Description |
228+
|----------|----------|-------------|
229+
| `GOOGLE_DRIVE_CLIENT_ID` | Yes | Google OAuth client ID |
230+
| `GOOGLE_DRIVE_CLIENT_SECRET` | Yes | Google OAuth client secret |
231+
| `GOOGLE_DRIVE_REDIRECT_URI` | Yes | Backend callback URL registered in Google Cloud |
232+
| `GOOGLE_DRIVE_OAUTH_COMPLETE_URL` | Yes | Frontend page that receives the final `status` and `message` query params after OAuth finishes |
233+
| `GOOGLE_DRIVE_AUTHORIZATION_URI` | No | Override Google authorization endpoint |
234+
| `GOOGLE_DRIVE_TOKEN_URI` | No | Override Google token endpoint |
235+
236+
### OAuth flow expectations
237+
238+
1. Frontend calls `POST /api/v1/google-drive/oauth/start` with the user's JWT bearer token.
239+
2. Backend creates a short-lived OAuth state tied to that authenticated user and returns:
240+
- `authorizationUrl`
241+
- `state`
242+
- `redirectUri`
243+
- `scopes`
244+
3. Frontend opens `authorizationUrl` in a new tab or popup.
245+
4. Google redirects back to `GET /api/v1/google-drive/oauth/callback`.
246+
5. Backend exchanges the authorization code for user-scoped Drive credentials, stores them, and redirects the browser to `GOOGLE_DRIVE_OAUTH_COMPLETE_URL` with:
247+
- `status=success|error`
248+
- `message=<url-encoded message>`
249+
250+
### Scope used
251+
252+
- `https://www.googleapis.com/auth/drive`
253+
254+
This scope is used so the backend can validate user-selected Drive folders, read chosen Google Docs metadata, create vacancy subfolders, and copy Google Docs files on behalf of the authenticated user.
255+
256+
### Supported files
257+
258+
- Base resumes must be **Google Docs** (`application/vnd.google-apps.document`).
259+
- The root folder must be a **Google Drive folder**.
260+
- The frontend Gemini button that opens `https://gemini.google.com/gem/f8ed7c14b062` is frontend-only and does not require a backend endpoint.
261+
262+
### Resume copy behavior
263+
264+
When the frontend later calls `POST /api/v1/google-drive/applications/{applicationId}/resume-copies`:
265+
266+
1. Backend verifies the current user owns the application.
267+
2. Backend refreshes the user's Google access token if needed.
268+
3. Backend verifies the configured root folder still exists and is a folder.
269+
4. Backend finds or creates a vacancy subfolder under that root folder using the application identity.
270+
5. Backend copies the selected base Google Doc into that subfolder.
271+
6. Backend renames the copy with an `APP-<application-uuid>` prefix plus vacancy context.
272+
7. Backend returns a Google Docs web URL for the copied file.
273+
274+
### Google Drive request/response shapes
275+
276+
`POST /api/v1/google-drive/oauth/start`
277+
278+
```json
279+
{}
280+
```
281+
282+
Response:
283+
284+
```json
285+
{
286+
"authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth?...",
287+
"state": "generated-state",
288+
"redirectUri": "http://localhost:8080/api/v1/google-drive/oauth/callback",
289+
"scopes": [
290+
"https://www.googleapis.com/auth/drive"
291+
]
292+
}
293+
```
294+
295+
`GET /api/v1/google-drive/status`
296+
297+
```json
298+
{
299+
"configured": true,
300+
"connected": true,
301+
"googleEmail": "user@gmail.com",
302+
"googleDisplayName": "User Name",
303+
"googleAccountId": "permission-id",
304+
"rootFolderId": "drive-folder-id",
305+
"rootFolderName": "Job Tracker Root",
306+
"connectedAt": "2026-05-05T12:00:00",
307+
"baseResumes": [
308+
{
309+
"id": "uuid",
310+
"googleFileId": "google-doc-id",
311+
"documentName": "Resume Base",
312+
"webViewLink": "https://docs.google.com/document/d/google-doc-id/edit",
313+
"createdAt": "2026-05-05T12:05:00"
314+
}
315+
]
316+
}
317+
```
318+
319+
`PUT /api/v1/google-drive/root-folder`
320+
321+
```json
322+
{
323+
"folderIdOrUrl": "https://drive.google.com/drive/folders/drive-folder-id"
324+
}
325+
```
326+
327+
`POST /api/v1/google-drive/base-resumes`
328+
329+
```json
330+
{
331+
"documentIdOrUrl": "https://docs.google.com/document/d/google-doc-id/edit"
332+
}
333+
```
334+
335+
`POST /api/v1/google-drive/applications/{applicationId}/resume-copies`
336+
337+
```json
338+
{
339+
"baseResumeId": "base-resume-uuid"
340+
}
341+
```
342+
343+
Response:
344+
345+
```json
346+
{
347+
"applicationId": "application-uuid",
348+
"baseResumeId": "base-resume-uuid",
349+
"copiedFileId": "copied-google-doc-id",
350+
"copiedFileName": "APP-application-uuid - Backend Engineer - Resume Base",
351+
"documentWebViewLink": "https://docs.google.com/document/d/copied-google-doc-id/edit",
352+
"vacancyFolderId": "vacancy-folder-id",
353+
"vacancyFolderName": "Backend Engineer - APP-application-uuid",
354+
"vacancyFolderWebViewLink": "https://drive.google.com/drive/folders/vacancy-folder-id"
355+
}
356+
```
357+
198358
## Running Tests
199359

200360
```bash
@@ -256,6 +416,10 @@ If `APP_SEED_ENABLED=true` and `APP_SEED_USER_EMAIL` is not provided (or the use
256416
| `JWT_ACCESS_TOKEN_EXPIRATION_MS` | `900000` | Access token TTL (15 min) |
257417
| `JWT_REFRESH_TOKEN_EXPIRATION_MS` | `604800000` | Refresh token TTL (7 days) |
258418
| `CORS_ALLOWED_ORIGINS` | `http://localhost:3000,http://localhost:5173` | Allowed CORS origins |
419+
| `GOOGLE_DRIVE_CLIENT_ID` | *(empty)* | Google OAuth client ID for Drive integration |
420+
| `GOOGLE_DRIVE_CLIENT_SECRET` | *(empty)* | Google OAuth client secret for Drive integration |
421+
| `GOOGLE_DRIVE_REDIRECT_URI` | `http://localhost:8080/api/v1/google-drive/oauth/callback` | OAuth callback URL registered in Google Cloud |
422+
| `GOOGLE_DRIVE_OAUTH_COMPLETE_URL` | `http://localhost:5173/settings/google-drive/callback` | Frontend URL that receives OAuth completion redirects |
259423
| `RATE_LIMIT_AUTH_LOGIN_LIMIT_FOR_PERIOD` | `10` | Max login requests allowed per refresh period |
260424
| `RATE_LIMIT_AUTH_LOGIN_REFRESH_PERIOD` | `1m` | Window used by the login rate limiter |
261425
| `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:4317` | OTLP gRPC endpoint (Jaeger/OpenTelemetry collector) |

pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@
148148
<artifactId>jsoup</artifactId>
149149
<version>1.17.2</version>
150150
</dependency>
151+
<dependency>
152+
<groupId>commons-codec</groupId>
153+
<artifactId>commons-codec</artifactId>
154+
</dependency>
151155

152156
<!-- Test -->
153157
<dependency>
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package com.jobtracker.config;
2+
3+
import org.springframework.beans.factory.annotation.Value;
4+
import org.springframework.stereotype.Component;
5+
6+
import java.util.List;
7+
8+
@Component
9+
public class GoogleDriveProperties {
10+
11+
private static final List<String> DEFAULT_SCOPES = List.of("https://www.googleapis.com/auth/drive");
12+
13+
private final String clientId;
14+
private final String clientSecret;
15+
private final String redirectUri;
16+
private final String oauthCompleteUrl;
17+
private final String authorizationUri;
18+
private final String tokenUri;
19+
private final List<String> scopes;
20+
21+
public GoogleDriveProperties(
22+
@Value("${app.google-drive.client-id:}") String clientId,
23+
@Value("${app.google-drive.client-secret:}") String clientSecret,
24+
@Value("${app.google-drive.redirect-uri:}") String redirectUri,
25+
@Value("${app.google-drive.oauth-complete-url:http://localhost:5173/settings/google-drive/callback}") String oauthCompleteUrl,
26+
@Value("${app.google-drive.authorization-uri:https://accounts.google.com/o/oauth2/v2/auth}") String authorizationUri,
27+
@Value("${app.google-drive.token-uri:https://oauth2.googleapis.com/token}") String tokenUri
28+
) {
29+
this.clientId = clientId;
30+
this.clientSecret = clientSecret;
31+
this.redirectUri = redirectUri;
32+
this.oauthCompleteUrl = oauthCompleteUrl;
33+
this.authorizationUri = authorizationUri;
34+
this.tokenUri = tokenUri;
35+
this.scopes = DEFAULT_SCOPES;
36+
}
37+
38+
public String getClientId() {
39+
return clientId;
40+
}
41+
42+
public String getClientSecret() {
43+
return clientSecret;
44+
}
45+
46+
public String getRedirectUri() {
47+
return redirectUri;
48+
}
49+
50+
public String getOauthCompleteUrl() {
51+
return oauthCompleteUrl;
52+
}
53+
54+
public String getAuthorizationUri() {
55+
return authorizationUri;
56+
}
57+
58+
public String getTokenUri() {
59+
return tokenUri;
60+
}
61+
62+
public List<String> getScopes() {
63+
return scopes;
64+
}
65+
66+
public String getScopeValue() {
67+
return String.join(" ", scopes);
68+
}
69+
70+
public boolean isConfigured() {
71+
return hasText(clientId) && hasText(clientSecret) && hasText(redirectUri);
72+
}
73+
74+
public void validateConfigured() {
75+
if (!isConfigured()) {
76+
throw new IllegalStateException("Google Drive integration is not configured on the server");
77+
}
78+
}
79+
80+
private boolean hasText(String value) {
81+
return value != null && !value.isBlank();
82+
}
83+
}

src/main/java/com/jobtracker/config/SecurityConfig.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
3939
.authorizeHttpRequests(auth -> auth
4040
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
4141
.requestMatchers("/api/v1/auth/**").permitAll()
42+
.requestMatchers(HttpMethod.GET, "/api/v1/google-drive/oauth/callback").permitAll()
4243
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
4344
// Actuator is served on a dedicated management port (8081) that is never
4445
// exposed to the host; security is enforced via Docker network isolation.

0 commit comments

Comments
 (0)