|
|
|
@@ -3,53 +3,59 @@
|
|
|
|
|
namespace App\Providers;
|
|
|
|
|
|
|
|
|
|
use App\Contracts\Images\SubjectDetectorInterface;
|
|
|
|
|
use Illuminate\Cache\RateLimiting\Limit;
|
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
use Illuminate\Support\Facades\RateLimiter;
|
|
|
|
|
use Illuminate\Support\ServiceProvider;
|
|
|
|
|
use App\Contracts\Moderation\CommentSpamClassifier;
|
|
|
|
|
use App\Events\Achievements\AchievementCheckRequested;
|
|
|
|
|
use App\Events\Achievements\UserXpUpdated;
|
|
|
|
|
use App\Events\Posts\ArtworkShared;
|
|
|
|
|
use App\Events\Posts\PostCommented;
|
|
|
|
|
use App\Http\Middleware\ConditionalCors;
|
|
|
|
|
use App\Listeners\Academy\HandleAcademyStripeWebhook;
|
|
|
|
|
use App\Listeners\Academy\HandleAcademyStripeWebhookHandled;
|
|
|
|
|
use App\Listeners\Achievements\CheckUserAchievements;
|
|
|
|
|
use App\Listeners\Posts\AwardXpForPostCommented;
|
|
|
|
|
use App\Listeners\Posts\SendArtworkSharedNotification;
|
|
|
|
|
use App\Listeners\Posts\SendPostCommentedNotification;
|
|
|
|
|
use App\Models\Artwork;
|
|
|
|
|
use App\Models\ArtworkAward;
|
|
|
|
|
use App\Models\ArtworkComment;
|
|
|
|
|
use App\Models\ArtworkFeature;
|
|
|
|
|
use App\Models\ArtworkFavourite;
|
|
|
|
|
use App\Models\ArtworkMedal;
|
|
|
|
|
use App\Models\ArtworkReaction;
|
|
|
|
|
use App\Models\ContentType;
|
|
|
|
|
use App\Models\GroupRelease;
|
|
|
|
|
use App\Models\GroupReleaseContributor;
|
|
|
|
|
use App\Models\HomepageAnnouncement;
|
|
|
|
|
use App\Observers\ArtworkAwardObserver;
|
|
|
|
|
use App\Observers\ArtworkCommentObserver;
|
|
|
|
|
use App\Observers\ArtworkFeatureObserver;
|
|
|
|
|
use App\Observers\ArtworkFavouriteObserver;
|
|
|
|
|
use App\Observers\ArtworkObserver;
|
|
|
|
|
use App\Observers\ArtworkReactionObserver;
|
|
|
|
|
use App\Observers\ContentTypeObserver;
|
|
|
|
|
use App\Observers\GroupReleaseContributorObserver;
|
|
|
|
|
use App\Observers\GroupReleaseObserver;
|
|
|
|
|
use App\Observers\HomepageAnnouncementObserver;
|
|
|
|
|
use App\Services\Upload\Contracts\UploadDraftServiceInterface;
|
|
|
|
|
use App\Services\Upload\UploadDraftService;
|
|
|
|
|
use App\Services\ContentTypes\ContentTypeSlugResolver;
|
|
|
|
|
use App\Services\Worlds\WorldService;
|
|
|
|
|
use Illuminate\Support\Facades\Blade;
|
|
|
|
|
use Illuminate\Support\Facades\View;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
|
use Illuminate\Support\Facades\Auth;
|
|
|
|
|
use Illuminate\Support\Facades\Event;
|
|
|
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
|
use Illuminate\Queue\Events\JobFailed;
|
|
|
|
|
use App\Services\ReceivedCommentsInboxService;
|
|
|
|
|
use App\Services\Countries\CountryRemoteProvider;
|
|
|
|
|
use App\Services\Countries\CountryRemoteProviderInterface;
|
|
|
|
|
use App\Services\EarlyGrowth\SpotlightEngine;
|
|
|
|
|
use App\Services\EarlyGrowth\SpotlightEngineInterface;
|
|
|
|
|
use App\Services\Images\Detectors\ChainedSubjectDetector;
|
|
|
|
|
use App\Services\Images\Detectors\HeuristicSubjectDetector;
|
|
|
|
|
use App\Services\Images\Detectors\NullSubjectDetector;
|
|
|
|
|
use App\Services\Images\Detectors\VisionSubjectDetector;
|
|
|
|
|
use App\Services\Moderation\TogetherCommentSpamClassifier;
|
|
|
|
|
use App\Services\ReceivedCommentsInboxService;
|
|
|
|
|
use App\Services\Recommendations\VectorSimilarity\VectorAdapterFactory;
|
|
|
|
|
use App\Services\Recommendations\VectorSimilarity\VectorAdapterInterface;
|
|
|
|
|
use App\Services\Upload\Contracts\UploadDraftServiceInterface;
|
|
|
|
|
use App\Services\Upload\UploadDraftService;
|
|
|
|
|
use App\Services\Worlds\WorldService;
|
|
|
|
|
use App\Support\Http\TimedInertiaSsrGateway;
|
|
|
|
|
use Illuminate\Cache\RateLimiting\Limit;
|
|
|
|
|
use Illuminate\Contracts\Http\Kernel;
|
|
|
|
|
use Illuminate\Foundation\Configuration\Middleware;
|
|
|
|
|
use Illuminate\Http\Middleware\HandleCors;
|
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
use Illuminate\Queue\Events\JobFailed;
|
|
|
|
|
use Illuminate\Support\Facades\Auth;
|
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
use Illuminate\Support\Facades\Event;
|
|
|
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
|
use Illuminate\Support\Facades\RateLimiter;
|
|
|
|
|
use Illuminate\Support\ServiceProvider;
|
|
|
|
|
use Illuminate\View\Factory;
|
|
|
|
|
use Inertia\Ssr\Gateway;
|
|
|
|
|
use Klevze\ControlPanel\Framework\Core\Menu;
|
|
|
|
|
use Laravel\Cashier\Events\WebhookHandled;
|
|
|
|
|
use Laravel\Cashier\Events\WebhookReceived;
|
|
|
|
|
use App\Listeners\Academy\HandleAcademyStripeWebhook;
|
|
|
|
|
use App\Listeners\Academy\HandleAcademyStripeWebhookHandled;
|
|
|
|
|
use Meilisearch\Client;
|
|
|
|
|
use SocialiteProviders\Discord\DiscordExtendSocialite;
|
|
|
|
|
use SocialiteProviders\Manager\SocialiteWasCalled;
|
|
|
|
|
|
|
|
|
|
class AppServiceProvider extends ServiceProvider
|
|
|
|
|
{
|
|
|
|
@@ -58,14 +64,18 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
*/
|
|
|
|
|
public function register(): void
|
|
|
|
|
{
|
|
|
|
|
$this->app->bind(CommentSpamClassifier::class, function ($app) {
|
|
|
|
|
return $app->make(TogetherCommentSpamClassifier::class);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$this->app->bind(
|
|
|
|
|
\Inertia\Ssr\Gateway::class,
|
|
|
|
|
\App\Support\Http\TimedInertiaSsrGateway::class,
|
|
|
|
|
Gateway::class,
|
|
|
|
|
TimedInertiaSsrGateway::class,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->app->singleton(
|
|
|
|
|
\App\Services\Countries\CountryRemoteProviderInterface::class,
|
|
|
|
|
\App\Services\Countries\CountryRemoteProvider::class,
|
|
|
|
|
CountryRemoteProviderInterface::class,
|
|
|
|
|
CountryRemoteProvider::class,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Bind UploadDraftService interface to implementation
|
|
|
|
@@ -75,14 +85,14 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
|
|
|
|
|
// Bind vector adapter interface for similarity system (resolves via factory)
|
|
|
|
|
$this->app->bind(
|
|
|
|
|
\App\Services\Recommendations\VectorSimilarity\VectorAdapterInterface::class,
|
|
|
|
|
fn () => \App\Services\Recommendations\VectorSimilarity\VectorAdapterFactory::make(),
|
|
|
|
|
VectorAdapterInterface::class,
|
|
|
|
|
fn () => VectorAdapterFactory::make(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// EGS: bind SpotlightEngineInterface to the concrete SpotlightEngine
|
|
|
|
|
$this->app->singleton(
|
|
|
|
|
\App\Services\EarlyGrowth\SpotlightEngineInterface::class,
|
|
|
|
|
\App\Services\EarlyGrowth\SpotlightEngine::class,
|
|
|
|
|
SpotlightEngineInterface::class,
|
|
|
|
|
SpotlightEngine::class,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->app->singleton(SubjectDetectorInterface::class, function ($app) {
|
|
|
|
@@ -97,7 +107,7 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
// with no HTTP timeout, so a slow/overloaded Meilisearch leaves PHP-FPM
|
|
|
|
|
// workers blocked forever in curl_exec(), draining the whole pool under
|
|
|
|
|
// a search traffic spike. Fail fast instead.
|
|
|
|
|
$this->app->singleton(\Meilisearch\Client::class, function ($app) {
|
|
|
|
|
$this->app->singleton(Client::class, function ($app) {
|
|
|
|
|
$config = $app['config']->get('scout.meilisearch');
|
|
|
|
|
|
|
|
|
|
$httpClient = new \GuzzleHttp\Client([
|
|
|
|
@@ -105,7 +115,7 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
'timeout' => 5,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return new \Meilisearch\Client($config['host'], $config['key'], $httpClient);
|
|
|
|
|
return new Client($config['host'], $config['key'], $httpClient);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -119,12 +129,29 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
// Map the 'legacy' view namespace to resources/views/_legacy so all
|
|
|
|
|
// view('legacy::foo') and @include('legacy::foo') calls resolve correctly
|
|
|
|
|
// after the folder was renamed from legacy/ to _legacy/.
|
|
|
|
|
View::addNamespace('legacy', resource_path('views/_legacy'));
|
|
|
|
|
$legacyViewPath = resource_path('views/_legacy');
|
|
|
|
|
$registerLegacyViewNamespace = static function (Factory $view) use ($legacyViewPath): void {
|
|
|
|
|
$view->addNamespace('legacy', $legacyViewPath);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if ($this->app->resolved('view')) {
|
|
|
|
|
$registerLegacyViewNamespace($this->app->make('view'));
|
|
|
|
|
} else {
|
|
|
|
|
$this->app->afterResolving('view', $registerLegacyViewNamespace);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$exceptionRendererComponentsPath = base_path('vendor/laravel/framework/src/Illuminate/Foundation/resources/exceptions/renderer/components');
|
|
|
|
|
|
|
|
|
|
$registerExceptionRendererComponents = static function ($compiler) use ($exceptionRendererComponentsPath): void {
|
|
|
|
|
if (is_dir($exceptionRendererComponentsPath)) {
|
|
|
|
|
Blade::anonymousComponentNamespace($exceptionRendererComponentsPath, 'laravel-exceptions-renderer');
|
|
|
|
|
$compiler->anonymousComponentNamespace($exceptionRendererComponentsPath, 'laravel-exceptions-renderer');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if ($this->app->resolved('blade.compiler')) {
|
|
|
|
|
$registerExceptionRendererComponents($this->app->make('blade.compiler'));
|
|
|
|
|
} else {
|
|
|
|
|
$this->app->afterResolving('blade.compiler', $registerExceptionRendererComponents);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->configureAuthRateLimiters();
|
|
|
|
@@ -140,45 +167,35 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
$this->configureSettingsRateLimiters();
|
|
|
|
|
$this->configureMailFailureLogging();
|
|
|
|
|
|
|
|
|
|
ArtworkAward::observe(ArtworkAwardObserver::class);
|
|
|
|
|
ArtworkMedal::observe(ArtworkAwardObserver::class);
|
|
|
|
|
Artwork::observe(ArtworkObserver::class);
|
|
|
|
|
ArtworkFeature::observe(ArtworkFeatureObserver::class);
|
|
|
|
|
ArtworkFavourite::observe(ArtworkFavouriteObserver::class);
|
|
|
|
|
ArtworkComment::observe(ArtworkCommentObserver::class);
|
|
|
|
|
ArtworkReaction::observe(ArtworkReactionObserver::class);
|
|
|
|
|
ContentType::observe(ContentTypeObserver::class);
|
|
|
|
|
GroupRelease::observe(GroupReleaseObserver::class);
|
|
|
|
|
GroupReleaseContributor::observe(GroupReleaseContributorObserver::class);
|
|
|
|
|
HomepageAnnouncement::observe(HomepageAnnouncementObserver::class);
|
|
|
|
|
|
|
|
|
|
// ── OAuth / SocialiteProviders ──────────────────────────────────────
|
|
|
|
|
Event::listen(
|
|
|
|
|
\SocialiteProviders\Manager\SocialiteWasCalled::class,
|
|
|
|
|
\SocialiteProviders\Discord\DiscordExtendSocialite::class,
|
|
|
|
|
SocialiteWasCalled::class,
|
|
|
|
|
DiscordExtendSocialite::class,
|
|
|
|
|
);
|
|
|
|
|
// Apple provider removed — no listener registered
|
|
|
|
|
|
|
|
|
|
// ── Posts / Feed System Events ──────────────────────────────────────
|
|
|
|
|
Event::listen(
|
|
|
|
|
\App\Events\Posts\ArtworkShared::class,
|
|
|
|
|
\App\Listeners\Posts\SendArtworkSharedNotification::class,
|
|
|
|
|
ArtworkShared::class,
|
|
|
|
|
SendArtworkSharedNotification::class,
|
|
|
|
|
);
|
|
|
|
|
Event::listen(
|
|
|
|
|
\App\Events\Posts\PostCommented::class,
|
|
|
|
|
\App\Listeners\Posts\SendPostCommentedNotification::class,
|
|
|
|
|
PostCommented::class,
|
|
|
|
|
SendPostCommentedNotification::class,
|
|
|
|
|
);
|
|
|
|
|
Event::listen(
|
|
|
|
|
\App\Events\Posts\PostCommented::class,
|
|
|
|
|
\App\Listeners\Posts\AwardXpForPostCommented::class,
|
|
|
|
|
PostCommented::class,
|
|
|
|
|
AwardXpForPostCommented::class,
|
|
|
|
|
);
|
|
|
|
|
Event::listen(
|
|
|
|
|
\App\Events\Achievements\AchievementCheckRequested::class,
|
|
|
|
|
\App\Listeners\Achievements\CheckUserAchievements::class,
|
|
|
|
|
AchievementCheckRequested::class,
|
|
|
|
|
CheckUserAchievements::class,
|
|
|
|
|
);
|
|
|
|
|
Event::listen(
|
|
|
|
|
\App\Events\Achievements\UserXpUpdated::class,
|
|
|
|
|
\App\Listeners\Achievements\CheckUserAchievements::class,
|
|
|
|
|
UserXpUpdated::class,
|
|
|
|
|
CheckUserAchievements::class,
|
|
|
|
|
);
|
|
|
|
|
Event::listen(
|
|
|
|
|
WebhookReceived::class,
|
|
|
|
@@ -190,7 +207,8 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Provide toolbar counts and user info to layout views (port of legacy toolbar logic)
|
|
|
|
|
View::composer(['layouts.nova', 'layouts.nova.*'], function ($view) {
|
|
|
|
|
$registerNovaLayoutComposer = function (Factory $factory): void {
|
|
|
|
|
$factory->composer(['layouts.nova', 'layouts.nova.*'], function ($view) {
|
|
|
|
|
$uploadCount = $favCount = $msgCount = $noticeCount = $receivedCommentsCount = 0;
|
|
|
|
|
$avatarHash = null;
|
|
|
|
|
$displayName = null;
|
|
|
|
@@ -198,7 +216,7 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
$toolbarContentTypes = collect();
|
|
|
|
|
$toolbarActiveCampaign = null;
|
|
|
|
|
$request = request();
|
|
|
|
|
$canReadSessionAuth = $request instanceof \Illuminate\Http\Request
|
|
|
|
|
$canReadSessionAuth = $request instanceof Request
|
|
|
|
|
&& $request->hasSession()
|
|
|
|
|
&& $request->attributes->get('skinbase.session_skipped') !== true;
|
|
|
|
|
$authUser = $canReadSessionAuth ? Auth::user() : null;
|
|
|
|
@@ -299,21 +317,28 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
$displayName = $authUser->name ?: ($authUser->username ?? '');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$view->with(compact('userId','uploadCount', 'favCount', 'msgCount', 'noticeCount', 'receivedCommentsCount', 'avatarHash', 'displayName', 'toolbarContentTypes', 'toolbarActiveCampaign'));
|
|
|
|
|
$view->with(compact('userId', 'uploadCount', 'favCount', 'msgCount', 'noticeCount', 'receivedCommentsCount', 'avatarHash', 'displayName', 'toolbarContentTypes', 'toolbarActiveCampaign'));
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if ($this->app->resolved('view')) {
|
|
|
|
|
$registerNovaLayoutComposer($this->app->make('view'));
|
|
|
|
|
} else {
|
|
|
|
|
$this->app->afterResolving('view', $registerNovaLayoutComposer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Replace the framework HandleCors with our ConditionalCors so the
|
|
|
|
|
// CP_ENABLE_CORS / config('cors.paths') toggle takes effect.
|
|
|
|
|
try {
|
|
|
|
|
$middlewareConfig = $this->app->make(\Illuminate\Foundation\Configuration\Middleware::class);
|
|
|
|
|
$middlewareConfig = $this->app->make(Middleware::class);
|
|
|
|
|
$middlewareConfig->replace(
|
|
|
|
|
\Illuminate\Http\Middleware\HandleCors::class,
|
|
|
|
|
\App\Http\Middleware\ConditionalCors::class
|
|
|
|
|
HandleCors::class,
|
|
|
|
|
ConditionalCors::class
|
|
|
|
|
);
|
|
|
|
|
} catch (\Throwable $_) {
|
|
|
|
|
// Fallback: push to kernel if replace isn't available in this app instance
|
|
|
|
|
$this->app->make(\Illuminate\Contracts\Http\Kernel::class)
|
|
|
|
|
->pushMiddleware(\App\Http\Middleware\ConditionalCors::class);
|
|
|
|
|
$this->app->make(Kernel::class)
|
|
|
|
|
->pushMiddleware(ConditionalCors::class);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -322,13 +347,13 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
RateLimiter::for('register-ip', function (Request $request): Limit {
|
|
|
|
|
$limit = max(1, (int) config('registration.ip_per_minute_limit', 3));
|
|
|
|
|
|
|
|
|
|
return Limit::perMinute($limit)->by('register:ip:' . $request->ip());
|
|
|
|
|
return Limit::perMinute($limit)->by('register:ip:'.$request->ip());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
RateLimiter::for('register-ip-daily', function (Request $request): Limit {
|
|
|
|
|
$limit = max(1, (int) config('registration.ip_per_day_limit', 20));
|
|
|
|
|
|
|
|
|
|
return Limit::perDay($limit)->by('register:ip:daily:' . $request->ip());
|
|
|
|
|
return Limit::perDay($limit)->by('register:ip:daily:'.$request->ip());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
RateLimiter::for('register', function (Request $request): array {
|
|
|
|
@@ -337,8 +362,8 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
$emailLimit = (int) config('registration.email_per_minute_limit', 6);
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
Limit::perMinute($ipLimit)->by('register:ip:' . $request->ip()),
|
|
|
|
|
Limit::perMinute($emailLimit)->by('register:email:' . $emailKey),
|
|
|
|
|
Limit::perMinute($ipLimit)->by('register:ip:'.$request->ip()),
|
|
|
|
|
Limit::perMinute($emailLimit)->by('register:email:'.$emailKey),
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
@@ -382,15 +407,15 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
private function configureNovaCardRateLimiters(): void
|
|
|
|
|
{
|
|
|
|
|
foreach (['drafts', 'autosave', 'publish', 'background_upload', 'render'] as $key) {
|
|
|
|
|
RateLimiter::for('nova-cards-' . str_replace('_', '-', $key), function (Request $request) use ($key): array {
|
|
|
|
|
$config = (array) config('nova_cards.rate_limits.' . $key, []);
|
|
|
|
|
RateLimiter::for('nova-cards-'.str_replace('_', '-', $key), function (Request $request) use ($key): array {
|
|
|
|
|
$config = (array) config('nova_cards.rate_limits.'.$key, []);
|
|
|
|
|
$perUser = max(1, (int) ($config['per_user'] ?? 30));
|
|
|
|
|
$perIp = max(1, (int) ($config['per_ip'] ?? 60));
|
|
|
|
|
$scope = 'nova-cards:' . $key . ':';
|
|
|
|
|
$scope = 'nova-cards:'.$key.':';
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
Limit::perMinute($perUser)->by($scope . 'user:' . ($request->user()?->id ?? 'guest')),
|
|
|
|
|
Limit::perMinute($perIp)->by($scope . 'ip:' . $request->ip()),
|
|
|
|
|
Limit::perMinute($perUser)->by($scope.'user:'.($request->user()?->id ?? 'guest')),
|
|
|
|
|
Limit::perMinute($perIp)->by($scope.'ip:'.$request->ip()),
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
@@ -398,7 +423,7 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
|
|
|
|
|
private function buildUploadLimits(Request $request, string $key): array
|
|
|
|
|
{
|
|
|
|
|
$config = (array) config('uploads.rate_limits.' . $key, []);
|
|
|
|
|
$config = (array) config('uploads.rate_limits.'.$key, []);
|
|
|
|
|
$decay = (int) config('uploads.rate_limits.decay_minutes', 1);
|
|
|
|
|
$perUser = (int) ($config['per_user'] ?? 0);
|
|
|
|
|
$perIp = (int) ($config['per_ip'] ?? 0);
|
|
|
|
@@ -407,11 +432,11 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
|
|
|
|
|
if ($perUser > 0) {
|
|
|
|
|
$userId = $request->user()?->id ?? 'guest';
|
|
|
|
|
$limits[] = Limit::perMinutes($decay, $perUser)->by('u:' . $userId);
|
|
|
|
|
$limits[] = Limit::perMinutes($decay, $perUser)->by('u:'.$userId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($perIp > 0) {
|
|
|
|
|
$limits[] = Limit::perMinutes($decay, $perIp)->by('ip:' . $request->ip());
|
|
|
|
|
$limits[] = Limit::perMinutes($decay, $perIp)->by('ip:'.$request->ip());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $limits;
|
|
|
|
@@ -423,8 +448,8 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
$userId = $request->user()?->id ?? 'guest';
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
Limit::perMinute(20)->by('messages:user:' . $userId),
|
|
|
|
|
Limit::perMinute(40)->by('messages:ip:' . $request->ip()),
|
|
|
|
|
Limit::perMinute(20)->by('messages:user:'.$userId),
|
|
|
|
|
Limit::perMinute(40)->by('messages:ip:'.$request->ip()),
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -432,8 +457,8 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
$userId = $request->user()?->id ?? 'guest';
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
Limit::perMinute(60)->by('messages:react:user:' . $userId),
|
|
|
|
|
Limit::perMinute(120)->by('messages:react:ip:' . $request->ip()),
|
|
|
|
|
Limit::perMinute(60)->by('messages:react:user:'.$userId),
|
|
|
|
|
Limit::perMinute(120)->by('messages:react:ip:'.$request->ip()),
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -441,8 +466,8 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
$userId = $request->user()?->id ?? 'guest';
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
Limit::perMinute(120)->by('messages:read:user:' . $userId),
|
|
|
|
|
Limit::perMinute(240)->by('messages:read:ip:' . $request->ip()),
|
|
|
|
|
Limit::perMinute(120)->by('messages:read:user:'.$userId),
|
|
|
|
|
Limit::perMinute(240)->by('messages:read:ip:'.$request->ip()),
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -451,8 +476,8 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
$conversationId = (int) $request->route('conversation_id');
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
Limit::perMinute(90)->by('messages:typing:user:' . $userId . ':conv:' . $conversationId),
|
|
|
|
|
Limit::perMinute(180)->by('messages:typing:ip:' . $request->ip()),
|
|
|
|
|
Limit::perMinute(90)->by('messages:typing:user:'.$userId.':conv:'.$conversationId),
|
|
|
|
|
Limit::perMinute(180)->by('messages:typing:ip:'.$request->ip()),
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -461,8 +486,8 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
$conversationId = (int) $request->route('conversation_id');
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
Limit::perMinute(30)->by('messages:recovery:user:' . $userId . ':conv:' . $conversationId),
|
|
|
|
|
Limit::perMinute(60)->by('messages:recovery:ip:' . $request->ip()),
|
|
|
|
|
Limit::perMinute(30)->by('messages:recovery:user:'.$userId.':conv:'.$conversationId),
|
|
|
|
|
Limit::perMinute(60)->by('messages:recovery:ip:'.$request->ip()),
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -470,8 +495,8 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
$userId = $request->user()?->id ?? 'guest';
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
Limit::perMinute(180)->by('messages:presence:user:' . $userId),
|
|
|
|
|
Limit::perMinute(300)->by('messages:presence:ip:' . $request->ip()),
|
|
|
|
|
Limit::perMinute(180)->by('messages:presence:user:'.$userId),
|
|
|
|
|
Limit::perMinute(300)->by('messages:presence:ip:'.$request->ip()),
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
@@ -484,8 +509,8 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
// Higher user-based allowance prevents false positives for active users,
|
|
|
|
|
// while IP limit still protects guest endpoints from bursts.
|
|
|
|
|
return [
|
|
|
|
|
Limit::perMinute(60)->by('downloads:user:' . ($userId ?? 'guest')),
|
|
|
|
|
Limit::perMinute(120)->by('downloads:ip:' . $request->ip()),
|
|
|
|
|
Limit::perMinute(60)->by('downloads:user:'.($userId ?? 'guest')),
|
|
|
|
|
Limit::perMinute(120)->by('downloads:ip:'.$request->ip()),
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
@@ -498,8 +523,8 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
// Search fans out to Meilisearch + DB queries per request, so IP
|
|
|
|
|
// limits are kept tight to blunt scripted floods of /search traffic.
|
|
|
|
|
return [
|
|
|
|
|
Limit::perMinute(20)->by('search:user:' . ($userId ?? 'guest')),
|
|
|
|
|
Limit::perMinute(30)->by('search:ip:' . $request->ip()),
|
|
|
|
|
Limit::perMinute(20)->by('search:user:'.($userId ?? 'guest')),
|
|
|
|
|
Limit::perMinute(30)->by('search:ip:'.$request->ip()),
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
@@ -514,8 +539,8 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
// per-IP allowance tight — unlike cached list endpoints, a flood of
|
|
|
|
|
// distinct artwork IDs can't be absorbed by cache alone.
|
|
|
|
|
return [
|
|
|
|
|
Limit::perMinute(30)->by('vector-search:user:' . ($userId ?? 'guest')),
|
|
|
|
|
Limit::perMinute(20)->by('vector-search:ip:' . $request->ip()),
|
|
|
|
|
Limit::perMinute(30)->by('vector-search:user:'.($userId ?? 'guest')),
|
|
|
|
|
Limit::perMinute(20)->by('vector-search:ip:'.$request->ip()),
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
@@ -529,10 +554,10 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
// Prevent burst spam on a single artwork while allowing normal exploration.
|
|
|
|
|
Limit::perMinute($perMinute)->by('awards:user:' . ($userId ?? 'guest') . ':art:' . $artworkId),
|
|
|
|
|
Limit::perMinute($perMinute)->by('awards:user:'.($userId ?? 'guest').':art:'.$artworkId),
|
|
|
|
|
// Global safety net for user/IP across all artworks.
|
|
|
|
|
Limit::perMinute($perMinute * 6)->by('awards:user:' . ($userId ?? 'guest')),
|
|
|
|
|
Limit::perMinute($perMinute * 9)->by('awards:ip:' . $request->ip()),
|
|
|
|
|
Limit::perMinute($perMinute * 6)->by('awards:user:'.($userId ?? 'guest')),
|
|
|
|
|
Limit::perMinute($perMinute * 9)->by('awards:ip:'.$request->ip()),
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
@@ -544,8 +569,8 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
// Comment-heavy pages can trigger many reaction reads at once.
|
|
|
|
|
Limit::perMinute(600)->by('reactions-read:user:' . ($userId ?? 'guest')),
|
|
|
|
|
Limit::perMinute(900)->by('reactions-read:ip:' . $request->ip()),
|
|
|
|
|
Limit::perMinute(600)->by('reactions-read:user:'.($userId ?? 'guest')),
|
|
|
|
|
Limit::perMinute(900)->by('reactions-read:ip:'.$request->ip()),
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -553,8 +578,8 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
$userId = $request->user()?->id;
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
Limit::perMinute(120)->by('reactions-write:user:' . ($userId ?? 'guest')),
|
|
|
|
|
Limit::perMinute(180)->by('reactions-write:ip:' . $request->ip()),
|
|
|
|
|
Limit::perMinute(120)->by('reactions-write:user:'.($userId ?? 'guest')),
|
|
|
|
|
Limit::perMinute(180)->by('reactions-write:ip:'.$request->ip()),
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
@@ -565,9 +590,9 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
$userId = $request->user()?->id ?? 'guest';
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
Limit::perMinute(12)->by('follow-write:minute:user:' . $userId),
|
|
|
|
|
Limit::perHour(120)->by('follow-write:hour:user:' . $userId),
|
|
|
|
|
Limit::perMinute(24)->by('follow-write:minute:ip:' . $request->ip()),
|
|
|
|
|
Limit::perMinute(12)->by('follow-write:minute:user:'.$userId),
|
|
|
|
|
Limit::perHour(120)->by('follow-write:hour:user:'.$userId),
|
|
|
|
|
Limit::perMinute(24)->by('follow-write:minute:ip:'.$request->ip()),
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -575,8 +600,8 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
$userId = $request->user()?->id ?? 'guest';
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
Limit::perMinute(60)->by('social-write:user:' . $userId),
|
|
|
|
|
Limit::perMinute(120)->by('social-write:ip:' . $request->ip()),
|
|
|
|
|
Limit::perMinute(60)->by('social-write:user:'.$userId),
|
|
|
|
|
Limit::perMinute(120)->by('social-write:ip:'.$request->ip()),
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -584,8 +609,8 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
$userId = $request->user()?->id ?? 'guest';
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
Limit::perMinute(240)->by('social-read:user:' . $userId),
|
|
|
|
|
Limit::perMinute(480)->by('social-read:ip:' . $request->ip()),
|
|
|
|
|
Limit::perMinute(240)->by('social-read:user:'.$userId),
|
|
|
|
|
Limit::perMinute(480)->by('social-read:ip:'.$request->ip()),
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
@@ -593,7 +618,7 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
private function configureSettingsRateLimiters(): void
|
|
|
|
|
{
|
|
|
|
|
RateLimiter::for('username-check', function (Request $request): Limit {
|
|
|
|
|
$key = 'username-check:ip:' . $request->ip();
|
|
|
|
|
$key = 'username-check:ip:'.$request->ip();
|
|
|
|
|
|
|
|
|
|
if (method_exists(Limit::class, 'perSecond')) {
|
|
|
|
|
return Limit::perSecond(5)->by($key);
|
|
|
|
@@ -605,8 +630,8 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
RateLimiter::for('email-change-request', function (Request $request): Limit {
|
|
|
|
|
$userId = $request->user()?->id;
|
|
|
|
|
$key = $userId !== null
|
|
|
|
|
? 'email-change-request:user:' . $userId
|
|
|
|
|
: 'email-change-request:ip:' . $request->ip();
|
|
|
|
|
? 'email-change-request:user:'.$userId
|
|
|
|
|
: 'email-change-request:ip:'.$request->ip();
|
|
|
|
|
|
|
|
|
|
return Limit::perHour(1)->by($key);
|
|
|
|
|
});
|
|
|
|
@@ -648,6 +673,6 @@ class AppServiceProvider extends ServiceProvider
|
|
|
|
|
|
|
|
|
|
$request = $this->app->make('request');
|
|
|
|
|
|
|
|
|
|
return $request->is($prefix) || $request->is($prefix . '/*');
|
|
|
|
|
return $request->is($prefix) || $request->is($prefix.'/*');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|