Implement random password generation (create/edit account)
This commit is contained in:
parent
48925f283f
commit
2ccee2169b
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue