- Refactor edit.blade.php to handle admin-specific fields (accepted and sats_paid) through conditional logic.

- 📦 Upgrade Laravel framework, Livewire, and dependencies to ensure compatibility with version `13.1.1`.
This commit is contained in:
HolgerHatGarKeineNode
2026-03-23 17:50:17 +00:00
parent 347082bbc8
commit 7a992cec3f
31 changed files with 496 additions and 525 deletions

View File

@@ -2,6 +2,7 @@
namespace App\Auth; namespace App\Auth;
use App\Models\EinundzwanzigPleb;
use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Auth\Authenticatable;
class NostrUser implements Authenticatable class NostrUser implements Authenticatable
@@ -13,7 +14,7 @@ class NostrUser implements Authenticatable
public function __construct(string $pubkey) public function __construct(string $pubkey)
{ {
$this->pubkey = $pubkey; $this->pubkey = $pubkey;
$this->pleb = \App\Models\EinundzwanzigPleb::query() $this->pleb = EinundzwanzigPleb::query()
->where('pubkey', $pubkey) ->where('pubkey', $pubkey)
->first(); ->first();
} }

View File

@@ -2,6 +2,7 @@
namespace App\Console\Commands\Nostr; namespace App\Console\Commands\Nostr;
use App\Models\EinundzwanzigPleb;
use App\Models\Event; use App\Models\Event;
use App\Traits\NostrEventRendererTrait; use App\Traits\NostrEventRendererTrait;
use Illuminate\Console\Command; use Illuminate\Console\Command;
@@ -35,7 +36,7 @@ class FetchEvents extends Command
*/ */
public function handle() public function handle()
{ {
$plebs = \App\Models\EinundzwanzigPleb::query() $plebs = EinundzwanzigPleb::query()
->get(); ->get();
$subscription = new Subscription; $subscription = new Subscription;

View File

@@ -2,6 +2,7 @@
namespace App\Console\Commands\Nostr; namespace App\Console\Commands\Nostr;
use App\Models\Event;
use App\Traits\NostrEventRendererTrait; use App\Traits\NostrEventRendererTrait;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Broadcast; use Illuminate\Support\Facades\Broadcast;
@@ -29,7 +30,7 @@ class RenderAllEvents extends Command
*/ */
public function handle() public function handle()
{ {
$events = \App\Models\Event::query() $events = Event::query()
->get(); ->get();
foreach ($events as $event) { foreach ($events as $event) {

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Traits; namespace App\Livewire\Traits;
use App\Models\EinundzwanzigPleb;
use App\Support\NostrAuth; use App\Support\NostrAuth;
use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\RateLimiter;
use Livewire\Attributes\On; use Livewire\Attributes\On;
@@ -32,7 +33,7 @@ trait WithNostrAuth
NostrAuth::login($pubkey); NostrAuth::login($pubkey);
$this->currentPubkey = $pubkey; $this->currentPubkey = $pubkey;
$this->currentPleb = \App\Models\EinundzwanzigPleb::query() $this->currentPleb = EinundzwanzigPleb::query()
->where('pubkey', $pubkey) ->where('pubkey', $pubkey)
->first(); ->first();

View File

@@ -22,13 +22,10 @@ class ProjectProposal extends Model implements HasMedia
/** @var list<string> */ /** @var list<string> */
protected $fillable = [ protected $fillable = [
'einundzwanzig_pleb_id',
'name', 'name',
'description', 'description',
'support_in_sats', 'support_in_sats',
'website', 'website',
'accepted',
'sats_paid',
]; ];
/** /**

View File

@@ -3,6 +3,7 @@
namespace App\Policies; namespace App\Policies;
use App\Auth\NostrUser; use App\Auth\NostrUser;
use App\Models\EinundzwanzigPleb;
use App\Models\ProjectProposal; use App\Models\ProjectProposal;
class ProjectProposalPolicy class ProjectProposalPolicy
@@ -87,7 +88,7 @@ class ProjectProposalPolicy
} }
/** /**
* @param \App\Models\EinundzwanzigPleb $pleb * @param EinundzwanzigPleb $pleb
*/ */
private function isBoardMember(object $pleb): bool private function isBoardMember(object $pleb): bool
{ {

View File

@@ -3,6 +3,7 @@
namespace App\Traits; namespace App\Traits;
use App\Models\Profile; use App\Models\Profile;
use Illuminate\Support\Facades\Http;
use swentel\nostr\Filter\Filter; use swentel\nostr\Filter\Filter;
use swentel\nostr\Key\Key; use swentel\nostr\Key\Key;
use swentel\nostr\Message\RequestMessage; use swentel\nostr\Message\RequestMessage;
@@ -21,7 +22,7 @@ trait NostrFetcherTrait
public function getNip05HandlesForPubkey(string $pubkey): array public function getNip05HandlesForPubkey(string $pubkey): array
{ {
try { try {
$response = \Illuminate\Support\Facades\Http::get( $response = Http::get(
'https://einundzwanzig.space/.well-known/nostr.json', 'https://einundzwanzig.space/.well-known/nostr.json',
); );
$data = $response->json(); $data = $response->json();

View File

@@ -4,6 +4,7 @@ use App\Services\SecurityMonitor;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Sentry\Laravel\Integration; use Sentry\Laravel\Integration;
return Application::configure(basePath: dirname(__DIR__)) return Application::configure(basePath: dirname(__DIR__))
@@ -16,7 +17,7 @@ return Application::configure(basePath: dirname(__DIR__))
) )
->withMiddleware(function (Middleware $middleware) { ->withMiddleware(function (Middleware $middleware) {
$middleware->api(prepend: [ $middleware->api(prepend: [
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api', ThrottleRequests::class.':api',
]); ]);
}) })
->withExceptions(function (Exceptions $exceptions) { ->withExceptions(function (Exceptions $exceptions) {

View File

@@ -1,7 +1,10 @@
<?php <?php
use App\Providers\AppServiceProvider;
use App\Providers\NostrAuthServiceProvider;
return [ return [
App\Providers\AppServiceProvider::class, AppServiceProvider::class,
// App\Providers\FolioServiceProvider::class, // Disabled - laravel/folio package removed during Laravel 12 upgrade // App\Providers\FolioServiceProvider::class, // Disabled - laravel/folio package removed during Laravel 12 upgrade
App\Providers\NostrAuthServiceProvider::class, NostrAuthServiceProvider::class,
]; ];

View File

@@ -12,10 +12,10 @@
"akuechler/laravel-geoly": "^1.0", "akuechler/laravel-geoly": "^1.0",
"archtechx/enums": "^1.1", "archtechx/enums": "^1.1",
"calebporzio/sushi": "^2.5", "calebporzio/sushi": "^2.5",
"laravel/framework": "^12.0", "laravel/framework": "^13.0",
"laravel/nightwatch": "^1.22", "laravel/nightwatch": "^1.22",
"laravel/reverb": "^1.0", "laravel/reverb": "^1.0",
"laravel/tinker": "^2.9", "laravel/tinker": "^3.0",
"livewire/flux": "^2.10", "livewire/flux": "^2.10",
"livewire/flux-pro": "^2.10", "livewire/flux-pro": "^2.10",
"livewire/livewire": "^4.0", "livewire/livewire": "^4.0",
@@ -27,7 +27,7 @@
"sentry/sentry-laravel": "^4.9", "sentry/sentry-laravel": "^4.9",
"simplesoftwareio/simple-qrcode": "^4.2", "simplesoftwareio/simple-qrcode": "^4.2",
"spatie/image": "^3.7", "spatie/image": "^3.7",
"spatie/laravel-backup": "^9.1", "spatie/laravel-backup": "^10.0",
"spatie/laravel-ciphersweet": "^1.6", "spatie/laravel-ciphersweet": "^1.6",
"spatie/laravel-google-fonts": "^1.4", "spatie/laravel-google-fonts": "^1.4",
"spatie/laravel-markdown": "^2.5", "spatie/laravel-markdown": "^2.5",
@@ -39,7 +39,7 @@
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.23", "fakerphp/faker": "^1.23",
"laravel/boost": "^1.8", "laravel/boost": "^2.0",
"laravel/pail": "^1.2", "laravel/pail": "^1.2",
"laravel/pint": "^1.13", "laravel/pint": "^1.13",
"mockery/mockery": "^1.6", "mockery/mockery": "^1.6",

737
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,7 @@
<?php <?php
use App\Models\User;
return [ return [
/* /*
@@ -66,7 +68,7 @@ return [
'providers' => [ 'providers' => [
'users' => [ 'users' => [
'driver' => 'eloquent', 'driver' => 'eloquent',
'model' => env('AUTH_MODEL', App\Models\User::class), 'model' => env('AUTH_MODEL', User::class),
], ],
'nostr' => [ 'nostr' => [
'driver' => 'nostr', 'driver' => 'nostr',

View File

@@ -1,5 +1,16 @@
<?php <?php
use Spatie\Backup\Notifications\Notifiable;
use Spatie\Backup\Notifications\Notifications\BackupHasFailedNotification;
use Spatie\Backup\Notifications\Notifications\BackupWasSuccessfulNotification;
use Spatie\Backup\Notifications\Notifications\CleanupHasFailedNotification;
use Spatie\Backup\Notifications\Notifications\CleanupWasSuccessfulNotification;
use Spatie\Backup\Notifications\Notifications\HealthyBackupWasFoundNotification;
use Spatie\Backup\Notifications\Notifications\UnhealthyBackupWasFoundNotification;
use Spatie\Backup\Tasks\Cleanup\Strategies\DefaultStrategy;
use Spatie\Backup\Tasks\Monitor\HealthChecks\MaximumAgeInDays;
use Spatie\Backup\Tasks\Monitor\HealthChecks\MaximumStorageInMegabytes;
return [ return [
'backup' => [ 'backup' => [
@@ -196,19 +207,19 @@ return [
*/ */
'notifications' => [ 'notifications' => [
'notifications' => [ 'notifications' => [
\Spatie\Backup\Notifications\Notifications\BackupHasFailedNotification::class => ['mail'], BackupHasFailedNotification::class => ['mail'],
\Spatie\Backup\Notifications\Notifications\UnhealthyBackupWasFoundNotification::class => ['mail'], UnhealthyBackupWasFoundNotification::class => ['mail'],
\Spatie\Backup\Notifications\Notifications\CleanupHasFailedNotification::class => ['mail'], CleanupHasFailedNotification::class => ['mail'],
\Spatie\Backup\Notifications\Notifications\BackupWasSuccessfulNotification::class => ['mail'], BackupWasSuccessfulNotification::class => ['mail'],
\Spatie\Backup\Notifications\Notifications\HealthyBackupWasFoundNotification::class => ['mail'], HealthyBackupWasFoundNotification::class => ['mail'],
\Spatie\Backup\Notifications\Notifications\CleanupWasSuccessfulNotification::class => ['mail'], CleanupWasSuccessfulNotification::class => ['mail'],
], ],
/* /*
* Here you can specify the notifiable to which the notifications should be sent. The default * Here you can specify the notifiable to which the notifications should be sent. The default
* notifiable will use the variables specified in this config file. * notifiable will use the variables specified in this config file.
*/ */
'notifiable' => \Spatie\Backup\Notifications\Notifiable::class, 'notifiable' => Notifiable::class,
'mail' => [ 'mail' => [
'to' => 'your@example.com', 'to' => 'your@example.com',
@@ -257,8 +268,8 @@ return [
'name' => env('APP_NAME', 'laravel-backup'), 'name' => env('APP_NAME', 'laravel-backup'),
'disks' => ['backups'], 'disks' => ['backups'],
'health_checks' => [ 'health_checks' => [
\Spatie\Backup\Tasks\Monitor\HealthChecks\MaximumAgeInDays::class => 1, MaximumAgeInDays::class => 1,
\Spatie\Backup\Tasks\Monitor\HealthChecks\MaximumStorageInMegabytes::class => 5000, MaximumStorageInMegabytes::class => 5000,
], ],
], ],
@@ -284,7 +295,7 @@ return [
* No matter how you configure it the default strategy will never * No matter how you configure it the default strategy will never
* delete the newest backup. * delete the newest backup.
*/ */
'strategy' => \Spatie\Backup\Tasks\Cleanup\Strategies\DefaultStrategy::class, 'strategy' => DefaultStrategy::class,
'default_strategy' => [ 'default_strategy' => [
/* /*

View File

@@ -105,4 +105,17 @@ return [
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'),
/*
|--------------------------------------------------------------------------
| Serializable Classes
|--------------------------------------------------------------------------
|
| This option allows you to specify which classes may be unserialized from
| the cache to help prevent PHP deserialization gadget chain attacks.
| Set to false to allow no classes to be unserialized from cache.
|
*/
'serializable_classes' => false,
]; ];

View File

@@ -1,5 +1,9 @@
<?php <?php
use PowerComponents\LivewirePowerGrid\Components\Exports\OpenSpout\v4\ExportToCsv;
use PowerComponents\LivewirePowerGrid\Components\Exports\OpenSpout\v4\ExportToXLS;
use PowerComponents\LivewirePowerGrid\Themes\Tailwind;
return [ return [
/* /*
@@ -11,7 +15,7 @@ return [
| Configure here the theme of your choice. | Configure here the theme of your choice.
*/ */
'theme' => \PowerComponents\LivewirePowerGrid\Themes\Tailwind::class, 'theme' => Tailwind::class,
// 'theme' => \PowerComponents\LivewirePowerGrid\Themes\Bootstrap5::class, // 'theme' => \PowerComponents\LivewirePowerGrid\Themes\Bootstrap5::class,
/* /*
@@ -130,12 +134,12 @@ return [
'exportable' => [ 'exportable' => [
'default' => 'openspout_v4', 'default' => 'openspout_v4',
'openspout_v4' => [ 'openspout_v4' => [
'xlsx' => \PowerComponents\LivewirePowerGrid\Components\Exports\OpenSpout\v4\ExportToXLS::class, 'xlsx' => ExportToXLS::class,
'csv' => \PowerComponents\LivewirePowerGrid\Components\Exports\OpenSpout\v4\ExportToCsv::class, 'csv' => ExportToCsv::class,
], ],
'openspout_v3' => [ 'openspout_v3' => [
'xlsx' => \PowerComponents\LivewirePowerGrid\Components\Exports\OpenSpout\v3\ExportToXLS::class, 'xlsx' => PowerComponents\LivewirePowerGrid\Components\Exports\OpenSpout\v3\ExportToXLS::class,
'csv' => \PowerComponents\LivewirePowerGrid\Components\Exports\OpenSpout\v3\ExportToCsv::class, 'csv' => PowerComponents\LivewirePowerGrid\Components\Exports\OpenSpout\v3\ExportToCsv::class,
], ],
], ],

View File

@@ -1,5 +1,7 @@
<?php <?php
use Spatie\LaravelMarkdown\MarkdownRenderer;
return [ return [
'code_highlighting' => [ 'code_highlighting' => [
/* /*
@@ -64,7 +66,7 @@ return [
* *
* More info: https://spatie.be/docs/laravel-markdown/v1/advanced-usage/customizing-the-rendering-process * More info: https://spatie.be/docs/laravel-markdown/v1/advanced-usage/customizing-the-rendering-process
*/ */
'renderer_class' => Spatie\LaravelMarkdown\MarkdownRenderer::class, 'renderer_class' => MarkdownRenderer::class,
/* /*
* These extensions should be added to the markdown environment. A valid * These extensions should be added to the markdown environment. A valid

View File

@@ -1,5 +1,30 @@
<?php <?php
use Spatie\ImageOptimizer\Optimizers\Avifenc;
use Spatie\ImageOptimizer\Optimizers\Cwebp;
use Spatie\ImageOptimizer\Optimizers\Gifsicle;
use Spatie\ImageOptimizer\Optimizers\Jpegoptim;
use Spatie\ImageOptimizer\Optimizers\Optipng;
use Spatie\ImageOptimizer\Optimizers\Pngquant;
use Spatie\ImageOptimizer\Optimizers\Svgo;
use Spatie\MediaLibrary\Conversions\ImageGenerators\Avif;
use Spatie\MediaLibrary\Conversions\ImageGenerators\Image;
use Spatie\MediaLibrary\Conversions\ImageGenerators\Pdf;
use Spatie\MediaLibrary\Conversions\ImageGenerators\Svg;
use Spatie\MediaLibrary\Conversions\ImageGenerators\Video;
use Spatie\MediaLibrary\Conversions\ImageGenerators\Webp;
use Spatie\MediaLibrary\Conversions\Jobs\PerformConversionsJob;
use Spatie\MediaLibrary\Downloaders\DefaultDownloader;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Spatie\MediaLibrary\ResponsiveImages\Jobs\GenerateResponsiveImagesJob;
use Spatie\MediaLibrary\ResponsiveImages\TinyPlaceholderGenerator\Blurred;
use Spatie\MediaLibrary\ResponsiveImages\WidthCalculator\FileSizeOptimizedWidthCalculator;
use Spatie\MediaLibrary\Support\FileNamer\DefaultFileNamer;
use Spatie\MediaLibrary\Support\FileRemover\DefaultFileRemover;
use Spatie\MediaLibrary\Support\PathGenerator\DefaultPathGenerator;
use Spatie\MediaLibrary\Support\UrlGenerator\DefaultUrlGenerator;
use Spatie\MediaLibraryPro\Models\TemporaryUpload;
return [ return [
/* /*
@@ -39,7 +64,7 @@ return [
/* /*
* The fully qualified class name of the media model. * The fully qualified class name of the media model.
*/ */
'media_model' => Spatie\MediaLibrary\MediaCollections\Models\Media::class, 'media_model' => Media::class,
/* /*
* When enabled, media collections will be serialised using the default * When enabled, media collections will be serialised using the default
@@ -54,7 +79,7 @@ return [
* *
* This model is only used in Media Library Pro (https://medialibrary.pro) * This model is only used in Media Library Pro (https://medialibrary.pro)
*/ */
'temporary_upload_model' => Spatie\MediaLibraryPro\Models\TemporaryUpload::class, 'temporary_upload_model' => TemporaryUpload::class,
/* /*
* When enabled, Media Library Pro will only process temporary uploads that were uploaded * When enabled, Media Library Pro will only process temporary uploads that were uploaded
@@ -71,17 +96,17 @@ return [
/* /*
* This is the class that is responsible for naming generated files. * This is the class that is responsible for naming generated files.
*/ */
'file_namer' => Spatie\MediaLibrary\Support\FileNamer\DefaultFileNamer::class, 'file_namer' => DefaultFileNamer::class,
/* /*
* The class that contains the strategy for determining a media file's path. * The class that contains the strategy for determining a media file's path.
*/ */
'path_generator' => Spatie\MediaLibrary\Support\PathGenerator\DefaultPathGenerator::class, 'path_generator' => DefaultPathGenerator::class,
/* /*
* The class that contains the strategy for determining how to remove files. * The class that contains the strategy for determining how to remove files.
*/ */
'file_remover_class' => Spatie\MediaLibrary\Support\FileRemover\DefaultFileRemover::class, 'file_remover_class' => DefaultFileRemover::class,
/* /*
* Here you can specify which path generator should be used for the given class. * Here you can specify which path generator should be used for the given class.
@@ -96,7 +121,7 @@ return [
* When urls to files get generated, this class will be called. Use the default * When urls to files get generated, this class will be called. Use the default
* if your files are stored locally above the site root or on s3. * if your files are stored locally above the site root or on s3.
*/ */
'url_generator' => Spatie\MediaLibrary\Support\UrlGenerator\DefaultUrlGenerator::class, 'url_generator' => DefaultUrlGenerator::class,
/* /*
* Moves media on updating to keep path consistent. Enable it only with a custom * Moves media on updating to keep path consistent. Enable it only with a custom
@@ -116,34 +141,34 @@ return [
* the optimizers that will be used by default. * the optimizers that will be used by default.
*/ */
'image_optimizers' => [ 'image_optimizers' => [
Spatie\ImageOptimizer\Optimizers\Jpegoptim::class => [ Jpegoptim::class => [
'-m85', // set maximum quality to 85% '-m85', // set maximum quality to 85%
'--force', // ensure that progressive generation is always done also if a little bigger '--force', // ensure that progressive generation is always done also if a little bigger
'--strip-all', // this strips out all text information such as comments and EXIF data '--strip-all', // this strips out all text information such as comments and EXIF data
'--all-progressive', // this will make sure the resulting image is a progressive one '--all-progressive', // this will make sure the resulting image is a progressive one
], ],
Spatie\ImageOptimizer\Optimizers\Pngquant::class => [ Pngquant::class => [
'--force', // required parameter for this package '--force', // required parameter for this package
], ],
Spatie\ImageOptimizer\Optimizers\Optipng::class => [ Optipng::class => [
'-i0', // this will result in a non-interlaced, progressive scanned image '-i0', // this will result in a non-interlaced, progressive scanned image
'-o2', // this set the optimization level to two (multiple IDAT compression trials) '-o2', // this set the optimization level to two (multiple IDAT compression trials)
'-quiet', // required parameter for this package '-quiet', // required parameter for this package
], ],
Spatie\ImageOptimizer\Optimizers\Svgo::class => [ Svgo::class => [
'--disable=cleanupIDs', // disabling because it is known to cause troubles '--disable=cleanupIDs', // disabling because it is known to cause troubles
], ],
Spatie\ImageOptimizer\Optimizers\Gifsicle::class => [ Gifsicle::class => [
'-b', // required parameter for this package '-b', // required parameter for this package
'-O3', // this produces the slowest but best results '-O3', // this produces the slowest but best results
], ],
Spatie\ImageOptimizer\Optimizers\Cwebp::class => [ Cwebp::class => [
'-m 6', // for the slowest compression method in order to get the best compression. '-m 6', // for the slowest compression method in order to get the best compression.
'-pass 10', // for maximizing the amount of analysis pass. '-pass 10', // for maximizing the amount of analysis pass.
'-mt', // multithreading for some speed improvements. '-mt', // multithreading for some speed improvements.
'-q 90', // quality factor that brings the least noticeable changes. '-q 90', // quality factor that brings the least noticeable changes.
], ],
Spatie\ImageOptimizer\Optimizers\Avifenc::class => [ Avifenc::class => [
'-a cq-level=23', // constant quality level, lower values mean better quality and greater file size (0-63). '-a cq-level=23', // constant quality level, lower values mean better quality and greater file size (0-63).
'-j all', // number of jobs (worker threads, "all" uses all available cores). '-j all', // number of jobs (worker threads, "all" uses all available cores).
'--min 0', // min quantizer for color (0-63). '--min 0', // min quantizer for color (0-63).
@@ -159,12 +184,12 @@ return [
* These generators will be used to create an image of media files. * These generators will be used to create an image of media files.
*/ */
'image_generators' => [ 'image_generators' => [
Spatie\MediaLibrary\Conversions\ImageGenerators\Image::class, Image::class,
Spatie\MediaLibrary\Conversions\ImageGenerators\Webp::class, Webp::class,
Spatie\MediaLibrary\Conversions\ImageGenerators\Avif::class, Avif::class,
Spatie\MediaLibrary\Conversions\ImageGenerators\Pdf::class, Pdf::class,
Spatie\MediaLibrary\Conversions\ImageGenerators\Svg::class, Svg::class,
Spatie\MediaLibrary\Conversions\ImageGenerators\Video::class, Video::class,
], ],
/* /*
@@ -192,8 +217,8 @@ return [
* your custom jobs extend the ones provided by the package. * your custom jobs extend the ones provided by the package.
*/ */
'jobs' => [ 'jobs' => [
'perform_conversions' => Spatie\MediaLibrary\Conversions\Jobs\PerformConversionsJob::class, 'perform_conversions' => PerformConversionsJob::class,
'generate_responsive_images' => Spatie\MediaLibrary\ResponsiveImages\Jobs\GenerateResponsiveImagesJob::class, 'generate_responsive_images' => GenerateResponsiveImagesJob::class,
], ],
/* /*
@@ -201,7 +226,7 @@ return [
* This is particularly useful when the url of the image is behind a firewall and * This is particularly useful when the url of the image is behind a firewall and
* need to add additional flags, possibly using curl. * need to add additional flags, possibly using curl.
*/ */
'media_downloader' => Spatie\MediaLibrary\Downloaders\DefaultDownloader::class, 'media_downloader' => DefaultDownloader::class,
/* /*
* When using the addMediaFromUrl method the SSL is verified by default. * When using the addMediaFromUrl method the SSL is verified by default.
@@ -232,7 +257,7 @@ return [
* *
* https://docs.spatie.be/laravel-medialibrary/v9/advanced-usage/generating-responsive-images * https://docs.spatie.be/laravel-medialibrary/v9/advanced-usage/generating-responsive-images
*/ */
'width_calculator' => Spatie\MediaLibrary\ResponsiveImages\WidthCalculator\FileSizeOptimizedWidthCalculator::class, 'width_calculator' => FileSizeOptimizedWidthCalculator::class,
/* /*
* By default rendering media to a responsive image will add some javascript and a tiny placeholder. * By default rendering media to a responsive image will add some javascript and a tiny placeholder.
@@ -245,7 +270,7 @@ return [
* This class will generate the tiny placeholder used for progressive image loading. By default * This class will generate the tiny placeholder used for progressive image loading. By default
* the media library will use a tiny blurred jpg image. * the media library will use a tiny blurred jpg image.
*/ */
'tiny_placeholder_generator' => Spatie\MediaLibrary\ResponsiveImages\TinyPlaceholderGenerator\Blurred::class, 'tiny_placeholder_generator' => Blurred::class,
], ],
/* /*

View File

@@ -2,10 +2,12 @@
namespace Database\Factories; namespace Database\Factories;
use App\Enums\AssociationStatus;
use App\Models\EinundzwanzigPleb;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
/** /**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\EinundzwanzigPleb> * @extends Factory<EinundzwanzigPleb>
*/ */
class EinundzwanzigPlebFactory extends Factory class EinundzwanzigPlebFactory extends Factory
{ {
@@ -20,14 +22,14 @@ class EinundzwanzigPlebFactory extends Factory
'pubkey' => $this->faker->sha256(), 'pubkey' => $this->faker->sha256(),
'npub' => $this->faker->word(), 'npub' => $this->faker->word(),
'email' => $this->faker->safeEmail(), 'email' => $this->faker->safeEmail(),
'association_status' => \App\Enums\AssociationStatus::DEFAULT, 'association_status' => AssociationStatus::DEFAULT,
]; ];
} }
public function active(): static public function active(): static
{ {
return $this->state(fn (array $attributes) => [ return $this->state(fn (array $attributes) => [
'association_status' => \App\Enums\AssociationStatus::ACTIVE, 'association_status' => AssociationStatus::ACTIVE,
]); ]);
} }
@@ -35,13 +37,13 @@ class EinundzwanzigPlebFactory extends Factory
{ {
return $this->state(fn (array $attributes) => [ return $this->state(fn (array $attributes) => [
'npub' => config('einundzwanzig.config.current_board')[0], 'npub' => config('einundzwanzig.config.current_board')[0],
'association_status' => \App\Enums\AssociationStatus::HONORARY, 'association_status' => AssociationStatus::HONORARY,
]); ]);
} }
public function withPaidCurrentYear(): static public function withPaidCurrentYear(): static
{ {
return $this->afterCreating(function (\App\Models\EinundzwanzigPleb $pleb) { return $this->afterCreating(function (EinundzwanzigPleb $pleb) {
$pleb->paymentEvents()->create([ $pleb->paymentEvents()->create([
'year' => date('Y'), 'year' => date('Y'),
'amount' => 21000, 'amount' => 21000,

View File

@@ -2,10 +2,11 @@
namespace Database\Factories; namespace Database\Factories;
use App\Models\Election;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
/** /**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Election> * @extends Factory<Election>
*/ */
class ElectionFactory extends Factory class ElectionFactory extends Factory
{ {

View File

@@ -2,10 +2,13 @@
namespace Database\Factories; namespace Database\Factories;
use App\Enums\NewsCategory;
use App\Models\EinundzwanzigPleb;
use App\Models\Notification;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
/** /**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Notification> * @extends Factory<Notification>
*/ */
class NotificationFactory extends Factory class NotificationFactory extends Factory
{ {
@@ -19,8 +22,8 @@ class NotificationFactory extends Factory
return [ return [
'name' => $this->faker->sentence(3), 'name' => $this->faker->sentence(3),
'description' => $this->faker->paragraph(), 'description' => $this->faker->paragraph(),
'category' => $this->faker->randomElement(\App\Enums\NewsCategory::cases()), 'category' => $this->faker->randomElement(NewsCategory::cases()),
'einundzwanzig_pleb_id' => \App\Models\EinundzwanzigPleb::factory(), 'einundzwanzig_pleb_id' => EinundzwanzigPleb::factory(),
]; ];
} }
} }

View File

@@ -2,17 +2,19 @@
namespace Database\Factories; namespace Database\Factories;
use App\Models\EinundzwanzigPleb;
use App\Models\PaymentEvent;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
/** /**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\PaymentEvent> * @extends Factory<PaymentEvent>
*/ */
class PaymentEventFactory extends Factory class PaymentEventFactory extends Factory
{ {
public function definition(): array public function definition(): array
{ {
return [ return [
'einundzwanzig_pleb_id' => \App\Models\EinundzwanzigPleb::factory(), 'einundzwanzig_pleb_id' => EinundzwanzigPleb::factory(),
'year' => fake()->year(), 'year' => fake()->year(),
'event_id' => fake()->uuid(), 'event_id' => fake()->uuid(),
'amount' => 21000, 'amount' => 21000,

View File

@@ -2,10 +2,12 @@
namespace Database\Factories; namespace Database\Factories;
use App\Models\EinundzwanzigPleb;
use App\Models\ProjectProposal;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
/** /**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\ProjectProposal> * @extends Factory<ProjectProposal>
*/ */
class ProjectProposalFactory extends Factory class ProjectProposalFactory extends Factory
{ {
@@ -17,7 +19,7 @@ class ProjectProposalFactory extends Factory
public function definition(): array public function definition(): array
{ {
return [ return [
'einundzwanzig_pleb_id' => \App\Models\EinundzwanzigPleb::factory(), 'einundzwanzig_pleb_id' => EinundzwanzigPleb::factory(),
'name' => $this->faker->sentence(3), 'name' => $this->faker->sentence(3),
'description' => $this->faker->paragraph(), 'description' => $this->faker->paragraph(),
'support_in_sats' => $this->faker->numberBetween(10000, 1000000), 'support_in_sats' => $this->faker->numberBetween(10000, 1000000),

View File

@@ -2,12 +2,13 @@
namespace Database\Factories; namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str; use Illuminate\Support\Str;
/** /**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User> * @extends Factory<User>
*/ */
class UserFactory extends Factory class UserFactory extends Factory
{ {

View File

@@ -107,10 +107,15 @@ class extends Component
'description' => $this->form['description'], 'description' => $this->form['description'],
'support_in_sats' => (int) $this->form['support_in_sats'], 'support_in_sats' => (int) $this->form['support_in_sats'],
'website' => $this->form['website'], 'website' => $this->form['website'],
'accepted' => $canAccept ? (bool) $this->form['accepted'] : $this->project->accepted,
'sats_paid' => $canAccept ? $this->form['sats_paid'] : $this->project->sats_paid,
]); ]);
// Update admin-only fields directly if user has permission
if ($canAccept) {
$this->project->accepted = (bool) $this->form['accepted'];
$this->project->sats_paid = $this->form['sats_paid'];
$this->project->save();
}
if ($this->file) { if ($this->file) {
$this->project->addMedia($this->file)->toMediaCollection('main'); $this->project->addMedia($this->file)->toMediaCollection('main');
} }

View File

@@ -1,5 +1,6 @@
<?php <?php
use App\Support\NostrAuth;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\Session;
@@ -41,7 +42,7 @@ Route::get('media/{media}', function (Media $media, Request $request) {
->middleware('signed'); ->middleware('signed');
Route::post('logout', function () { Route::post('logout', function () {
\App\Support\NostrAuth::logout(); NostrAuth::logout();
Session::flush(); Session::flush();
return redirect('/'); return redirect('/');

View File

@@ -22,15 +22,15 @@ beforeEach(function () {
'event_id' => 'test_event_'.Str::random(40), 'event_id' => 'test_event_'.Str::random(40),
]); ]);
$this->project = ProjectProposal::query()->create([ $this->project = new ProjectProposal;
'einundzwanzig_pleb_id' => $this->pleb->id, $this->project->einundzwanzig_pleb_id = $this->pleb->id;
'name' => 'Original Project', $this->project->name = 'Original Project';
'description' => 'Original Description', $this->project->description = 'Original Description';
'support_in_sats' => 21000, $this->project->support_in_sats = 21000;
'website' => 'https://original.example.com', $this->project->website = 'https://original.example.com';
'accepted' => false, $this->project->accepted = false;
'sats_paid' => 0, $this->project->sats_paid = 0;
]); $this->project->save();
// Get board member pubkeys from config // Get board member pubkeys from config
$this->boardMember = EinundzwanzigPleb::query()->create([ $this->boardMember = EinundzwanzigPleb::query()->create([

View File

@@ -1,6 +1,7 @@
<?php <?php
use App\Models\ProjectProposal; use App\Models\ProjectProposal;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
it('serves original media via signed route', function () { it('serves original media via signed route', function () {
@@ -9,7 +10,7 @@ it('serves original media via signed route', function () {
$project = ProjectProposal::factory()->create(); $project = ProjectProposal::factory()->create();
$project->addMedia( $project->addMedia(
\Illuminate\Http\UploadedFile::fake()->image('test.jpg', 100, 100) UploadedFile::fake()->image('test.jpg', 100, 100)
)->toMediaCollection('main'); )->toMediaCollection('main');
$media = $project->getFirstMedia('main'); $media = $project->getFirstMedia('main');
@@ -25,7 +26,7 @@ it('serves conversion media via signed route when conversion parameter is provid
$project = ProjectProposal::factory()->create(); $project = ProjectProposal::factory()->create();
$project->addMedia( $project->addMedia(
\Illuminate\Http\UploadedFile::fake()->image('test.jpg', 500, 500) UploadedFile::fake()->image('test.jpg', 500, 500)
)->toMediaCollection('main'); )->toMediaCollection('main');
$media = $project->getFirstMedia('main'); $media = $project->getFirstMedia('main');
@@ -44,7 +45,7 @@ it('falls back to original when conversion does not exist', function () {
$project = ProjectProposal::factory()->create(); $project = ProjectProposal::factory()->create();
$project->addMedia( $project->addMedia(
\Illuminate\Http\UploadedFile::fake()->image('test.jpg', 100, 100) UploadedFile::fake()->image('test.jpg', 100, 100)
)->toMediaCollection('main'); )->toMediaCollection('main');
$media = $project->getFirstMedia('main'); $media = $project->getFirstMedia('main');
@@ -63,7 +64,7 @@ it('rejects unsigned media requests', function () {
$project = ProjectProposal::factory()->create(); $project = ProjectProposal::factory()->create();
$project->addMedia( $project->addMedia(
\Illuminate\Http\UploadedFile::fake()->image('test.jpg', 100, 100) UploadedFile::fake()->image('test.jpg', 100, 100)
)->toMediaCollection('main'); )->toMediaCollection('main');
$media = $project->getFirstMedia('main'); $media = $project->getFirstMedia('main');
@@ -77,7 +78,7 @@ it('generates signed url with conversion parameter', function () {
$project = ProjectProposal::factory()->create(); $project = ProjectProposal::factory()->create();
$project->addMedia( $project->addMedia(
\Illuminate\Http\UploadedFile::fake()->image('test.jpg', 500, 500) UploadedFile::fake()->image('test.jpg', 500, 500)
)->toMediaCollection('main'); )->toMediaCollection('main');
$urlWithoutConversion = $project->getSignedMediaUrl('main'); $urlWithoutConversion = $project->getSignedMediaUrl('main');

View File

@@ -1,6 +1,7 @@
<?php <?php
use App\Auth\NostrUser; use App\Auth\NostrUser;
use App\Enums\AssociationStatus;
use App\Models\EinundzwanzigPleb; use App\Models\EinundzwanzigPleb;
use App\Models\Election; use App\Models\Election;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
@@ -77,7 +78,7 @@ it('allows active member to vote in an election', function () {
it('allows honorary member to vote in an election', function () { it('allows honorary member to vote in an election', function () {
$pleb = EinundzwanzigPleb::factory()->create([ $pleb = EinundzwanzigPleb::factory()->create([
'association_status' => \App\Enums\AssociationStatus::HONORARY, 'association_status' => AssociationStatus::HONORARY,
]); ]);
$election = Election::factory()->create(); $election = Election::factory()->create();
$nostrUser = new NostrUser($pleb->pubkey); $nostrUser = new NostrUser($pleb->pubkey);
@@ -87,7 +88,7 @@ it('allows honorary member to vote in an election', function () {
it('denies passive member from voting in an election', function () { it('denies passive member from voting in an election', function () {
$pleb = EinundzwanzigPleb::factory()->create([ $pleb = EinundzwanzigPleb::factory()->create([
'association_status' => \App\Enums\AssociationStatus::PASSIVE, 'association_status' => AssociationStatus::PASSIVE,
]); ]);
$election = Election::factory()->create(); $election = Election::factory()->create();
$nostrUser = new NostrUser($pleb->pubkey); $nostrUser = new NostrUser($pleb->pubkey);

View File

@@ -1,6 +1,7 @@
<?php <?php
use App\Auth\NostrUser; use App\Auth\NostrUser;
use App\Enums\AssociationStatus;
use App\Models\EinundzwanzigPleb; use App\Models\EinundzwanzigPleb;
use App\Models\ProjectProposal; use App\Models\ProjectProposal;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
@@ -48,7 +49,7 @@ it('denies creation for active member without paid membership', function () {
it('denies creation for passive member without paid membership', function () { it('denies creation for passive member without paid membership', function () {
$pleb = EinundzwanzigPleb::factory()->create([ $pleb = EinundzwanzigPleb::factory()->create([
'association_status' => \App\Enums\AssociationStatus::PASSIVE, 'association_status' => AssociationStatus::PASSIVE,
]); ]);
$nostrUser = new NostrUser($pleb->pubkey); $nostrUser = new NostrUser($pleb->pubkey);
@@ -57,7 +58,7 @@ it('denies creation for passive member without paid membership', function () {
it('allows passive member with paid membership to create project proposals', function () { it('allows passive member with paid membership to create project proposals', function () {
$pleb = EinundzwanzigPleb::factory()->withPaidCurrentYear()->create([ $pleb = EinundzwanzigPleb::factory()->withPaidCurrentYear()->create([
'association_status' => \App\Enums\AssociationStatus::PASSIVE, 'association_status' => AssociationStatus::PASSIVE,
]); ]);
$nostrUser = new NostrUser($pleb->pubkey); $nostrUser = new NostrUser($pleb->pubkey);

View File

@@ -2,6 +2,7 @@
use App\Models\EinundzwanzigPleb; use App\Models\EinundzwanzigPleb;
use App\Models\ProjectProposal; use App\Models\ProjectProposal;
use App\Models\Vote;
use App\Support\NostrAuth; use App\Support\NostrAuth;
use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\RateLimiter;
use Livewire\Livewire; use Livewire\Livewire;
@@ -46,7 +47,7 @@ test('voting actions are rate limited after 10 attempts', function () {
RateLimiter::attempt('voting:127.0.0.1', 10, function () {}); RateLimiter::attempt('voting:127.0.0.1', 10, function () {});
} }
Livewire::test('association.project-support.show', ['projectProposal' => $project->slug]) Livewire::test('association.project-support.show', ['projectProposal' => $project])
->call('handleApprove') ->call('handleApprove')
->assertStatus(429); ->assertStatus(429);
}); });
@@ -93,7 +94,7 @@ test('project proposal update is rate limited after 5 attempts', function () {
RateLimiter::attempt('project-proposal-update:127.0.0.1', 5, function () {}); RateLimiter::attempt('project-proposal-update:127.0.0.1', 5, function () {});
} }
Livewire::test('association.project-support.form.edit', ['projectProposal' => $project->slug]) Livewire::test('association.project-support.form.edit', ['projectProposal' => $project])
->set('form.name', 'Updated Name') ->set('form.name', 'Updated Name')
->call('update') ->call('update')
->assertStatus(429); ->assertStatus(429);
@@ -105,11 +106,11 @@ test('voting works within rate limit', function () {
NostrAuth::login($pleb->pubkey); NostrAuth::login($pleb->pubkey);
Livewire::test('association.project-support.show', ['projectProposal' => $project->slug]) Livewire::test('association.project-support.show', ['projectProposal' => $project])
->call('handleApprove') ->call('handleApprove')
->assertHasNoErrors(); ->assertHasNoErrors();
$vote = \App\Models\Vote::query() $vote = Vote::query()
->where('project_proposal_id', $project->id) ->where('project_proposal_id', $project->id)
->where('einundzwanzig_pleb_id', $pleb->id) ->where('einundzwanzig_pleb_id', $pleb->id)
->first(); ->first();

View File

@@ -1,5 +1,8 @@
<?php <?php
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Test Case | Test Case
@@ -12,8 +15,8 @@
*/ */
uses( uses(
Tests\TestCase::class, TestCase::class,
Illuminate\Foundation\Testing\RefreshDatabase::class, RefreshDatabase::class,
)->in('Feature'); )->in('Feature');
/* /*