Frontend - React-JobApplyTracker
A production-ready Spring Boot REST API for tracking job applications, built with Java 21, Spring Security JWT authentication, MariaDB, and comprehensive test coverage. Exposes all domain services via a Model Context Protocol (MCP) server so AI assistants (Claude Desktop, Claude.ai, and any MCP-compatible client) can manage applications on behalf of authenticated users.
- Java 21
- Spring Boot 3.5 (Web, Data JPA, Security, Validation)
- Spring AI 1.0.0 — MCP server (
spring-ai-starter-mcp-server-webmvc, Streamable HTTP transport) - Spring OAuth2 Authorization Server (JDBC-backed, issues tokens for both GPT Actions and MCP clients)
- Spring Security with stateless JWT authentication + role-based authorization (
USER,BETA,ADMIN) - JWT + Refresh Tokens (access: 15 min, refresh: 7 days with rotation)
- Resilience4j Rate Limiting on auth endpoints
- MariaDB (production) / Testcontainers (tests)
- Flyway for DB migrations
- JUnit 5 + Mockito (unit tests)
- Testcontainers + MockMvc (integration tests)
- RestAssured (E2E tests)
- Maven
.
├── src/
│ ├── main/java/com/jobtracker/
│ │ ├── config/ # Security, JWT, CORS, OAuth2 AS, filters
│ │ ├── controller/ # REST controllers
│ │ ├── dto/ # Request/Response DTOs
│ │ ├── entity/ # JPA entities
│ │ ├── exception/ # Global exception handling
│ │ ├── mapper/ # Entity-DTO mappers
│ │ ├── mcp/ # MCP server — tools and prompts
│ │ │ ├── tools/ # @Tool-annotated service wrappers
│ │ │ ├── McpToolsConfig.java
│ │ │ └── McpPromptsConfig.java
│ │ ├── repository/ # Spring Data JPA repositories
│ │ ├── service/ # Business logic
│ │ └── util/ # Utilities
│ ├── main/resources/
│ │ ├── application.yml
│ │ └── db/migration/ # Flyway migrations
│ └── test/java/com/jobtracker/
│ ├── unit/ # Mockito unit tests
│ │ └── mcp/ # MCP tool unit tests
│ ├── integration/ # SpringBootTest + Testcontainers + MockMvc
│ │ └── mcp/ # MCP auth and tool integration tests
│ └── e2e/ # RestAssured end-to-end tests
├── pom.xml
├── Dockerfile
├── docker-compose.yml
└── .github/workflows/ci.yml
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/auth/register |
Register a new user |
| POST | /api/v1/auth/login |
Login and receive tokens |
| POST | /api/v1/auth/refresh |
Refresh access token |
| POST | /api/v1/auth/logout |
Logout and revoke refresh token |
| POST | /api/v1/auth/forgot-password |
Request password reset |
| POST | /api/v1/auth/reset-password |
Reset password with token |
| GET | /api/v1/auth/me |
Get current user info |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/applications |
Create application |
| GET | /api/v1/applications |
List all (paginated + filterable) |
| GET | /api/v1/applications/{id} |
Get by ID |
| PUT | /api/v1/applications/{id} |
Full update |
| PATCH | /api/v1/applications/{id}/status |
Update status |
| PATCH | /api/v1/applications/{id}/reminder |
Toggle reminder |
| DELETE | /api/v1/applications/{id} |
Delete |
| GET | /api/v1/applications/upcoming |
Upcoming next steps |
| GET | /api/v1/applications/overdue |
Overdue next steps |
- JWT access tokens now include a
rolesclaim (e.g.,ROLE_USER,ROLE_ADMIN). - Existing users are backfilled with
ROLE_USERduring migration. - A default
ROLE_USERis assigned on registration.
Flyway seeds the roles catalog (USER, BETA, ADMIN) and then assigns ROLE_USER to all existing users.
GET /api/v1/auth/mePUT /api/v1/auth/mePUT /api/v1/auth/me/passwordPOST|GET|PUT|PATCH|DELETE /api/v1/applications/**GET|POST /api/v1/gamification/**GET /api/v1/dashboard/summaryPOST /api/v1/account/test-email
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/gamification/profile |
Get current XP, level, rank title and streak snapshot |
| GET | /api/v1/gamification/achievements |
List achievement catalog with unlocked state |
| POST | /api/v1/gamification/events |
Apply a tracked XP event and return updated profile |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/google-drive/oauth/start |
Generate the Google OAuth authorization URL for the authenticated user |
| GET | /api/v1/google-drive/oauth/callback |
Google OAuth callback endpoint used by Google Cloud |
| GET | /api/v1/google-drive/status |
Return current Google Drive connection status, configured root folder, and base resumes |
| DELETE | /api/v1/google-drive/connection |
Disconnect the current user's Google account and remove stored Drive preferences |
| PUT | /api/v1/google-drive/root-folder |
Validate and save the user's base Drive folder |
| POST | /api/v1/google-drive/base-resumes |
Register a Google Docs base resume by Google Docs URL or file ID |
| DELETE | /api/v1/google-drive/base-resumes/{baseResumeId} |
Remove a configured base resume |
| POST | /api/v1/google-drive/applications/{applicationId}/resume-copies |
Copy a configured base resume into the application's Drive subfolder |
RHFiz a RH - Aguardando AtualizaçãoFiz a Hiring Manager - Aguardando AtualizaçãoTeste TécnicoFiz teste Técnico - aguardando atualizaçãoRH (Negociação)
The backend tracks gamification in user_gamification, achievements, and user_achievements. XP is awarded from application lifecycle events and stored per user, while each application keeps one-time award flags so the same action is not counted twice. The service also derives the current streak and unlocks achievements from the user's non-archived applications.
| Action | Backend event | XP |
|---|---|---|
| New application | APPLICATION_CREATED |
+10 |
| Recruiter DM sent | RECRUITER_DM_SENT |
+15 |
| Interview progress | INTERVIEW_PROGRESS |
+50 |
| Note added | NOTE_ADDED |
+5 |
| Offer / win | OFFER_WON |
+500 |
level = floor(sqrt(totalXp / 100)) + 1XP required for level N = 100 * (N - 1)^2
Examples:
| Level | Total XP required |
|---|---|
| 1 | 0 |
| 2 | 100 |
| 3 | 400 |
| 4 | 900 |
| 5 | 1600 |
| Milestone level | XP threshold | Rank title |
|---|---|---|
| 1 | 0 | Desempregado de Aluguel |
| 6 | 2500 | Job Hunter Iniciante |
| 16 | 22500 | Sobrevivente do LinkedIn |
| 31 | 90000 | Mestre das Soft Skills |
| 51 | 250000 | Lenda das Contratacoes |
| Code | Name | Unlock condition in the backend today |
|---|---|---|
EARLY_BIRD |
Early Bird | Have 5 non-archived applications with applicationDate set and createdAt before 09:00 |
NETWORKING_PRO |
Networking Pro | Have 10 recruiter DMs sent inside any rolling 7-day window |
PERSISTENT |
Persistent | Reach a 5-day longest streak based on distinct applicationDate values |
GHOSTBUSTER |
Ghostbuster | Have any non-archived application currently in GHOSTING status |
The backend does not currently have literal INTERVIEW or HIRED statuses.
INTERVIEW_PROGRESSis awarded wheninterviewScheduled = trueor when the application enters one of these statuses:Fiz a RH - Aguardando Atualização,Fiz a Hiring Manager - Aguardando Atualização,Teste Técnico,Fiz teste Técnico - aguardando atualização, orRH (Negociação).OFFER_WONis currently mapped toRH (Negociação)(RH_NEGOCIACAOin code), which is the backend's current closing-stage proxy until dedicated offer/hired statuses exist.GHOSTBUSTERcurrently unlocks fromGHOSTINGstatus itself; although the seeded achievement description mentions "30 days", the implemented unlock rule is status-based today.
Development (build locally):
docker-compose up -dProduction (use pre-built image from GitHub Container Registry):
- Log in to GHCR (requires a Personal Access Token with
read:packages):
# POSIX / macOS / WSL
echo $CR_PAT | docker login ghcr.io -u vitorhugo-java --password-stdin
# Windows PowerShell
# $env:CR_PAT | docker login ghcr.io -u vitorhugo-java --password-stdin- Pull the production compose file image(s):
docker compose -f docker-compose.prod.yml pull- Start the services from the production compose file:
docker compose -f docker-compose.prod.yml up -dBy default this compose file pulls image: ghcr.io/vitorhugo-java/springboot-jobapplytracker:latest. Change the image name in docker-compose.prod.yml if you publish with a different tag or repository.
The API will be available at http://localhost:8080.
export DB_URL=jdbc:mariadb://localhost:3306/jobtracker?createDatabaseIfNotExist=true
export DB_USERNAME=jobtracker
export DB_PASSWORD=jobtracker
export JWT_SECRET=your-secret-key-at-least-256-bits-long
export GOOGLE_DRIVE_CLIENT_ID=your-google-client-id
export GOOGLE_DRIVE_CLIENT_SECRET=your-google-client-secret
export GOOGLE_DRIVE_REDIRECT_URI=http://localhost:8080/api/v1/google-drive/oauth/callback
export GOOGLE_DRIVE_OAUTH_COMPLETE_URL=http://localhost:5173/settings/google-drive/callback
export OPENAI_GPT_CLIENT_ID=your-openai-gpt-client-id
export OPENAI_GPT_CLIENT_SECRET=your-openai-gpt-client-secret
export OPENAI_GPT_REDIRECT_URIS=https://chat.openai.com/aip/default/callback
export OPENAI_GPT_SCOPES=read:profile,read:applications,write:applications,read:resume,read:google-drive,read:metrics
# MCP client (optional — omit to disable MCP OAuth2 registration)
export MCP_CLIENT_ID=your-mcp-client-id
export MCP_REDIRECT_URIS=http://localhost:3000/mcp/callback
mvn spring-boot:runThis 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.
- Create a Google Cloud OAuth client for a web application.
- Enable the Google Drive API.
- Add the backend callback URL as an authorized redirect URI. Example local value:
http://localhost:8080/api/v1/google-drive/oauth/callback
- Configure these environment variables:
| Variable | Required | Description |
|---|---|---|
GOOGLE_DRIVE_CLIENT_ID |
Yes | Google OAuth client ID |
GOOGLE_DRIVE_CLIENT_SECRET |
Yes | Google OAuth client secret |
GOOGLE_DRIVE_REDIRECT_URI |
Yes | Backend callback URL registered in Google Cloud |
GOOGLE_DRIVE_OAUTH_COMPLETE_URL |
Yes | Frontend page that receives the final status and message query params after OAuth finishes |
GOOGLE_DRIVE_AUTHORIZATION_URI |
No | Override Google authorization endpoint |
GOOGLE_DRIVE_TOKEN_URI |
No | Override Google token endpoint |
- Frontend calls
POST /api/v1/google-drive/oauth/startwith the user's JWT bearer token. - Backend creates a short-lived OAuth state tied to that authenticated user and returns:
authorizationUrlstateredirectUriscopes
- Frontend opens
authorizationUrlin a new tab or popup. - Google redirects back to
GET /api/v1/google-drive/oauth/callback. - Backend exchanges the authorization code for user-scoped Drive credentials, stores them, and redirects the browser to
GOOGLE_DRIVE_OAUTH_COMPLETE_URLwith:status=success|errormessage=<url-encoded message>
https://www.googleapis.com/auth/drive
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.
- Base resumes must be Google Docs (
application/vnd.google-apps.document). - The root folder must be a Google Drive folder.
- The frontend Gemini button that opens
https://gemini.google.com/gem/f8ed7c14b062is frontend-only and does not require a backend endpoint.
When the frontend later calls POST /api/v1/google-drive/applications/{applicationId}/resume-copies:
- Backend verifies the current user owns the application.
- Backend refreshes the user's Google access token if needed.
- Backend verifies the configured root folder still exists and is a folder.
- Backend finds or creates a vacancy subfolder under that root folder using the application identity.
- Backend copies the selected base Google Doc into that subfolder.
- Backend renames the copy with an
APP-<application-uuid>prefix plus vacancy context. - Backend returns a Google Docs web URL for the copied file.
POST /api/v1/google-drive/oauth/start
{}Response:
{
"authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth?...",
"state": "generated-state",
"redirectUri": "http://localhost:8080/api/v1/google-drive/oauth/callback",
"scopes": [
"https://www.googleapis.com/auth/drive"
]
}The backend exposes all core domain services as MCP tools via a Streamable HTTP endpoint at POST /mcp/messages. Any MCP-compatible AI client (Claude Desktop, Claude.ai, mcp-cli, etc.) can call these tools on behalf of an authenticated user.
MCP requests use the same OAuth2 Authorization Server already used by GPT Actions. There are no static tokens, no synthetic service accounts, and no parallel auth system. The flow is:
- MCP client completes OAuth2 Authorization Code + PKCE against the existing AS.
- The AS issues a JWT carrying the user's
id,roles(ROLE_USER,ROLE_BETA, etc.), and scopes. - MCP client presents the JWT as
Authorization: Bearer <token>on everyPOST /mcp/messagesrequest. - The existing
BearerTokenAuthenticationFiltervalidates the JWT, populates theSecurityContextwith the real user, and all domain service ownership checks (SecurityUtils.getCurrentUser()) and role guards (@PreAuthorize) work exactly as they do for REST requests.
| Variable | Required | Description |
|---|---|---|
MCP_CLIENT_ID |
Yes | OAuth2 client ID for your MCP client registration |
MCP_CLIENT_SECRET |
No | If set, enables CLIENT_SECRET_BASIC / CLIENT_SECRET_POST auth methods (omit for public PKCE-only clients) |
MCP_REDIRECT_URIS |
Yes | Comma-separated list of allowed redirect URIs for your MCP client |
If
MCP_CLIENT_IDis not set, the MCP OAuth2 client is not registered and MCP authentication will fail. The MCP server endpoint itself always starts.
| Scope | Grants access to |
|---|---|
openid |
OIDC identity |
read:profile |
User profile |
read:applications |
List / read applications |
write:applications |
Create / update / delete applications |
read:resume |
Base resume metadata |
read:google-drive |
Google Drive status (BETA role also required) |
read:metrics |
Dashboard and gamification data |
POST /mcp/messages
Authorization: Bearer <access_token>
Content-Type: application/json
The endpoint implements JSON-RPC 2.0 over HTTP (Streamable HTTP transport). All MCP protocol messages (initialize, tools/list, tools/call, prompts/list, prompts/get) are sent as POST requests to this single URL.
| Tool | Description |
|---|---|
listApplications |
List paginated applications with optional filters (status, recruiter, date range, archived, etc.) |
getApplication |
Get a single application by UUID |
getUpcomingApplications |
Applications with a next-step date/time in the near future |
getOverdueApplications |
Applications whose next-step date/time has passed |
createApplication |
Create a new application |
updateApplication |
Full update of an existing application |
updateApplicationStatus |
Change status only |
updateApplicationReminder |
Enable or disable the recruiter DM reminder |
markRecruiterDmSent |
Record that a recruiter DM was sent |
archiveApplication |
Archive an application |
deleteApplication |
Permanently delete an application |
| Tool | Description |
|---|---|
getPipelineSummary |
Aggregate pipeline statistics |
getGamificationProfile |
Current XP, level, rank, and streak |
getAchievements |
Achievement catalog with unlocked state |
| Tool | Description |
|---|---|
getDriveStatus |
Google Drive connection status and configured base resumes |
listBaseResumes |
List available base resume templates |
copyResumeToApplication |
Copy a base resume into the application's Drive folder |
Calling any Google Drive tool without
ROLE_BETAreturns 403 — the same restriction enforced on the REST controller.
Prompts are pre-built guided workflows. Call prompts/get with the prompt name and arguments to receive an instruction message that tells the AI which tools to call and in what order.
| Prompt | Required args | Description |
|---|---|---|
Prepare-New-Application |
vacancyName, recruiterName, organization (all optional) |
Guides the AI to gather any missing fields and call createApplication |
Tailor-Resume |
applicationId |
Fetches the application, lists base resumes, asks which to use, calls copyResumeToApplication, and returns the Google Docs link |
Summarize-Pipeline |
(none) | Calls getPipelineSummary, listApplications (recent 10), getOverdueApplications, and getGamificationProfile to produce a full pipeline report |
# 1. Get an access token (exchange auth code from OAuth2 flow — or use a test user's JWT from /api/v1/auth/login)
TOKEN="<your-access-token>"
# 2. List available tools
curl -X POST http://localhost:8080/mcp/messages \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
# 3. Call a tool
curl -X POST http://localhost:8080/mcp/messages \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc":"2.0","id":2,
"method":"tools/call",
"params":{
"name":"listApplications",
"arguments":{"page":0,"size":5}
}
}'
# 4. Get a prompt
curl -X POST http://localhost:8080/mcp/messages \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc":"2.0","id":3,
"method":"prompts/get",
"params":{
"name":"Summarize-Pipeline",
"arguments":{}
}
}'Add the following to your Claude Desktop claude_desktop_config.json:
{
"mcpServers": {
"job-tracker": {
"command": "mcp-remote",
"args": [
"http://localhost:8080/mcp/messages",
"--header",
"Authorization: Bearer <your-access-token>"
]
}
}
}Replace
<your-access-token>with a valid JWT obtained from the OAuth2 Authorization Server or fromPOST /api/v1/auth/login. For production deployments, replacehttp://localhost:8080with your API base URL and use tokens from the full OAuth2 PKCE flow.
- Authorization:
GET/POST /oauth2/authorize - Token:
POST /oauth2/token - JWKS:
GET /oauth2/jwks - Discovery:
GET /.well-known/openid-configuration
This backend now exposes a dedicated OAuth 2.0 Authorization Code + PKCE flow for GPT Actions without changing the existing JWT login flow for human users. GPT-issued access tokens are scoped, bearer-only, and isolated to /api/v1/gpt/**.
| Variable | Required | Description |
|---|---|---|
OPENAI_GPT_CLIENT_ID |
Yes | OAuth client ID configured for the GPT Action |
OPENAI_GPT_CLIENT_SECRET |
Yes | OAuth client secret configured for the GPT Action |
OPENAI_GPT_REDIRECT_URIS |
Yes | Comma-separated list of allowed GPT Action redirect URIs |
OPENAI_GPT_SCOPES |
No | Comma-separated allowed GPT scopes; defaults to the built-in GPT scopes |
read:profileread:applicationswrite:applicationsread:resumeread:google-driveread:metrics
- Authorization endpoint:
GET/POST /oauth2/authorize - Token endpoint:
POST /oauth2/token
- Create or update your GPT Action OAuth client with the backend base URL.
- Register the same callback URL in
OPENAI_GPT_REDIRECT_URIS. - Configure the client ID and client secret with
OPENAI_GPT_CLIENT_IDandOPENAI_GPT_CLIENT_SECRET. - Set the action scopes to the minimum required set from
OPENAI_GPT_SCOPES. - In the GPT Action OAuth settings, use:
- Authorization URL:
https://<your-api-host>/oauth2/authorize - Token URL:
https://<your-api-host>/oauth2/token
- Authorization URL:
- After OAuth succeeds, call the GPT-friendly endpoints under
/api/v1/gpt/**.
GET /api/v1/gpt/profileGET /api/v1/gpt/applicationsGET /api/v1/gpt/applications/{id}POST /api/v1/gpt/applicationsPATCH /api/v1/gpt/applications/{id}/statusGET /api/v1/gpt/resumes/baseGET /api/v1/gpt/resumes/base/{resumeId}/contentGET /api/v1/gpt/resumes/generated/{applicationId}/contentGET /api/v1/gpt/google-drive/statusGET /api/v1/gpt/metrics/summary
Google Drive and resume GPT endpoints still enforce the user's existing BETA role in addition to the new OAuth scopes, so the GPT flow does not bypass the repository's current authorization rules.
GET /api/v1/google-drive/status
{
"configured": true,
"connected": true,
"googleEmail": "user@gmail.com",
"googleDisplayName": "User Name",
"googleAccountId": "permission-id",
"rootFolderId": "drive-folder-id",
"rootFolderName": "Job Tracker Root",
"connectedAt": "2026-05-05T12:00:00",
"baseResumes": [
{
"id": "uuid",
"googleFileId": "google-doc-id",
"documentName": "Resume Base",
"webViewLink": "https://docs.google.com/document/d/google-doc-id/edit",
"createdAt": "2026-05-05T12:05:00"
}
]
}PUT /api/v1/google-drive/root-folder
{
"folderIdOrUrl": "https://drive.google.com/drive/folders/drive-folder-id"
}POST /api/v1/google-drive/base-resumes
{
"documentIdOrUrl": "https://docs.google.com/document/d/google-doc-id/edit"
}POST /api/v1/google-drive/applications/{applicationId}/resume-copies
{
"baseResumeId": "base-resume-uuid"
}Response:
{
"applicationId": "application-uuid",
"baseResumeId": "base-resume-uuid",
"copiedFileId": "copied-google-doc-id",
"copiedFileName": "APP-application-uuid - Backend Engineer - Resume Base",
"documentWebViewLink": "https://docs.google.com/document/d/copied-google-doc-id/edit",
"vacancyFolderId": "vacancy-folder-id",
"vacancyFolderName": "Backend Engineer - APP-application-uuid",
"vacancyFolderWebViewLink": "https://drive.google.com/drive/folders/vacancy-folder-id"
}# All tests
mvn verify
# Unit tests only
mvn test -Dtest="com.jobtracker.unit.*"
# Integration tests only
mvn test -Dtest="com.jobtracker.integration.*"
# E2E tests only
mvn test -Dtest="com.jobtracker.e2e.*"Note: Integration and E2E tests require Docker to be running (Testcontainers pulls a MariaDB image automatically).
This project includes a startup seeder that can generate fake job applications using the Java library net.datafaker:datafaker.
The seeder is disabled by default and only runs when explicitly enabled.
Required parameters:
APP_SEED_ENABLED=trueAPP_SEED_USER_EMAIL=<existing user email>
Optional:
APP_SEED_COUNT=1000(default is1000)
Example with Maven:
export APP_SEED_ENABLED=true
export APP_SEED_USER_EMAIL=user@example.com
export APP_SEED_COUNT=1000
mvn spring-boot:runExample with java -jar:
APP_SEED_ENABLED=true APP_SEED_USER_EMAIL=user@example.com APP_SEED_COUNT=1500 java -jar target/job-tracker-1.0.0.jarIf APP_SEED_ENABLED=true and APP_SEED_USER_EMAIL is not provided (or the user does not exist), the application startup fails with a clear error.
| Variable | Default | Description |
|---|---|---|
DB_URL |
jdbc:mariadb://localhost:3306/jobtracker |
JDBC URL |
DB_USERNAME |
jobtracker |
DB username |
DB_PASSWORD |
jobtracker |
DB password |
JWT_SECRET |
(dev default) | JWT signing secret (min 256 bits) |
JWT_ACCESS_TOKEN_EXPIRATION_MS |
900000 |
Access token TTL (15 min) |
JWT_REFRESH_TOKEN_EXPIRATION_MS |
604800000 |
Refresh token TTL (7 days) |
CORS_ALLOWED_ORIGINS |
http://localhost:3000,http://localhost:5173 |
Allowed CORS origins |
GOOGLE_DRIVE_CLIENT_ID |
(empty) | Google OAuth client ID for Drive integration |
GOOGLE_DRIVE_CLIENT_SECRET |
(empty) | Google OAuth client secret for Drive integration |
GOOGLE_DRIVE_REDIRECT_URI |
http://localhost:8080/api/v1/google-drive/oauth/callback |
OAuth callback URL registered in Google Cloud |
GOOGLE_DRIVE_OAUTH_COMPLETE_URL |
(empty) | Frontend URL that receives OAuth completion redirects |
OPENAI_GPT_CLIENT_ID |
(empty) | OAuth client ID for GPT Actions |
OPENAI_GPT_CLIENT_SECRET |
(empty) | OAuth client secret for GPT Actions |
OPENAI_GPT_REDIRECT_URIS |
(empty) | Comma-separated GPT Action redirect URIs |
OPENAI_GPT_SCOPES |
read:profile,read:applications,write:applications,read:resume,read:google-drive,read:metrics |
Allowed GPT Action scopes |
GPT_FALLBACK_AUTH_ENABLED |
false |
Enables the temporary static bearer fallback |
APP_GPT_FALLBACK_AUTH_ENABLED |
No | Enables the GPT fallback auth filter |
APP_GPT_FALLBACK_AUTH_TOKEN |
No | Static bearer token accepted by the fallback flow |
APP_GPT_FALLBACK_AUTH_ACCOUNT_EMAIL |
No | Email of the account used by the fallback flow |
APP_GPT_FALLBACK_AUTH_ACCOUNT_NAME |
No | Display name used when the fallback user is created |
GPT_FALLBACK_AUTH_TOKEN |
(empty) | Static bearer token used when fallback auth is enabled |
MCP_CLIENT_ID |
(empty) | OAuth2 client ID for MCP clients; if not set, no MCP client is registered |
MCP_CLIENT_SECRET |
(empty) | OAuth2 client secret for MCP clients (omit for public PKCE-only clients) |
MCP_REDIRECT_URIS |
(empty) | Comma-separated redirect URIs allowed for the MCP OAuth2 client |
RATE_LIMIT_AUTH_LOGIN_LIMIT_FOR_PERIOD |
10 |
Max login requests allowed per refresh period |
RATE_LIMIT_AUTH_LOGIN_REFRESH_PERIOD |
1m |
Window used by the login rate limiter |
OTEL_EXPORTER_OTLP_ENDPOINT |
http://localhost:4317 |
OTLP gRPC endpoint (Jaeger/OpenTelemetry collector) |
PROMETHEUS_URL |
http://localhost:9090 |
Prometheus base URL for observability integrations |
SERVER_PORT |
8080 |
Server port |
Spring Boot Actuator runs on a dedicated management port 8081, completely separate from the main API port (8080). This port is never exposed to the host machine in Docker Compose — it is only reachable within the internal infra_network Docker network.
Prometheus scrapes metrics directly from the container over the internal network:
scrape_configs:
- job_name: job-tracker
static_configs:
- targets: ['app:8081']
metrics_path: /actuator/prometheus
scheme: http
scrape_interval: 15sNo authentication token is required — network-level isolation (Docker bridge network) is the security boundary. The Actuator is unreachable from outside the Docker network.
Auth endpoints are protected with Resilience4j rate limiters. When a limit is exceeded, the API returns 429 Too Many Requests with the standard error payload used by the application.
GitHub Actions workflow (.github/workflows/ci.yml) triggers on push/PR to main:
- Checkout
- Setup Java 21
- Build project
- Run unit tests
- Run integration tests (Testcontainers)
- Run E2E tests (Testcontainers + RestAssured)
- Full
mvn verify
Swagger UI is available at http://localhost:8080/swagger-ui.html when the app is running.