diff --git a/app/Http/Controllers/AccountsController.php b/app/Http/Controllers/AccountsController.php index e37c620..cea3a54 100644 --- a/app/Http/Controllers/AccountsController.php +++ b/app/Http/Controllers/AccountsController.php @@ -28,20 +28,11 @@ class AccountsController extends Controller */ public function store(CreateAccountRequest $request): RedirectResponse { - try { + (new CreateAccountService($request->validated()))->handle(); - (new CreateAccountService($request->validated()))->handle(); + session()->flash('success', 'Account created successfully!'); - session()->flash('success', 'Account created successfully!'); - - return redirect()->route('accounts.index'); - } catch (CreateAccountException $e) { - session()->flash('error', $e->getMessage()); - return back(); - } catch (Exception $e) { - session()->flash('error', $e->getMessage()); - return back(); - } + return redirect()->route('accounts.index'); } @@ -60,6 +51,8 @@ class AccountsController extends Controller { (new DeleteAccountService(User::findOrFail($account)))->handle(); + session()->flash('success', 'Account deleted successfully!'); + return redirect()->route('accounts.index'); } diff --git a/app/Models/PhpVersion.php b/app/Models/PhpVersion.php new file mode 100644 index 0000000..bb599a0 --- /dev/null +++ b/app/Models/PhpVersion.php @@ -0,0 +1,20 @@ + */ + use HasFactory; + + protected function casts(): array + { + return [ + 'active' => 'boolean', + 'is_default' => 'boolean', + ]; + } +} diff --git a/app/Services/Accounts/CreateAccountService.php b/app/Services/Accounts/CreateAccountService.php index 0d56872..45eb294 100644 --- a/app/Services/Accounts/CreateAccountService.php +++ b/app/Services/Accounts/CreateAccountService.php @@ -2,7 +2,9 @@ namespace App\Services\Accounts; +use App\Models\PhpVersion; use App\Models\User; +use App\Services\Laranode\CreatePhpFpmPoolService; use Illuminate\Auth\Events\Registered; use Illuminate\Support\Facades\Process; use Exception; @@ -17,7 +19,7 @@ class CreateAccountService public function __construct(private array $validated) { // path to laranode user manager bin|ssh script - $this->laranodeBinPath = '/usr/local/bin/laranode'; + $this->laranodeBinPath = config('laranode.laranode_bin_path'); // appends _ln to all users to avoid all sort of issues (conflicts, control, security, files, etc.) $this->systemUsername = $validated['username'] . '_ln'; @@ -55,8 +57,14 @@ class CreateAccountService if ($createUser->failed()) { throw new CreateAccountException('Failed to create system user: ' . $createUser->errorOutput()); } + + $this->createDefaultPHPFpmPool(); } - // @TODO: implement add user php-fpm pools based on each php version - private function addPhpFpmPools(): void {} + private function createDefaultPHPFpmPool(): void + { + $defaultPhpVersion = PhpVersion::where('is_default', true)->firstOrFail(); + + (new CreatePhpFpmPoolService($this->systemUsername, $defaultPhpVersion))->handle(); + } } diff --git a/app/Services/Accounts/DeleteAccountService.php b/app/Services/Accounts/DeleteAccountService.php index 52eb3e9..31547de 100644 --- a/app/Services/Accounts/DeleteAccountService.php +++ b/app/Services/Accounts/DeleteAccountService.php @@ -14,14 +14,22 @@ class DeleteAccountService public function __construct(private User $user) { - $this->laranodeBinPath = '/usr/local/bin/laranode'; + // path to laranode user manager bin|ssh script + $this->laranodeBinPath = config('laranode.laranode_bin_path'); } public function handle(): void { + // delete php-fpm pools + $this->deletePhpFpmPools(); + + // wait for pools to be deleted && fpm to restart + sleep(1); + // delete system user $this->deleteSystemUser(); + // remove user from database User::findOrFail($this->user->id)->delete(); } @@ -39,9 +47,19 @@ class DeleteAccountService } } + private function deletePhpFpmPools(): void + { + $deletePhpFpmPool = Process::run([ + 'sudo', + $this->laranodeBinPath . '/laranode-remove-php-fpm-pool.sh', + $this->user->systemUsername, + ]); + + if ($deletePhpFpmPool->failed()) { + throw new DeleteAccountException('Failed to delete PHP-FPM pool: ' . $deletePhpFpmPool->errorOutput()); + } + } + // @TODO: implement delete all DB's of this user private function deleteDatabases(): void {} - - // @TODO: implement delete user php-fpm pools - private function deletePhpFpmPools(): void {} } diff --git a/app/Services/Laranode/CreatePhpFpmPoolService.php b/app/Services/Laranode/CreatePhpFpmPoolService.php new file mode 100644 index 0000000..f782ae3 --- /dev/null +++ b/app/Services/Laranode/CreatePhpFpmPoolService.php @@ -0,0 +1,39 @@ +laranodeBinPath = config('laranode.laranode_bin_path'); + + // path to php-fpm pool template + $this->phpFpmPoolTemplate = config('laranode.php_fpm_pool_template'); + } + + public function handle(): void + { + $createPhpFpmPool = Process::run([ + 'sudo', + $this->laranodeBinPath . '/laranode-add-php-fpm-pool.sh', + $this->systemUser, + '7.4', // @TODO: remove this and replace with $this->phpVersion + $this->phpFpmPoolTemplate + /*$this->phpVersion,*/ + ]); + + if ($createPhpFpmPool->failed()) { + throw new CreatePhpFpmPoolException('Failed to create PHP-FPM pool: ' . $createPhpFpmPool->errorOutput()); + } + } +} diff --git a/config/laranode.php b/config/laranode.php new file mode 100644 index 0000000..cffd818 --- /dev/null +++ b/config/laranode.php @@ -0,0 +1,37 @@ + base_path('laranode-scripts/bin'), + + /* + |-------------------------------------------------------------------------- + | Laranode PHP-FPM Pools + |-------------------------------------------------------------------------- + | + | This option allows you to specify the path to the laranode PHP-FPM pool + | configuration template. This is used to create and delete PHP-FPM pools. + | + */ + 'php_fpm_pool_template' => base_path('laranode-scripts/templates/php-fpm-pool.template'), + + /* + |-------------------------------------------------------------------------- + | Laranode Apache Virtual Hosts + |-------------------------------------------------------------------------- + | + | This option allows you to specify the path to the laranode Apache virtual + | host configuration template. This is used to create and delete Apache + | virtual hosts. + */ + 'apache_vhost_template' => base_path('laranode-scripts/templates/apache-vhost.template'), + +]; diff --git a/database/factories/PhpVersionFactory.php b/database/factories/PhpVersionFactory.php new file mode 100644 index 0000000..98927ae --- /dev/null +++ b/database/factories/PhpVersionFactory.php @@ -0,0 +1,25 @@ + + */ +class PhpVersionFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'version' => '8.3', + 'active' => true, + 'is_default' => true, + ]; + } +} diff --git a/database/migrations/2025_02_23_081100_create_php_versions_table.php b/database/migrations/2025_02_23_081100_create_php_versions_table.php new file mode 100644 index 0000000..a62aa41 --- /dev/null +++ b/database/migrations/2025_02_23_081100_create_php_versions_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('version'); + $table->boolean('active')->default(false); + $table->boolean('is_default')->default(false); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('php_versions'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index d01a0ef..f46f28e 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -3,6 +3,7 @@ namespace Database\Seeders; use App\Models\User; +use Database\Factories\PhpVersionFactory; // use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; @@ -13,11 +14,6 @@ class DatabaseSeeder extends Seeder */ public function run(): void { - // User::factory(10)->create(); - - User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', - ]); + PhpVersionFactory::new()->create(); } } diff --git a/laranode-scripts/bin/laranode-add-php-fpm-pool.sh b/laranode-scripts/bin/laranode-add-php-fpm-pool.sh new file mode 100755 index 0000000..a829266 --- /dev/null +++ b/laranode-scripts/bin/laranode-add-php-fpm-pool.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Check if at least two arguments are provided (system user and php version) +if [ $# -lt 3 ]; then + echo "Usage: $0 {system user} {php version} {template_file_path}" + exit 1 +fi + +SYSTEM_USER=$1 +PHP_VERSION=$2 +TEMPLATE_FILE_PATH=$3 + +# Automatically append _ln to $USERNAME if not already present +if echo "$SYSTEM_USER" | grep -qv '_ln$'; then + SYSTEM_USER+="_ln" +fi + +# read template file +TEMPLATE_FILE=$(cat "$TEMPLATE_FILE_PATH") + +# replace {user} and {version} in template file +TEMPLATE_FILE=$(echo "$TEMPLATE_FILE" | sed "s/{user}/$SYSTEM_USER/g") +TEMPLATE_FILE=$(echo "$TEMPLATE_FILE" | sed "s/{version}/$PHP_VERSION/g") + +# write template file to /etc/php/{version}/fpm/pool.d/pool-{systemUser}.conf +echo "$TEMPLATE_FILE" > "/etc/php/$PHP_VERSION/fpm/pool.d/$SYSTEM_USER.conf" + +# reload php{version}-fpm +echo "Reloading php$PHP_VERSION-fpm..." +systemctl reload php"$PHP_VERSION"-fpm diff --git a/laranode-scripts/bin/laranode-remove-php-fpm-pool.sh b/laranode-scripts/bin/laranode-remove-php-fpm-pool.sh new file mode 100755 index 0000000..28052ae --- /dev/null +++ b/laranode-scripts/bin/laranode-remove-php-fpm-pool.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +if [ -z "$1" ]; then + echo "Usage: $0 SYSTEM_USER" + exit 1 +fi + +SYSTEM_USER="$1" +count=0 +affected_versions=() + +# Find and process pool configs +while IFS= read -r file; do + if [ -f "$file" ]; then + version=$(echo "$file" | grep -oP '/etc/php/\K[0-9]+\.[0-9]+') + echo "Removing $file" + rm -v "$file" + affected_versions+=("$version") + ((count++)) + fi +done < <(find /etc/php/*/fpm/pool.d -name "${SYSTEM_USER}.conf") + +echo "Removed $count pool configuration(s)" + +# Restart only affected PHP-FPM versions +for version in "${affected_versions[@]}"; do + echo "Restarting php${version}-fpm" + systemctl restart "php${version}-fpm" +done + +if [ $count -eq 0 ]; then + echo "No pool configurations found for $SYSTEM_USER" +fi diff --git a/laranode-scripts/bin/laranode-user-manager.sh b/laranode-scripts/bin/laranode-user-manager.sh new file mode 100755 index 0000000..3407e2c --- /dev/null +++ b/laranode-scripts/bin/laranode-user-manager.sh @@ -0,0 +1,99 @@ +#!/bin/bash + +# Check if at least two arguments are provided (action and username) +if [ $# -lt 2 ]; then + echo "Usage: $0 {create|delete} username {allow shell/ssh login (optional): yes|no} {password (required if allow shell/ssh login is yes)}" + exit 1 +fi + +ACTION=$1 +USERNAME=$2 +ALLOW_LOGIN=${3:-no} # Default to "no" if not specified +PASSWORD=$4 + +# Ensure password is provided if allow shell/ssh login is "yes" +if [ "$ALLOW_LOGIN" = "yes" ] && [ -z "$PASSWORD" ]; then + echo "Error: Password is required when allow shell/ssh login is 'yes'." + exit 1 +fi + +# Automatically append _ln to $USERNAME if not already present +if echo "$USERNAME" | grep -qv '_ln$'; then + USERNAME+="_ln" +fi + +# Function to create a user +create_user() { + # Check if the user already exists + if id "$USERNAME" &>/dev/null; then + echo "User $USERNAME already exists." + exit 1 + fi + + # Create the user + if [ "$ALLOW_LOGIN" = "yes" ]; then + # Create user with login shell + useradd -m "$USERNAME" + if [ $? -ne 0 ]; then + echo "Error: Failed to create user $USERNAME." + exit 1 + fi + + # add password to user + echo "$USERNAME:$PASSWORD" | chpasswd + echo "User $USERNAME created successfully with SSH login allowed." + else + # Create user with no login shell + useradd -m -s /usr/sbin/nologin "$USERNAME" + if [ $? -ne 0 ]; then + echo "Error: Failed to create user $USERNAME." + exit 1 + fi + echo "User $USERNAME created successfully with no SSH login." + fi + + # add this user group to www-data group too + usermod -aG "$USERNAME" www-data + + # create /home/{user}/logs directory + mkdir -p "/home/$USERNAME/logs" + chown "$USERNAME:$USERNAME" "/home/$USERNAME/logs" + chmod 770 "/home/$USERNAME/logs" +} + +# Function to delete a user +delete_user() { + # Check if the user exists + if ! id "$USERNAME" &>/dev/null; then + echo "User $USERNAME does not exist." + exit 1 + fi + + # remove this user from www-data's groups + deluser www-data "$USERNAME" + + # Delete the user and their home directory + userdel -r "$USERNAME" + if [ $? -ne 0 ]; then + echo "Error: Failed to delete user $USERNAME." + exit 1 + fi + + + echo "User $USERNAME deleted successfully." +} + +# Perform the action based on the first argument (create or delete) +case $ACTION in + create) + create_user + ;; + delete) + delete_user + ;; + *) + echo "Error: Invalid action. Use 'create' or 'delete'." + exit 1 + ;; +esac + diff --git a/laranode-scripts/templates/apache-vhost.template b/laranode-scripts/templates/apache-vhost.template new file mode 100644 index 0000000..f14a766 --- /dev/null +++ b/laranode-scripts/templates/apache-vhost.template @@ -0,0 +1,8 @@ +# @TODO: complete this + + ServerName {domain} + DocumentRoot /home/{user}/public + + ErrorLog /home/{user}/logs/apache-error.log + CustomLog /home/{user}/logs/apache-access.log combined + diff --git a/laranode-scripts/templates/php-fpm-pool.template b/laranode-scripts/templates/php-fpm-pool.template new file mode 100644 index 0000000..6f87c60 --- /dev/null +++ b/laranode-scripts/templates/php-fpm-pool.template @@ -0,0 +1,33 @@ +[{user}] + +; configure user & group +user = {user} +group = {user} + +; configure socket +listen = /run/php/php-fpm-{version}.{user}.sock +listen.owner = {user} +listen.group = {user} +listen.mode = 0660 + +; pool settings +pm = dynamic +pm.max_children = 5 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 +pm.max_requests = 500 + +; Security settings +php_admin_value[open_basedir] = /home/{user}:/tmp/ +php_admin_flag[expose_php] = Off + +; Performance settings +php_admin_value[memory_limit] = 128M +php_admin_value[upload_max_filesize] = 128M +php_admin_value[post_max_size] = 128M + +; Error handling +php_admin_value[error_log] = /home/{user}/logs/php-fpm-error.log +php_admin_flag[log_errors] = on +php_admin_flag[display_errors] = on diff --git a/resources/js/Layouts/Partials/SidebarNavi.jsx b/resources/js/Layouts/Partials/SidebarNavi.jsx index 8849f89..60c0011 100644 --- a/resources/js/Layouts/Partials/SidebarNavi.jsx +++ b/resources/js/Layouts/Partials/SidebarNavi.jsx @@ -88,17 +88,19 @@ const SidebarNavi = () => { -
  • - -
    - -
    - PHP Manager - -
  • + {auth.user.role == 'admin' && ( +
  • + +
    + +
    + PHP Manager + +
  • + )}
  • { router.delete(route('accounts.destroy', { account: id }), { - onSuccess: page => { - toast("Account deleted successfully."); + onBefore: () => { + toast("Please wait, deleting account and it's resources..."); }, onError: errors => { toast("Error occured while deleting account."); diff --git a/resources/js/Pages/Accounts/Partials/CreateAccountForm.jsx b/resources/js/Pages/Accounts/Partials/CreateAccountForm.jsx index c42c688..79a1b11 100644 --- a/resources/js/Pages/Accounts/Partials/CreateAccountForm.jsx +++ b/resources/js/Pages/Accounts/Partials/CreateAccountForm.jsx @@ -60,7 +60,6 @@ export default function CreateAccountForm() { onSuccess: () => { closeModal(); reset(); - toast("Account created successfully."); }, }); };