'datetime', 'password' => 'hashed', 'is_admin' => 'boolean', 'is_active' => 'boolean', 'sftp_password' => 'encrypted', ]; } public function canAccesJabali(Panel $panel): bool { if (! $this->is_active) { return false; } if ($panel->getId() === 'admin') { return $this->is_admin; } return true; } public function isAdmin(): bool { return $this->is_admin; } public function getHomeDirectoryAttribute($value): string { return $value ?? "/home/{$this->username}"; } protected static function booted() { static::deleting(function ($user) { if ($user->is_admin && (int) $user->getKey() === 1) { throw new \RuntimeException(__('Primary admin account cannot be deleted.')); } // Clean up email forwarders from system maps before DB cascade deletes them try { $agent = new \App\Services\Agent\AgentClient; $domains = $user->domains()->with('emailDomain.forwarders', 'emailDomain.domain')->get(); foreach ($domains as $domain) { $forwarders = $domain->emailDomain?->forwarders ?? collect(); foreach ($forwarders as $forwarder) { $agent->send('email.forwarder_delete', [ 'username' => $user->username, 'email' => $forwarder->email, ]); } } } catch (\Exception $e) { \Log::warning("Failed to delete email forwarders for user {$user->username}: ".$e->getMessage()); } // Delete master MySQL user when Jabali user is deleted $masterUser = $user->username.'_admin'; try { // Use credentials from environment variables $mysqli = new \mysqli( config('database.connections.mysql.host', 'localhost'), config('database.connections.mysql.username'), config('database.connections.mysql.password') ); if (! $mysqli->connect_error) { // Use prepared statement to prevent SQL injection // MySQL doesn't support prepared statements for DROP USER, // so we validate the username format strictly if (! preg_match('/^[a-zA-Z0-9_]+$/', $masterUser)) { throw new \Exception('Invalid MySQL username format'); } // Escape the username as an additional safety measure $escapedUser = $mysqli->real_escape_string($masterUser); $mysqli->query("DROP USER IF EXISTS '{$escapedUser}'@'localhost'"); $mysqli->close(); } // Delete stored credentials \App\Models\MysqlCredential::where('user_id', $user->id)->delete(); } catch (\Exception $e) { \Log::error('Failed to delete master MySQL user: '.$e->getMessage()); } }); } /** * Determine if the user can access the Filament panel. */ public function canAccessPanel(\Filament\Panel $panel): bool { if ($panel->getId() === 'admin') { return $this->is_admin && $this->is_active; } return $this->is_active ?? true; } /** * Get the domains owned by the user. */ public function domains(): HasMany { return $this->hasMany(Domain::class); } /** * Get disk usage in bytes. */ public function getDiskUsageBytes(): int { // Try to get usage from quota system first (more accurate) try { $agent = new \App\Services\Agent\AgentClient; $result = $agent->quotaGet($this->username, '/'); if (($result['success'] ?? false) && isset($result['used_mb'])) { return (int) ($result['used_mb'] * 1024 * 1024); } } catch (\Exception $e) { // Fall back to du command } // Fallback: try du command (may not work if www-data can't read home dir) $homeDir = $this->home_directory; if (! is_dir($homeDir)) { return 0; } $output = shell_exec('du -sb '.escapeshellarg($homeDir).' 2>/dev/null | cut -f1'); return (int) trim($output ?: '0'); } /** * Get formatted disk usage string. */ public function getDiskUsageFormattedAttribute(): string { $bytes = $this->getDiskUsageBytes(); return $this->formatBytes($bytes); } /** * Get quota in bytes. */ public function getQuotaBytesAttribute(): int { return (int) (($this->disk_quota_mb ?? 0) * 1024 * 1024); } /** * Get disk usage percentage. */ public function getDiskUsagePercentAttribute(): float { if (! $this->disk_quota_mb || $this->disk_quota_mb <= 0) { return 0; } $used = $this->getDiskUsageBytes(); $quota = $this->quota_bytes; return $quota > 0 ? min(100, round(($used / $quota) * 100, 1)) : 0; } /** * Format bytes to human readable string. */ protected function formatBytes(int $bytes, int $precision = 1): string { $units = ['B', 'KB', 'MB', 'GB', 'TB']; $bytes = max($bytes, 0); $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); $pow = min($pow, count($units) - 1); $bytes /= pow(1024, $pow); return round($bytes, $precision).' '.$units[$pow]; } }