Handles authentication and user management for the FoodChain platform. Provides JWT-based login, registration, token refresh, password reset, Google OAuth2 sign-in, and role-based user administration.
- Tech Stack
- Running the Service
- Environment Variables
- API Base URL
- Authentication
- Roles
- Auth Endpoints
- User Endpoints
- Admin User Endpoints
- Error Responses
- Password Rules
- Swagger UI
- Java 17 / Spring Boot 3.2.3
- Spring Security — JWT filter chain, stateless sessions
- Spring Data JPA — MySQL 8 persistence
- Spring Data Redis — refresh token storage and token blacklist
- Spring Cloud — Eureka service discovery, Config Server (optional in standalone mode)
- jjwt 0.12.6 — JWT generation and validation
- Springdoc OpenAPI 2.3.0 — Swagger UI
Spins up MySQL, Redis, and the service — no Eureka or Config Server required.
Step 1 — build the JAR:
cd user-service
mvn package -Dmaven.test.skip=trueStep 2 — start the stack:
docker compose -f docker-compose.dev.yml up --build| URL | Description |
|---|---|
http://localhost:8081/api/swagger-ui/index.html |
Swagger UI |
http://localhost:8081/api/actuator/health |
Health check |
Ports exposed on the host:
| Service | Host Port | Container Port |
|---|---|---|
| user-service | 8081 | 8081 |
| MySQL | 3307 | 3306 |
| Redis | 6380 | 6379 |
To stop:
docker compose -f docker-compose.dev.yml downTo stop and wipe data volumes:
docker compose -f docker-compose.dev.yml down -vRequires MySQL and Redis already running locally.
cd user-service
mvn spring-boot:runOr with overrides:
mvn spring-boot:run \
-Dspring-boot.run.arguments="--DB_HOST=localhost --DB_NAME=user_db --DB_USERNAME=root --DB_PASSWORD=secret"| Variable | Default | Description |
|---|---|---|
DB_HOST |
localhost |
MySQL host |
DB_NAME |
user_db |
MySQL database name |
DB_USERNAME |
root |
MySQL username |
DB_PASSWORD |
devpassword |
MySQL password |
REDIS_HOST |
localhost |
Redis host |
REDIS_PORT |
6379 |
Redis port |
REDIS_PASSWORD |
(empty) | Redis password |
JWT_SECRET |
(required, 32+ chars) | HMAC-SHA secret for signing JWTs |
JWT_ACCESS_EXPIRY_MS |
900000 |
Access token lifetime (ms) — default 15 min |
JWT_REFRESH_EXPIRY_MS |
604800000 |
Refresh token lifetime (ms) — default 7 days |
FRONTEND_URL |
http://localhost:5173 |
Used in password-reset email links |
OAUTH2_ENABLED |
false |
Enable server-side Google OAuth2 flow |
http://localhost:8081/api
All paths below are relative to this base.
Protected endpoints require a Bearer token in the Authorization header:
Authorization: Bearer <access_token>
The access token is returned by /auth/login, /auth/register, /auth/google, and /auth/refresh.
Role values are serialised as display names in all API responses and accepted as display names in requests.
| Display name (JSON) | Enum constant | Description |
|---|---|---|
Customer |
CUSTOMER |
Default role. Can view and manage their own profile. |
Kitchen Staff |
KITCHEN_STAFF |
Kitchen operations staff. Must be assigned a branch. |
Branch Manager |
BRANCH_MANAGER |
Manages a specific branch. Must be assigned a branch. |
Admin |
HEAD_OFFICE_ADMIN |
Full administrative access across all users and branches. |
Spring Security authority strings continue to use the enum constant names prefixed with ROLE_ internally (e.g. ROLE_HEAD_OFFICE_ADMIN).
Creates a new user account. Returns the created user's profile.
Request body:
{
"name": "John Doe",
"email": "john@example.com",
"password": "Secure@123!",
"role": "Customer",
"branchId": null
}| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | No | Full name |
email |
string | Yes | Must be a valid email address |
password |
string | Yes | See Password Rules |
role |
string | No | Defaults to Customer. One of: Customer, Kitchen Staff, Branch Manager, Admin |
branchId |
UUID | No | Required for Branch Manager and Kitchen Staff. Leave null for Customer |
Response 201 Created:
{
"id": "7264-58d3-...",
"name": "John Doe",
"email": "john@example.com",
"role": "Customer",
"branchId": null,
"isActive": true,
"createdAt": "2026-05-10T04:30:00Z"
}Error responses:
| Code | Reason |
|---|---|
400 |
Validation failed (invalid email, weak password, email already registered) |
Authenticates a user and returns a token pair. This endpoint is intercepted by the JWT filter — it does not reach the controller method.
Request body:
{
"email": "john@example.com",
"password": "Secure@123!"
}Response 200 OK:
{
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"token": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
"tokenType": "Bearer",
"expiresIn": 900,
"user": {
"id": "7264-58d3-...",
"name": "John Doe",
"email": "john@example.com",
"role": "Customer",
"branchId": null,
"isActive": true,
"createdAt": "2026-05-10T04:30:00Z"
}
}| Field | Type | Description |
|---|---|---|
accessToken |
string | Short-lived JWT. Include in Authorization: Bearer <token> header |
token |
string | Alias for accessToken — same value, provided for frontend convenience |
refreshToken |
string | Long-lived token. Use with /auth/refresh to get a new pair |
tokenType |
string | Always "Bearer" |
expiresIn |
number | Access token lifetime in seconds (default 900 = 15 min) |
user |
object | The authenticated user's profile |
Error responses:
| Code | Reason |
|---|---|
401 |
Invalid email or password |
Authenticates using a Google ID token obtained from Google Identity Services on the client side. Creates an account automatically if the email is not yet registered.
Request body:
{
"credential": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9..."
}| Field | Type | Required | Notes |
|---|---|---|---|
credential |
string | Yes | JWT ID token from window.google.accounts.id.initialize callback |
Response 200 OK: Same shape as login response.
Error responses:
| Code | Reason |
|---|---|
400 |
Invalid or expired Google credential |
Exchanges a valid refresh token for a new access token and refresh token. The old refresh token is deleted (rotation).
Request body:
{
"refreshToken": "eyJhbGciOiJIUzI1NiJ9..."
}Response 200 OK: Same shape as login response with a new token pair.
Error responses:
| Code | Reason |
|---|---|
400 |
Refresh token is missing or blank |
401 |
Refresh token is expired, invalid, or already rotated |
404 |
The user associated with the token no longer exists |
Invalidates the access token (adds the JTI to the blacklist) and deletes the refresh token from Redis.
Both the Authorization header and the request body are optional — provide what you have.
Headers (optional):
Authorization: Bearer <access_token>
Request body (optional):
{
"refreshToken": "eyJhbGciOiJIUzI1NiJ9..."
}Response 204 No Content — no body.
Sends a one-time password-reset link to the given email address. Always returns 200 regardless of whether the email is registered (prevents user enumeration).
Request body:
{
"email": "john@example.com"
}Response 200 OK:
{
"message": "If that email is registered, a reset link has been sent."
}Sets a new password using the one-time token from the reset link.
Request body:
{
"token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"newPassword": "NewSecure@456!"
}| Field | Type | Required | Notes |
|---|---|---|---|
token |
string | Yes | One-time token received via email |
newPassword |
string | Yes | Must satisfy Password Rules |
Response 200 OK:
{
"message": "Password updated successfully."
}Error responses:
| Code | Reason |
|---|---|
400 |
Token is invalid, expired, or new password is too weak |
Returns the currently authenticated user's profile. Used for session rehydration on page load.
Headers:
Authorization: Bearer <access_token>
Response 200 OK:
{
"id": "7264-58d3-...",
"name": "John Doe",
"email": "john@example.com",
"role": "Customer",
"branchId": null,
"isActive": true,
"createdAt": "2026-05-10T04:30:00Z"
}Error responses:
| Code | Reason |
|---|---|
401 |
Missing or invalid token |
Returns the profile of the currently authenticated user.
Headers:
Authorization: Bearer <access_token>
Response 200 OK: See UserResponse shape above.
Error responses:
| Code | Reason |
|---|---|
401 |
Missing or invalid token |
Returns all users. Requires Admin role.
Headers:
Authorization: Bearer <access_token>
Response 200 OK:
[
{
"id": "7264-58d3-...",
"name": "John Doe",
"email": "john@example.com",
"role": "Customer",
"branchId": null,
"isActive": true,
"createdAt": "2026-05-10T04:30:00Z"
}
]Error responses:
| Code | Reason |
|---|---|
401 |
Missing or invalid token |
403 |
Authenticated but not Admin |
Returns a single user's profile by UUID. Requires Admin or Branch Manager role.
Headers:
Authorization: Bearer <access_token>
Path parameter:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | The user's unique identifier |
Response 200 OK: See UserResponse shape above.
Error responses:
| Code | Reason |
|---|---|
401 |
Missing or invalid token |
403 |
Insufficient role |
404 |
User not found |
Partially updates a user. All fields are optional — only provided fields are changed. Requires Admin or Branch Manager role.
Headers:
Authorization: Bearer <access_token>
Path parameter:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | The user's unique identifier |
Request body (all fields optional):
{
"role": "Branch Manager",
"branchId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"isActive": true
}| Field | Type | Notes |
|---|---|---|
role |
string | One of: Customer, Kitchen Staff, Branch Manager, Admin |
branchId |
UUID | null | Set to null to remove branch association |
isActive |
boolean | Set to false to deactivate the account |
Response 200 OK: Updated user profile (see UserResponse shape).
Error responses:
| Code | Reason |
|---|---|
401 |
Missing or invalid token |
403 |
Insufficient role |
404 |
User not found |
These endpoints read the X-User-Role header forwarded by the API gateway. Accepted values: Admin or HEAD_OFFICE_ADMIN. No JWT verification is performed inside the service — the gateway is expected to validate the token and set the header.
List all users. Optionally filter by role.
Request headers — X-User-Role: Admin
Query parameters
| Name | Required | Description |
|---|---|---|
role |
No | Filter by role display name: Customer, Kitchen Staff, Branch Manager, Admin |
Response 200 OK:
[
{
"id": "7264-58d3-...",
"name": "Alice",
"email": "alice@example.com",
"role": "Customer",
"status": "active",
"branchId": null
}
]Error responses:
| Code | Reason |
|---|---|
400 |
Unrecognised role filter value |
403 |
Caller is not an Admin |
Fetch a single user by UUID.
Request headers — X-User-Role: Admin
Path parameter:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | The user's unique identifier |
Response 200 OK: Single AdminUserResponse object (same fields as list item).
Error responses:
| Code | Reason |
|---|---|
403 |
Caller is not an Admin |
404 |
User not found |
Activate or deactivate a user account.
Request headers — X-User-Role: Admin
Path parameter:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | The user's unique identifier |
Request body:
{ "status": "inactive" }Accepted values: active, inactive.
Response 200 OK: Updated AdminUserResponse.
Error responses:
| Code | Reason |
|---|---|
400 |
Invalid status value |
403 |
Caller is not an Admin |
404 |
User not found |
All errors return a consistent JSON body:
{
"status": 401,
"error": "Unauthorized",
"message": "Authentication required — provide a valid Bearer token",
"path": "/api/users",
"timestamp": "2026-05-10T04:41:48Z"
}For validation errors (400), a fields map is also included:
{
"status": 400,
"error": "Validation Failed",
"message": "One or more fields are invalid",
"path": "/api/auth/register",
"timestamp": "2026-05-10T04:41:48Z",
"fields": {
"password": "Password must be at least 8 characters and include at least one uppercase letter, one lowercase letter, one digit, and one special character (@#$!%*?&-_+=)",
"email": "Invalid email format"
}
}| Status | Meaning |
|---|---|
400 |
Bad request — validation failed or duplicate email |
401 |
Unauthenticated — missing, expired, or invalid token |
403 |
Forbidden — authenticated but insufficient role |
404 |
Resource not found |
500 |
Unexpected server error |
Passwords must satisfy all of the following:
| Rule | Requirement |
|---|---|
| Minimum length | 8 characters |
| Uppercase letter | At least one (A–Z) |
| Lowercase letter | At least one (a–z) |
| Digit | At least one (0–9) |
| Special character | At least one from @ # $ ! % * ? & - _ + = |
Valid example: Secure@123!
Invalid examples:
| Password | Reason |
|---|---|
password |
No uppercase, digit, or special character |
Password1 |
No special character |
SECURE@1 |
No lowercase letter |
Secure@! |
No digit |
Se@1 |
Too short |
Interactive API explorer available at:
http://localhost:8081/api/swagger-ui/index.html
To test protected endpoints:
- Call
POST /auth/loginorPOST /auth/registerusing Try it out - Copy the
accessTokenfrom the response - Click the Authorize button (top right)
- Paste the token (without the
Bearerprefix) and click Authorize - All subsequent requests will include the token automatically