From 33068b1356ddf8d65ed9d4569e747ecff0588190 Mon Sep 17 00:00:00 2001 From: Pinga <121483313+getpinga@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:12:28 +0200 Subject: [PATCH] Added ability to update user accounts --- cp/app/Controllers/UsersController.php | 155 ++++++++++++++++++ .../views/admin/users/updateUser.twig | 126 ++++++++++++++ cp/resources/views/layouts/app.twig | 2 +- cp/resources/views/partials/js-users.twig | 13 +- cp/routes/web.php | 2 + 5 files changed, 294 insertions(+), 4 deletions(-) create mode 100644 cp/resources/views/admin/users/updateUser.twig diff --git a/cp/app/Controllers/UsersController.php b/cp/app/Controllers/UsersController.php index ecfd725..5661ab1 100644 --- a/cp/app/Controllers/UsersController.php +++ b/cp/app/Controllers/UsersController.php @@ -172,4 +172,159 @@ class UsersController extends Controller 'registrar' => $registrar, ]); } + + public function updateUser(Request $request, Response $response, $args) + { + $db = $this->container->get('db'); + // Get the current URI + $uri = $request->getUri()->getPath(); + $registrars = $db->select("SELECT id, clid, name FROM registrar"); + + if ($args) { + $args = trim($args); + + if (!preg_match('/^[a-z0-9_-]+$/', $args)) { + $this->container->get('flash')->addMessage('error', 'Invalid user name'); + return $response->withHeader('Location', '/users')->withStatus(302); + } + + $user = $db->selectRow('SELECT id,email,username,status,verified,roles_mask,registered,last_login FROM users WHERE username = ?', + [ $args ]); + $user_asso = $db->selectValue('SELECT registrar_id FROM registrar_users WHERE user_id = ?', + [ $user['id'] ]); + $registrar_name = $db->selectValue('SELECT name FROM registrar WHERE id = ?', + [ $user_asso ]); + + if ($user) { + // Check if the user is not an admin (assuming role 0 is admin) + if ($_SESSION["auth_roles"] != 0) { + return $response->withHeader('Location', '/dashboard')->withStatus(302); + } + + $_SESSION['user_to_update'] = [$args]; + + return view($response,'admin/users/updateUser.twig', [ + 'user' => $user, + 'currentUri' => $uri, + 'registrars' => $registrars, + 'user_asso' => $user_asso, + 'registrar_name' => $registrar_name + ]); + } else { + // User does not exist, redirect to the users view + return $response->withHeader('Location', '/users')->withStatus(302); + } + } else { + // Redirect to the users view + return $response->withHeader('Location', '/users')->withStatus(302); + } + } + + public function updateUserProcess(Request $request, Response $response) + { + if ($_SESSION["auth_roles"] != 0) { + return $response->withHeader('Location', '/dashboard')->withStatus(302); + } + + if ($request->getMethod() === 'POST') { + // Retrieve POST data + $data = $request->getParsedBody(); + $db = $this->container->get('db'); + + $email = $data['email'] ?? null; + $old_username = $_SESSION['user_to_update'][0]; + $username = $data['username'] ?? null; + $password = $data['password'] ?? null; + $password_confirmation = $data['password_confirmation'] ?? null; + $status = $data['status'] ?? null; + $verified = $data['verified'] ?? null; + + // Define validation rules + $validators = [ + 'email' => v::email()->notEmpty()->setName('Email'), + 'username' => v::regex('/^[a-zA-Z0-9_-]+$/')->length(3, 20)->setName('Username'), + 'status' => v::in(['0', '1'])->setName('Status'), + 'verified' => v::in(['0', '1'])->setName('Verified'), // Ensure verified is checked as 0 or 1 + ]; + + // Add password validation only if provided + if (!empty($password)) { + $validators['password'] = v::stringType()->notEmpty()->length(6, 255)->setName('Password'); + + // Add password confirmation check only if both fields are provided + if (!empty($password_confirmation)) { + $validators['password_confirmation'] = v::equals($password)->setName('Password Confirmation'); + } + } + + // Validate data + $errors = []; + foreach ($validators as $field => $validator) { + try { + $validator->assert($data[$field] ?? null); + } catch (\Respect\Validation\Exceptions\ValidationException $exception) { + $errors[$field] = $exception->getMessages(); // Collect all error messages + } + } + + // If errors exist, return with errors + if (!empty($errors)) { + // Flatten the errors array into a string + $errorMessages = []; + foreach ($errors as $field => $fieldErrors) { + $fieldMessages = implode(', ', $fieldErrors); // Concatenate messages for the field + $errorMessages[] = ucfirst($field) . ': ' . $fieldMessages; // Prefix with field name + } + $errorString = implode('; ', $errorMessages); // Join all fields' errors + + // Add the flattened error string as a flash message + $this->container->get('flash')->addMessage('error', 'Error: ' . $errorString); + return $response->withHeader('Location', '/user/update/'.$old_username)->withStatus(302); + } + + if (empty($email)) { + $this->container->get('flash')->addMessage('error', 'No email specified for update'); + return $response->withHeader('Location', '/user/update/'.$old_username)->withStatus(302); + } + + $db->beginTransaction(); + + try { + $currentDateTime = new \DateTime(); + $update = $currentDateTime->format('Y-m-d H:i:s.v'); + + // Prepare the data to update + $updateData = [ + 'email' => $email, + 'username' => $username, + 'verified' => $verified, + 'status' => $status, + ]; + + if (!empty($password)) { + $password_hashed = password_hash($password, PASSWORD_ARGON2ID, ['memory_cost' => 1024 * 128, 'time_cost' => 6, 'threads' => 4]); + $updateData['password'] = $password_hashed; + } + + $db->update( + 'users', + $updateData, + [ + 'username' => $old_username + ] + ); + + $db->commit(); + } catch (Exception $e) { + $db->rollBack(); + $this->container->get('flash')->addMessage('error', 'Database failure during update: ' . $e->getMessage()); + return $response->withHeader('Location', '/user/update/'.$old_username)->withStatus(302); + } + + unset($_SESSION['user_to_update']); + $this->container->get('flash')->addMessage('success', 'User ' . $username . ' has been updated successfully on ' . $update); + return $response->withHeader('Location', '/user/update/'.$username)->withStatus(302); + } + } + } \ No newline at end of file diff --git a/cp/resources/views/admin/users/updateUser.twig b/cp/resources/views/admin/users/updateUser.twig new file mode 100644 index 0000000..cbf525e --- /dev/null +++ b/cp/resources/views/admin/users/updateUser.twig @@ -0,0 +1,126 @@ +{% extends "layouts/app.twig" %} + +{% block title %}{{ __('Update User') }}{% endblock %} + +{% block content %} +
+ + + +
+
+
+ {% include 'partials/flash.twig' %} +
+ {{ csrf.field | raw }} +
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ +
+ {% if user.roles_mask == 0 %} + {{ __('Admin') }} + {% elseif user.roles_mask == 4 %} + {{ __('Registrar') }} + {% if user_asso is defined and user_asso is not null %} + - {{ __('Associated with Registrar') }} {{ registrar_name }} + {% endif %} + {% else %} + {{ __('Unknown Role') }} + {% endif %} +
+
+
+
+
+ + +
+
+
+
+
+ {% include 'partials/footer.twig' %} +
+{% endblock %} \ No newline at end of file diff --git a/cp/resources/views/layouts/app.twig b/cp/resources/views/layouts/app.twig index bcc4c52..c365fd1 100644 --- a/cp/resources/views/layouts/app.twig +++ b/cp/resources/views/layouts/app.twig @@ -147,7 +147,7 @@ - {% if roles == 0 %}
  • + {% if roles == 0 %}
  • `; } - + + function actionsFormatter(cell, formatterParams, onRendered) { + return ` + + `; + } + function statusFormatter(cell) { var statusArray = cell.getValue(); if (statusArray && Array.isArray(statusArray)) { @@ -116,8 +122,9 @@ {title:"{{ __('Name') }}", field:"username", width:200, resizable:false, headerSort:true, formatter: userLinkFormatter, responsive:0}, {title:"{{ __('Email') }}", field:"email", width:300, resizable:false, headerSort:true, responsive:2}, {title:"{{ __('Roles') }}", field:"roles_mask", width:200, resizable:false, headerSort:true, formatter: roleLabelFormatter, responsive:2}, - {title:"{{ __('Verified') }}", field:"verified", width:200, resizable:false, headerSort:true, formatter: verifiedFormatter, responsive:2}, - {title:"{{ __('Status') }}", field:"status", width:200, resizable:false, headerSort:true, formatter: statusBadgeFormatter, responsive:2}, + {title:"{{ __('Verified') }}", field:"verified", width:150, resizable:false, headerSort:true, formatter: verifiedFormatter, responsive:2}, + {title:"{{ __('Status') }}", field:"status", width:150, resizable:false, headerSort:true, formatter: statusBadgeFormatter, responsive:2}, + {title: "{{ __('Actions') }}", formatter: actionsFormatter, responsive:0, headerSort: false, download:false, hozAlign: "center", cellClick:function(e, cell){ e.stopPropagation(); }}, ] }); var searchInput = document.getElementById("search-input"); diff --git a/cp/routes/web.php b/cp/routes/web.php index 2915755..5fcee35 100644 --- a/cp/routes/web.php +++ b/cp/routes/web.php @@ -103,6 +103,8 @@ $app->group('', function ($route) { $route->get('/users', UsersController::class .':listUsers')->setName('listUsers'); $route->map(['GET', 'POST'], '/user/create', UsersController::class . ':createUser')->setName('createUser'); + $route->get('/user/update/{user}', UsersController::class . ':updateUser')->setName('updateUser'); + $route->post('/user/update', UsersController::class . ':updateUserProcess')->setName('updateUserProcess'); $route->get('/epphistory', LogsController::class .':view')->setName('epphistory'); $route->get('/poll', LogsController::class .':poll')->setName('poll');