REPO - Fix justinplayer's external_id to a stable UUID for local dev See merge request guitartech/guitartech-auth-service!27 |
||
|---|---|---|
| deploy | ||
| gradle/wrapper | ||
| src | ||
| .env.example | ||
| .gitattributes | ||
| .gitignore | ||
| .gitlab-ci.yml | ||
| build.gradle | ||
| CLAUDE.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| gradlew | ||
| gradlew.bat | ||
| guitartech-auth-service.code-workspace | ||
| README.md | ||
| settings.gradle | ||
guitartech-auth-service
A Spring Boot 4.1.0 REST API for user authentication and management. Implements JWT-based authentication with email-verified user registration, login, profile management, and role/entitlement-based authorization.
Quick Start
Prerequisites
- JDK 21+
- PostgreSQL 16 (running locally or via Docker)
- Gradle (included via wrapper:
./gradlew)
Local Development
-
Set up environment variables:
source .env.localThis loads
JWT_SECRETandDB_PASSWORDfor local development. -
Start PostgreSQL:
docker run -d \ --name guitartech-postgres \ -e POSTGRES_DB=guitartech_auth_service \ -e POSTGRES_PASSWORD=postgres \ -p 5432:5432 \ postgres:16-alpine -
Run the application: Using the Spring Boot Dashboard extension in VS Code:
- Source
.env.localin your terminal:source .env.local - Click the run button in the Dashboard
Or via CLI:
source .env.local ./gradlew bootRun - Source
-
Verify it's running: Check the console for
Started GuitartechAuthServiceApplication, then confirm the security filter chain responds (401 without a token is expected here):curl -i http://localhost:8080/roles
The application will automatically apply Liquibase database migrations on startup.
Dev Profile & Seed Data
For local development, run with the dev Spring profile instead of the default:
source .env.local
./gradlew bootRunDev
Entitlements and roles (ADMIN/PLAYER) are seeded in every environment, every startup, by PermissionsSeeder (config/PermissionsSeeder.java), which reads src/main/resources/permissions.yaml and upserts additively — it fills in whatever's missing but never overwrites a role's name/description or removes an entitlement association, so live edits made through the /roles API are never clobbered on restart.
This Gradle task (build.gradle) runs the app with spring.profiles.active=dev, which additionally activates DevEnvironmentInitializer (config/DevEnvironmentInitializer.java) — a CommandLineRunner that looks up the already-seeded ADMIN/PLAYER roles and seeds two ready-to-use demo accounts (safe to restart repeatedly without duplicating data), both with password Megadude#13:
justinadmin(bill@microsoft.com) —ADMINrolejustinplayer(sergey@google.com) —PLAYERrole
Adding a new protected endpoint? Add its entitlement to permissions.yaml (and to the relevant role's entitlements list if it should be granted) — PermissionsSeeder picks it up on the next startup in every environment, no migration needed.
API Endpoints
Registration is a two-step, email-verified flow: POST /users creates an unregistered user and emails a 6-character verification code, then POST /tokens/verifications exchanges that code for a JWT and completes registration. Returning users authenticate with POST /tokens.
Public Endpoints
-
POST /users — Start registration (sends a verification code by email)
{ "username": "john_doe", "password": "SecurePass123!", "email": "john@example.com" } -
POST /tokens/verifications — Complete registration with the emailed code, returns a JWT
{ "verification_code": "AB12CD" } -
POST /tokens — Log in an existing user, returns a JWT
{ "username": "john_doe", "password": "SecurePass123!" }
Protected Endpoints (Require JWT Bearer Token)
All endpoints below require Authorization: Bearer <jwt-token> and are additionally gated by entitlement (e.g. guitartech-auth-service.role.read) via @PreAuthorize.
- PUT /users/me/username — Update your own username
- PUT /users/me/password — Update your own password
- GET /roles — List all roles (HAL/hypermedia) — requires
role.read - GET /roles/{id} — Get a single role — requires
role.read - POST /roles — Create a role — requires
role.create{ "name": "BETA_TESTER", "description": "GuitarTech Beta Tester", "entitlement_ids": [2, 3, 4] } - PUT /roles/{id} — Update a role's name/description — requires
role.update - DELETE /roles/{id} — Delete a role (blocked if any user still has it assigned) — requires
role.delete - PUT /roles/{roleId}/entitlements/{entitlementId} — Add an entitlement to a role (idempotent) — requires
role.update - DELETE /roles/{roleId}/entitlements/{entitlementId} — Remove an entitlement from a role (idempotent) — requires
role.update - PUT /users/{externalId}/roles/{roleId} — Assign a role to a user (idempotent), returns the user's updated roles — requires
role.update - DELETE /users/{externalId}/roles/{roleId} — Remove a role from a user (idempotent, 204) — requires
role.update - GET /users — List all users (HAL/hypermedia, admin view incl. email and timestamps) — requires
user.read - GET /users/{externalId} — Get a single user — requires
user.read
Role names must be uppercase letters/underscores only (e.g. ADMIN, BETA_TESTER). Entitlement names follow a <service>.<resource>.<action> convention (e.g. guitartech-auth-service.role.create).
Building & Testing
./gradlew clean build # Full clean build
./gradlew build -x test # Build without tests
./gradlew test # Run all tests
./gradlew test --tests SomeTest # Run a specific test
Tests use Testcontainers with a real PostgreSQL database (containerized), not an in-memory DB.
macOS Note: For best performance with Testcontainers, use Orbstack instead of Docker Desktop. Orbstack is lightweight, faster, and handles container networking better on macOS.
Environment Variables
Create a .env.local file (git-ignored) with:
export JWT_SECRET="your-secret-min-64-chars"
export DB_PASSWORD="postgres"
Or set them directly:
JWT_SECRET=my-secret DB_PASSWORD=postgres ./gradlew bootRun
For Deployed Environments
The app reads these environment variables at runtime:
JWT_SECRET— JWT signing secret (required in production)SPRING_DATASOURCE_URL— Database URL (defaults to localhost)SPRING_DATASOURCE_USERNAME— Database userSPRING_DATASOURCE_PASSWORD— Database passwordSMTP_HOST/SMTP_ADDRESS/SMTP_TOKEN— SMTP host, from-address, and credential used to email registration verification codes (seeEmailServiceImpl)
Database
Local Setup
The default application.properties connects to:
jdbc:postgresql://localhost:5432/guitartech_auth_service
username: postgres
password: postgres
Start PostgreSQL with the Docker command above, and migrations run automatically on app startup.
Resetting the Database Locally
# Connect to PostgreSQL
psql -U postgres -d guitartech_auth_service
# Drop tables to reset
DROP TABLE auth_user CASCADE;
DROP TABLE database_changelog CASCADE;
DROP TABLE database_changelog_lock CASCADE;
Then restart the app — migrations will re-apply.
Security
This project uses GitLab's free SAST (Static Application Security Testing) to automatically scan for vulnerabilities:
- Semgrep — Scans Java source code for security issues and code quality problems
- SpotBugs — Detects common Java bugs
- Runs on all merge requests and branches before code can be merged
- Results appear in the Merge Request pipeline UI
To view SAST reports:
- Go to your merge request in GitLab
- Click the Checks or Pipeline tab
- Look for SAST job details and any detected issues
Deployment
This service deploys to production only via GitLab CI/CD:
- Production (guitartech.app) — Manual deployment to VPS (requires manual trigger in GitLab pipeline)
The pipeline automatically runs:
- Build — Compiles code
- Test — Runs tests + SAST security scanning
- Containerize — Builds Docker image
- Publish — Pushes image to registry (main branch only)
- Deploy — Manual production deployment (main branch only)
Local Development: Run locally with source .env.local && ./gradlew bootRun
Production Setup: Runs in Docker Compose on a 2GB VPS with Postgres. See docker-compose.yml for configuration.
Project Structure
src/main/java/tech/guitar/auth/
├── GuitartechAuthServiceApplication.java # Entry point
├── config/ # SecurityConfig, DevEnvironmentInitializer
├── controller/ # REST endpoints (Role, Token, User)
├── facade/ # RoleFacade — orchestrates across services
├── service/ # Business logic + UserFacade/TokenFacade
├── repository/ # Data access (JPA)
├── entity/ # JPA entities
├── domain/ # Domain models (Role, User, Entitlement, ...)
├── dto/ # Request/response DTOs
├── filter/ # JWT validation filter
├── mapper/ # Entity <-> domain converters
└── exception/ # Custom exceptions + global handler
src/main/resources/
├── application.properties # Configuration
└── db/changelog/ # Liquibase migrations
src/test/
├── AbstractIntegrationTest.java # Base test class (Testcontainers)
├── repository/ # Repository tests
├── service/ # Service tests
├── facade/ # Facade tests
├── controller/ # Controller integration tests
└── testutil/ # Test utilities
Key Features
- JWT Authentication — HS512 algorithm, 24-hour expiration
- Email-Verified Registration — registration completes only after the emailed verification code is submitted
- Role & Entitlement Authorization —
@PreAuthorize-gated endpoints backed by roles and fine-grained entitlements - Password Security — BCrypt hashing (strength factor 13)
- Input Validation — DTO-level with Jakarta validation annotations
- Database Migrations — Liquibase with UTC timestamps
- Global Exception Handling — Consistent error responses, including RFC 7807
ProblemDetailresponses for role-related conflicts - Comprehensive Tests — Repository, service, facade, and controller layers, all against a real DB via Testcontainers
Development Workflow
- Create a feature branch off
main - Write tests first (repository, service, controller layers)
- Implement the feature
- Run all tests locally:
./gradlew test - Open a merge request
- GitLab CI automatically runs:
- Build: Compiles code
- Test: Runs unit/integration tests + SAST security scanning
- Containerize: Builds Docker image
- All must pass before merge
- Merge to
mainonce approved - Publish: Image is pushed to GitLab Container Registry
- Manually trigger production deployment from the GitLab pipeline UI (only available for
mainbranch)
Test Coverage
The project enforces 80% minimum code coverage on the main branch:
- On main branch: Pipeline fails if coverage drops below 80%
- On merge requests: Coverage below 80% is flagged as a warning (doesn't block merge)
- View locally:
./gradlew jacocoTestReportto generate coverage report
This ensures code quality stays consistent while allowing flexibility during feature development on branches.
Useful Commands
# Format and check code
./gradlew spotlessApply # Auto-format code
# View test coverage
./gradlew jacocoTestReport
# Clean up everything
./gradlew clean
# Run integration tests only
./gradlew test -i integration
Troubleshooting
"Could not connect to PostgreSQL"
- Ensure PostgreSQL is running:
docker ps | grep postgres - Check credentials match
.env.localorapplication.properties
"JWT_SECRET is not set"
- Run
source .env.localbefore starting the app - Or set:
export JWT_SECRET=<value>
"Liquibase migration failed"
- Check database permissions and connectivity
- Review migration files in
src/main/resources/db/changelog/migrations/
Support
For issues or questions, refer to CLAUDE.md for detailed architecture and testing guidelines.