A Spring Boot REST API for a simple bank simulation with JWT authentication, role-based access, card management, transfer operations, and email notifications.
- Java 21
- Spring Boot 3.2.5
- Spring Security + JWT
- Spring Data JPA (Hibernate)
- PostgreSQL
- Maven Wrapper (
./mvnw) - OpenAPI / Swagger UI
- User registration and login with JWT token issuance.
- Role-aware authorization (
USER,ADMIN). - User card operations:
- add card
- delete own card
- deposit
- withdraw
- transfer between cards
- view own profile with linked cards
- Admin operations:
- list users with cards (paginated)
- update card status (
ACTIVE,BLOCKED,OUTDATED)
- Email notifications for deposit, withdraw, and transfer events.
The project is configured through environment variables in src/main/resources/application.yml.
| Variable | Required | Default | Description |
|---|---|---|---|
APP_PORT |
No | 8080 |
Spring Boot server port |
DB_HOST |
No | localhost |
PostgreSQL host |
DB_PORT |
No | 5432 |
PostgreSQL port |
DB_NAME |
No | postgres |
Database name |
DB_USERNAME |
Yes | none | Database username |
DB_PASSWORD |
Yes | none | Database password |
MAIL_HOST |
No | smtp.gmail.com |
SMTP host |
MAIL_PORT |
No | 587 |
SMTP port |
MAIL_USERNAME |
Yes | none | Sender email account |
MAIL_PASSWORD |
Yes | none | SMTP app password |
JWT_SECRET |
Yes | none | JWT signing key |
JWT_EXPIRATION |
No | 86400000 |
JWT TTL in milliseconds |
Important for Gmail SMTP:
MAIL_PASSWORDis not your regular Gmail login password.- You must generate a Google App Password and use that value.
- Create it here: https://myaccount.google.com/apppasswords
Use .env.example as a template:
cp .env.example .envThen fill real values in .env.
docker-compose.yml uses environment variables for DB credentials.
docker compose up -ddocker compose psdocker compose downdocker compose down -vBuild the image:
docker build -t bank-management .Run the container (uses your .env file):
docker run --env-file .env -p 8080:8080 bank-managementIf the database is running in Docker, make sure DB_HOST in .env points to the DB container name from docker-compose.yml.
./mvnw clean package./mvnw spring-boot:runApplication base URL:
http://localhost:8080
- Swagger UI:
http://localhost:8080/swagger-ui.html - OpenAPI JSON:
http://localhost:8080/v3/api-docs
- Start PostgreSQL:
docker compose up -d- Start the application:
./mvnw spring-boot:run- Open Swagger UI:
http://localhost:8080/swagger-ui.html
- Register a user at
POST /api/v1/auth/register(or login if already created). - Copy the JWT token from the response.
- In Swagger, click
Authorizeand paste:
Bearer <your-jwt-token>
- Call protected endpoints (user/admin) directly from Swagger.
- Register or login at
/api/v1/auth/**. - Receive token in response.
- Send token as header:
Authorization: Bearer <your-jwt-token>- Public endpoints:
/api/v1/auth/**- Swagger/OpenAPI endpoints
- All other endpoints require authentication.
- Admin endpoints are guarded with method security:
@PreAuthorize("hasRole('ADMIN')")
| Role | Capabilities |
|---|---|
USER |
Manage own cards, do balance operations, transfer, view own profile |
ADMIN |
Everything a user can do (if authenticated) + admin APIs for user list and card status updates |
Base path: /api/v1
Registers a user and returns JWT.
Request body:
{
"email": "user@example.com",
"fullName": "John Doe",
"password": "secret123",
"role": "USER"
}Authenticates and returns JWT.
Request body:
{
"email": "user@example.com",
"password": "secret123"
}Response shape:
{
"token": "<jwt>",
"bearer": "Bearer"
}Require Authorization: Bearer <token>.
Request:
{
"cardNum": 8600123412341234,
"balance": 100000,
"executesAt": "2028-12-31"
}Request:
{
"cardNum": 8600123412341234,
"amount": 5000
}Request:
{
"cardNum": 8600123412341234,
"amount": 3000
}Request:
{
"fromCard": 8600123412341234,
"toCard": 8600432112345678,
"amount": 10000
}Returns current user profile + cards.
Deletes one of the authenticated user cards.
Require authenticated user with ADMIN role.
Returns paginated users with cards.
Request:
{
"cardNum": 8600123412341234,
"status": "BLOCKED"
}Allowed status values:
ACTIVEBLOCKEDOUTDATED
- User registers with email/password/role.
- Password is encoded with BCrypt.
- JWT token is generated with email subject and role claim.
- User uses token for authenticated endpoints.
- Current user is resolved from
SecurityContext. - Card ownership checks are performed for the source card.
- Balance is updated in DB.
- Notification email is sent.
- Operation result returns current card balance summary.
- Admin requests card status update.
- Card is fetched by
cardNum. - Status is changed and persisted.
idemail(unique)fullNamepasswordrole(USER/ADMIN)- timestamps
- one-to-many relation to
Card
idcardNumber(unique)balancestatusexecutesAt- many-to-one relation to
User
src/main/java/org/example/email_entity
βββ controller
βββ dto
βββ entity
βββ repository
βββ security
βββ service
- Global error handling is centralized in:
org.example.email_entity.exception.GlobalExceptionHandlerorg.example.email_entity.exception.ApiErrorResponse
- Service methods that throw exceptions are now returned as a consistent JSON error response shape.
- Runtime business errors (for example: card not found, negative amount, not enough money) are returned as
400 Bad Request. - Admin access violations are returned as
403 Forbidden. - Authentication failures are returned as
401 Unauthorized. - Validation and malformed payload issues are returned as
400 Bad Request. - Unexpected server-side failures are returned as
500 Internal Server Error. - Registration-related DB constraint errors are returned as
409 Conflict.
Standard error response format:
{
"timestamp": "2026-03-05T13:20:30.123+05:00",
"status": 400,
"error": "Bad Request",
"message": "Card not found",
"path": "/api/v1/users/user/deposit",
"details": null
}Before pushing to GitHub:
- Confirm real credentials are only in local
.env. - Confirm
.envis ignored and not tracked. - Keep
.env.examplewith placeholders only. - Verify
JWT_SECRETis strong and at least 32+ bytes for HS256 safety. - Run:
./mvnw clean test# Start database
docker compose up -d
# Run app
./mvnw spring-boot:run
# Run tests
./mvnw test
# Build jar
./mvnw clean package