Add Redis-based permission caching for users and subusers

- Introduced Redis-backed caching for user and subuser permission evaluations in the `route_t` trait, reducing database queries.
- Enhanced `Redis` class with methods for permission caching: `cache_permission`, `get_permission`, and `clear_permission`.
- Added test coverage for the new caching logic in `PermissionRedisCacheTest.php`.
- Implemented Redis caching for authentication sessions with `cache_auth_session`, `get_auth_session`, and `clear_auth_session`.
- Improved CORS handling for preflight requests in `index.php`.
This commit is contained in:
Jeppe Bundgaard
2026-02-24 15:00:22 +01:00
parent 2534ffb1b9
commit 5cc311ae32
7 changed files with 367 additions and 12 deletions
+39 -5
View File
@@ -46,8 +46,21 @@ class authRoute
if (!isset($data['password']) || empty($data['password']) || strlen($data['password']) < 1) {
$response->error('Password is required', 400);
}
// Try to log the user in
$isCredentialsValid = (new authentication())->authenticate($data['customer_number'], $data['password']);
// Use Redis-backed user and property caches to validate credentials with a single user load
$user = (new users_o())->getUserByCustomerNumber($data['customer_number']);
if (!$user->exists() || !$user->hasPassword()) {
(new logs_o())->add('auth', 'global', 1, 0, 'AUTH_FAILURE', 'Customer number: ' . $data['customer_number']);
$response->error('Invalid credentials', 401);
}
$isCredentialsValid = false;
try {
$isCredentialsValid = $user->passwordMatches($data['password']);
} catch (Exception $e) {
$isCredentialsValid = false;
}
// Log the incident
if ($isCredentialsValid) {
(new logs_o())->add('auth', 'global', 1, 0, 'AUTH_SUCCESS', 'Customer number: ' . $data['customer_number']);
@@ -55,14 +68,15 @@ class authRoute
(new logs_o())->add('auth', 'global', 1, 0, 'AUTH_FAILURE', 'Customer number: ' . $data['customer_number']);
$response->error('Invalid credentials', 401);
}
// If the credentials are valid, create a token
$user = (new users_o())->getUserByCustomerNumber($data['customer_number']);
// If the credentials are valid, check 2FA and create a token
if ($user->exists() && $user->isTwoFactorEnabled()) {
$token = (new authentication())->create_2fa_token($user->id, '2FA_VERIFICATION_USER');
$response->success(['2fa_required' => true, '2fa_token' => $token]);
}
$token = (new authentication())->create_token($data['customer_number']);
// Create the auth token without reloading the user from DB
$token = (new authentication())->create_token_by_user_id((int)$user->id);
// Return the token
$response->success(['token' => $token]);
});
@@ -79,6 +93,8 @@ class authRoute
}
// Delete the token
(new tokens_o())->delete($token);
// Clear any cached session for this token
try { redis->clear_auth_session($token); } catch (\Throwable $e) {}
// Return a success message
$response->success(['message' => 'Logged out']);
});
@@ -93,6 +109,17 @@ class authRoute
if (!(new authentication())->validate_token($token)) {
$response->error('Invalid token', 401);
}
// Try Redis cache first for session payload
try {
$cached = redis->get_auth_session($token);
if (is_array($cached)) {
$response->success($cached);
}
} catch (\Throwable $e) {
// Ignore Redis errors and continue to compute session
}
// Get the user object
$user = (new authentication())->get_user();
// Check if the user exists
@@ -103,6 +130,13 @@ class authRoute
$user_data = $user->includeIncludes(['economicCustomer', 'permissions'])->asArray();
$user_data['two_factor_enabled'] = $user->isTwoFactorEnabled();
// Cache the session payload briefly to reduce DB load on hot paths
try {
redis->cache_auth_session($token, $user_data, 60);
} catch (\Throwable $e) {
// Best-effort caching only
}
// Return the (session) user object
$response->success($user_data);
});