Create & Delete Accounts - includes php-fpm pools now

This commit is contained in:
Alex Crivion
2025-02-23 10:15:05 +00:00
parent d41a9de364
commit 2d58ebcccd
17 changed files with 410 additions and 39 deletions

View File

@@ -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');
}

20
app/Models/PhpVersion.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class PhpVersion extends Model
{
/** @use HasFactory<\Database\Factories\PhpVersionFactory> */
use HasFactory;
protected function casts(): array
{
return [
'active' => 'boolean',
'is_default' => 'boolean',
];
}
}

View File

@@ -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();
}
}

View File

@@ -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 {}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Services\Laranode;
use Illuminate\Support\Facades\Process;
use Exception;
class CreatePhpFpmPoolException extends Exception {}
class CreatePhpFpmPoolService
{
private string $laranodeBinPath;
private string $phpFpmPoolTemplate;
public function __construct(private string $systemUser, private string $phpVersion)
{
// path to laranode user manager bin|ssh script
$this->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());
}
}
}

37
config/laranode.php Normal file
View File

@@ -0,0 +1,37 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Laranode User Manager
|--------------------------------------------------------------------------
|
| This option allows you to specify the path to the laranode user manager
| binary. This is used to create and delete system users.
|
*/
'laranode_bin_path' => 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'),
];

View File

@@ -0,0 +1,25 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\PhpVersion>
*/
class PhpVersionFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'version' => '8.3',
'active' => true,
'is_default' => true,
];
}
}

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('php_versions', function (Blueprint $table) {
$table->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');
}
};

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,8 @@
# @TODO: complete this
<VirtualHost *:80>
ServerName {domain}
DocumentRoot /home/{user}/public
ErrorLog /home/{user}/logs/apache-error.log
CustomLog /home/{user}/logs/apache-access.log combined
</VirtualHost>

View File

@@ -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

View File

@@ -88,17 +88,19 @@ const SidebarNavi = () => {
</Link>
</li>
<li>
<Link
to="/admin/php-manager"
className="relative flex flex-row items-center h-11 focus:outline-none hover:bg-gray-900 text-gray-300 border-l-4 border-transparent hover:border-indigo-900 pr-6"
>
<div>
<FaPhp className="ml-3 w-5 h-5" />
</div>
<span className="ml-2 text-sm tracking-wide truncate">PHP Manager</span>
</Link>
</li>
{auth.user.role == 'admin' && (
<li>
<Link
to="/php-manager"
className="relative flex flex-row items-center h-11 focus:outline-none hover:bg-gray-900 text-gray-300 border-l-4 border-transparent hover:border-indigo-900 pr-6"
>
<div>
<FaPhp className="ml-3 w-5 h-5" />
</div>
<span className="ml-2 text-sm tracking-wide truncate">PHP Manager</span>
</Link>
</li>
)}
<li>
<Link

View File

@@ -12,10 +12,11 @@ import { FaDatabase, FaEdit } from "react-icons/fa";
export default function Accounts({ accounts }) {
const deleteUser = (id) => {
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.");

View File

@@ -60,7 +60,6 @@ export default function CreateAccountForm() {
onSuccess: () => {
closeModal();
reset();
toast("Account created successfully.");
},
});
};