Skip to content

Commit

Permalink
Merge pull request #309 from leepeuker/304-add-user-managment-ui
Browse files Browse the repository at this point in the history
Add user managment settings page
  • Loading branch information
leepeuker authored Apr 12, 2023
2 parents 7fabc2f + 8e1249e commit 0d3cbca
Show file tree
Hide file tree
Showing 16 changed files with 550 additions and 18 deletions.
3 changes: 3 additions & 0 deletions public/css/settings-user.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.invalid-input {
border-color: rgb(220, 53, 69)!important;
}
252 changes: 252 additions & 0 deletions public/js/settings-users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
const userModal = new bootstrap.Modal('#userModal', {keyboard: false})

const table = document.getElementById('usersTable');
const rows = table.getElementsByTagName('tr');

reloadTable()

function registerTableRowClickEvent() {
for (let i = 0; i < rows.length; i++) {
if (i === 0) continue

rows[i].onclick = function () {
prepareEditUserModal(
this.cells[0].innerHTML,
this.cells[1].innerHTML,
this.cells[2].innerHTML,
this.cells[3].innerHTML === '1'
)

userModal.show()
};
}
}

function showCreateUserModal() {
prepareCreateUserModal()
userModal.show()
}

function prepareCreateUserModal(name) {
document.getElementById('userModalHeaderTitle').innerHTML = 'Create user'

document.getElementById('userModalPasswordInput').required = true
document.getElementById('userModalRepeatPasswordInput').required = false

document.getElementById('userModalPasswordInputRequiredStar').classList.remove('d-none')
document.getElementById('userModalRepeatPasswordInputRequiredStar').classList.remove('d-none')
document.getElementById('userModalFooterCreateButton').classList.remove('d-none')
document.getElementById('userModalFooterButtons').classList.add('d-none')

document.getElementById('userModalIdInput').value = ''
document.getElementById('userModalNameInput').value = ''
document.getElementById('userModalEmailInput').value = ''
document.getElementById('userModalPasswordInput').value = ''
document.getElementById('userModalRepeatPasswordInput').value = ''
document.getElementById('userModalIsAdminInput').checked = ''

document.getElementById('userModalAlerts').innerHTML = ''

// Remove class invalid-input from all (input) elements
Array.from(document.querySelectorAll('.invalid-input')).forEach((el) => el.classList.remove('invalid-input'));
}

function prepareEditUserModal(id, name, email, isAdmin, password, repeatPassword) {
document.getElementById('userModalHeaderTitle').innerHTML = 'Edit user'

document.getElementById('userModalPasswordInput').required = false
document.getElementById('userModalRepeatPasswordInput').required = false

document.getElementById('userModalPasswordInputRequiredStar').classList.add('d-none')
document.getElementById('userModalRepeatPasswordInputRequiredStar').classList.add('d-none')
document.getElementById('userModalFooterCreateButton').classList.add('d-none')
document.getElementById('userModalFooterButtons').classList.remove('d-none')

document.getElementById('userModalIdInput').value = id
document.getElementById('userModalNameInput').value = name
document.getElementById('userModalEmailInput').value = email
document.getElementById('userModalIsAdminInput').checked = isAdmin
document.getElementById('userModalPasswordInput').value = ''
document.getElementById('userModalRepeatPasswordInput').value = ''

document.getElementById('userModalAlerts').innerHTML = ''

// Remove class invalid-input from all (input) elements
Array.from(document.querySelectorAll('.invalid-input')).forEach((el) => el.classList.remove('invalid-input'));
}

