Quick Guide: Custom Layout

Sorry for my English, I’m doing it with the help of ChatGPT.

I created this small guide to assist those who wish to customize the HestiaCP dashboard according to their preferences.

This guide is a response to the previous topic where I asked about this option.

My goal is to customize the dashboard so that regular users cannot access the sections for WEB, DNS, DATABASES, and TASKS, leaving only the option to manage EMAILS available. Additionally, I will replace the logo and favicon.

I will accomplish this using PHP, JS, and CSS.

Another objective is to ensure that this customization remains active even after future updates of HestiaCP.

I hope this guide proves useful to you!

Customizing the HestiaCP Control Panel

The first thing we need to know is that HestiaCP offers developers the option to create their own theme, including the possibility of adding custom code in PHP, JS, and CSS.

1 - Creating a theme:

Let’s navigate to:

cd /usr/local/hestia/web/css/src/themes/

Within this folder, we have several themes.

dark.css default.css flat.css vestia.css

Let’s name our theme “meutema.css”. Since I liked the dark theme, let’s make a copy of it.

cp -p dark.css meutema.css

Now that we’ve done that, we should also include the minified version. If we don’t, our theme won’t appear for selection in the HestiaCP panel.

Let’s navigate to:

cd /usr/local/hestia/web/css/themes/

Let’s once again copy our dark theme.

cp -p dark.min.css meutema.min.css

There you go! By doing this, we can now select our theme in:

https://meupainel.com:8083/edit/server/

Let’s apply our theme.

In our theme, I’d also like to work with PHP and JS code. To do that, let’s navigate to the folder:

cd /usr/local/hestia/web/js/custom_scripts/

All files included in this folder are automatically included at the beginning.

Let’s create the files.

meutema.php
meutema.js

Let’s also put our images in this folder.

favicon.png
logo.png

After creating, we’ll have this structure inside the folder.

meutema.php
meutema.js
favicon.png
logo.png

There you go! Our theme is now finished.

2 - Customizing the common user so they don’t have access to the options for USER, WEB, DNS, TASK, and BACKUP.

Basically, I hide the unwanted functionalities with JavaScript only when the userContext is different from admin. If the user tries to force access, I check the URL to redirect them to Webmail management.

I also make some customizations on the login page to match the visual identity of my company, replacing the main logo with my company’s. Additionally, I make a small adjustment to add “a(o)” to differentiate between male and female clients.

Here are the codes:

meutema.php


<?php

if (isset($_SESSION['userContext']) && $_SESSION['userContext'] !== 'admin') :
    // Get the current URL.
    $currentUrl = "https://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";

    // Call the function to check and redirect.
    checkAndRedirect($currentUrl);
?>

<script>
  
 document.addEventListener("DOMContentLoaded", function () {
  var buttons = document.querySelectorAll(
    ".button.button-secondary.js-button-create"
  );
  var currentUrl = window.location.href;

  var mainMenu = document.querySelector(".main-menu");
  if (mainMenu) {
    mainMenu.style.display = "none";
  }

  var notificationsDiv = document.querySelector(".top-bar-notifications");
  if (notificationsDiv) {
    notificationsDiv.remove();
  }
  var menuItem = document.querySelector("li.top-bar-menu-item");
  if (menuItem) {
    menuItem.remove();
  }

  // Hide the "Add Email Domain" button.
  buttons.forEach(function (button) {
    if (button.textContent.includes("Adicionar Domínio de E-mail")) {
      button.style.display = "none";
    }
  });

  // Hide the "Edit Email Domain" link.
  var links = document.querySelectorAll(
    "a.button.button-secondary.js-button-create"
  );
  links.forEach(function (link) {
    if (
      link.textContent.includes("Editar domínio de email")
    ) {
      link.style.display = "none";
    }
  });

  // Hide the external links.
  var linksFora = document.querySelectorAll("a.units-table-row-action-link");
  linksFora.forEach(function (link) {
    if (
      link.title.includes("Registros de DNS") &&
      link.querySelector("span.u-hide-desktop").style.display !== "none"
    ) {
      link.style.display = "none";
    }
    if (
      link.title.includes("Editar domínio de email") &&
      link.querySelector("span.u-hide-desktop").style.display !== "none"
    ) {
      link.style.display = "none";
    }
    if (!currentUrl.includes("/list/mail/?domain=")) {
      if (
        link.title.includes("Suspender") &&
        link.querySelector("span.u-hide-desktop").style.display !== "none"
      ) {
        link.style.display = "none";
      }
      if (
        link.title.includes("Excluir") &&
        link.querySelector("span.u-hide-desktop").style.display !== "none"
      ) {
        link.style.display = "none";
      }
    }
  });

  var deleteOption = document.querySelector(
    'select[name="action"] option[value="delete"]'
  );
  if (deleteOption) {
    deleteOption.remove();
  }

  // Select all elements with the class .u-mt10.u-mb10.
  var elements = document.querySelectorAll(".u-mt10.u-mb10");

  // Iterate over each element.
  elements.forEach(function (element) {
    //Check if the element has a child <input> with the attribute 'name' equal to 'v_rate'.
    var inputElement = element.querySelector('input[name="v_rate"]');
    if (inputElement) {
      // Completely hide the parent element.
      element.style.display = "none";
    }
  });
  
});

