diff --git a/config/vufind/LOTS.ini b/config/vufind/LOTS.ini index e29d4f829a5..567117dec54 100644 --- a/config/vufind/LOTS.ini +++ b/config/vufind/LOTS.ini @@ -36,6 +36,11 @@ kohaOpacUrl = https://opac/ ;forgot_password_regex = forgot_password_regex = "/^\d{4}$/" +; Set to true to write password reset events to a separate log file. +; Log file will be created in the same directory as vufind.log (from config.ini [Logging] file setting). +; Log file name: password_reset.log +password_reset_log = true + [NewPatron] enable=false url= /koha-user/create @@ -133,6 +138,11 @@ showAvailabilityInResults = false ; Set to false to hide the availability secti enableDebug = false ; Set to true to enable debug logging maxBranches = 100 ; Maximum number of branches to process +; LotsLerum-XXX +; When true, Exemplartyp/Avdelning/Placering are shown only in item collapse rows, +; not in the library summary row. +holdingsColumnsInCollapseOnly = false + [NotFinished] showBankIDOption = false diff --git a/languages/sv.ini b/languages/sv.ini index 5f252960671..41bb27fd8c6 100644 --- a/languages/sv.ini +++ b/languages/sv.ini @@ -1830,3 +1830,4 @@ search_aria_description = "Sök i bibliotekskatalogen (titel, författare, ämne ; vufind v11 holds_descriptive_text = "Textbeskrivning" select_page = "Sida" +Cancel = "Avbryt" diff --git a/module/LOTS/src/LOTS/Controller/ForgotPasswordController.php b/module/LOTS/src/LOTS/Controller/ForgotPasswordController.php index 7e27f15f261..c6b4bbea9f1 100644 --- a/module/LOTS/src/LOTS/Controller/ForgotPasswordController.php +++ b/module/LOTS/src/LOTS/Controller/ForgotPasswordController.php @@ -14,6 +14,7 @@ class ForgotPasswordController extends \VuFind\Controller\AbstractBase implement { use \VuFindHttp\HttpServiceAwareTrait; use \VuFind\ILS\Driver\OAuth2TokenTrait; + use PasswordResetLogTrait; protected $koha_rest_config = null; protected $oauth_token = null; @@ -47,6 +48,10 @@ public function homeAction() // Handle form submission $username = $this->params()->fromPost('username'); + if ($this->getRequest()->isPost() && empty($username)) { + $this->logPasswordReset('Password reset: form submitted with empty username'); + } + if (!empty($username)) { try { // Search for patron using configured search fields @@ -60,16 +65,23 @@ public function homeAction() // Send email $this->sendResetEmail($patron['email'], $token); + $this->logPasswordReset('Password reset: email sent to patron_id=' . $patron['patron_id']); // Generic message (don't reveal if user exists) $message = $this->translate('password_reset_email_sent'); $messageType = 'success'; } else { + // Log why reset was not sent (for troubleshooting), but show generic message + if (!$patron) { + $this->logPasswordReset('Password reset: patron not found for input=' . $username); + } else { + $this->logPasswordReset('Password reset: patron found but no email configured, patron_id=' . ($patron['patron_id'] ?? 'unknown')); + } // Generic message (don't reveal if user exists or has no email) $message = $this->translate('password_reset_email_sent'); $messageType = 'success'; } } catch (\Exception $e) { - error_log('Password reset error: ' . $e->getMessage()); + $this->logPasswordReset('Password reset error: ' . $e->getMessage()); $message = $this->translate('password_reset_error'); $messageType = 'error'; } @@ -109,7 +121,7 @@ protected function findPatron(string $searchValue): ?array $searchFields = array_intersect($searchFields, $validFields); if (empty($searchFields)) { - error_log('No valid patron search fields configured'); + $this->logPasswordReset('No valid patron search fields configured'); return null; } @@ -169,7 +181,7 @@ protected function searchPatronByField(string $field, string $value): ?array 'userid' => $patron['userid'] ?? null ]; } catch (\Exception $e) { - error_log("Patron search by $field failed: " . $e->getMessage()); + $this->logPasswordReset("Patron search by $field failed: " . $e->getMessage()); return null; } } @@ -228,4 +240,5 @@ protected function getOAuth2Token(): string } return $token->getHeaderValue(); } + } diff --git a/module/LOTS/src/LOTS/Controller/PasswordResetLogTrait.php b/module/LOTS/src/LOTS/Controller/PasswordResetLogTrait.php new file mode 100644 index 00000000000..548c275f92d --- /dev/null +++ b/module/LOTS/src/LOTS/Controller/PasswordResetLogTrait.php @@ -0,0 +1,40 @@ +getConfig('LOTS'); + $enabled = $lotsConfig->PasswordRecovery->password_reset_log ?? false; + + $logMessage = date('Y-m-d H:i:s') . ' [PasswordReset] ' . $message . PHP_EOL; + + if ($enabled) { + // Derive log dir from main config [Logging] file setting + $mainConfig = $this->getConfig(); + $logFile = $mainConfig->Logging->file ?? ''; + // Strip alert level suffix (e.g. "/var/log/vufind/vufind.log:alert,error") + $logFilePath = explode(':', $logFile)[0]; + $logDir = $logFilePath ? dirname($logFilePath) : '/var/log/vufind'; + $resetLogFile = rtrim($logDir, '/') . '/password_reset.log'; + error_log($logMessage, 3, $resetLogFile); + } else { + error_log('[PasswordReset] ' . $message); + } + } +} diff --git a/module/LOTS/src/LOTS/Controller/ResetPasswordController.php b/module/LOTS/src/LOTS/Controller/ResetPasswordController.php index f27802848a5..859c656648a 100644 --- a/module/LOTS/src/LOTS/Controller/ResetPasswordController.php +++ b/module/LOTS/src/LOTS/Controller/ResetPasswordController.php @@ -14,6 +14,7 @@ class ResetPasswordController extends \VuFind\Controller\AbstractBase implements { use \VuFindHttp\HttpServiceAwareTrait; use \VuFind\ILS\Driver\OAuth2TokenTrait; + use PasswordResetLogTrait; protected $koha_rest_config = null; protected $oauth_token = null; @@ -26,8 +27,8 @@ class ResetPasswordController extends \VuFind\Controller\AbstractBase implements public function homeAction() { $token = $this->params()->fromQuery('token') ?? $this->params()->fromPost('token'); - error_log("DEBUG: Received token from URL: " . var_export($token, true)); - error_log("DEBUG: Token length: " . strlen($token)); + $this->logPasswordReset("DEBUG: Received token from URL: " . var_export($token, true)); + $this->logPasswordReset("DEBUG: Token length: " . strlen($token)); $message = ''; $messageType = 'info'; $tokenValid = false; @@ -40,7 +41,7 @@ public function homeAction() // Validate token $tokenTable = $this->getTable('PasswordResetToken'); $tokenData = $tokenTable->getValidToken($token); - error_log("DEBUG: Token data from DB: " . var_export($tokenData, true)); + $this->logPasswordReset("DEBUG: Token data from DB: " . var_export($tokenData, true)); if (!$tokenData) { $message = $this->translate('password_reset_token_expired'); @@ -64,15 +65,13 @@ public function homeAction() // Mark token as used $tokenTable->markAsUsed($token); - $message = $this->translate('password_reset_success'); - $messageType = 'success'; - $tokenValid = false; // Hide form - - // Redirect to login after 3 seconds - $this->layout()->setVariable('redirectUrl', '/vufind/MyResearch/Home'); - $this->layout()->setVariable('redirectDelay', 3000); + // Redirect to login with success flash message + // Important: do NOT render on /ResetPassword?token=... URL + // so VuFind does not store it as followup after login + $this->flashMessenger()->addMessage('password_reset_success', 'success'); + return $this->redirect()->toRoute('myresearch-userlogin'); } catch (\Exception $e) { - error_log('Password update error: ' . $e->getMessage()); + $this->logPasswordReset('Password update error: ' . $e->getMessage()); $message = $this->translate('password_reset_update_error'); $messageType = 'error'; } @@ -145,7 +144,7 @@ protected function updatePatronPassword(string $patronId, string $newPin): void $this->koha_rest_config = $this->getConfig('KohaRest'); $this->oauth_token = $this->getOAuth2Token(); - error_log("DEBUG: Updating password for patron: " . $patronId); + $this->logPasswordReset("DEBUG: Updating password for patron: " . $patronId); // Use correct endpoint: POST /patrons/{id}/password $data = [ @@ -155,7 +154,7 @@ protected function updatePatronPassword(string $patronId, string $newPin): void $response = $this->json_http("POST", "/patrons/$patronId/password", json_encode($data)); - error_log("Koha password update response: " . $response); + $this->logPasswordReset("Koha password update response: " . $response); // Check if response contains error $result = json_decode($response, true); @@ -215,6 +214,7 @@ public function json_http($method, $api, $postData = null) // Get the response body/JSON return $response->getBody(); -} + + } } diff --git a/module/LOTS/src/LOTS/Db/Table/PasswordResetToken.php b/module/LOTS/src/LOTS/Db/Table/PasswordResetToken.php index b0449ae06d7..ad2d429c43f 100644 --- a/module/LOTS/src/LOTS/Db/Table/PasswordResetToken.php +++ b/module/LOTS/src/LOTS/Db/Table/PasswordResetToken.php @@ -10,11 +10,9 @@ * @link https://vufind.org Main Site */ namespace LOTS\Db\Table; - use Laminas\Db\Adapter\Adapter; use VuFind\Db\Row\RowGateway; use VuFind\Db\Table\PluginManager; - /** * Table Definition for lots_password_reset_tokens * @@ -42,6 +40,28 @@ public function __construct( $table = 'lots_password_reset_tokens' ) { parent::__construct($adapter, $tm, $cfg, $rowObj, $table); + $this->ensureTableExists(); + } + + /** + * Create the table if it does not exist + * + * @return void + */ + protected function ensureTableExists(): void + { + $sql = "CREATE TABLE IF NOT EXISTS lots_password_reset_tokens ( + id int(11) NOT NULL AUTO_INCREMENT, + user_id varchar(255) NOT NULL, + token varchar(255) NOT NULL, + email varchar(255) NOT NULL, + created_at timestamp NOT NULL DEFAULT current_timestamp(), + expires_at timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + used tinyint(4) DEFAULT 0, + PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci"; + + $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); } /** @@ -54,13 +74,9 @@ public function __construct( */ public function createToken(string $userId, string $email): string { - // Generate secure random token $token = bin2hex(random_bytes(32)); - - // Calculate expiration (2 days from now) $expiresAt = date('Y-m-d H:i:s', strtotime('+2 days')); - - // Insert into database + $this->insert([ 'user_id' => $userId, 'token' => $token, @@ -68,7 +84,7 @@ public function createToken(string $userId, string $email): string 'expires_at' => $expiresAt, 'used' => 0 ]); - + return $token; } @@ -88,13 +104,13 @@ public function getValidToken(string $token): ?array ]); $select->where->lessThanOrEqualTo('created_at', date('Y-m-d H:i:s')); $select->where->greaterThan('expires_at', date('Y-m-d H:i:s')); - + $result = $this->selectWith($select)->current(); - + if (!$result) { return null; } - + return [ 'id' => $result->id, 'user_id' => $result->user_id, diff --git a/themes/lots/templates/RecordTab/holdingsils.phtml b/themes/lots/templates/RecordTab/holdingsils.phtml index f31bbae8b74..99e0e4d8e87 100644 --- a/themes/lots/templates/RecordTab/holdingsils.phtml +++ b/themes/lots/templates/RecordTab/holdingsils.phtml @@ -44,6 +44,9 @@ } $columns = array_map('trim', explode(',', $defaultColumns)); $lots_showItemsInVufind = $config->Record->showItemsInVufind ?? false; + // LotsLerum-XXX: When true, Exemplartyp/Avdelning/Placering are shown only in + // item collapse rows, not in the library summary row. + $holdingsColumnsInCollapseOnly = $config->Record->holdingsColumnsInCollapseOnly ?? false; if ($lots_showItemsInVufind) { $global_total_items = 0; $global_avail_items = 0; @@ -169,9 +172,14 @@ ".$this->transEsc($holding["items"]["0"]['item']['item_type_id']).""; + if ($holdingsColumnsInCollapseOnly) { + echo "