function validateCreateUserInput() {
let error = false

const nameInput = document.getElementById('userModalNameInput');
const passwordInput = document.getElementById('userModalPasswordInput');
const passwordRepeatInput = document.getElementById('userModalRepeatPasswordInput');
const emailInput = document.getElementById('userModalEmailInput');

let mustNotBeEmptyInputs = [nameInput, emailInput]

if (passwordInput.required === true) {
mustNotBeEmptyInputs.push(passwordInput, passwordRepeatInput)
}

mustNotBeEmptyInputs.forEach((input) => {
input.classList.remove('invalid-input');
if (input.value.toString() === '') {
input.classList.add('invalid-input');

error = true
}
})

if (passwordInput.required === true || passwordInput.value.length > 0) {
if (passwordInput.value.length < 8 || passwordInput.value !== passwordRepeatInput.value) {
if (passwordInput.value.length < 8) {
passwordInput.classList.add('invalid-input');
}
passwordRepeatInput.classList.add('invalid-input');

error = true
}
}

if (emailInput.value.includes('@') === false) {
emailInput.classList.add('invalid-input');

error = true
}

if (nameInput.value.match(/^[a-zA-Z0-9]+$/) === null) {
nameInput.classList.add('invalid-input');

error = true
}

return error
}

document.getElementById('createUserButton').addEventListener('click', async () => {
if (validateCreateUserInput() === true) {
return;
}

const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
'name': document.getElementById('userModalNameInput').value,
'password': document.getElementById('userModalPasswordInput').value,
'email': document.getElementById('userModalEmailInput').value,
'isAdmin': document.getElementById('userModalIsAdminInput').checked,
})
})

if (response.status !== 200) {
setUserModalAlertServerError(await response.text())
return
}

setUserManagementAlert('User was created: ' + document.getElementById('userModalNameInput').value)

reloadTable()
userModal.hide()
})

function setUserModalAlertServerError(message = "Server error, please try again.") {
document.getElementById('userModalAlerts').innerHTML = '<div class="alert alert-danger alert-dismissible" role="alert">' + message + '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>'
}

document.getElementById('updateUserButton').addEventListener('click', async () => {
if (validateCreateUserInput() === true) {
return;
}

let password = document.getElementById('userModalPasswordInput').value;
if (password === '') {
password = null
}

const response = await fetch('/api/users/' + document.getElementById('userModalIdInput').value, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
'name': document.getElementById('userModalNameInput').value,
'email': document.getElementById('userModalEmailInput').value,
'isAdmin': document.getElementById('userModalIsAdminInput').checked,
'password': password,
})
})

if (response.status !== 200) {
setUserModalAlertServerError(await response.text())

return
}

setUserManagementAlert('User was updated: ' + document.getElementById('userModalNameInput').value)

reloadTable()
userModal.hide()
})

document.getElementById('deleteUserButton').addEventListener('click', async () => {
if (confirm('Are you sure you want to delete the user?') === false) {
return
}

const response = await fetch('/api/users/' + document.getElementById('userModalIdInput').value, {
method: 'DELETE'
});

if (response.status !== 200) {
setUserModalAlertServerError()
return
}

setUserManagementAlert('User was deleted: ' + document.getElementById('userModalNameInput').value)

reloadTable()
userModal.hide()
})

function setUserManagementAlert(message, type = 'success') {
const userManagementAlerts = document.getElementById('userManagementAlerts');
userManagementAlerts.classList.remove('d-none')
userManagementAlerts.innerHTML = ''
userManagementAlerts.innerHTML = '<div class="alert alert-' + type + ' alert-dismissible" role="alert">' + message + '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>'
userManagementAlerts.style.textAlign = 'center'
}

