Implement random password generation (create/edit account)

This commit is contained in:
Lexi / Zoe 2021-09-16 19:55:38 +02:00
parent 48925f283f
commit 2ccee2169b
Signed by: binaryDiv
GPG Key ID: F8D4956E224DA232
9 changed files with 127 additions and 54 deletions

View File

@ -12,7 +12,7 @@ body {
font-family: sans-serif; font-family: sans-serif;
} }
/* --- Header --- */ /* -- Header -- */
header { header {
margin: 1rem; margin: 1rem;
padding: 0 1rem; padding: 0 1rem;
@ -26,7 +26,7 @@ header h1 {
margin: 1rem; margin: 1rem;
} }
/* --- Navigation bar --- */ /* -- Navigation bar -- */
nav { nav {
} }
@ -75,12 +75,12 @@ nav li.nav_current_page a {
border-bottom-color: #ffffff; border-bottom-color: #ffffff;
} }
/* --- Main section --- */ /* -- Main section -- */
main { main {
margin: 2rem; margin: 2rem;
} }
/* --- Login page --- */ /* -- Login page -- */
main.login_page { main.login_page {
margin: 2rem; margin: 2rem;
padding: 1rem; padding: 1rem;
@ -88,7 +88,7 @@ main.login_page {
width: 40rem; width: 40rem;
} }
/* --- Text and other styling --- */ /* -- Text and other styling -- */
h2, h4 { h2, h4 {
margin: 0 0 0.5em 0; margin: 0 0 0.5em 0;
} }
@ -103,20 +103,6 @@ a:hover, a:focus {
text-decoration: underline; text-decoration: underline;
} }
.error_box, .success_box {
max-width: 50rem;
margin: 1rem 0;
padding: 1rem;
}
.error_box {
background: #ff4444;
}
.success_box {
background: #00cc00;
}
.gray { .gray {
color: gray; color: gray;
} }
@ -137,7 +123,7 @@ button {
color: gray; color: gray;
} }
/* --- Tables --- */ /* -- Tables -- */
table td, table th { table td, table th {
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
@ -156,16 +142,49 @@ table.bordered_table th {
text-align: left; text-align: left;
} }
/* --- Boxes --- */ /* -- Boxes -- */
.filter_options,
.form_box,
.confirmation_box,
.error_box,
.success_box {
margin: 1rem 0;
padding: 1rem;
}
.filter_options, .filter_options,
.form_box, .form_box,
.confirmation_box { .confirmation_box {
border: 1px solid #999999; 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 { details {
margin: 1rem 0; margin: 1rem 0;
} }
@ -179,7 +198,7 @@ details > p {
font-family: monospace; font-family: monospace;
} }
/* --- Detail columns --- */ /* -- Detail columns -- */
input#show_details_checkbox { input#show_details_checkbox {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
@ -188,12 +207,7 @@ input#show_details_checkbox:not(:checked) ~ table .detail_column {
display: none; display: none;
} }
/* --- Form box --- */ /* -- Confirmation box -- */
.form_box p:last-child {
margin-bottom: 0;
}
/* --- Confirmation box --- */
.confirmation_box p:first-child { .confirmation_box p:first-child {
margin-top: 0; margin-top: 0;
} }

View File

@ -24,4 +24,20 @@ class PasswordHelper
$passwordHashInfo = password_get_info($passwordHash); $passwordHashInfo = password_get_info($passwordHash);
return $passwordHashInfo['algoName'] ?? 'unknown'; 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);
}
} }

View File

