Authentication¶
OpenSTR uses Better-Auth for authentication across all clients, with bcrypt password hashing and database-backed sessions.
Overview¶
| Aspect | Implementation |
|---|---|
| Library | Better-Auth 1.5.6 |
| Password hashing | Bcrypt (12 rounds) |
| Session storage | PostgreSQL session table |
| Token delivery | HTTP-only cookies + Bearer token header |
| User roles | owner, admin, cleaner, guest |
Authentication Flow¶
Login (Admin Panel)¶
- User submits email/password on the login form
- Better-Auth client calls
POST /api/auth/sign-in/email - Server validates credentials against the
accounttable (bcrypt comparison) - Creates a session in the
sessiontable - Returns session cookie (HTTP-only)
- Admin panel stores session and redirects to dashboard
Login (Mobile App)¶
- User submits email/password on the login screen
AuthService.login()callsPOST /api/auth/sign-in/email- Server returns JWT token + user object
- Token stored securely:
- iOS: Keychain via
flutter_secure_storage - Android: Keystore via
flutter_secure_storage - Web:
SharedPreferences
- iOS: Keychain via
- All subsequent API calls include
Authorization: Bearer <token>header
Session Persistence¶
On app cold start, the mobile app:
- Retrieves stored token from secure storage
- Calls
GET /api/auth/get-sessionto validate - If valid, populates user state and shows main screen
- If invalid/expired, clears token and shows login screen
Authorization¶
Middleware Chain¶
The API uses three middleware functions for authorization:
requireAuth()¶
- Validates Better-Auth session from cookies or Bearer token
- Populates
req.userwith{ userId, role, propertyIds } - Returns
401 Unauthorizedif no valid session
requireRole(...roles)¶
- Checks
req.user.roleagainst the allowed roles - Returns
403 Forbiddenif role is not permitted - Example:
requireRole('owner', 'admin')restricts to owners and admins
requirePropertyAccess(paramName)¶
- For cleaners: verifies the requested property is in their assigned properties
- Owners and admins bypass this check (full access)
- Returns
403 Forbiddenif cleaner is not assigned to the property
Role Permissions¶
| Capability | Owner | Admin | Cleaner | Guest |
|---|---|---|---|---|
| Manage properties | ✅ | ❌ | ❌ | ❌ |
| Create/edit users | ✅ | ✅ | ❌ | ❌ |
| Create sessions | ✅ | ✅ | ❌ | ❌ |
| Accept/claim sessions | ❌ | ❌ | ✅ | ❌ |
| Execute cleaning workflow | ❌ | ❌ | ✅ | ❌ |
| Review sessions | ✅ | ✅ | ❌ | ❌ |
| View all sessions | ✅ | ✅ | ❌ | ❌ |
| View own sessions | ✅ | ✅ | ✅ | ❌ |
| Manage standards | ✅ | ❌ | ❌ | ❌ |
| View property guide | ✅ | ✅ | ✅ | ✅ |
| Report issues | ✅ | ✅ | ✅ | ✅ |
| Send messages | ❌ | ❌ | ❌ | ✅ |
Security Configuration¶
Better-Auth Setup¶
// Configured in api/src/lib/auth.ts
{
database: pool, // PostgreSQL connection
secret: BETTER_AUTH_SECRET, // Session signing secret
emailAndPassword: {
enabled: true,
hashFunction: bcrypt (12 rounds)
}
}
CORS¶
- Development: localhost origins allowed automatically
- Production: set
ALLOWED_ORIGINSenv var (comma-separated) - Credentials (
withCredentials: true) enabled for cookie-based auth
401 Handling¶
- Admin panel: Axios interceptor redirects to
/loginon 401 - Mobile app: Dio interceptor clears token and navigates to login screen
- Both clients treat 401 as "session expired, re-authenticate"