Authentication
Base path: /api/auth
POST /auth/login
Log in with email and password.
Auth: None
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Email address |
password | string | Yes | Password |
mobile | boolean | No | If true, a 30-day refresh token is returned |
Response (200):
{
"token": "jwt-access-token",
"refresh_token": "only-when-mobile-true",
"user": {
"id": 1,
"name": "Admin",
"email": "admin@salon.nl",
"role": "admin",
"require_password_change": false
}
}
Access tokens expire after 8 hours. When mobile: true is sent, a refresh token with 30-day lifetime is also returned and stored in the refresh_tokens table.
Errors: 401 — Invalid credentials
POST /auth/logout
Log out and revoke refresh/device token.
Auth: Required (any role)
Request body:
| Field | Type | Description |
|---|---|---|
refresh_token | string | Refresh token to revoke |
device_token | string | Push device token to remove |
Both fields are optional; send whichever tokens should be revoked.
POST /auth/refresh
Renew access token via refresh token. The old refresh token is deleted and a new one is issued (token rotation).
Auth: None
Request body:
| Field | Type | Required |
|---|---|---|
refresh_token | string | Yes |
Response (200):
{
"token": "new-access-token",
"refresh_token": "new-refresh-token",
"user": {
"id": 1,
"name": "Admin",
"email": "admin@salon.nl",
"role": "admin",
"require_password_change": false
}
}
Errors: 401 — Invalid or expired refresh token
POST /auth/register
Create a new user (admin only). Sends a welcome email with a temporary password. The new user will have require_password_change: true.
Auth: Admin
Request body:
| Field | Type | Required | Default |
|---|---|---|---|
email | string | Yes | — |
name | string | Yes | — |
role | string | No | staff |
Response (200): { id, email, role, name }
Errors: 409 — Email address already registered
POST /auth/forgot-password
Send a password reset email. Always returns 200 (prevents email enumeration). Reset links expire after 1 hour. A 60-second throttle prevents duplicate requests.
Auth: None
Request body: { "email": "user@example.com" }
POST /auth/reset-password
Reset password with token from the reset email.
Auth: None
Request body:
| Field | Type | Required |
|---|---|---|
email | string | Yes |
token | string | Yes |
password | string (min 8) | Yes |
On success, a password-changed confirmation email is sent and all pending reset tokens for the email are deleted.
Errors: 400 — Invalid or expired token
POST /auth/change-password
Change password for a logged-in user. Requires the current password and sends a confirmation email on success.
Auth: Required (any role, no middleware — validated by email/current password match)
Request body:
| Field | Type | Required |
|---|---|---|
email | string | Yes |
currentPassword | string | Yes |
newPassword | string (min 8) | Yes |
confirmPassword | string (min 8) | Yes |
Errors: 400 — Passwords do not match; 401 — Wrong current password
GET /auth/users
Get all users (admin only).
Auth: Admin
Response: Array of { id, name, email, role } objects.
POST /auth/device
Register a push notification device token. If the token already exists for another user it is reassigned.
Auth: Required
Request body:
| Field | Type | Required |
|---|---|---|
token | string (max 512) | Yes |
platform | ios / android / web | Yes |
DELETE /auth/device
Remove a device token.
Auth: Required
Request body: { "token": "device-token" }
GET /auth/setup-status
Check if initial setup is needed. Returns needsSetup: true when the database has zero user accounts.
Auth: None
Response:
{ "needsSetup": true }
POST /auth/setup
Initial setup endpoint. Only works when zero user accounts exist.
Auth: None
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
admin_name | string | Yes | Admin display name |
admin_email | string | Yes | Admin email |
admin_password | string (min 8) | Yes | Admin password |
salon_name | string | Yes | Salon name |
salon_email | string | No | Salon contact email |
salon_phone | string | No | Salon phone number |
salon_address | string | No | Salon address |
opening_hours | array | No | Array of { day_of_week, open_time, close_time, is_closed } |
Response (201):
{
"token": "jwt-access-token",
"user": { "id": 1, "name": "Admin", "role": "admin" }
}
Errors: 400 — Setup already completed (users exist)
The setup wizard in the web app calls this endpoint. Once setup is complete, the endpoint rejects all further calls.