Base:
http://localhost:8080· Content:application/json· Auth:Authorization: Bearer <JWT>unless whitelisted · Source:application/controllers/**·domain/dto/**·infra/config/security/SecurityConfig.java
Swagger interactive: http://localhost:8080/swagger-ui/index.html · OpenAPI JSON: http://localhost:8080/v3/api-docs (springdoc 2.2.0). Remove springfox 3.0.0 before exposing prod.
| Need JWT? | Paths | Config |
|---|---|---|
| No (whitelist) | /, /auth/login, /v3/api-docs/**, /api-docs.yaml, /swagger-resources/**, /swagger-ui/**, /swagger-ui.html |
SecurityConfig.WHITE_LIST (9 entries) permitAll() |
| Yes | everything else → authenticated() |
JWTAuthMiddleware before UsernamePasswordAuthenticationFilter |
| Extra RBAC | GET /users → hasAnyRole(USER,ADMIN); POST /auth/register → hasAuthority(CREATE_USER) (ADMIN only); POST /transactions → EXECUTE_TRANSACTION; GET /transactions → READ_TRANSACTION |
authorizeHttpRequests line 37-48 |
Logout: POST /auth/logout → SecurityContextHolder.clearContext().
POST /auth/login HTTP/1.1
Content-Type: application/json
{ "email": "alice@example.com", "password": "alice123" }- DTO:
LoginDTO(@Email String email, @NotBlank String password)viaAuthenticateUserUseCase→DaoAuthenticationProvider(BCrypt). - Success 200:
LoginResponseDTO { String token }— JWT HMAC256, 2h expiry (seesecurity.md). Token placed inAuthorization: Bearer. - Errors:
401 Unauthorizedbad credentials (UsernameNotFoundException/BadCredentialsException→ControllerExceptionHandler). - Example:
curl -s -X POST http://localhost:8080/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"alice@example.com","password":"alice123"}' | jq
# {"token":"eyJhbGciOiJIUzI1NiIs..."}
TOKEN=$(curl -s -X POST http://localhost:8080/auth/login -H 'Content-Type: application/json' -d '{"email":"alice@example.com","password":"alice123"}' | jq -r .token)POST /auth/register HTTP/1.1
Authorization: Bearer <ADMIN_JWT>
Content-Type: application/json
{
"firstName":"Bob","lastName":"Buyer",
"email":"bob@example.com","document":"12345678901",
"balance":100.00,"password":"bob123",
"userType":"COMMON","role":"USER"
}- DTO:
UserDTOrecord —@NotBlank @Email @Size(min=3) @NotNull. Validation errors →400. - Policy:
EncryptPasswordBeforeRegisteringUserPolicyBCrypt encodes beforeSaveUserUseCase. - Success 201/200: persisted
UserJSON (no password returned? currently leaks — should omit). - Errors:
403if caller not ADMIN;400duplicate email/document (DB UK violation → 500 currently → should map 409).
curl -s -X POST http://localhost:8080/auth/register \
-H "Authorization: Bearer $ADMIN_TOKEN" -H 'Content-Type: application/json' \
-d '{"firstName":"Bob","lastName":"Buyer","email":"bob@example.com","document":"12345678901","balance":100,"password":"bob123","userType":"COMMON","role":"USER"}' | jqPOST /auth/logout
Authorization: Bearer <JWT>Clears SecurityContextHolder. No token blacklist — JWT remains valid until 2h expiry (stateless limitation).
GET /users
Authorization: Bearer <JWT>
Accept: application/json- Success 200:
List<User>— returns all (no paging, no filter). ConsiderPage<User>withPageable. - Errors:
401missing JWT,403insufficient role.
curl -s http://localhost:8080/users -H "Authorization: Bearer $TOKEN" | jqLeak note: Returns entity
Userincludingpasswordhash +balance. Should return projectionUserDTOwithout password.
POST /transactions
Authorization: Bearer <JWT>
Content-Type: application/json
{ "value": 25.50, "senderId": "aaaaaaaa-bbbb-...-...", "receiverId": "cccccccc-dddd-...-..." }- DTO:
TransactionDTO(@NotNull BigDecimal value, @NotNull UUID senderId, @NotNull UUID receiverId)— no@Positiveso zero/negative passes validation. - Flow (happy):
find sender → validate(R1 merchant, R2 balance, R3 authorizer Mocky) → find receiver → TransactionUseCase.execute() → save tx + update balances → notify. - Success 200:
Transaction {id, amount, sender:{…}, receiver:{…}, createdAt}. - Errors:
403 Merchant User is not authorized to transact;400 Balance is not enough;403 Transaction Not Authorized, try again later...(fromTransactionServiceImpl.validate()throwing genericException— maps to 500 now, should be 4xx).
curl -s -X POST http://localhost:8080/transactions \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"value":10.00,"senderId":"<COMMON_UUID>","receiverId":"<MERCHANT_UUID>"}' | jqFailure catalog:
# merchant as sender
curl -s -X POST http://localhost:8080/transactions -H "Authorization: Bearer $TOKEN" \
-d '{"value":10,"senderId":"<MERCHANT_UUID>","receiverId":"<COMMON_UUID>"}' # 403 Merchant...
# insufficient funds
curl -s -X POST http://localhost:8080/transactions -H "Authorization: Bearer $TOKEN" \
-d '{"value":9999,"senderId":"<COMMON_UUID>","receiverId":"<MERCHANT_UUID>"}' # 400 Balance...
# unauthorized by Mocky (simulate by stubbing auth URL down)
curl -v # → 403 Transaction Not AuthorizedGET /transactions
Authorization: Bearer <JWT>- Success 200:
List<Transaction>(all).
curl -s http://localhost:8080/transactions -H "Authorization: Bearer $TOKEN" | jq '.[].amount'| Path | Method | Notes |
|---|---|---|
/ |
GET | AppController root, whitelisted, 200 |
domain/entities/auth/Permissions.java → Role.java
| Role | Permissions (granted as authorities) | Effective |
|---|---|---|
| USER | READ_USER, EXECUTE_TRANSACTION, READ_TRANSACTION |
COMMON default |
| ADMIN | USER + DELETE_USER, CREATE_USER, UPDATE_USER |
Full |
getAuthorities() adds both permission.getPermission() and ROLE_USER/ROLE_ADMIN. So hasRole('USER') and hasAuthority('transaction:execute') both work.
Endpoint enforcement:
.requestMatchers(GET, "/users").hasAnyRole(USER.name(), ADMIN.name())
.requestMatchers(POST, "/auth/register").hasAuthority(CREATE_USER.getPermission())
.requestMatchers(POST,"/transactions").hasAuthority(EXECUTE_TRANSACTION.getPermission())
.requestMatchers(GET, "/transactions").hasAuthority(READ_TRANSACTION.getPermission())
.anyRequest().authenticated()infra/middleware/error/ControllerExceptionHandler.java → ExceptionDTO { String message } + HTTP status. Current: MethodArgumentNotValidException → 400 with field errors; generic Exception from validate() bubbles as 500 (should map to 400/403). DTO validation errors are per-field.
// 400 validation
{"message":"firstName: must not be blank"}
// 403 merchant
{"message":"Merchant User is not authorized to transact"}Add: DataIntegrityViolationException → 409 (duplicate email/document), AccessDeniedException → 403.
- springdoc 2.2.0 auto-generates at
/v3/api-docs(JSON) +/v3/api-docs.yaml+ Swagger UI at/swagger-ui/index.html. SwaggerConfig.javais disabled (springfox) — delete file + deps.- No manual
@Operationannotations — add for richer docs.
Screenshot slot: docs/assets/screenshots/swagger-ui.png.
BASE=http://localhost:8080
# 1) ensure admin exists (bootstrap via SQL: INSERT ... role='ADMIN', userType='COMMON', password BCrypt of 'admin123')
# or create first user via direct repo then promote
# 2) login as admin
ADMIN_TOKEN=$(curl -s -X POST $BASE/auth/login -H 'Content-Type: application/json' \
-d '{"email":"admin@picpay.com","password":"admin123"}' | jq -r .token)
# 3) create two COMMON + one MERCHANT
curl -s -X POST $BASE/auth/register -H "Authorization: Bearer $ADMIN_TOKEN" -H 'Content-Type: application/json' \
-d '{"firstName":"Alice","lastName":"A","email":"alice@example.com","document":"11111111111","balance":500,"password":"alice123","userType":"COMMON","role":"USER"}' | jq
curl -s -X POST $BASE/auth/register -H "Authorization: Bearer $ADMIN_TOKEN" -H 'Content-Type: application/json' \
-d '{"firstName":"Bob","lastName":"B","email":"bob@example.com","document":"22222222222","balance":100,"password":"bob123","userType":"COMMON","role":"USER"}' | jq
curl -s -X POST $BASE/auth/register -H "Authorization: Bearer $ADMIN_TOKEN" -H 'Content-Type: application/json' \
-d '{"firstName":"Shop","lastName":"Lda","email":"shop@example.com","document":"33333333333","balance":0,"password":"shop123","userType":"MERCHANT","role":"USER"}' | jq
# 4) login as alice (COMMON)
TOKEN=$(curl -s -X POST $BASE/auth/login -H 'Content-Type: application/json' \
-d '{"email":"alice@example.com","password":"alice123"}' | jq -r .token)
# 5) resolve UUIDs
USERS=$(curl -s $BASE/users -H "Authorization: Bearer $TOKEN")
ALICE_ID=$(echo $USERS | jq -r '.[] | select(.email=="alice@example.com") | .id')
SHOP_ID=$(echo $USERS | jq -r '.[] | select(.email=="shop@example.com") | .id')
# 6) transact
curl -s -X POST $BASE/transactions -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d "{\"value\":50.00,\"senderId\":\"$ALICE_ID\",\"receiverId\":\"$SHOP_ID\"}" | jq
# 7) verify
curl -s $BASE/transactions -H "Authorization: Bearer $TOKEN" | jq
# 8) OpenAPI
curl -s $BASE/v3/api-docs | jq '.info'
open $BASE/swagger-ui/index.html- Security — JWT & filter chain
- Business Rules — validation logic behind 4xx
- Configuration — base URL & profiles