Demo REST API showcasing the Javloom library ecosystem — JWT authentication, passwordless SMS login, fine-grained permissions, and User CRUD.
| Layer | Technology |
|---|---|
| Runtime | Java 21 + Spring Boot 3.4.1 |
| Security | Spring Security + spring-security-jwt-lib |
| Persistence | PostgreSQL 16 + Spring Data JPA + Flyway |
| Cache / OTP | Redis 7 |
| Validation | spring-rest-commons validators |
| Build | Maven 3.9+ |
- Docker Desktop 3.x+
- Java 21+
- Maven 3.9+
docker compose up -dThis starts:
- PostgreSQL on
localhost:5432 - Redis on
localhost:6379
./mvnw spring-boot:runThe API starts on http://localhost:8080 by default. 🌐
Main config file: src/main/resources/application.yaml
Default local settings include:
- PostgreSQL:
localhost:5432(javloom_demo) - Redis:
localhost:6379 - App port:
8080
On startup, the app checks whether an admin user exists by email and creates one if missing.
Config keys:
javloom:
admin:
enabled: true
email: admin@javloom.io
phone: +33612345678
password: Admin1234!
first-name: Admin
last-name: JavloomIf javloom.admin.enabled=false, startup bootstrap is skipped. ⏭️
POST /api/auth/login
Content-Type: application/json
{
"email": "admin@javloom.io",
"password": "Admin1234!"
}Response:
{
"userId": "00000000-0000-0000-0000-000000000001",
"email": "admin@javloom.io",
"permissions": ["USER_DELETE", "USER_READ", "USER_WRITE"],
"tokens": {
"accessToken": "eyJ...",
"refreshToken": "eyJ...",
"accessTokenExpiresIn": 900000,
"refreshTokenExpiresIn": 604800000
}
}POST /api/auth/refresh
Content-Type: application/json
{
"refreshToken": "eyJ..."
}POST /api/auth/logout
Authorization: Bearer POST /api/auth/otp/send
Content-Type: application/json
{
"phone": "+33612345678"
}In development, the OTP is printed in the application logs instead of being sent via SMS. Look for a line starting with
📱 SMS to.
POST /api/auth/otp/verify
Content-Type: application/json
{
"phone": "+33612345678",
"code": "123456"
}All user endpoints require a valid Bearer token and the appropriate permission.
GET /api/users?page=0&size=20
Authorization: Bearer Required permission: USER_READ
Response:
{
"content": [...],
"page": 0,
"size": 20,
"totalElements": 1,
"totalPages": 1,
"first": true,
"last": true
}GET /api/users/{id}
Authorization: Bearer Required permission: USER_READ
POST /api/users
Authorization: Bearer
Content-Type: application/json
{
"email": "john@example.com",
"phone": "+33698765432",
"password": "Secret1234!",
"firstName": "John",
"lastName": "Doe"
}Required permission: USER_WRITE
PUT /api/users/{id}
Authorization: Bearer
Content-Type: application/json
{
"firstName": "Johnny",
"phone": "+33611223344"
}Required permission: USER_WRITE
All fields are optional — only provided fields are updated.
DELETE /api/users/{id}
Authorization: Bearer Required permission: USER_DELETE
# 1. Login
TOKEN=$(curl -s -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@javloom.io","password":"Admin1234!"}' \
| jq -r '.tokens.accessToken')
# 2. List users
curl http://localhost:8080/api/users \
-H "Authorization: Bearer $TOKEN"
# 3. Create user
curl -X POST http://localhost:8080/api/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "jane@example.com",
"password": "Secret1234!",
"firstName": "Jane",
"lastName": "Doe"
}'
# 4. Send OTP
curl -X POST http://localhost:8080/api/auth/otp/send \
-H "Content-Type: application/json" \
-d '{"phone":"+33612345678"}'
# → check logs for OTP code
# 5. Verify OTP
curl -X POST http://localhost:8080/api/auth/otp/verify \
-H "Content-Type: application/json" \
-d '{"phone":"+33612345678","code":""}'All errors follow the ApiError format from spring-rest-commons:
{
"message": "User not found with id: abc",
"exceptionName": "ResourceNotFoundException",
"timestamp": "2026-04-18T19:00:00Z"
}Validation errors include field-level details:
{
"message": "Validation failed",
"exceptionName": "ValidationException",
"timestamp": "2026-04-18T19:00:00Z",
"fieldErrors": [
{ "field": "email", "message": "must not be blank" },
{ "field": "phone", "message": "must be a valid E.164 phone number" }
]
}javloom-demo-api/
├── docker-compose.yml
├── pom.xml
└── src/main/
├── java/io/javloom/demo/
│ ├── DemoApplication.java
│ ├── config/
│ │ ├── SecurityConfig.java
│ │ ├── RedisConfig.java
│ │ └── JpaConfig.java
│ ├── auth/
│ │ ├── AuthController.java
│ │ ├── JpaRefreshTokenStore.java
│ │ ├── RedisOtpStore.java
│ │ ├── ConsoleSmsAdapter.java
│ │ ├── UserDetailsServiceImpl.java
│ │ ├── RefreshTokenEntity.java
│ │ └── RefreshTokenRepository.java
│ └── user/
│ ├── Permission.java
│ ├── User.java
│ ├── UserRepository.java
│ ├── UserMapper.java
│ ├── UserService.java
│ ├── UserController.java
│ └── dto/
│ ├── UserDto.java
│ ├── CreateUserRequest.java
│ └── UpdateUserRequest.java
└── resources/
├── application.yml
└── db/migration/
└── V1__init.sql
| Library | Role |
|---|---|
spring-rest-commons |
ApiError, PageResponse, @NoHtml, @NullOrNotBlank |
spring-security-jwt-lib |
JWT generation, refresh token rotation, @HasPermission, passwordless SMS |
The JWT library defines interfaces (ports) that must be implemented by the consuming project. Here is how each port is wired in this demo:
| Port | Implementation | Technology | Role |
|---|---|---|---|
RefreshTokenStore |
JpaRefreshTokenStore |
PostgreSQL + JPA | Persists and rotates refresh tokens |
OtpStore |
RedisOtpStore |
Redis | Stores OTP codes with TTL |
SmsPort |
ConsoleSmsAdapter |
Console logs | Simulates SMS dispatch via logs |
SecurityUserService |
UserDetailsServiceImpl |
PostgreSQL + JPA | Loads users by email and phone |
ConsoleSmsAdapter logs OTPs to the console. To use a real SMS provider,
implement SmsPort and register it as a Spring bean:
@Component
@RequiredArgsConstructor
public class TwilioSmsAdapter implements SmsPort {
private final TwilioProperties props;
@Override
public void send(String phone, String message) {
// Twilio SDK call
}
}MIT — free to use in personal and commercial projects.