- Java 21
- MySQL 8+
- Windows: use
gradlew.bat
- Copy
backend/.env.example->backend/.env. - Configure DB + JWT + mail values.
- Run:
cd backend
.\gradlew.bat bootRunDefault local base URL: http://localhost:8080
- Backend port is configurable via:
SERVER_PORT(default8080)
- If port
8080is occupied, either:- Stop old process:
Get-NetTCPConnection -LocalPort 8080 -State Listen | Select-Object OwningProcess
Stop-Process -Id <PID> -Force- Or run backend on another port:
$env:SERVER_PORT=8081
.\gradlew.bat bootRunIf you change backend port, update frontend:
frontend/.env.local:NEXT_PUBLIC_API_BASE_URL=http://localhost:<your_port>NEXT_PUBLIC_STORAGE_BASE_URL=http://localhost:<your_port>
spring.mail.username<-MAIL_USERNAME(fallbackJOBHUNTER_MAIL_USERNAME, thenGMAIL_USERNAME)spring.mail.password<-MAIL_PASSWORD(fallbackJOBHUNTER_MAIL_PASSWORD, thenGMAIL_APP_PASSWORD)jobhunter.mail.from<-MAIL_FROM(fallbackspring.mail.username)
Do not hardcode app password in source code.
POST /api/v1/email/testPOST /api/v1/email/test-templatePOST /api/v1/email/subscribersPOST /api/v1/email/recommendations/weekly/triggerPOST /api/v1/email/logs/cleanup/trigger
- Login as admin/super admin and keep auth cookie.
- Call:
POST /api/v1/email/test-template- Header:
Content-Type: application/json - Body:
{
"recipient": "your_email@example.com",
"recipientName": "Nguoi nhan",
"subject": "Jobhunter - Kiem tra gui email HTML",
"title": "Thong bao kiem tra he thong email",
"message": "Neu ban nhan duoc email nay, cau hinh SMTP + Thymeleaf dang hoat dong dung.",
"actionText": "Mo Jobhunter",
"actionUrl": "http://localhost:3000"
}Success response returns recipient, sender, subject, templateName, sentAt.
- Global switch:
JOBHUNTER_SCHEDULER_ENABLED
- Mail cron switch:
JOBHUNTER_MAIL_CRON_ENABLED
- Cron expression:
JOBHUNTER_MAIL_CRON
- Timezone:
JOBHUNTER_MAIL_CRON_ZONE
- Recipient:
JOBHUNTER_MAIL_CRON_RECIPIENT
- Template:
JOBHUNTER_MAIL_CRON_TEMPLATE(defaultmail/scheduler-heartbeat)
Manual trigger endpoint:
POST /api/v1/email/scheduler/trigger
- Switch:
JOBHUNTER_WEEKLY_RECOMMENDATION_ENABLED
- Cron:
JOBHUNTER_WEEKLY_RECOMMENDATION_CRON(default0 0 8 * * MON)
- Timezone:
JOBHUNTER_WEEKLY_RECOMMENDATION_ZONE(defaultAsia/Ho_Chi_Minh)
- Recommendation limits:
JOBHUNTER_WEEKLY_RECOMMENDATION_MAX_JOBS(default8)JOBHUNTER_WEEKLY_RECOMMENDATION_RECENT_APPLY_DAYS(default180)JOBHUNTER_WEEKLY_RECOMMENDATION_FALLBACK_ENABLED(defaulttrue)
Data source used for recommendations:
- Candidate users (
role=USER) with opt-in flag enabled - Applied jobs history from
resumes - Subscriber skills by matching candidate email (
subscribers) - Open jobs (
active=true, valid by start/end date)
User opt-in setting:
- Persisted field:
users.weekly_job_recommendation_enabled - Authenticated APIs:
GET /api/v1/auth/preferences/emailPATCH /api/v1/auth/preferences/email- Request body:
{
"weeklyJobRecommendationEnabled": true
}Recommendation rules:
- Prefer jobs matching skills/title keywords from recent applications
- Mix in subscriber skills when available
- Exclude jobs already applied by user
- Skip inactive/expired jobs
- Fallback to recent open jobs when enabled
- Prevent duplicate send per week with
weekly_recommendation_dispatches
Manual weekly trigger endpoint:
POST /api/v1/email/recommendations/weekly/trigger- Requires authenticated account with permission
Trigger weekly recommendation email - Requires role
ADMINorSUPER_ADMIN - Runtime guard:
JOBHUNTER_WEEKLY_RECOMMENDATION_MANUAL_TRIGGER_ENABLED=trueto always allow, or- active profile in
JOBHUNTER_WEEKLY_RECOMMENDATION_MANUAL_TRIGGER_PROFILES(default:dev,local,docker,test)
Recommendation runtime tuning:
JOBHUNTER_WEEKLY_RECOMMENDATION_CANDIDATE_PAGE_SIZE(default200)JOBHUNTER_WEEKLY_RECOMMENDATION_MAX_CANDIDATES(default2000)- Dispatch duplicate check now preloads sent emails by
week_keyto avoid per-userexistsqueries.
- Switch:
JOBHUNTER_LOG_CLEANUP_ENABLED
- Cron:
JOBHUNTER_LOG_CLEANUP_CRON(default0 0 3 * * *)
- Timezone:
JOBHUNTER_LOG_CLEANUP_ZONE(defaultAsia/Ho_Chi_Minh)
- Retention:
JOBHUNTER_LOG_RETENTION_DAYS(default7)
- Safe cleanup scope:
JOBHUNTER_LOG_CLEANUP_PATHS(defaultlogs)JOBHUNTER_LOG_CLEANUP_PATTERNS(default*.log,*.log.*)JOBHUNTER_LOG_CLEANUP_MAX_SCAN_FILES(default10000)
- Manual cleanup trigger:
POST /api/v1/email/logs/cleanup/trigger- Requires role
ADMINorSUPER_ADMIN - Runtime guard:
JOBHUNTER_LOG_CLEANUP_MANUAL_TRIGGER_ENABLED=true, or- active profile in
JOBHUNTER_LOG_CLEANUP_MANUAL_TRIGGER_PROFILES(defaultdev,local,docker,test)
devprofile (application-dev.properties):- Root/info-friendly logs for local debugging.
- More detail for
EmailService.
testprofile (application-test.properties):- Reduced noise (
root=ERROR). gradlew testforcesspring.profiles.active=test.
- Reduced noise (
prodprofile (application-prod.properties):- Safe default levels (
root=WARN, appINFO). - Rolling file logs enabled via:
JOBHUNTER_LOG_FILEJOBHUNTER_LOG_ROLLING_PATTERNJOBHUNTER_LOG_MAX_FILE_SIZEJOBHUNTER_LOG_MAX_HISTORYJOBHUNTER_LOG_TOTAL_SIZE_CAPJOBHUNTER_LOG_CLEAN_HISTORY_ON_START
- Safe default levels (
stagingprofile (application-staging.properties):- Uses INFO-level application logs and detailed health for release-candidate checks.
- Enables tracing by default for the local OpenTelemetry Collector.
Recommended prod start:
$env:SPRING_PROFILES_ACTIVE="prod"
.\gradlew.bat bootRun- Enable/disable docs:
JOBHUNTER_SWAGGER_ENABLED=true|false
- Swagger UI path:
http://localhost:${SERVER_PORT}/swagger-ui.htmlhttp://localhost:${SERVER_PORT}/swagger-ui/index.html
- OpenAPI docs path:
http://localhost:${SERVER_PORT}/v3/api-docs
Swagger UI is public in local/dev. Protected business APIs still require JWT Bearer token.
- Login:
POST /api/v1/auth/login
- This API issues HttpOnly cookies (
access_token,refresh_token) on login. - For protected APIs in Postman:
- Use the same Postman session/cookie jar after login, or
- Manually set header
Authorization: Bearer <access_token>if you explicitly extract token value.
Do not put username/password in unrelated GET request body.
.\gradlew.bat test
.\gradlew.bat build -x testTest task already disables runtime seed and scheduler:
jobhunter.seed.enabled=falsejobhunter.scheduler.enabled=falsejobhunter.scheduler.mail.enabled=false
- Backend container reads env from runtime (
docker compose), no secret is hardcoded in image. - Docker profile is available in
application-docker.propertiesand should be activated in compose (SPRING_PROFILES_ACTIVE=dev,docker). - Docker datasource fallback uses compose service host
db(notlocalhost). - JWT secret for docker/runtime must be base64 of at least 64 bytes when using HS512 (
JWT_BASE64_SECRET). - CORS origins should include frontend browser origin (
CORS_ALLOWED_ORIGINS), default includeslocalhost:3000,localhost:3001,127.0.0.1:3000,127.0.0.1:3001. - Seed toggle for runtime:
JOBHUNTER_SEED_ENABLED=true(default in compose)
- Upload/storage path inside container should be:
UPLOAD_BASE_URI=file:///app/storage/
- Upload size limit:
UPLOAD_MAX_SIZE_BYTES(default5242880, ~5MB)- Spring multipart caps:
UPLOAD_MAX_FILE_SIZE,UPLOAD_MAX_REQUEST_SIZE
- Healthcheck endpoint:
GET /actuator/health
- Swagger URLs in docker:
http://localhost:8080/swagger-ui.htmlhttp://localhost:8080/v3/api-docs
The Docker operations stack adds Prometheus, Alertmanager, Loki, Grafana, and OpenTelemetry Collector:
npm run prod:localBackend tracing is controlled by:
MANAGEMENT_TRACING_ENABLEDMANAGEMENT_TRACING_SAMPLING_PROBABILITYOTEL_EXPORTER_OTLP_TRACES_ENDPOINTOTEL_RESOURCE_ATTRIBUTES
The backend also exposes Prometheus metrics at:
GET /actuator/prometheus