@ -71,13 +71,14 @@ class AccountController extends BaseController
try { try {
// Validate input // Validate input
$validatedCreateData = AccountCreateData::createFromArray($createData); $validatedCreateData = AccountCreateData::createFromArray($createData);
$newAccountId = $this->accountHandler->createNewAccount($validatedCreateData); $createResult = $this->accountHandler->createNewAccount($validatedCreateData);
// Save success result // Save success result
$newAccountName = $validatedCreateData->getUsername(); $successMessage = "Account <a href=\"/accounts/{$createResult['id']}\">{$createResult['username']}</a> was created.";
$this->sessionHelper->setLastActionResult(ActionResult::createSuccessResult( if (!empty($createResult['generatedPassword'])) {
'Account <a href="/accounts/' . $newAccountId . '">' . $newAccountName . '</a> was created.' $successMessage .= "\nThe password generated for this account is: <i>{$createResult['generatedPassword']}</i>";
)); }
$this->sessionHelper->setLastActionResult(ActionResult::createSuccessResult($successMessage));
} catch (InputValidationError $e) { } catch (InputValidationError $e) {
// Save error result // Save error result
$this->sessionHelper->setLastActionResult(ActionResult::createErrorResult($e->getMessage(), $createData)); $this->sessionHelper->setLastActionResult(ActionResult::createErrorResult($e->getMessage(), $createData));
@ -116,10 +117,14 @@ class AccountController extends BaseController
try { try {
// Validate input // Validate input
$validatedEditData = AccountEditData::createFromArray($editData); $validatedEditData = AccountEditData::createFromArray($editData);
$this->accountHandler->editAccountData($accountId, $validatedEditData); $editResult = $this->accountHandler->editAccountData($accountId, $validatedEditData);
// Save success result // 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: <i>{$editResult['generatedPassword']}</i>";
}
$this->sessionHelper->setLastActionResult(ActionResult::createSuccessResult($successMessage));
} catch (InputValidationError $e) { } catch (InputValidationError $e) {
// Save error result // Save error result
$this->sessionHelper->setLastActionResult(ActionResult::createErrorResult($e->getMessage(), $editData)); $this->sessionHelper->setLastActionResult(ActionResult::createErrorResult($e->getMessage(), $editData));

View File

@ -8,12 +8,12 @@ use MailAccountAdmin\Common\FormData;
class AccountCreateData extends FormData class AccountCreateData extends FormData
{ {
private string $username; private string $username;
private string $password; private ?string $password;
private bool $active; private bool $active;
private ?string $homeDir; private ?string $homeDir;
private string $memo; 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->username = $username;
$this->password = $password; $this->password = $password;
@ -26,7 +26,7 @@ class AccountCreateData extends FormData
{ {
return new self( return new self(
self::validateUsername(trim($raw['username'] ?? '')), 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::validateBoolOption(trim($raw['is_active'] ?? '')),
self::validateHomeDir(trim($raw['home_dir'] ?? ''), false), self::validateHomeDir(trim($raw['home_dir'] ?? ''), false),
self::validateMemo(trim($raw['memo'] ?? '')), self::validateMemo(trim($raw['memo'] ?? '')),
@ -38,7 +38,7 @@ class AccountCreateData extends FormData
return $this->username; return $this->username;
} }
public function getPassword(): string public function getPassword(): ?string
{ {
return $this->password; return $this->password;
} }

View File

@ -11,17 +11,19 @@ class AccountEditData extends FormData
private bool $usernameCreateAlias; private bool $usernameCreateAlias;
private bool $usernameReplaceAlias; private bool $usernameReplaceAlias;
private ?string $password; private ?string $password;
private bool $passwordGenerateRandom;
private bool $active; private bool $active;
private ?string $homeDir; private ?string $homeDir;
private string $memo; private string $memo;
private function __construct(?string $username, bool $usernameCreateAlias, bool $usernameReplaceAlias, ?string $password, bool $active, private function __construct(?string $username, bool $usernameCreateAlias, bool $usernameReplaceAlias, ?string $password,
?string $homeDir, string $memo) bool $passwordGenerateRandom, bool $active, ?string $homeDir, string $memo)
{ {
$this->username = $username; $this->username = $username;
$this->usernameCreateAlias = $usernameCreateAlias; $this->usernameCreateAlias = $usernameCreateAlias;
$this->usernameReplaceAlias = $usernameReplaceAlias; $this->usernameReplaceAlias = $usernameReplaceAlias;
$this->password = $password; $this->password = $password;
$this->passwordGenerateRandom = $passwordGenerateRandom;
$this->active = $active; $this->active = $active;
$this->homeDir = $homeDir; $this->homeDir = $homeDir;
$this->memo = $memo; $this->memo = $memo;
@ -34,6 +36,7 @@ class AccountEditData extends FormData
self::validateBoolOption(trim($raw['username_create_alias'] ?? '')), self::validateBoolOption(trim($raw['username_create_alias'] ?? '')),
self::validateBoolOption(trim($raw['username_replace_alias'] ?? '')), self::validateBoolOption(trim($raw['username_replace_alias'] ?? '')),
self::validatePassword(trim($raw['password'] ?? ''), trim($raw['password_repeat'] ?? ''), false), self::validatePassword(trim($raw['password'] ?? ''), trim($raw['password_repeat'] ?? ''), false),
self::validateBoolOption(trim($raw['password_generate_random'] ?? '')),
self::validateBoolOption(trim($raw['is_active'] ?? '')), self::validateBoolOption(trim($raw['is_active'] ?? '')),
self::validateHomeDir(trim($raw['home_dir'] ?? ''), false), self::validateHomeDir(trim($raw['home_dir'] ?? ''), false),
self::validateMemo(trim($raw['memo'] ?? '')), self::validateMemo(trim($raw['memo'] ?? '')),
@ -60,6 +63,11 @@ class AccountEditData extends FormData
return $this->password; return $this->password;
} }
public function getPasswordGenerateRandom(): bool
{
return $this->passwordGenerateRandom;
}
public function getActive(): bool public function getActive(): bool
{ {
return $this->active; return $this->active;

View File

@ -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 // Check if new username is still available
$username = $createData->getUsername(); $username = $createData->getUsername();
@ -80,8 +80,18 @@ class AccountHandler
throw new InputValidationError("Username \"$username\" is not available."); throw new InputValidationError("Username \"$username\" is not available.");
} }
// Array returned by the function on success
$returnData = [
'username' => $username,
];
// Hash new password // 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 // Construct home directory from username if necessary
if ($createData->getHomeDir() !== null) { if ($createData->getHomeDir() !== null) {
@ -92,13 +102,15 @@ class AccountHandler
} }
// Create account in database // Create account in database
return $this->accountRepository->insertAccount( $returnData['id'] = $this->accountRepository->insertAccount(
$username, $username,
$passwordHash, $passwordHash,
$createData->getActive(), $createData->getActive(),
$homeDir, $homeDir,
$createData->getMemo() $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 // Check if account exists
try { try {
$account = $this->accountRepository->fetchAccountById($accountId); $account = $this->accountRepository->fetchAccountById($accountId);
@ -175,12 +190,16 @@ class AccountHandler
$this->aliasRepository->createNewAlias($accountId, $oldUsername); $this->aliasRepository->createNewAlias($accountId, $oldUsername);
} }
// Hash new password // Generate new password (if wanted)
$newPasswordHash = null; $newPassword = $editData->getPassword();
if ($editData->getPassword() !== null) { if ($editData->getPasswordGenerateRandom()) {
$newPasswordHash = $this->passwordHelper->hashPassword($editData->getPassword()); $newPassword = $this->passwordHelper->generateRandomPassword();
$returnData['generatedPassword'] = $newPassword;
} }
// Hash new password
$newPasswordHash = $newPassword !== null ? $this->passwordHelper->hashPassword($newPassword) : null;
// Update account in database // Update account in database
$this->accountRepository->updateAccountWithId( $this->accountRepository->updateAccountWithId(
$accountId, $accountId,
@ -198,6 +217,8 @@ class AccountHandler
// Commit database transaction // Commit database transaction
$this->accountRepository->commitTransaction(); $this->accountRepository->commitTransaction();
return $returnData;
} }

View File

@ -37,6 +37,7 @@
<div class="form_box"> <div class="form_box">
<h4>Password</h4> <h4>Password</h4>
<p>The password will be hashed using the current default hash algorithm.</p> <p>The password will be hashed using the current default hash algorithm.</p>
<p>Leave blank to generate a random password.</p>
<table> <table>
<tr> <tr>
<td><label for="create_password">New password:</label></td> <td><label for="create_password">New password:</label></td>

View File

@ -62,6 +62,14 @@
<td><label for="edit_password_repeat">Repeat password:</label></td> <td><label for="edit_password_repeat">Repeat password:</label></td>
<td><input type="password" id="edit_password_repeat" name="password_repeat" value="{{ formData['password_repeat'] | default('') }}"/></td> <td><input type="password" id="edit_password_repeat" name="password_repeat" value="{{ formData['password_repeat'] | default('') }}"/></td>
</tr> </tr>
<tr>
<td colspan="2">
<label>
<input type="checkbox" name="password_generate_random" {{ formData['password_generate_random'] | default('') ? 'checked' : '' }}/>
Generate new random password
</label>
</td>
</tr>
</table> </table>
</div> </div>

View File

@ -1,11 +1,11 @@
{% if success is defined %} {% if success is defined %}
<div class="success_box"> <div class="success_box">
<h4>Success</h4> <h4>Success</h4>
{{ success | raw }} <p>{{ success | split('\n') | join('</p><p>') | raw }}</p>
</div> </div>
{% elseif error is defined %} {% elseif error is defined %}
<div class="error_box"> <div class="error_box">
<h4>Error</h4> <h4>Error</h4>
{{ error | raw }} <p>{{ error | split('\n') | join('</p><p>') | raw }}</p>
</div> </div>
{% endif %} {% endif %}