</script>

<?php
endif;

//Function to check and redirect.
function checkAndRedirect($url)
{
    //Parts we want to check.
    $pathsToCheck = ['/web/', '/dns/', '/db/', '/user/', '/cron/', '/backup/'];

    // Check each part.
    foreach ($pathsToCheck as $path) {
        if (strpos($url, $path) !== false) {
            // If any of the paths are found, redirect to /list/mail/.
            header("Location: /list/mail/");
            exit();
        }
    }
}

meutema.js

// Find the <img> tag within the element with the class "top-bar-left".
var imagem = document.querySelector('.top-bar-left img[alt="Minha Empresa"]');

// Check if the img tag was found and if the alt attribute is "Minha Empresa".
if (imagem && imagem.alt === "Minha Empresa") {
     // Set the new path for the image.
    var novoCaminho = "/js/custom_scripts/logo.png";

    // Set the new value for the src attribute.
    imagem.src = novoCaminho;

    // Remove the width and height attributes.
    imagem.removeAttribute("width");
    imagem.removeAttribute("height");
}

// Find the <h1> element with the class "login-title" within the element with the class "login".
var tituloLogin = document.querySelector('.login-title');


// Check if the <h1> element was found.
if (tituloLogin) {
    tituloLogin.style.marginBottom = "5px"; 
    tituloLogin.innerText = tituloLogin.innerText.replace("Bem-vindo ao Minha Empresa", "Bem-vindo(a)"); 
  	tituloLogin.innerText = tituloLogin.innerText.replace("Bem Vindo", "Bem-vindo(a)"); 
    // Create a new image element.
    var novaImagem = document.createElement('img');

    //Set the attributes of the new image.
    novaImagem.src = "/js/custom_scripts/logo.png";
    novaImagem.alt = "Minha Empresa";

    // Apply CSS styles to center the image.
    novaImagem.style.display = "block";
    novaImagem.style.margin = "0 auto";

    // Insert the new image before the text in the <h1> element.
    tituloLogin.insertBefore(novaImagem, tituloLogin.firstChild);    
  
    novaImagem.style.marginBottom = "10px"; 
}

// Find the <img> tag within the element with the class "login".
var imagemLogin = document.querySelector('.login img[src="/images/logo.svg"][alt="Minha Empresa"]');

// Check if the img tag was found.
if (imagemLogin) {
    // Remove a tag img
    imagemLogin.parentNode.removeChild(imagemLogin);
}

// Find the element with the class "login".
var loginElement = document.querySelector('.login');

// Check if the element was found.
if (loginElement) {
    // Modify the CSS style of the "login" class.
    loginElement.style.gap = "0px";
  	loginElement.style.maxWidth = "400px";  
    loginElement.style.padding = "20px 50px";
}

document.addEventListener('DOMContentLoaded', function() {
    // Update the default favicon.
    var standardFavicon = document.querySelector('link[rel="icon"]');
    if (standardFavicon) {
        standardFavicon.href = '/js/custom_scripts/favicon.png';
        console.log('Favicon padrão atualizado com sucesso.');
    } else {
        console.log('Elemento <link> com rel="icon" não encontrado.');
    }

    // Update the alternate favicon.
    var alternateFavicon = document.querySelector('link[rel="alternate icon"]');
    if (alternateFavicon) {
        alternateFavicon.href = '/js/custom_scripts/favicon.png';
        console.log('Favicon alternativo atualizado com sucesso.');
    } else {
        console.log('Elemento <link> com rel="alternate icon" não encontrado.');
    }
});

This is the final result of my customizations.

Login Page

User Page

Admin Page

This is just a small sample of the infinite possibilities you can explore with customizing the HestiaCP panel.

3 Likes