feat(profile): add uploadable avatars with local/S3 storage

Users can upload and remove profile pictures on the profile page.
Admins choose local or instance S3 storage in advanced settings.
Avatars are compressed to JPEG and served via a private cached route.
This commit is contained in:
Andras Bacsai
2026-08-07 21:39:56 +02:00
parent e50108a905
commit 51561e4bee
23 changed files with 677 additions and 44 deletions
+19 -13
View File
@@ -7,6 +7,7 @@ use App\Rules\ValidS3BucketName;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
@@ -184,19 +185,7 @@ class S3Storage extends BaseModel
throw new \RuntimeException('S3 bucket name is not allowed: '.$validator->errors()->first('bucket'));
}
$disk = Storage::build([
'driver' => 's3',
'region' => $this['region'],
'key' => $this['key'],
'secret' => $this['secret'],
'bucket' => $this['bucket'],
'endpoint' => $this['endpoint'],
'use_path_style_endpoint' => true,
'http' => array_merge(SafeWebhookUrl::httpClientOptions($this['endpoint'], $this->trustedInternalHosts()), [
'connect_timeout' => self::CONNECTION_TIMEOUT_SECONDS,
'timeout' => self::REQUEST_TIMEOUT_SECONDS,
]),
]);
$disk = $this->filesystem();
// Test the connection by listing files with ListObjectsV2 (S3)
$disk->files();
@@ -235,6 +224,23 @@ class S3Storage extends BaseModel
}
}
public function filesystem(): FilesystemAdapter
{
return Storage::build([
'driver' => 's3',
'region' => $this['region'],
'key' => $this['key'],
'secret' => $this['secret'],
'bucket' => $this['bucket'],
'endpoint' => $this['endpoint'],
'use_path_style_endpoint' => true,
'http' => array_merge(SafeWebhookUrl::httpClientOptions($this['endpoint'], $this->trustedInternalHosts()), [
'connect_timeout' => self::CONNECTION_TIMEOUT_SECONDS,
'timeout' => self::REQUEST_TIMEOUT_SECONDS,
]),
]);
}
/**
* The bundled MinIO container is a trusted internal S3 target, not a user-supplied webhook destination.
*