async function reloadTable() {
table.getElementsByTagName('tbody')[0].innerHTML = ''
document.getElementById('userTableLoadingSpinner').classList.remove('d-none')

const response = await fetch('/api/users');

if (response.status !== 200) {
setUserManagementAlert('Could not load users', 'danger')
document.getElementById('userTableLoadingSpinner').classList.add('d-none')

return
}

const users = await response.json();

document.getElementById('userTableLoadingSpinner').classList.add('d-none')

users.forEach((user) => {
let row = document.createElement('tr');
row.innerHTML = '<td>' + user.id + '</td>';
row.innerHTML += '<td>' + user.name + '</td>';
row.innerHTML += '<td>' + user.email + '</td>';
row.innerHTML += '<td>' + user.isAdmin + '</td>';
row.style.cursor = 'pointer'

table.getElementsByTagName('tbody')[0].appendChild(row);
})

registerTableRowClickEvent()
}
29 changes: 29 additions & 0 deletions settings/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@
'/settings/account',
[\Movary\HttpController\SettingsController::class, 'renderAccountPage'],
);
$routeCollector->addRoute(
'GET',
'/settings/users',
[\Movary\HttpController\SettingsController::class, 'renderUsersPage'],
);
$routeCollector->addRoute(
'POST',
'/settings/account',
Expand Down Expand Up @@ -313,4 +318,28 @@
'/{username:[a-zA-Z0-9]+}[/]',
[\Movary\HttpController\DashboardController::class, 'redirectToDashboard'],
);

############
# REST Api #
############
$routeCollector->addRoute(
'GET',
'/api/users',
[\Movary\HttpController\Rest\UserController::class, 'fetchUsers'],
);
$routeCollector->addRoute(
'POST',
'/api/users',
[\Movary\HttpController\Rest\UserController::class, 'createUser'],
);
$routeCollector->addRoute(
'PUT',
'/api/users/{userId:\d+}',
[\Movary\HttpController\Rest\UserController::class, 'updateUser'],
);
$routeCollector->addRoute(
'DELETE',
'/api/users/{userId:\d+}',
[\Movary\HttpController\Rest\UserController::class, 'deleteUser'],
);
};
1 change: 1 addition & 0 deletions src/Command/UserList.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ protected function configure() : void
protected function execute(InputInterface $input, OutputInterface $output) : int
{
$users = $this->userApi->fetchAll();

foreach ($users as $user) {
$this->generateOutput($output, sprintf('id: %s, email: %s', $user['id'], $user['email']));
}
Expand Down
7 changes: 7 additions & 0 deletions src/Domain/User/UserEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ private function __construct(
private readonly int $id,
private readonly string $name,
private readonly string $passwordHash,
private readonly bool $isAdmin,
private readonly int $privacyLevel,
private readonly bool $coreAccountChangesDisabled,
private readonly int $dateFormatId,
Expand All @@ -26,6 +27,7 @@ public static function createFromArray(array $data) : self
(int)$data['id'],
$data['name'],
$data['password'],
(bool)$data['is_admin'],
$data['privacy_level'],
(bool)$data['core_account_changes_disabled'],
$data['date_format_id'],
Expand Down Expand Up @@ -102,4 +104,9 @@ public function hasPlexScrobbleWatchesEnabled() : bool
{
return $this->plexScrobbleWatches;
}

public function isAdmin() : bool
{
return $this->isAdmin;
}
}
2 changes: 1 addition & 1 deletion src/Domain/User/UserRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public function deleteUser(int $userId) : void

public function fetchAll() : array
{
return $this->dbConnection->fetchAllAssociative('SELECT * FROM `user` ORDER BY name');
return $this->dbConnection->fetchAllAssociative('SELECT id, name, email, is_admin as isAdmin FROM `user` ORDER BY id');
}

public function fetchAllHavingWatchedMovieInternVisibleUsernames(int $movieId) : array
Expand Down
3 changes: 2 additions & 1 deletion src/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,8 @@ public static function createTwigEnvironment(ContainerInterface $container) : Tw
$dataFormatJavascript = DateFormat::getJavascriptById($user->getDateFormatId());
}

$twig->addGlobal('currentUsername', $user?->getName());
$twig->addGlobal('currentUserName', $user?->getName());
$twig->addGlobal('currentUserIsAdmin', $user?->isAdmin());
$twig->addGlobal('routeUsername', $routeUsername ?? null);
$twig->addGlobal('dateFormatPhp', $dateFormatPhp);
$twig->addGlobal('dateFormatJavascript', $dataFormatJavascript);
Expand Down
Loading

0 comments on commit 0d3cbca

Please sign in to comment.