diff --git a/public/static/style.css b/public/static/style.css index 6874ca5..6064a86 100644 --- a/public/static/style.css +++ b/public/static/style.css @@ -12,7 +12,7 @@ body { font-family: sans-serif; } -/* --- Header --- */ +/* -- Header -- */ header { margin: 1rem; padding: 0 1rem; @@ -26,7 +26,7 @@ header h1 { margin: 1rem; } -/* --- Navigation bar --- */ +/* -- Navigation bar -- */ nav { } @@ -75,12 +75,12 @@ nav li.nav_current_page a { border-bottom-color: #ffffff; } -/* --- Main section --- */ +/* -- Main section -- */ main { margin: 2rem; } -/* --- Login page --- */ +/* -- Login page -- */ main.login_page { margin: 2rem; padding: 1rem; @@ -88,7 +88,7 @@ main.login_page { width: 40rem; } -/* --- Text and other styling --- */ +/* -- Text and other styling -- */ h2, h4 { margin: 0 0 0.5em 0; } @@ -103,20 +103,6 @@ a:hover, a:focus { text-decoration: underline; } -.error_box, .success_box { - max-width: 50rem; - margin: 1rem 0; - padding: 1rem; -} - -.error_box { - background: #ff4444; -} - -.success_box { - background: #00cc00; -} - .gray { color: gray; } @@ -137,7 +123,7 @@ button { color: gray; } -/* --- Tables --- */ +/* -- Tables -- */ table td, table th { padding: 0.25rem 0.5rem; @@ -156,16 +142,49 @@ table.bordered_table th { text-align: left; } -/* --- Boxes --- */ +/* -- Boxes -- */ +.filter_options, +.form_box, +.confirmation_box, +.error_box, +.success_box { + margin: 1rem 0; + padding: 1rem; +} + .filter_options, .form_box, .confirmation_box { border: 1px solid #999999; - padding: 1rem; - margin: 1rem 0; } -/* --- Detail boxes --- */ +.form_box p, +.error_box p, +.success_box p { + margin: 0.75rem 0; +} + +.form_box p:last-child, +.error_box p:last-child, +.success_box p:last-child { + margin-bottom: 0; +} + +/* -- Error / success boxes -- */ +.error_box, +.success_box { + max-width: 50rem; +} + +.error_box { + background: #ff4444; +} + +.success_box { + background: #00cc00; +} + +/* -- Detail boxes -- */ details { margin: 1rem 0; } @@ -179,7 +198,7 @@ details > p { font-family: monospace; } -/* --- Detail columns --- */ +/* -- Detail columns -- */ input#show_details_checkbox { margin-bottom: 1rem; } @@ -188,12 +207,7 @@ input#show_details_checkbox:not(:checked) ~ table .detail_column { display: none; } -/* --- Form box --- */ -.form_box p:last-child { - margin-bottom: 0; -} - -/* --- Confirmation box --- */ +/* -- Confirmation box -- */ .confirmation_box p:first-child { margin-top: 0; } diff --git a/src/Common/PasswordHelper.php b/src/Common/PasswordHelper.php index 2b14c66..c533cb1 100644 --- a/src/Common/PasswordHelper.php +++ b/src/Common/PasswordHelper.php @@ -24,4 +24,20 @@ class PasswordHelper $passwordHashInfo = password_get_info($passwordHash); return $passwordHashInfo['algoName'] ?? 'unknown'; } + + public function generateRandomPassword(int $length = 24): string + { + // We want a random string of alphanumeric characters. Simple way to do this is to get random bytes, convert them to base64, + // strip all characters from it that aren't alphanumeric (+, /, =), and take the first $length characters from it. + + // Get lots of random bytes (will be much more than we need, but this way we don't need to worry about the string being too short + // after stripping the non-alphanumeric base64 characters). + $randomBytes = random_bytes(2 * $length); + + // Convert to base64 and strip non-alphanumeric characters + $randomAlphaNumeric = str_replace(['+', '/', '='], '', base64_encode($randomBytes)); + + // Shorten string + return substr($randomAlphaNumeric, 0, $length); + } } diff --git a/src/Frontend/Accounts/AccountController.php b/src/Frontend/Accounts/AccountController.php index 3b46c6e..29a0762 100644 --- a/src/Frontend/Accounts/AccountController.php +++ b/src/Frontend/Accounts/AccountController.php @@ -71,13 +71,14 @@ class AccountController extends BaseController try { // Validate input $validatedCreateData = AccountCreateData::createFromArray($createData); - $newAccountId = $this->accountHandler->createNewAccount($validatedCreateData); + $createResult = $this->accountHandler->createNewAccount($validatedCreateData); // Save success result - $newAccountName = $validatedCreateData->getUsername(); - $this->sessionHelper->setLastActionResult(ActionResult::createSuccessResult( - 'Account ' . $newAccountName . ' was created.' - )); + $successMessage = "Account {$createResult['username']} was created."; + if (!empty($createResult['generatedPassword'])) { + $successMessage .= "\nThe password generated for this account is: {$createResult['generatedPassword']}"; + } + $this->sessionHelper->setLastActionResult(ActionResult::createSuccessResult($successMessage)); } catch (InputValidationError $e) { // Save error result $this->sessionHelper->setLastActionResult(ActionResult::createErrorResult($e->getMessage(), $createData)); @@ -116,10 +117,14 @@ class AccountController extends BaseController try { // Validate input $validatedEditData = AccountEditData::createFromArray($editData); - $this->accountHandler->editAccountData($accountId, $validatedEditData); + $editResult = $this->accountHandler->editAccountData($accountId, $validatedEditData); // Save success result - $this->sessionHelper->setLastActionResult(ActionResult::createSuccessResult('Account data was saved.')); + $successMessage = "Account data was saved."; + if (!empty($editResult['generatedPassword'])) { + $successMessage .= "\nThe new password generated for this account is: {$editResult['generatedPassword']}"; + } + $this->sessionHelper->setLastActionResult(ActionResult::createSuccessResult($successMessage)); } catch (InputValidationError $e) { // Save error result $this->sessionHelper->setLastActionResult(ActionResult::createErrorResult($e->getMessage(), $editData)); diff --git a/src/Frontend/Accounts/AccountCreateData.php b/src/Frontend/Accounts/AccountCreateData.php index 5da7bcd..265e5ed 100644 --- a/src/Frontend/Accounts/AccountCreateData.php +++ b/src/Frontend/Accounts/AccountCreateData.php @@ -8,12 +8,12 @@ use MailAccountAdmin\Common\FormData; class AccountCreateData extends FormData { private string $username; - private string $password; + private ?string $password; private bool $active; private ?string $homeDir; private string $memo; - private function __construct(string $username, string $password, bool $active, ?string $homeDir, string $memo) + private function __construct(string $username, ?string $password, bool $active, ?string $homeDir, string $memo) { $this->username = $username; $this->password = $password; @@ -26,7 +26,7 @@ class AccountCreateData extends FormData { return new self( self::validateUsername(trim($raw['username'] ?? '')), - self::validatePassword(trim($raw['password'] ?? ''), trim($raw['password_repeat'] ?? '')), + self::validatePassword(trim($raw['password'] ?? ''), trim($raw['password_repeat'] ?? ''), false), self::validateBoolOption(trim($raw['is_active'] ?? '')), self::validateHomeDir(trim($raw['home_dir'] ?? ''), false), self::validateMemo(trim($raw['memo'] ?? '')), @@ -38,7 +38,7 @@ class AccountCreateData extends FormData return $this->username; } - public function getPassword(): string + public function getPassword(): ?string { return $this->password; } diff --git a/src/Frontend/Accounts/AccountEditData.php b/src/Frontend/Accounts/AccountEditData.php index 9f5f67d..a48a88e 100644 --- a/src/Frontend/Accounts/AccountEditData.php +++ b/src/Frontend/Accounts/AccountEditData.php @@ -11,17 +11,19 @@ class AccountEditData extends FormData private bool $usernameCreateAlias; private bool $usernameReplaceAlias; private ?string $password; + private bool $passwordGenerateRandom; private bool $active; private ?string $homeDir; private string $memo; - private function __construct(?string $username, bool $usernameCreateAlias, bool $usernameReplaceAlias, ?string $password, bool $active, - ?string $homeDir, string $memo) + private function __construct(?string $username, bool $usernameCreateAlias, bool $usernameReplaceAlias, ?string $password, + bool $passwordGenerateRandom, bool $active, ?string $homeDir, string $memo) { $this->username = $username; $this->usernameCreateAlias = $usernameCreateAlias; $this->usernameReplaceAlias = $usernameReplaceAlias; $this->password = $password; + $this->passwordGenerateRandom = $passwordGenerateRandom; $this->active = $active; $this->homeDir = $homeDir; $this->memo = $memo; @@ -34,6 +36,7 @@ class AccountEditData extends FormData self::validateBoolOption(trim($raw['username_create_alias'] ?? '')), self::validateBoolOption(trim($raw['username_replace_alias'] ?? '')), self::validatePassword(trim($raw['password'] ?? ''), trim($raw['password_repeat'] ?? ''), false), + self::validateBoolOption(trim($raw['password_generate_random'] ?? '')), self::validateBoolOption(trim($raw['is_active'] ?? '')), self::validateHomeDir(trim($raw['home_dir'] ?? ''), false), self::validateMemo(trim($raw['memo'] ?? '')), @@ -60,6 +63,11 @@ class AccountEditData extends FormData return $this->password; } + public function getPasswordGenerateRandom(): bool + { + return $this->passwordGenerateRandom; + } + public function getActive(): bool { return $this->active; diff --git a/src/Frontend/Accounts/AccountHandler.php b/src/Frontend/Accounts/AccountHandler.php index 4d798e0..4e58e5a 100644 --- a/src/Frontend/Accounts/AccountHandler.php +++ b/src/Frontend/Accounts/AccountHandler.php @@ -72,7 +72,7 @@ class AccountHandler ]; } - public function createNewAccount(AccountCreateData $createData): int + public function createNewAccount(AccountCreateData $createData): array { // Check if new username is still available $username = $createData->getUsername(); @@ -80,8 +80,18 @@ class AccountHandler throw new InputValidationError("Username \"$username\" is not available."); } + // Array returned by the function on success + $returnData = [ + 'username' => $username, + ]; + // Hash new password - $passwordHash = $this->passwordHelper->hashPassword($createData->getPassword()); + $password = $createData->getPassword(); + if ($password === null) { + $password = $this->passwordHelper->generateRandomPassword(); + $returnData['generatedPassword'] = $password; + } + $passwordHash = $this->passwordHelper->hashPassword($password); // Construct home directory from username if necessary if ($createData->getHomeDir() !== null) { @@ -92,13 +102,15 @@ class AccountHandler } // Create account in database - return $this->accountRepository->insertAccount( + $returnData['id'] = $this->accountRepository->insertAccount( $username, $passwordHash, $createData->getActive(), $homeDir, $createData->getMemo() ); + + return $returnData; } @@ -116,8 +128,11 @@ class AccountHandler ]; } - public function editAccountData(int $accountId, AccountEditData $editData): void + public function editAccountData(int $accountId, AccountEditData $editData): array { + // Array returned by the function on success + $returnData = []; + // Check if account exists try { $account = $this->accountRepository->fetchAccountById($accountId); @@ -175,12 +190,16 @@ class AccountHandler $this->aliasRepository->createNewAlias($accountId, $oldUsername); } - // Hash new password - $newPasswordHash = null; - if ($editData->getPassword() !== null) { - $newPasswordHash = $this->passwordHelper->hashPassword($editData->getPassword()); + // Generate new password (if wanted) + $newPassword = $editData->getPassword(); + if ($editData->getPasswordGenerateRandom()) { + $newPassword = $this->passwordHelper->generateRandomPassword(); + $returnData['generatedPassword'] = $newPassword; } + // Hash new password + $newPasswordHash = $newPassword !== null ? $this->passwordHelper->hashPassword($newPassword) : null; + // Update account in database $this->accountRepository->updateAccountWithId( $accountId, @@ -198,6 +217,8 @@ class AccountHandler // Commit database transaction $this->accountRepository->commitTransaction(); + + return $returnData; } diff --git a/templates/account_create.html.twig b/templates/account_create.html.twig index d881f7b..246aae7 100644 --- a/templates/account_create.html.twig +++ b/templates/account_create.html.twig @@ -37,6 +37,7 @@
The password will be hashed using the current default hash algorithm.
+Leave blank to generate a random password.
| diff --git a/templates/account_edit.html.twig b/templates/account_edit.html.twig index 6b9c71a..e1dbfa0 100644 --- a/templates/account_edit.html.twig +++ b/templates/account_edit.html.twig @@ -62,6 +62,14 @@ | ||
| + + | +||
{{ success | split('\n') | join('
') | raw }}
{{ error | split('\n') | join('
') | raw }}