diff --git a/.env.example b/.env.local-example similarity index 82% rename from .env.example rename to .env.local-example index d07fde3bfc10bfd333798f632ff40852eddab5cb..004ebc7adb39a9800ad2126fb1989674b8ce6005 100644 --- a/.env.example +++ b/.env.local-example @@ -77,4 +77,13 @@ BUNNYCDN_PULL_ZONE=https://cdn.rann-graphic-design.fr BUNNYCDN_API_KEY="api-key" BUNNYCDN_REGION=de -CDN_FILESYSTEM_DISK=bunnycdn \ No newline at end of file +CDN_FILESYSTEM_DISK=bunnycdn + +AI_PROVIDER=openai +OPENAI_URL=https://api.openai.com/v1/chat/completions +OPENAI_API_KEY= +OPENAI_MODEL=gpt-4o-mini + +IP_ADDRESS_RESOLVER_URL=http://ip-api.com/batch +IP_ADDRESS_RESOLVER_CALL_LIMIT_PER_MINUTE=15 +IP_ADDRESS_RESOLVER_MAX_IP_ADDRESSES_PER_CALL=100 \ No newline at end of file diff --git a/.env.production b/.env.production index 5801e3a773fce2ce7d46008656c0fa9d1f54b82b..31ad2117f53d2b40e76814c23386cfdab406ec38 100644 --- a/.env.production +++ b/.env.production @@ -77,4 +77,15 @@ BUNNYCDN_PULL_ZONE=https://cdn.rann-graphic-design.fr BUNNYCDN_API_KEY="api-key" BUNNYCDN_REGION=de -CDN_FILESYSTEM_DISK=bunnycdn \ No newline at end of file +CDN_FILESYSTEM_DISK=bunnycdn + +AI_PROVIDER=openai +OPENAI_URL=https://api.openai.com/v1/chat/completions +OPENAI_API_KEY= +OPENAI_MODEL=gpt-4o-mini + +IP_ADDRESS_RESOLVER_URL=http://ip-api.com/batch +IP_ADDRESS_RESOLVER_CALL_LIMIT_PER_MINUTE=15 +IP_ADDRESS_RESOLVER_MAX_IP_ADDRESSES_PER_CALL=100 + +APP_PRIVATE_MODE_SECRET=secret \ No newline at end of file diff --git a/.env.testing b/.env.testing index e17ecb8ba9cac602565c98671f78502ceb042507..7e40aa8641d219fb6f21e09ad7e1760f43b7c0fe 100644 --- a/.env.testing +++ b/.env.testing @@ -73,4 +73,15 @@ SENTRY_PROFILES_SAMPLE_RATE=1.0 BUNNYCDN_STORAGE_ZONE=testing_storage_zone BUNNYCDN_PULL_ZONE=https://testing.b-cdn.net BUNNYCDN_API_KEY="api-key" -BUNNYCDN_REGION=de \ No newline at end of file +BUNNYCDN_REGION=de + +AI_PROVIDER=openai +OPENAI_URL=https://api.test-provider.com/v1/chat/completions +OPENAI_API_KEY=fake-api-key +OPENAI_MODEL=gpt-4o-mini + +IP_ADDRESS_RESOLVER_URL=http://api.test-provider.com/batch +IP_ADDRESS_RESOLVER_CALL_LIMIT_PER_MINUTE=15 +IP_ADDRESS_RESOLVER_MAX_IP_ADDRESSES_PER_CALL=100 + +APP_PRIVATE_MODE_SECRET=secret \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml index 832f137ea710891bc0a20ad3db1d8a42ac54bcf5..29277c40d287df16d06fa5bcf1acf8ce5595d3c3 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -3,7 +3,7 @@ <component name="LaravelPint"> <laravel_pint_settings> <LaravelPintConfiguration tool_path="$PROJECT_DIR$/vendor/bin/pint" /> - <laravel_pint_by_interpreter asDefaultInterpreter="true" interpreter_id="41eb493c-2ba1-4c74-b9ea-41d9bb4c70c9" tool_path="/var/www/vendor/bin/pint"> + <laravel_pint_by_interpreter asDefaultInterpreter="true" interpreter_id="41eb493c-2ba1-4c74-b9ea-41d9bb4c70c9" tool_path="/app/vendor/bin/pint"> <option name="timeout" value="30000" /> </laravel_pint_by_interpreter> </laravel_pint_settings> @@ -176,6 +176,9 @@ <path value="$PROJECT_DIR$/vendor/theseer/tokenizer" /> <path value="$PROJECT_DIR$/vendor/laminas/laminas-diactoros" /> <path value="$PROJECT_DIR$/vendor/laravel/octane" /> + <path value="$PROJECT_DIR$/vendor/_laravel_idea" /> + <path value="$PROJECT_DIR$/vendor/mkocansey/bladewind" /> + <path value="$PROJECT_DIR$/vendor/laravel/horizon" /> </include_path> </component> <component name="PhpInterpreters"> diff --git a/Dockerfile b/Dockerfile index 72ac0d50ee6945429c2b2ddf0b302173b49b2a9a..a9e02cb9ba3c62f16b3878e380c80522fcfca82e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,5 +20,9 @@ RUN curl -fsSL https://deb.nodesource.com/setup_22.x -o nodesource_setup.sh && \ bash nodesource_setup.sh && \ apt-get install -y nodejs +# Install supervisor +RUN apt-get install -y supervisor +COPY docker-init/supervisord.conf /etc/supervisor/conf.d/supervisord.conf + # Setting PHP Configuration COPY docker-init/php.ini $PHP_INI_DIR/php.ini \ No newline at end of file diff --git a/README.md b/README.md index 3baed05eaefdb5e364186c5bead5128a60991f39..f60d50dcaa41906164541649d954ffbeb58c9f55 100644 --- a/README.md +++ b/README.md @@ -62,25 +62,6 @@ #### Laravel Idea #### Configuration -##### Configuration des projets multiples - -Comme le projet est dépendant de quelques paquets développés en interne, il est nécessaire de configurer PHPStorm pour -vous faciliter leur développement en simultané. - -1. Ouvrez les préférences de PHPStorm (Ctrl + Alt + S) -2. Allez dans `Version Control` -3. Allez dans `Directory Mappings` -4. Cliquez sur `+` puis `Directory` -5. Sélectionnez le dossier du projet sous-jacent -6. Cliquez sur `OK` - - - -Vous devriez voir apparaître les multiples projets Git dans l'onglet vcs de PHPStorm. À présent, vous devriez pouvoir -créer des branches sur les différents projets simultanément. - - - ##### Interpréteur PHP Il est nécessaire de renseigner le container Docker comme interpréteur PHP. Pour cela, suivez les étapes suivantes : diff --git a/app/Console/Commands/ProcessIpAdressesCommand.php b/app/Console/Commands/ProcessIpAdressesCommand.php new file mode 100644 index 0000000000000000000000000000000000000000..c9f8b9e07e5ca2c41dfaa11a1386b3115a4f2d3e --- /dev/null +++ b/app/Console/Commands/ProcessIpAdressesCommand.php @@ -0,0 +1,31 @@ +<?php + +namespace App\Console\Commands; + +use App\Jobs\ProcessIpAddressesJob; +use Illuminate\Console\Command; +use SlProjects\LaravelRequestLogger\app\Models\IpAddress; + +class ProcessIpAdressesCommand extends Command +{ + protected $signature = 'process:ip-adresses'; + + protected $description = 'Process ip adresses to resolve their location'; + + public function handle(): void + { + $ipAddresses = IpAddress::leftJoin('ip_address_metadata', 'ip_addresses.id', '=', 'ip_address_metadata.ip_address_id') + ->whereNull('ip_address_metadata.id') + ->select('ip_addresses.id', 'ip_addresses.ip') + ->get(); + + if ($ipAddresses->isEmpty()) { + $this->info('No ip adresses to process'); + + return; + } + + $this->info("Processing {$ipAddresses->count()} ip adresses"); + ProcessIpAddressesJob::dispatch($ipAddresses); + } +} diff --git a/app/Console/Commands/ProcessUserAgentsCommand.php b/app/Console/Commands/ProcessUserAgentsCommand.php new file mode 100644 index 0000000000000000000000000000000000000000..17cb7bc086f826fd13098b8ecb1520e3b6dd2b52 --- /dev/null +++ b/app/Console/Commands/ProcessUserAgentsCommand.php @@ -0,0 +1,34 @@ +<?php + +namespace App\Console\Commands; + +use App\Jobs\ProcessUserAgentJob; +use Illuminate\Console\Command; +use SlProjects\LaravelRequestLogger\app\Models\UserAgent; + +class ProcessUserAgentsCommand extends Command +{ + protected $signature = 'process:user-agents'; + + protected $description = 'Process user agents to detect if they are bots'; + + public function handle(): void + { + $userAgents = UserAgent::leftJoin('user_agent_metadata', 'user_agents.id', '=', 'user_agent_metadata.user_agent_id') + ->whereNull('user_agent_metadata.id') + ->select('user_agents.id', 'user_agents.user_agent') + ->get(); + + if ($userAgents->isEmpty()) { + $this->info('No user agents to process.'); + + return; + } + + foreach ($userAgents as $userAgent) { + ProcessUserAgentJob::dispatch($userAgent); + } + + $this->info('Jobs dispatched for processing '.$userAgents->count().' user agents.'); + } +} diff --git a/app/Http/Controllers/Admin/CreationController.php b/app/Http/Controllers/Admin/CreationController.php index 93f4590310abaaf1fb1c4b4a49123722506eaa34..792b41cf0547811b75577778e475c6e0b8d6b052 100644 --- a/app/Http/Controllers/Admin/CreationController.php +++ b/app/Http/Controllers/Admin/CreationController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; +use App\Jobs\TranslateCreationJob; use App\Models\Category; use App\Models\Creation; use App\Models\Translation; @@ -350,4 +351,15 @@ public function removeAdditionalImage(int $creationId, int $uploadedPictureId): 'success' => true, ]); } + + public function translateWithAi(int $creationId): JsonResponse + { + $creation = Creation::findOrFail($creationId); + $job = new TranslateCreationJob($creation); + dispatch($job); + + return response()->json([ + 'success' => true, + ]); + } } diff --git a/app/Http/Controllers/Admin/HomeController.php b/app/Http/Controllers/Admin/HomeController.php index ff5529d25461dc1a869bdc2c24dddf6b1a39ed02..9fc2501b928a6c5f56a393c1d723cf25395dbfce 100644 --- a/app/Http/Controllers/Admin/HomeController.php +++ b/app/Http/Controllers/Admin/HomeController.php @@ -19,24 +19,30 @@ public function index(Request $request): View 'end_date' => 'nullable|date', ]); - $publicRoutes = collect(Route::getRoutes()->getRoutes()) - ->filter(fn ($route) => ! Str::startsWith($route->uri, ['admin', 'api']) && ! empty($route->uri) && $route->uri !== '/') - ->pluck('uri'); - - $visits = LoggedRequest::selectRaw(' - logged_requests.url_id, - logged_requests.ip_address_id, - logged_requests.country_code, - logged_requests.created_at, - urls.url - ') + $routes = Route::getRoutes()->getRoutes(); + + $individualExcludedRoutes = []; + foreach ($routes as $route) { + if (Str::startsWith($route->uri, ['login', 'register', 'password', 'admin', 'forgot-password'])) { + $individualExcludedRoutes[] = config('app.url').'/'.$route->uri; + } + } + + $visits = LoggedRequest::select([ + 'logged_requests.url_id', + 'logged_requests.ip_address_id', + 'ip_address_metadata.country_code', + 'logged_requests.created_at', + 'urls.url']) ->distinct('logged_requests.url_id', 'logged_requests.ip_address_id') ->join('urls', 'logged_requests.url_id', '=', 'urls.id') - ->where(function ($query) use ($publicRoutes) { - foreach ($publicRoutes as $uri) { - $query->orWhere('urls.url', 'like', "%$uri%"); - } - }) + ->join('user_agent_metadata', 'logged_requests.user_agent_id', '=', 'user_agent_metadata.user_agent_id') + ->join('ip_address_metadata', 'logged_requests.ip_address_id', '=', 'ip_address_metadata.ip_address_id') + ->whereLike('urls.url', config('app.url').'%') + ->whereNotIn('urls.url', $individualExcludedRoutes) + ->where('user_agent_metadata.is_bot', false) + ->where('status_code', 200) + ->whereNull('logged_requests.user_id') ->get(); $now = now(); @@ -60,29 +66,18 @@ public function index(Request $request): View $selectedPeriod = $startDate; // Now, all the stats are calculated for the selected period - - /*$visitsPerDay = $visits->groupBy(fn ($visit) => Carbon::parse($visit->created_at)->format('Y-m-d')) - ->map(fn ($group) => ['date' => $group->first()->created_at->format('Y-m-d'), 'count' => $group->count()]) - ->values();*/ $visitsPerDay = $visits->where('created_at', '>=', $startDate) ->where('created_at', '<=', $dateEnd) ->groupBy(fn ($visit) => Carbon::parse($visit->created_at)->format('Y-m-d')) ->map(fn ($group) => ['date' => $group->first()->created_at->format('Y-m-d'), 'count' => $group->count()]) ->values(); - /*$visitsByCountry = $visits->groupBy('country_code') - ->map(fn ($group, $country) => ['country_code' => $country, 'count' => $group->count()]) - ->values();*/ $visitsByCountry = $visits->where('created_at', '>=', $startDate) ->where('created_at', '<=', $dateEnd) ->groupBy('country_code') ->map(fn ($group, $country) => ['country_code' => $country, 'count' => $group->count()]) ->values(); - /*$mostVisitedPages = $visits->groupBy('url') - ->map(fn ($group, $url) => ['url' => $url, 'count' => $group->count()]) - ->sortByDesc('count') - ->values();*/ $mostVisitedPages = $visits->where('created_at', '>=', $startDate) ->where('created_at', '<=', $dateEnd) ->groupBy('url') @@ -90,6 +85,24 @@ public function index(Request $request): View ->sortByDesc('count') ->values(); + $mostVisitedPagesForPastTwentyFourHours = $visits->where('created_at', '>=', $now->copy()->subDay()) + ->groupBy('url') + ->map(fn ($group, $url) => ['url' => $url, 'count' => $group->count()]) + ->sortByDesc('count') + ->values(); + + $mostVisitedPagesForPastSevenDays = $visits->where('created_at', '>=', $now->copy()->subDays(7)) + ->groupBy('url') + ->map(fn ($group, $url) => ['url' => $url, 'count' => $group->count()]) + ->sortByDesc('count') + ->values(); + + $mostVisitedPagesForPastThirtyDays = $visits->where('created_at', '>=', $now->copy()->subDays(30)) + ->groupBy('url') + ->map(fn ($group, $url) => ['url' => $url, 'count' => $group->count()]) + ->sortByDesc('count') + ->values(); + return view('admin.home', [ 'totalVisitsPastTwentyFourHours' => $totalVisitsPastTwentyFourHours, 'totalVisitsPastSevenDays' => $totalVisitsPastSevenDays, @@ -100,6 +113,9 @@ public function index(Request $request): View 'mostVisitedPages' => $mostVisitedPages, 'periods' => $periods, 'selectedPeriod' => $selectedPeriod, + 'mostVisitedPagesForPastTwentyFourHours' => $mostVisitedPagesForPastTwentyFourHours, + 'mostVisitedPagesForPastSevenDays' => $mostVisitedPagesForPastSevenDays, + 'mostVisitedPagesForPastThirtyDays' => $mostVisitedPagesForPastThirtyDays, ]); } } diff --git a/app/Http/Controllers/Public/LegalMentionsController.php b/app/Http/Controllers/Public/LegalMentionsController.php new file mode 100644 index 0000000000000000000000000000000000000000..4098697920ba33c78d7742a818a9902af53125af --- /dev/null +++ b/app/Http/Controllers/Public/LegalMentionsController.php @@ -0,0 +1,21 @@ +<?php + +namespace App\Http\Controllers\Public; + +use App\Http\Controllers\Controller; +use App\Models\LegalMention; + +class LegalMentionsController extends Controller +{ + public function __invoke() + { + $activeLegalMentionsText = LegalMention::where('active', true)->first(); + + if (! $activeLegalMentionsText) { + abort(404); + } + $activeLegalMentionsText = $activeLegalMentionsText->contentTransKey->getTranslation(); + + return view('public.legal-mentions', compact('activeLegalMentionsText')); + } +} diff --git a/app/Http/Controllers/Public/TermsController.php b/app/Http/Controllers/Public/TermsController.php new file mode 100644 index 0000000000000000000000000000000000000000..a20736b21e40f1970a758be6b82ebd599c7956b0 --- /dev/null +++ b/app/Http/Controllers/Public/TermsController.php @@ -0,0 +1,22 @@ +<?php + +namespace App\Http\Controllers\Public; + +use App\Http\Controllers\Controller; +use App\Models\TermsSection; +use Illuminate\View\View; + +class TermsController extends Controller +{ + public function __invoke(): View + { + $activeTermsText = TermsSection::where('active', true)->first(); + + if (! $activeTermsText) { + abort(404); + } + $activeTermsText = $activeTermsText->contentTranslationKey->getTranslation(); + + return view('public.terms', compact('activeTermsText')); + } +} diff --git a/app/Http/Middleware/CheckPrivateModeMiddleware.php b/app/Http/Middleware/CheckPrivateModeMiddleware.php index 2e9ba639dc3fd4fac8951d165f007a3aa89a07e7..7cf814038d6d7cc8d3516ce8e2c0f69652ecd3b1 100644 --- a/app/Http/Middleware/CheckPrivateModeMiddleware.php +++ b/app/Http/Middleware/CheckPrivateModeMiddleware.php @@ -9,9 +9,20 @@ class CheckPrivateModeMiddleware { public function handle(Request $request, Closure $next) { - if (config('app.private_mode') && ! $request->is('maintenance') && ! auth()->check()) { - return redirect()->route('maintenance'); - } elseif (! config('app.private_mode') && $request->is('maintenance')) { + $privateModeEnabled = config('app.private_mode'); + $privateModeSecret = config('app.private_mode_secret'); + $userSecretInput = $request->input('secret'); + $secretIsUsable = ! empty($privateModeSecret) && $privateModeSecret === $userSecretInput; + + if ($privateModeEnabled) { + if (! $secretIsUsable && ! auth()->check()) { + if (! $request->is('maintenance')) { + return redirect()->route('maintenance'); + } + } + } + + if (! $privateModeEnabled && $request->is('maintenance') && ! auth()->check()) { return redirect()->route('index'); } diff --git a/app/Http/Requests/UserAgentMetadataRequest.php b/app/Http/Requests/UserAgentMetadataRequest.php new file mode 100644 index 0000000000000000000000000000000000000000..347e7e3dfc83c2ca937f4809732690d805933605 --- /dev/null +++ b/app/Http/Requests/UserAgentMetadataRequest.php @@ -0,0 +1,21 @@ +<?php + +namespace App\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; + +class UserAgentMetadataRequest extends FormRequest +{ + public function rules(): array + { + return [ + 'user_agent_id' => ['required', 'exists:user_agents'], + 'is_bot' => ['boolean'], + ]; + } + + public function authorize(): bool + { + return true; + } +} diff --git a/app/Jobs/ProcessIpAddressesJob.php b/app/Jobs/ProcessIpAddressesJob.php new file mode 100644 index 0000000000000000000000000000000000000000..d02f8cd33ecd23af984eb247bfdb10128ff18392 --- /dev/null +++ b/app/Jobs/ProcessIpAddressesJob.php @@ -0,0 +1,51 @@ +<?php + +namespace App\Jobs; + +use App\Models\IpAddressMetadata; +use App\Services\IpAddressMetadataResolverService; +use Exception; +use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; + +class ProcessIpAddressesJob implements ShouldQueue +{ + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + + public function __construct(private readonly Collection $ipAddresses) {} + + public function handle(IpAddressMetadataResolverService $ipAddressMetadataResolver): void + { + try { + $metadataObjects = $ipAddressMetadataResolver->resolve($this->ipAddresses); + } catch (Exception $exception) { + report($exception); + $this->fail($exception); + + return; + } + + foreach ($metadataObjects as $metadata) { + if ($metadata['status'] === 'fail') { + Log::warning('Failed to resolve IP address metadata.', [ + 'query' => $metadata['query'], + 'message' => $metadata['message'], + ]); + + continue; + } + + IpAddressMetadata::create([ + 'ip_address_id' => $this->ipAddresses->where('ip', $metadata['query'])->first()->id, + 'country_code' => $metadata['countryCode'], + 'lat' => $metadata['lat'], + 'lon' => $metadata['lon'], + ]); + } + } +} diff --git a/app/Jobs/ProcessUserAgentJob.php b/app/Jobs/ProcessUserAgentJob.php new file mode 100644 index 0000000000000000000000000000000000000000..fce0bbc7a1e6e9a8a71e09a82e2f541b030f2ee3 --- /dev/null +++ b/app/Jobs/ProcessUserAgentJob.php @@ -0,0 +1,44 @@ +<?php + +namespace App\Jobs; + +use App\Models\UserAgentMetadata; +use App\Services\AiProviderService; +use Exception; +use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Log; +use SlProjects\LaravelRequestLogger\app\Models\UserAgent; + +class ProcessUserAgentJob implements ShouldQueue +{ + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + + public function __construct(private readonly UserAgent $userAgent) {} + + public function handle(AiProviderService $aiProviderService): void + { + try { + $result = $aiProviderService->prompt( + 'You are a robot detector designed to output JSON. ', + "Is this user agent a robot or a tool that is not a web browser? Please respond in the format {'is_bot': true/false}. The user agent is: {$this->userAgent->user_agent}" + ); + + $isBot = $result['is_bot'] ?? false; + + UserAgentMetadata::create([ + 'user_agent_id' => $this->userAgent->id, + 'is_bot' => $isBot, + ]); + } catch (Exception $e) { + Log::error("Failed to process UserAgent: {$this->userAgent->user_agent}", [ + 'exception' => $e->getMessage(), + ]); + + $this->fail($e); + } + } +} diff --git a/app/Jobs/TranslateCreationJob.php b/app/Jobs/TranslateCreationJob.php new file mode 100644 index 0000000000000000000000000000000000000000..152419f5cf4d53057709885073e673910ec8b018 --- /dev/null +++ b/app/Jobs/TranslateCreationJob.php @@ -0,0 +1,81 @@ +<?php + +namespace App\Jobs; + +use App\Models\Creation; +use App\Models\Translation; +use App\Services\AiProviderService; +use Exception; +use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Log; + +class TranslateCreationJob implements ShouldQueue +{ + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + + public function __construct(private readonly Creation $creation) {} + + public function handle(AiProviderService $aiProviderService): void + { + $frenchName = $this->creation->nameTranslationKey->getTranslation('fr'); + $frenchShortDesc = $this->creation->shortDescriptionTranslationKey->getTranslation('fr'); + $frenchDesc = $this->creation->descriptionTranslationKey->getTranslation('fr'); + + $shortDescriptionTranslationKeyId = $this->creation->shortDescriptionTranslationKey->id; + $descriptionTranslationKeyId = $this->creation->descriptionTranslationKey->id; + + if (! empty($frenchName)) { + Translation::updateOrCreate( + ['translation_key_id' => $this->creation->name_translation_key_id, 'locale' => 'en'], + ['text' => $frenchName] + ); + Cache::forget("translation_key_{$this->creation->name_translation_key_id}_en"); + } + + if (! empty($frenchShortDesc)) { + Translation::updateOrCreate( + ['translation_key_id' => $shortDescriptionTranslationKeyId, 'locale' => 'en'], + ['text' => $this->translate($frenchShortDesc, $aiProviderService)] + ); + Cache::forget("translation_key_{$shortDescriptionTranslationKeyId}_en"); + } + + if (! empty($frenchDesc)) { + Translation::updateOrCreate( + ['translation_key_id' => $descriptionTranslationKeyId, 'locale' => 'en'], + ['text' => $this->translate($frenchDesc, $aiProviderService)] + ); + Cache::forget("translation_key_{$descriptionTranslationKeyId}_en"); + } + } + + /** + * Translate the given text from French to English + * + * @param string $text The text to translate + * @param AiProviderService $aiProviderService The AI provider service + * @return string The translated text + */ + private function translate(string $text, AiProviderService $aiProviderService): string + { + try { + $result = $aiProviderService->prompt( + 'You are a helpful assistant that translates french markdown text in english and that outputs JSON in the format {message:string}. Markdown is supported.', + $text + ); + + return $result['message'] ?? ''; + } catch (Exception $e) { + Log::error("Failed to translate text: {$text}", [ + 'exception' => $e->getMessage(), + ]); + } + + return ''; + } +} diff --git a/app/Models/IpAddressMetadata.php b/app/Models/IpAddressMetadata.php new file mode 100644 index 0000000000000000000000000000000000000000..6c0f61999c3b791d570c8b0ec353d43614b7c725 --- /dev/null +++ b/app/Models/IpAddressMetadata.php @@ -0,0 +1,29 @@ +<?php + +namespace App\Models; + +use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use SlProjects\LaravelRequestLogger\app\Models\IpAddress; + +class IpAddressMetadata extends Model +{ + use HasFactory; + + public $timestamps = false; + + protected $fillable = [ + 'ip_address_id', + 'country_code', + 'lat', + 'lon', + ]; + + const COUNTRY_CODES = ['AA', 'AB', 'AC', 'AD', 'AE', 'AF', 'AG', 'AH', 'AI', 'AJ', 'AK', 'AL', 'AM', 'AN', 'AO', 'AP', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AV', 'AW', 'AX', 'AY', 'AZ', 'BA', 'BB', 'BC', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BK', 'BL', 'BM', 'BN', 'BO', 'BP', 'BQ', 'BR', 'BS', 'BT', 'BU', 'BV', 'BW', 'BX', 'BY', 'BZ', 'CA', 'CB', 'CC', 'CD', 'CE', 'CF', 'CG', 'CH', 'CI', 'CJ', 'CK', 'CL', 'CM', 'CN', 'CO', 'CP', 'CQ', 'CR', 'CS', 'CT', 'CU', 'CV', 'CW', 'CX', 'CY', 'CZ', 'DA', 'DB', 'DC', 'DD', 'DE', 'DF', 'DG', 'DH', 'DI', 'DJ', 'DK', 'DL', 'DM', 'DN', 'DO', 'DP', 'DQ', 'DR', 'DS', 'DT', 'DU', 'DV', 'DW', 'DX', 'DY', 'DZ', 'EA', 'EB', 'EC', 'ED', 'EE', 'EF', 'EG', 'EH', 'EI', 'EJ', 'EK', 'EL', 'EM', 'EN', 'EO', 'EP', 'EQ', 'ER', 'ES', 'ET', 'EU', 'EV', 'EW', 'EX', 'EY', 'EZ', 'FA', 'FB', 'FC', 'FD', 'FE', 'FF', 'FG', 'FH', 'FI', 'FJ', 'FK', 'FL', 'FM', 'FN', 'FO', 'FP', 'FQ', 'FR', 'FS', 'FT', 'FU', 'FV', 'FW', 'FX', 'FY', 'FZ', 'GA', 'GB', 'GC', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI', 'GJ', 'GK', 'GL', 'GM', 'GN', 'GO', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GV', 'GW', 'GX', 'GY', 'GZ', 'HA', 'HB', 'HC', 'HD', 'HE', 'HF', 'HG', 'HH', 'HI', 'HJ', 'HK', 'HL', 'HM', 'HN', 'HO', 'HP', 'HQ', 'HR', 'HS', 'HT', 'HU', 'HV', 'HW', 'HX', 'HY', 'HZ', 'IA', 'IB', 'IC', 'ID', 'IE', 'IF', 'IG', 'IH', 'II', 'IJ', 'IK', 'IL', 'IM', 'IN', 'IO', 'IP', 'IQ', 'IR', 'IS', 'IT', 'IU', 'IV', 'IW', 'IX', 'IY', 'IZ', 'JA', 'JB', 'JC', 'JD', 'JE', 'JF', 'JG', 'JH', 'JI', 'JJ', 'JK', 'JL', 'JM', 'JN', 'JO', 'JP', 'JQ', 'JR', 'JS', 'JT', 'JU', 'JV', 'JW', 'JX', 'JY', 'JZ', 'KA', 'KB', 'KC', 'KD', 'KE', 'KF', 'KG', 'KH', 'KI', 'KJ', 'KK', 'KL', 'KM', 'KN', 'KO', 'KP', 'KQ', 'KR', 'KS', 'KT', 'KU', 'KV', 'KW', 'KX', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LD', 'LE', 'LF', 'LG', 'LH', 'LI', 'LJ', 'LK', 'LL', 'LM', 'LN', 'LO', 'LP', 'LQ', 'LR', 'LS', 'LT', 'LU', 'LV', 'LW', 'LX', 'LY', 'LZ', 'MA', 'MB', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MI', 'MJ', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NB', 'NC', 'ND', 'NE', 'NF', 'NG', 'NH', 'NI', 'NJ', 'NK', 'NL', 'NM', 'NN', 'NO', 'NP', 'NQ', 'NR', 'NS', 'NT', 'NU', 'NV', 'NW', 'NX', 'NY', 'NZ', 'OA', 'OB', 'OC', 'OD', 'OE', 'OF', 'OG', 'OH', 'OI', 'OJ', 'OK', 'OL', 'OM', 'ON', 'OO', 'OP', 'OQ', 'OR', 'OS', 'OT', 'OU', 'OV', 'OW', 'OX', 'OY', 'OZ', 'PA', 'PB', 'PC', 'PD', 'PE', 'PF', 'PG', 'PH', 'PI', 'PJ', 'PK', 'PL', 'PM', 'PN', 'PO', 'PP', 'PQ', 'PR', 'PS', 'PT', 'PU', 'PV', 'PW', 'PX', 'PY', 'PZ', 'QA', 'QB', 'QC', 'QD', 'QE', 'QF', 'QG', 'QH', 'QI', 'QJ', 'QK', 'QL', 'QM', 'QN', 'QO', 'QP', 'QQ', 'QR', 'QS', 'QT', 'QU', 'QV', 'QW', 'QX', 'QY', 'QZ', 'RA', 'RB', 'RC', 'RD', 'RE', 'RF', 'RG', 'RH', 'RI', 'RJ', 'RK', 'RL', 'RM', 'RN', 'RO', 'RP', 'RQ', 'RR', 'RS', 'RT', 'RU', 'RV', 'RW', 'RX', 'RY', 'RZ', 'SA', 'SB', 'SC', 'SD', 'SE', 'SF', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SP', 'SQ', 'SR', 'SS', 'ST', 'SU', 'SV', 'SW', 'SX', 'SY', 'SZ', 'TA', 'TB', 'TC', 'TD', 'TE', 'TF', 'TG', 'TH', 'TI', 'TJ', 'TK', 'TL', 'TM', 'TN', 'TO', 'TP', 'TQ', 'TR', 'TS', 'TT', 'TU', 'TV', 'TW', 'TX', 'TY', 'TZ', 'UA', 'UB', 'UC', 'UD', 'UE', 'UF', 'UG', 'UH', 'UI', 'UJ', 'UK', 'UL', 'UM', 'UN', 'UO', 'UP', 'UQ', 'UR', 'US', 'UT', 'UU', 'UV', 'UW', 'UX', 'UY', 'UZ', 'VA', 'VB', 'VC', 'VD', 'VE', 'VF', 'VG', 'VH', 'VI', 'VJ', 'VK', 'VL', 'VM', 'VN', 'VO', 'VP', 'VQ', 'VR', 'VS', 'VT', 'VU', 'VV', 'VW', 'VX', 'VY', 'VZ', 'WA', 'WB', 'WC', 'WD', 'WE', 'WF', 'WG', 'WH', 'WI', 'WJ', 'WK', 'WL', 'WM', 'WN', 'WO', 'WP', 'WQ', 'WR', 'WS', 'WT', 'WU', 'WV', 'WW', 'WX', 'WY', 'WZ', 'XA', 'XB', 'XC', 'XD', 'XE', 'XF', 'XG', 'XH', 'XI', 'XJ', 'XK', 'XL', 'XM', 'XN', 'XO', 'XP', 'XQ', 'XR', 'XS', 'XT', 'XU', 'XV', 'XW', 'XX', 'XY', 'XZ', 'YA', 'YB', 'YC', 'YD', 'YE', 'YF', 'YG', 'YH', 'YI', 'YJ', 'YK', 'YL', 'YM', 'YN', 'YO', 'YP', 'YQ', 'YR', 'YS', 'YT', 'YU', 'YV', 'YW', 'YX', 'YY', 'YZ', 'ZA', 'ZB', 'ZC', 'ZD', 'ZE', 'ZF', 'ZG', 'ZH', 'ZI', 'ZJ', 'ZK', 'ZL', 'ZM', 'ZN', 'ZO', 'ZP', 'ZQ', 'ZR', 'ZS', 'ZT', 'ZU', 'ZV', 'ZW', 'ZX', 'ZY', 'ZZ']; + + public function ipAddress(): BelongsTo + { + return $this->belongsTo(IpAddress::class); + } +} diff --git a/app/Models/UserAgentMetadata.php b/app/Models/UserAgentMetadata.php new file mode 100644 index 0000000000000000000000000000000000000000..384ddf174d61873b99d57e76343c8521b8adfcdc --- /dev/null +++ b/app/Models/UserAgentMetadata.php @@ -0,0 +1,32 @@ +<?php + +namespace App\Models; + +use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use SlProjects\LaravelRequestLogger\app\Models\UserAgent; + +class UserAgentMetadata extends Model +{ + use HasFactory; + + public $timestamps = false; + + protected $fillable = [ + 'user_agent_id', + 'is_bot', + ]; + + public function userAgent(): BelongsTo + { + return $this->belongsTo(UserAgent::class); + } + + protected function casts(): array + { + return [ + 'is_bot' => 'boolean', + ]; + } +} diff --git a/app/Providers/HorizonServiceProvider.php b/app/Providers/HorizonServiceProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..9811982b62bd5bc009df34beaba5a3c8e24fd987 --- /dev/null +++ b/app/Providers/HorizonServiceProvider.php @@ -0,0 +1,36 @@ +<?php + +namespace App\Providers; + +use Illuminate\Support\Facades\Gate; +use Laravel\Horizon\Horizon; +use Laravel\Horizon\HorizonApplicationServiceProvider; + +class HorizonServiceProvider extends HorizonApplicationServiceProvider +{ + /** + * Bootstrap any application services. + */ + public function boot(): void + { + parent::boot(); + + // Horizon::routeSmsNotificationsTo('15556667777'); + // Horizon::routeMailNotificationsTo('example@example.com'); + // Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel'); + } + + /** + * Register the Horizon gate. + * + * This gate determines who can access Horizon in non-local environments. + */ + protected function gate(): void + { + Gate::define('viewHorizon', function ($user) { + return in_array($user->email, [ + // + ]); + }); + } +} diff --git a/app/Services/AiProviderService.php b/app/Services/AiProviderService.php new file mode 100644 index 0000000000000000000000000000000000000000..89800c6f85507dbdd688893115a0fc4e8b8d587a --- /dev/null +++ b/app/Services/AiProviderService.php @@ -0,0 +1,164 @@ +<?php + +namespace App\Services; + +use App\Models\UploadedPicture; +use Illuminate\Http\Client\ConnectionException; +use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Storage; +use RuntimeException; + +class AiProviderService +{ + /** + * Prompt the AI provider with a text and pictures + * + * @param string $systemRole The system role to send to the AI provider. E.g. "You are a helpful assistant." + * @param string $prompt The prompt to send to the AI provider + * @param UploadedPicture ...$pictures The pictures to send to the AI provider + * @return array The response from the AI provider. + */ + public function promptWithPictures(string $systemRole, string $prompt, UploadedPicture ...$pictures): array + { + $transcodingService = app(ImageTranscodingService::class); + + $transcodedPictures = []; + foreach ($pictures as $picture) { + $picturePath = Storage::disk('public')->get($picture->path_original); + $transcodedPicture = $transcodingService->transcode($picturePath, UploadedPicture::MEDIUM_SIZE, 'jpeg'); + + if (! $transcodedPicture) { + Log::error('Failed to transcode picture', [ + 'picture' => $picture, + ]); + throw new RuntimeException('Failed to transcode picture'); + } + + $transcodedPictures[] = $transcodedPicture; + } + + $selectedProvider = config('ai-provider.selected-provider'); + + $picturesArray = array_map(fn (string $transcodedPicture) => [ + 'type' => 'image_url', + 'image_url' => [ + 'url' => 'data:image/jpeg;base64,'.base64_encode($transcodedPicture), + ], + ], $transcodedPictures); + + $requestBody = [ + 'headers' => [ + 'Authorization' => 'Bearer '.config('ai-provider.providers.'.$selectedProvider.'.api-key'), + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ], + 'json' => [ + 'model' => config('ai-provider.providers.'.$selectedProvider.'.model'), + 'messages' => [ + [ + 'role' => 'system', + 'content' => [ + [ + 'type' => 'text', + 'text' => $systemRole, + ], + ], + ], + [ + 'role' => 'user', + 'content' => [ + [ + 'type' => 'text', + 'text' => $prompt, + ], + ...$picturesArray, + ], + ], + ], + 'max_tokens' => config('ai-provider.providers.'.$selectedProvider.'.max-tokens'), + 'response_format' => [ + 'type' => 'json_object', + ], + ], + ]; + + return $this->callApi(config('ai-provider.providers.'.$selectedProvider.'.url'), $requestBody); + } + + /** + * Prompt the AI provider with a text + * + * @param string $systemRole The system role to send to the AI provider. E.g. "You are a helpful assistant." + * @param string $prompt The prompt to send to the AI provider + * @return array The response from the AI provider. + */ + public function prompt(string $systemRole, string $prompt): array + { + $selectedProvider = config('ai-provider.selected-provider'); + + $requestBody = ['model' => config('ai-provider.providers.'.$selectedProvider.'.model'), + 'messages' => [ + [ + 'role' => 'system', + 'content' => [ + [ + 'type' => 'text', + 'text' => $systemRole, + ], + ], + ], + [ + 'role' => 'user', + 'content' => [ + [ + 'type' => 'text', + 'text' => $prompt, + ], + ], + ], + ], + 'max_tokens' => config('ai-provider.providers.'.$selectedProvider.'.max-tokens'), + 'response_format' => [ + 'type' => 'json_object', + ], + ]; + + return $this->callApi(config('ai-provider.providers.'.$selectedProvider.'.url'), $requestBody); + } + + /** + * Call the AI provider API + * + * @param string $url The URL of the AI provider API + * @param array $requestBody The request body to send to the AI provider API + * @return array The response from the AI provider + */ + private function callApi(string $url, array $requestBody): array + { + $selectedProvider = config('ai-provider.selected-provider'); + + try { + $response = Http::withHeaders([ + 'Authorization' => 'Bearer '.config('ai-provider.providers.'.$selectedProvider.'.api-key'), + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ])->post($url, $requestBody); + } catch (ConnectionException $e) { + Log::error('Failed to call AI provider API', [ + 'exception' => $e, + ]); + throw new RuntimeException('Failed to call AI provider API'); + } + $result = $response->json(); + + if (! isset($result['choices'][0]['message']['content'])) { + Log::error('Failed to get response from AI provider', [ + 'response' => $result, + ]); + throw new RuntimeException('Failed to get response from AI provider'); + } + + return json_decode($result['choices'][0]['message']['content'], true); + } +} diff --git a/app/Services/ImageTranscodingService.php b/app/Services/ImageTranscodingService.php index e99468e1a3f59e2e81780c9fd7cdc1eddb0b00ce..3e5461e05bd15ab763e149c6c2ac7f7ccfeb4ec0 100644 --- a/app/Services/ImageTranscodingService.php +++ b/app/Services/ImageTranscodingService.php @@ -5,6 +5,9 @@ use Imagick; use Intervention\Image\Drivers\Imagick\Driver as ImagickDriver; use Intervention\Image\Encoders\AvifEncoder; +use Intervention\Image\Encoders\JpegEncoder; +use Intervention\Image\Encoders\PngEncoder; +use Intervention\Image\Encoders\WebpEncoder; use Intervention\Image\Exceptions\RuntimeException; use Intervention\Image\ImageManager; use Log; @@ -23,9 +26,10 @@ public function __construct(ImagickDriver $driver) * * @param string $source The source image path or content. Eg: /path/to/image.jpg or file_get_contents('/path/to/image.jpg') * @param int|null $resolution The new resolution to transcode the image to + * @param string $codec The codec to use for transcoding. Eg: jpeg, webp, png, avif * @return string|null The transcoded image content */ - public function transcode(string $source, ?int $resolution = null): ?string + public function transcode(string $source, ?int $resolution = null, string $codec = 'avif'): ?string { $image = $this->imageManager->read($source); try { @@ -53,7 +57,12 @@ public function transcode(string $source, ?int $resolution = null): ?string $image->scale($resolution); } - return $image->encode(new AvifEncoder(quality: 85))->toString(); + return match ($codec) { + 'jpeg' => $image->encode(new JpegEncoder(quality: 85))->toString(), + 'webp' => $image->encode(new WebpEncoder(quality: 85))->toString(), + 'png' => $image->encode(new PngEncoder)->toString(), + default => $image->encode(new AvifEncoder(quality: 85))->toString(), + }; } catch (RuntimeException $exception) { Log::error('Failed to transcode image', [ 'exception' => $exception, diff --git a/app/Services/IpAddressMetadataResolverService.php b/app/Services/IpAddressMetadataResolverService.php new file mode 100644 index 0000000000000000000000000000000000000000..13f4eaec3cc317a5d6536bbe8ace6df74f42457a --- /dev/null +++ b/app/Services/IpAddressMetadataResolverService.php @@ -0,0 +1,69 @@ +<?php + +namespace App\Services; + +use Exception; +use Illuminate\Http\Client\ConnectionException; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Log; +use SlProjects\LaravelRequestLogger\app\Models\IpAddress; + +class IpAddressMetadataResolverService +{ + /** + * Resolve metadata for the given IP addresses. + * This method has a fail tolerance. The only exception that is thrown is when the API server returns a 422 status code. + * Server errors are logged and the method returns without throwing an exception. + * + * @param Collection $ipAddresses The IP addresses to resolve metadata for. Instances of IpAddress. + * @return array{array{status: 'success'|'fail', message?: string, countryCode?: string, lat?: float, lon?: float, query: string}} The resolved metadata for each IP address. + * + * @throws ConnectionException + */ + public static function resolve(Collection $ipAddresses): array + { + $url = config('ip-address-resolver.url').'?fields=status,message,countryCode,lat,lon,query'; + $maxIpPerCall = config('ip-address-resolver.max_ip_addresses_per_call'); + $maxCallsPerMinute = config('ip-address-resolver.call_limit_per_minute'); + $currentCallsCount = Cache::get('ip-address-resolver.calls_count', 0); + + if ($currentCallsCount >= $maxCallsPerMinute) { + Log::info('Max calls per minute reached. Skipping metadata resolution.'); + + return []; + } + + Cache::increment('ip-address-resolver.calls_count', 1, 60); + + if ($ipAddresses->count() > $maxIpPerCall) { + $ipAddresses = $ipAddresses->take($maxIpPerCall); + } + + $response = Http::post($url, $ipAddresses->pluck('ip')->toArray()); + + if ($response->failed()) { + $returnedError = [ + 'status' => $response->status(), + 'message' => $response->body(), + ]; + + if ($response->unprocessableContent()) { + $apiResponse = $response->json(); + Log::error('The API rejected the request with a 422 unprocessable entity status code. ', $returnedError); + throw new Exception('The API rejected the request with a 422 unprocessable entity status code. '.$apiResponse['message']); + } + if ($response->serverError()) { + Log::info('The API server encountered an error while processing the request. Skipping metadata resolution.', $returnedError); + + return []; + } + Log::error('The API server returned an unexpected status code. Skipping metadata resolution.', $returnedError); + + return []; + } + + return $response->json(); + } +} diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 0ad9c5732e62e505e0c80a86e584e6a1f6a78642..63da17d7f2f4d80e87b3aaeb99521018ed33d79f 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -3,4 +3,5 @@ return [ App\Providers\AppServiceProvider::class, App\Providers\FortifyServiceProvider::class, + App\Providers\HorizonServiceProvider::class, ]; diff --git a/composer.json b/composer.json index 53d134543b9bdb7054973b3fa7312ee037733ff2..2147a7f861e665f181b2a1f66456c8ed9d257094 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "itsgoingd/clockwork": "^5.3", "laravel/fortify": "^1.24", "laravel/framework": "^11.9", + "laravel/horizon": "^5.30", "laravel/octane": "^2.6", "laravel/tinker": "^2.9", "platformcommunity/flysystem-bunnycdn": "*", diff --git a/composer.lock b/composer.lock index d74a19c34a0ef03a4ed7ada5c00d24754430d379..31c03844cc31ebc1a50c1657014fe21bba8ec132 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0fe551a6979ff36ef2315ffbe54b9038", + "content-hash": "01fe61521ac3505612dd88366956b8b8", "packages": [ { "name": "bacon/bacon-qr-code", @@ -1074,16 +1074,16 @@ }, { "name": "guzzlehttp/uri-template", - "version": "v1.0.4", + "version": "v1.0.4", "source": { "type": "git", "url": "https://github.com/guzzle/uri-template.git", - "reference": "30e286560c137526eccd4ce21b2de477ab0676d2" + "reference": "30e286560c137526eccd4ce21b2de477ab0676d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2", - "reference": "30e286560c137526eccd4ce21b2de477ab0676d2", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2", + "reference": "30e286560c137526eccd4ce21b2de477ab0676d2", "shasum": "" }, "require": { @@ -1140,7 +1140,7 @@ ], "support": { "issues": "https://github.com/guzzle/uri-template/issues", - "source": "https://github.com/guzzle/uri-template/tree/v1.0.4" + "source": "https://github.com/guzzle/uri-template/tree/v1.0.4" }, "funding": [ { @@ -1156,7 +1156,7 @@ "type": "tidelift" } ], - "time": "2025-02-03T10:55:03+00:00" + "time": "2025-02-03T10:55:03+00:00" }, { "name": "intervention/gif", @@ -1228,16 +1228,16 @@ }, { "name": "intervention/image", - "version": "3.11.1", + "version": "3.11.1", "source": { "type": "git", "url": "https://github.com/Intervention/image.git", - "reference": "0f87254688e480fbb521e2a1ac6c11c784ca41af" + "reference": "0f87254688e480fbb521e2a1ac6c11c784ca41af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Intervention/image/zipball/0f87254688e480fbb521e2a1ac6c11c784ca41af", - "reference": "0f87254688e480fbb521e2a1ac6c11c784ca41af", + "url": "https://api.github.com/repos/Intervention/image/zipball/0f87254688e480fbb521e2a1ac6c11c784ca41af", + "reference": "0f87254688e480fbb521e2a1ac6c11c784ca41af", "shasum": "" }, "require": { @@ -1284,7 +1284,7 @@ ], "support": { "issues": "https://github.com/Intervention/image/issues", - "source": "https://github.com/Intervention/image/tree/3.11.1" + "source": "https://github.com/Intervention/image/tree/3.11.1" }, "funding": [ { @@ -1300,30 +1300,30 @@ "type": "ko_fi" } ], - "time": "2025-02-01T07:28:26+00:00" + "time": "2025-02-01T07:28:26+00:00" }, { "name": "intervention/validation", - "version": "4.4.6", + "version": "4.5.0", "source": { "type": "git", "url": "https://github.com/Intervention/validation.git", - "reference": "10129adb07c0a57fffaf046828e7ec22a27797a3" + "reference": "9e3eba1a293438d72d5198744d800302a85fe4ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Intervention/validation/zipball/10129adb07c0a57fffaf046828e7ec22a27797a3", - "reference": "10129adb07c0a57fffaf046828e7ec22a27797a3", + "url": "https://api.github.com/repos/Intervention/validation/zipball/9e3eba1a293438d72d5198744d800302a85fe4ae", + "reference": "9e3eba1a293438d72d5198744d800302a85fe4ae", "shasum": "" }, "require": { "ext-mbstring": "*", - "illuminate/validation": "^10|^11", + "illuminate/validation": "^10 || ^11 || ^12", "php": "^8.1" }, "require-dev": { "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^10.0 || ^11.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", "slevomat/coding-standard": "~8.0", "squizlabs/php_codesniffer": "^3.8", "symfony/uid": "^5.1|^6.2" @@ -1372,7 +1372,7 @@ ], "support": { "issues": "https://github.com/Intervention/validation/issues", - "source": "https://github.com/Intervention/validation/tree/4.4.6" + "source": "https://github.com/Intervention/validation/tree/4.5.0" }, "funding": [ { @@ -1388,20 +1388,20 @@ "type": "ko_fi" } ], - "time": "2025-01-05T13:58:03+00:00" + "time": "2025-02-09T14:15:30+00:00" }, { "name": "itsgoingd/clockwork", - "version": "v5.3.4", + "version": "v5.3.4", "source": { "type": "git", "url": "https://github.com/itsgoingd/clockwork.git", - "reference": "c27ad77a08a9e58bf0049de46969fa4fe3b506e5" + "reference": "c27ad77a08a9e58bf0049de46969fa4fe3b506e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/c27ad77a08a9e58bf0049de46969fa4fe3b506e5", - "reference": "c27ad77a08a9e58bf0049de46969fa4fe3b506e5", + "url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/c27ad77a08a9e58bf0049de46969fa4fe3b506e5", + "reference": "c27ad77a08a9e58bf0049de46969fa4fe3b506e5", "shasum": "" }, "require": { @@ -1456,7 +1456,7 @@ ], "support": { "issues": "https://github.com/itsgoingd/clockwork/issues", - "source": "https://github.com/itsgoingd/clockwork/tree/v5.3.4" + "source": "https://github.com/itsgoingd/clockwork/tree/v5.3.4" }, "funding": [ { @@ -1464,7 +1464,7 @@ "type": "github" } ], - "time": "2025-02-09T15:57:21+00:00" + "time": "2025-02-09T15:57:21+00:00" }, { "name": "jean85/pretty-package-versions", @@ -1526,120 +1526,120 @@ "time": "2024-11-18T16:19:46+00:00" }, { - "name": "laminas/laminas-diactoros", - "version": "3.5.0", - "source": { - "type": "git", - "url": "https://github.com/laminas/laminas-diactoros.git", - "reference": "143a16306602ce56b8b092a7914fef03c37f9ed2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/143a16306602ce56b8b092a7914fef03c37f9ed2", - "reference": "143a16306602ce56b8b092a7914fef03c37f9ed2", - "shasum": "" - }, - "require": { - "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", - "psr/http-factory": "^1.1", - "psr/http-message": "^1.1 || ^2.0" - }, - "conflict": { - "amphp/amp": "<2.6.4" - }, - "provide": { - "psr/http-factory-implementation": "^1.0", - "psr/http-message-implementation": "^1.1 || ^2.0" - }, - "require-dev": { - "ext-curl": "*", - "ext-dom": "*", - "ext-gd": "*", - "ext-libxml": "*", - "http-interop/http-factory-tests": "^2.2.0", - "laminas/laminas-coding-standard": "~2.5.0", - "php-http/psr7-integration-tests": "^1.4.0", - "phpunit/phpunit": "^10.5.36", - "psalm/plugin-phpunit": "^0.19.0", - "vimeo/psalm": "^5.26.1" - }, - "type": "library", - "extra": { - "laminas": { - "module": "Laminas\\Diactoros", - "config-provider": "Laminas\\Diactoros\\ConfigProvider" - } - }, - "autoload": { - "files": [ - "src/functions/create_uploaded_file.php", - "src/functions/marshal_headers_from_sapi.php", - "src/functions/marshal_method_from_sapi.php", - "src/functions/marshal_protocol_version_from_sapi.php", - "src/functions/normalize_server.php", - "src/functions/normalize_uploaded_files.php", - "src/functions/parse_cookie_header.php" - ], - "psr-4": { - "Laminas\\Diactoros\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "PSR HTTP Message implementations", - "homepage": "https://laminas.dev", - "keywords": [ - "http", - "laminas", - "psr", - "psr-17", - "psr-7" - ], - "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-diactoros/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-diactoros/issues", - "rss": "https://github.com/laminas/laminas-diactoros/releases.atom", - "source": "https://github.com/laminas/laminas-diactoros" - }, - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], - "time": "2024-10-14T11:59:49+00:00" - }, - { + "name": "laminas/laminas-diactoros", + "version": "3.5.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-diactoros.git", + "reference": "143a16306602ce56b8b092a7914fef03c37f9ed2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/143a16306602ce56b8b092a7914fef03c37f9ed2", + "reference": "143a16306602ce56b8b092a7914fef03c37f9ed2", + "shasum": "" + }, + "require": { + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "conflict": { + "amphp/amp": "<2.6.4" + }, + "provide": { + "psr/http-factory-implementation": "^1.0", + "psr/http-message-implementation": "^1.1 || ^2.0" + }, + "require-dev": { + "ext-curl": "*", + "ext-dom": "*", + "ext-gd": "*", + "ext-libxml": "*", + "http-interop/http-factory-tests": "^2.2.0", + "laminas/laminas-coding-standard": "~2.5.0", + "php-http/psr7-integration-tests": "^1.4.0", + "phpunit/phpunit": "^10.5.36", + "psalm/plugin-phpunit": "^0.19.0", + "vimeo/psalm": "^5.26.1" + }, + "type": "library", + "extra": { + "laminas": { + "module": "Laminas\\Diactoros", + "config-provider": "Laminas\\Diactoros\\ConfigProvider" + } + }, + "autoload": { + "files": [ + "src/functions/create_uploaded_file.php", + "src/functions/marshal_headers_from_sapi.php", + "src/functions/marshal_method_from_sapi.php", + "src/functions/marshal_protocol_version_from_sapi.php", + "src/functions/normalize_server.php", + "src/functions/normalize_uploaded_files.php", + "src/functions/parse_cookie_header.php" + ], + "psr-4": { + "Laminas\\Diactoros\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "PSR HTTP Message implementations", + "homepage": "https://laminas.dev", + "keywords": [ + "http", + "laminas", + "psr", + "psr-17", + "psr-7" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-diactoros/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-diactoros/issues", + "rss": "https://github.com/laminas/laminas-diactoros/releases.atom", + "source": "https://github.com/laminas/laminas-diactoros" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2024-10-14T11:59:49+00:00" + }, + { "name": "laravel/fortify", - "version": "v1.25.4", + "version": "v1.25.4", "source": { "type": "git", "url": "https://github.com/laravel/fortify.git", - "reference": "f185600e2d3a861834ad00ee3b7863f26ac25d3f" + "reference": "f185600e2d3a861834ad00ee3b7863f26ac25d3f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/fortify/zipball/f185600e2d3a861834ad00ee3b7863f26ac25d3f", - "reference": "f185600e2d3a861834ad00ee3b7863f26ac25d3f", + "url": "https://api.github.com/repos/laravel/fortify/zipball/f185600e2d3a861834ad00ee3b7863f26ac25d3f", + "reference": "f185600e2d3a861834ad00ee3b7863f26ac25d3f", "shasum": "" }, "require": { "bacon/bacon-qr-code": "^3.0", "ext-json": "*", - "illuminate/support": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", "php": "^8.1", "pragmarx/google2fa": "^8.0", "symfony/console": "^6.0|^7.0" }, "require-dev": { "mockery/mockery": "^1.0", - "orchestra/testbench": "^8.16|^9.0|^10.0", + "orchestra/testbench": "^8.16|^9.0|^10.0", "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^10.4|^11.3" + "phpunit/phpunit": "^10.4|^11.3" }, "type": "library", "extra": { @@ -1676,20 +1676,20 @@ "issues": "https://github.com/laravel/fortify/issues", "source": "https://github.com/laravel/fortify" }, - "time": "2025-01-26T19:34:46+00:00" + "time": "2025-01-26T19:34:46+00:00" }, { "name": "laravel/framework", - "version": "v11.42.1", + "version": "v11.43.1", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "ff392f42f6c55cc774ce75553a11c6b031da67f8" + "reference": "053f26afb699c845945e7380b407dd019a0a2c74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/ff392f42f6c55cc774ce75553a11c6b031da67f8", - "reference": "ff392f42f6c55cc774ce75553a11c6b031da67f8", + "url": "https://api.github.com/repos/laravel/framework/zipball/053f26afb699c845945e7380b407dd019a0a2c74", + "reference": "053f26afb699c845945e7380b407dd019a0a2c74", "shasum": "" }, "require": { @@ -1797,11 +1797,11 @@ "league/flysystem-read-only": "^3.25.1", "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", - "orchestra/testbench-core": "^9.9.4", + "orchestra/testbench-core": "^9.9.4", "pda/pheanstalk": "^5.0.6", "php-http/discovery": "^1.15", - "phpstan/phpstan": "^2.0", - "phpunit/phpunit": "^10.5.35|^11.3.6|^12.0.1", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.5.35|^11.3.6|^12.0.1", "predis/predis": "^2.3", "resend/resend-php": "^0.10.0", "symfony/cache": "^7.0.3", @@ -1833,7 +1833,7 @@ "mockery/mockery": "Required to use mocking (^1.6).", "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", - "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.3.6|^12.0.1).", + "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.3.6|^12.0.1).", "predis/predis": "Required to use the predis connector (^2.3).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", @@ -1891,110 +1891,190 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-02-12T20:58:18+00:00" - }, - { - "name": "laravel/octane", - "version": "v2.7.0", - "source": { - "type": "git", - "url": "https://github.com/laravel/octane.git", - "reference": "c9580d430fa8459823bd1dfbcb2bcfd591548cec" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel/octane/zipball/c9580d430fa8459823bd1dfbcb2bcfd591548cec", - "reference": "c9580d430fa8459823bd1dfbcb2bcfd591548cec", - "shasum": "" - }, - "require": { - "laminas/laminas-diactoros": "^3.0", - "laravel/framework": "^10.10.1|^11.0", - "laravel/prompts": "^0.1.24|^0.2.0|^0.3.0", - "laravel/serializable-closure": "^1.3|^2.0", - "nesbot/carbon": "^2.66.0|^3.0", - "php": "^8.1.0", - "symfony/console": "^6.0|^7.0", - "symfony/psr-http-message-bridge": "^2.2.0|^6.4|^7.0" - }, - "conflict": { - "spiral/roadrunner": "<2023.1.0", - "spiral/roadrunner-cli": "<2.6.0", - "spiral/roadrunner-http": "<3.3.0" - }, - "require-dev": { - "guzzlehttp/guzzle": "^7.6.1", - "inertiajs/inertia-laravel": "^1.3.2|^2.0", - "laravel/scout": "^10.2.1", - "laravel/socialite": "^5.6.1", - "livewire/livewire": "^2.12.3|^3.0", - "mockery/mockery": "^1.5.1", - "nunomaduro/collision": "^6.4.0|^7.5.2|^8.0", - "orchestra/testbench": "^8.21|^9.0", - "phpstan/phpstan": "^1.10.15", - "phpunit/phpunit": "^10.4", - "spiral/roadrunner-cli": "^2.6.0", - "spiral/roadrunner-http": "^3.3.0" - }, - "bin": [ - "bin/roadrunner-worker", - "bin/swoole-server" - ], - "type": "library", - "extra": { - "laravel": { - "aliases": { - "Octane": "Laravel\\Octane\\Facades\\Octane" - }, - "providers": [ - "Laravel\\Octane\\OctaneServiceProvider" - ] - }, - "branch-alias": { - "dev-master": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Laravel\\Octane\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "Supercharge your Laravel application's performance.", - "keywords": [ - "frankenphp", - "laravel", - "octane", - "roadrunner", - "swoole" - ], - "support": { - "issues": "https://github.com/laravel/octane/issues", - "source": "https://github.com/laravel/octane" - }, - "time": "2025-02-11T15:04:38+00:00" - }, - { + "time": "2025-02-19T16:06:03+00:00" + }, + { + "name": "laravel/horizon", + "version": "v5.30.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/horizon.git", + "reference": "7b9ee870bf0e425b956fd0433f616f98fe951f72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/horizon/zipball/7b9ee870bf0e425b956fd0433f616f98fe951f72", + "reference": "7b9ee870bf0e425b956fd0433f616f98fe951f72", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcntl": "*", + "ext-posix": "*", + "illuminate/contracts": "^9.21|^10.0|^11.0|^12.0", + "illuminate/queue": "^9.21|^10.0|^11.0|^12.0", + "illuminate/support": "^9.21|^10.0|^11.0|^12.0", + "nesbot/carbon": "^2.17|^3.0", + "php": "^8.0", + "ramsey/uuid": "^4.0", + "symfony/console": "^6.0|^7.0", + "symfony/error-handler": "^6.0|^7.0", + "symfony/polyfill-php83": "^1.28", + "symfony/process": "^6.0|^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.0|^10.4|^11.5", + "predis/predis": "^1.1|^2.0" + }, + "suggest": { + "ext-redis": "Required to use the Redis PHP driver.", + "predis/predis": "Required when not using the Redis PHP driver (^1.1|^2.0)." + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Horizon": "Laravel\\Horizon\\Horizon" + }, + "providers": [ + "Laravel\\Horizon\\HorizonServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Horizon\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Dashboard and code-driven configuration for Laravel queues.", + "keywords": [ + "laravel", + "queue" + ], + "support": { + "issues": "https://github.com/laravel/horizon/issues", + "source": "https://github.com/laravel/horizon/tree/v5.30.3" + }, + "time": "2025-02-11T13:52:50+00:00" + }, + { + "name": "laravel/octane", + "version": "v2.8.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/octane.git", + "reference": "1c5190cc5ad67eb4aadbf1816dcbfedc692851e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/octane/zipball/1c5190cc5ad67eb4aadbf1816dcbfedc692851e7", + "reference": "1c5190cc5ad67eb4aadbf1816dcbfedc692851e7", + "shasum": "" + }, + "require": { + "laminas/laminas-diactoros": "^3.0", + "laravel/framework": "^10.10.1|^11.0", + "laravel/prompts": "^0.1.24|^0.2.0|^0.3.0", + "laravel/serializable-closure": "^1.3|^2.0", + "nesbot/carbon": "^2.66.0|^3.0", + "php": "^8.1.0", + "symfony/console": "^6.0|^7.0", + "symfony/psr-http-message-bridge": "^2.2.0|^6.4|^7.0" + }, + "conflict": { + "spiral/roadrunner": "<2023.1.0", + "spiral/roadrunner-cli": "<2.6.0", + "spiral/roadrunner-http": "<3.3.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.6.1", + "inertiajs/inertia-laravel": "^1.3.2|^2.0", + "laravel/scout": "^10.2.1", + "laravel/socialite": "^5.6.1", + "livewire/livewire": "^2.12.3|^3.0", + "mockery/mockery": "^1.5.1", + "nunomaduro/collision": "^6.4.0|^7.5.2|^8.0", + "orchestra/testbench": "^8.21|^9.0", + "phpstan/phpstan": "^1.10.15", + "phpunit/phpunit": "^10.4", + "spiral/roadrunner-cli": "^2.6.0", + "spiral/roadrunner-http": "^3.3.0" + }, + "bin": [ + "bin/roadrunner-worker", + "bin/swoole-server" + ], + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Octane": "Laravel\\Octane\\Facades\\Octane" + }, + "providers": [ + "Laravel\\Octane\\OctaneServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Octane\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Supercharge your Laravel application's performance.", + "keywords": [ + "frankenphp", + "laravel", + "octane", + "roadrunner", + "swoole" + ], + "support": { + "issues": "https://github.com/laravel/octane/issues", + "source": "https://github.com/laravel/octane" + }, + "time": "2025-02-18T15:18:13+00:00" + }, + { "name": "laravel/prompts", - "version": "v0.3.5", + "version": "v0.3.5", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1" + "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/57b8f7efe40333cdb925700891c7d7465325d3b1", - "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1", + "url": "https://api.github.com/repos/laravel/prompts/zipball/57b8f7efe40333cdb925700891c7d7465325d3b1", + "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1", "shasum": "" }, "require": { @@ -2008,7 +2088,7 @@ "laravel/framework": ">=10.17.0 <10.25.0" }, "require-dev": { - "illuminate/collections": "^10.0|^11.0|^12.0", + "illuminate/collections": "^10.0|^11.0|^12.0", "mockery/mockery": "^1.5", "pestphp/pest": "^2.3|^3.4", "phpstan/phpstan": "^1.11", @@ -2038,31 +2118,31 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.5" + "source": "https://github.com/laravel/prompts/tree/v0.3.5" }, - "time": "2025-02-11T13:34:40+00:00" + "time": "2025-02-11T13:34:40+00:00" }, { "name": "laravel/serializable-closure", - "version": "v2.0.3", + "version": "v2.0.3", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "f379c13663245f7aa4512a7869f62eb14095f23f" + "reference": "f379c13663245f7aa4512a7869f62eb14095f23f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/f379c13663245f7aa4512a7869f62eb14095f23f", - "reference": "f379c13663245f7aa4512a7869f62eb14095f23f", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/f379c13663245f7aa4512a7869f62eb14095f23f", + "reference": "f379c13663245f7aa4512a7869f62eb14095f23f", "shasum": "" }, "require": { "php": "^8.1" }, "require-dev": { - "illuminate/support": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", "nesbot/carbon": "^2.67|^3.0", - "pestphp/pest": "^2.36|^3.0", + "pestphp/pest": "^2.36|^3.0", "phpstan/phpstan": "^2.0", "symfony/var-dumper": "^6.2.0|^7.0.0" }, @@ -2101,26 +2181,26 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-02-11T15:03:05+00:00" + "time": "2025-02-11T15:03:05+00:00" }, { "name": "laravel/tinker", - "version": "v2.10.1", + "version": "v2.10.1", "source": { "type": "git", "url": "https://github.com/laravel/tinker.git", - "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3" + "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/tinker/zipball/22177cc71807d38f2810c6204d8f7183d88a57d3", - "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3", + "url": "https://api.github.com/repos/laravel/tinker/zipball/22177cc71807d38f2810c6204d8f7183d88a57d3", + "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3", "shasum": "" }, "require": { - "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", - "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", - "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", "php": "^7.2.5|^8.0", "psy/psysh": "^0.11.1|^0.12.0", "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0" @@ -2128,10 +2208,10 @@ "require-dev": { "mockery/mockery": "~1.3.3|^1.4.2", "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^8.5.8|^9.3.3|^10.0" + "phpunit/phpunit": "^8.5.8|^9.3.3|^10.0" }, "suggest": { - "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0)." + "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0)." }, "type": "library", "extra": { @@ -2165,9 +2245,9 @@ ], "support": { "issues": "https://github.com/laravel/tinker/issues", - "source": "https://github.com/laravel/tinker/tree/v2.10.1" + "source": "https://github.com/laravel/tinker/tree/v2.10.1" }, - "time": "2025-01-27T14:24:01+00:00" + "time": "2025-01-27T14:24:01+00:00" }, { "name": "league/commonmark", @@ -2825,16 +2905,16 @@ }, { "name": "nesbot/carbon", - "version": "3.8.5", + "version": "3.8.5", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "b1a53a27898639579a67de42e8ced5d5386aa9a4" + "reference": "b1a53a27898639579a67de42e8ced5d5386aa9a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/b1a53a27898639579a67de42e8ced5d5386aa9a4", - "reference": "b1a53a27898639579a67de42e8ced5d5386aa9a4", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/b1a53a27898639579a67de42e8ced5d5386aa9a4", + "reference": "b1a53a27898639579a67de42e8ced5d5386aa9a4", "shasum": "" }, "require": { @@ -2910,8 +2990,8 @@ ], "support": { "docs": "https://carbon.nesbot.com/docs", - "issues": "https://github.com/CarbonPHP/carbon/issues", - "source": "https://github.com/CarbonPHP/carbon" + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" }, "funding": [ { @@ -2927,7 +3007,7 @@ "type": "tidelift" } ], - "time": "2025-02-11T16:28:45+00:00" + "time": "2025-02-11T16:28:45+00:00" }, { "name": "nette/schema", @@ -4358,20 +4438,20 @@ }, { "name": "sentry/sentry-laravel", - "version": "4.12.0", + "version": "4.13.0", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-laravel.git", - "reference": "da1ee3417dfb3576a6aaa0f8b25892ebdb98fdb0" + "reference": "d232ac494258e0d50a77c575a5af5f1a426d3f87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/da1ee3417dfb3576a6aaa0f8b25892ebdb98fdb0", - "reference": "da1ee3417dfb3576a6aaa0f8b25892ebdb98fdb0", + "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/d232ac494258e0d50a77c575a5af5f1a426d3f87", + "reference": "d232ac494258e0d50a77c575a5af5f1a426d3f87", "shasum": "" }, "require": { - "illuminate/support": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0", + "illuminate/support": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0", "nyholm/psr7": "^1.0", "php": "^7.2 | ^8.0", "sentry/sentry": "^4.10", @@ -4381,12 +4461,12 @@ "friendsofphp/php-cs-fixer": "^3.11", "guzzlehttp/guzzle": "^7.2", "laravel/folio": "^1.1", - "laravel/framework": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0", + "laravel/framework": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0", "livewire/livewire": "^2.0 | ^3.0", "mockery/mockery": "^1.3", - "orchestra/testbench": "^4.7 | ^5.1 | ^6.0 | ^7.0 | ^8.0 | ^9.0", + "orchestra/testbench": "^4.7 | ^5.1 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0", "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^8.4 | ^9.3 | ^10.4" + "phpunit/phpunit": "^8.4 | ^9.3 | ^10.4 | ^11.5" }, "type": "library", "extra": { @@ -4431,7 +4511,7 @@ ], "support": { "issues": "https://github.com/getsentry/sentry-laravel/issues", - "source": "https://github.com/getsentry/sentry-laravel/tree/4.12.0" + "source": "https://github.com/getsentry/sentry-laravel/tree/4.13.0" }, "funding": [ { @@ -4443,20 +4523,20 @@ "type": "custom" } ], - "time": "2025-02-05T13:13:03+00:00" + "time": "2025-02-18T10:09:29+00:00" }, { "name": "sl-projects/laravel-request-logger", - "version": "v1.0.2", + "version": "v1.0.6", "source": { "type": "git", "url": "https://github.com/SofianeLasri/laravel-request-logger.git", - "reference": "e494b95ca6ac752f815df9875f8f3d25e58bfc36" + "reference": "e42196f70e1c3b7b1ad2ea9a202b6d1e8c5a585f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/SofianeLasri/laravel-request-logger/zipball/e494b95ca6ac752f815df9875f8f3d25e58bfc36", - "reference": "e494b95ca6ac752f815df9875f8f3d25e58bfc36", + "url": "https://api.github.com/repos/SofianeLasri/laravel-request-logger/zipball/e42196f70e1c3b7b1ad2ea9a202b6d1e8c5a585f", + "reference": "e42196f70e1c3b7b1ad2ea9a202b6d1e8c5a585f", "shasum": "" }, "require": { @@ -4477,7 +4557,8 @@ }, "autoload": { "psr-4": { - "SlProjects\\LaravelRequestLogger\\": "src/" + "SlProjects\\LaravelRequestLogger\\": "src/", + "SlProjects\\LaravelRequestLogger\\Database\\Factories\\": "src/database/factories/" } }, "notification-url": "https://packagist.org/downloads/", @@ -4495,9 +4576,9 @@ "description": "A Laravel package to log all incoming HTTP requests", "support": { "issues": "https://github.com/SofianeLasri/laravel-request-logger/issues", - "source": "https://github.com/SofianeLasri/laravel-request-logger/tree/v1.0.2" + "source": "https://github.com/SofianeLasri/laravel-request-logger/tree/v1.0.6" }, - "time": "2024-12-15T14:29:11+00:00" + "time": "2025-02-19T16:59:10+00:00" }, { "name": "spatie/commonmark-shiki-highlighter", @@ -4561,16 +4642,16 @@ }, { "name": "spatie/db-dumper", - "version": "3.7.1", + "version": "3.8.0", "source": { "type": "git", "url": "https://github.com/spatie/db-dumper.git", - "reference": "55d4d6710e1ab18c1e7ce2b22b8ad4bea2a30016" + "reference": "91e1fd4dc000aefc9753cda2da37069fc996baee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/db-dumper/zipball/55d4d6710e1ab18c1e7ce2b22b8ad4bea2a30016", - "reference": "55d4d6710e1ab18c1e7ce2b22b8ad4bea2a30016", + "url": "https://api.github.com/repos/spatie/db-dumper/zipball/91e1fd4dc000aefc9753cda2da37069fc996baee", + "reference": "91e1fd4dc000aefc9753cda2da37069fc996baee", "shasum": "" }, "require": { @@ -4608,7 +4689,7 @@ "spatie" ], "support": { - "source": "https://github.com/spatie/db-dumper/tree/3.7.1" + "source": "https://github.com/spatie/db-dumper/tree/3.8.0" }, "funding": [ { @@ -4620,7 +4701,7 @@ "type": "github" } ], - "time": "2024-11-18T14:54:31+00:00" + "time": "2025-02-14T15:04:22+00:00" }, { "name": "spatie/emoji", @@ -4690,26 +4771,26 @@ }, { "name": "spatie/laravel-backup", - "version": "9.2.5", + "version": "9.2.7", "source": { "type": "git", "url": "https://github.com/spatie/laravel-backup.git", - "reference": "50effa86d6614282da747ae1194912e0ed273daf" + "reference": "0438eef46188e990cf6ddb34ce1eb4c94f3b3a05" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-backup/zipball/50effa86d6614282da747ae1194912e0ed273daf", - "reference": "50effa86d6614282da747ae1194912e0ed273daf", + "url": "https://api.github.com/repos/spatie/laravel-backup/zipball/0438eef46188e990cf6ddb34ce1eb4c94f3b3a05", + "reference": "0438eef46188e990cf6ddb34ce1eb4c94f3b3a05", "shasum": "" }, "require": { "ext-zip": "^1.14.0", - "illuminate/console": "^10.10.0|^11.0", - "illuminate/contracts": "^10.10.0|^11.0", - "illuminate/events": "^10.10.0|^11.0", - "illuminate/filesystem": "^10.10.0|^11.0", - "illuminate/notifications": "^10.10.0|^11.0", - "illuminate/support": "^10.10.0|^11.0", + "illuminate/console": "^10.10.0|^11.0|^12.0", + "illuminate/contracts": "^10.10.0|^11.0|^12.0", + "illuminate/events": "^10.10.0|^11.0|^12.0", + "illuminate/filesystem": "^10.10.0|^11.0|^12.0", + "illuminate/notifications": "^10.10.0|^11.0|^12.0", + "illuminate/support": "^10.10.0|^11.0|^12.0", "league/flysystem": "^3.0", "php": "^8.2", "spatie/db-dumper": "^3.7", @@ -4722,12 +4803,12 @@ "require-dev": { "composer-runtime-api": "^2.0", "ext-pcntl": "*", - "larastan/larastan": "^2.7.0", + "larastan/larastan": "^2.7.0|^3.0", "laravel/slack-notification-channel": "^2.5|^3.0", "league/flysystem-aws-s3-v3": "^2.0|^3.0", "mockery/mockery": "^1.4", - "orchestra/testbench": "^8.0|^9.0", - "pestphp/pest": "^1.20|^2.0", + "orchestra/testbench": "^8.0|^9.0|^10.0", + "pestphp/pest": "^1.20|^2.0|^3.0", "phpstan/extension-installer": "^1.1", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-phpunit": "^1.1", @@ -4774,7 +4855,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-backup/issues", - "source": "https://github.com/spatie/laravel-backup/tree/9.2.5" + "source": "https://github.com/spatie/laravel-backup/tree/9.2.7" }, "funding": [ { @@ -4786,7 +4867,7 @@ "type": "other" } ], - "time": "2025-01-31T11:14:27+00:00" + "time": "2025-02-16T16:59:01+00:00" }, { "name": "spatie/laravel-markdown", @@ -4866,27 +4947,27 @@ }, { "name": "spatie/laravel-package-tools", - "version": "1.19.0", + "version": "1.19.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-package-tools.git", - "reference": "1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa" + "reference": "1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa", - "reference": "1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa", + "reference": "1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa", "shasum": "" }, "require": { - "illuminate/contracts": "^9.28|^10.0|^11.0|^12.0", + "illuminate/contracts": "^9.28|^10.0|^11.0|^12.0", "php": "^8.0" }, "require-dev": { "mockery/mockery": "^1.5", - "orchestra/testbench": "^7.7|^8.0|^9.0|^10.0", - "pestphp/pest": "^1.23|^2.1|^3.1", - "phpunit/phpunit": "^9.5.24|^10.5|^11.5", + "orchestra/testbench": "^7.7|^8.0|^9.0|^10.0", + "pestphp/pest": "^1.23|^2.1|^3.1", + "phpunit/phpunit": "^9.5.24|^10.5|^11.5", "spatie/pest-plugin-test-time": "^1.1|^2.2" }, "type": "library", @@ -4914,7 +4995,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-package-tools/issues", - "source": "https://github.com/spatie/laravel-package-tools/tree/1.19.0" + "source": "https://github.com/spatie/laravel-package-tools/tree/1.19.0" }, "funding": [ { @@ -4922,24 +5003,24 @@ "type": "github" } ], - "time": "2025-02-06T14:58:20+00:00" + "time": "2025-02-06T14:58:20+00:00" }, { "name": "spatie/laravel-signal-aware-command", - "version": "2.0.1", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-signal-aware-command.git", - "reference": "5af15853cf593093e6b1abae3cca446ba59c30e8" + "reference": "8e8a226ed7fb45302294878ef339e75ffa9a878d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-signal-aware-command/zipball/5af15853cf593093e6b1abae3cca446ba59c30e8", - "reference": "5af15853cf593093e6b1abae3cca446ba59c30e8", + "url": "https://api.github.com/repos/spatie/laravel-signal-aware-command/zipball/8e8a226ed7fb45302294878ef339e75ffa9a878d", + "reference": "8e8a226ed7fb45302294878ef339e75ffa9a878d", "shasum": "" }, "require": { - "illuminate/contracts": "^11.0", + "illuminate/contracts": "^11.0|^12.0", "php": "^8.2", "spatie/laravel-package-tools": "^1.4.3", "symfony/console": "^7.0" @@ -4948,8 +5029,8 @@ "brianium/paratest": "^6.2|^7.0", "ext-pcntl": "*", "nunomaduro/collision": "^5.3|^6.0|^7.0|^8.0", - "orchestra/testbench": "^9.0", - "pestphp/pest-plugin-laravel": "^1.3|^2.0", + "orchestra/testbench": "^9.0|^10.0", + "pestphp/pest-plugin-laravel": "^1.3|^2.0|^3.0", "phpunit/phpunit": "^9.5|^10|^11", "spatie/laravel-ray": "^1.17" }, @@ -4989,7 +5070,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-signal-aware-command/issues", - "source": "https://github.com/spatie/laravel-signal-aware-command/tree/2.0.1" + "source": "https://github.com/spatie/laravel-signal-aware-command/tree/2.1.0" }, "funding": [ { @@ -4997,20 +5078,20 @@ "type": "github" } ], - "time": "2025-02-05T08:24:50+00:00" + "time": "2025-02-14T09:55:51+00:00" }, { "name": "spatie/shiki-php", - "version": "2.3.0", + "version": "2.3.1", "source": { "type": "git", "url": "https://github.com/spatie/shiki-php.git", - "reference": "50919178a6865f1165bf1a8f08430b88ef3a53de" + "reference": "24b4dcc161f37144180edbef49557edb96c1dc2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/shiki-php/zipball/50919178a6865f1165bf1a8f08430b88ef3a53de", - "reference": "50919178a6865f1165bf1a8f08430b88ef3a53de", + "url": "https://api.github.com/repos/spatie/shiki-php/zipball/24b4dcc161f37144180edbef49557edb96c1dc2d", + "reference": "24b4dcc161f37144180edbef49557edb96c1dc2d", "shasum": "" }, "require": { @@ -5054,7 +5135,7 @@ "spatie" ], "support": { - "source": "https://github.com/spatie/shiki-php/tree/2.3.0" + "source": "https://github.com/spatie/shiki-php/tree/2.3.1" }, "funding": [ { @@ -5062,7 +5143,7 @@ "type": "github" } ], - "time": "2025-02-10T15:41:13+00:00" + "time": "2025-02-18T13:18:46+00:00" }, { "name": "spatie/temporary-directory", @@ -5426,16 +5507,16 @@ }, { "name": "symfony/error-handler", - "version": "v7.2.3", + "version": "v7.2.3", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "959a74d044a6db21f4caa6d695648dcb5584cb49" + "reference": "959a74d044a6db21f4caa6d695648dcb5584cb49" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/959a74d044a6db21f4caa6d695648dcb5584cb49", - "reference": "959a74d044a6db21f4caa6d695648dcb5584cb49", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/959a74d044a6db21f4caa6d695648dcb5584cb49", + "reference": "959a74d044a6db21f4caa6d695648dcb5584cb49", "shasum": "" }, "require": { @@ -5481,7 +5562,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.2.3" + "source": "https://github.com/symfony/error-handler/tree/v7.2.3" }, "funding": [ { @@ -5497,7 +5578,7 @@ "type": "tidelift" } ], - "time": "2025-01-07T09:39:55+00:00" + "time": "2025-01-07T09:39:55+00:00" }, { "name": "symfony/event-dispatcher", @@ -5721,16 +5802,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.2.3", + "version": "v7.2.3", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "ee1b504b8926198be89d05e5b6fc4c3810c090f0" + "reference": "ee1b504b8926198be89d05e5b6fc4c3810c090f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ee1b504b8926198be89d05e5b6fc4c3810c090f0", - "reference": "ee1b504b8926198be89d05e5b6fc4c3810c090f0", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ee1b504b8926198be89d05e5b6fc4c3810c090f0", + "reference": "ee1b504b8926198be89d05e5b6fc4c3810c090f0", "shasum": "" }, "require": { @@ -5779,7 +5860,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.2.3" + "source": "https://github.com/symfony/http-foundation/tree/v7.2.3" }, "funding": [ { @@ -5795,20 +5876,20 @@ "type": "tidelift" } ], - "time": "2025-01-17T10:56:55+00:00" + "time": "2025-01-17T10:56:55+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.2.3", + "version": "v7.2.3", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b" + "reference": "caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b", - "reference": "caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b", + "reference": "caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b", "shasum": "" }, "require": { @@ -5893,7 +5974,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.2.3" + "source": "https://github.com/symfony/http-kernel/tree/v7.2.3" }, "funding": [ { @@ -5909,20 +5990,20 @@ "type": "tidelift" } ], - "time": "2025-01-29T07:40:13+00:00" + "time": "2025-01-29T07:40:13+00:00" }, { "name": "symfony/mailer", - "version": "v7.2.3", + "version": "v7.2.3", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "f3871b182c44997cf039f3b462af4a48fb85f9d3" + "reference": "f3871b182c44997cf039f3b462af4a48fb85f9d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/f3871b182c44997cf039f3b462af4a48fb85f9d3", - "reference": "f3871b182c44997cf039f3b462af4a48fb85f9d3", + "url": "https://api.github.com/repos/symfony/mailer/zipball/f3871b182c44997cf039f3b462af4a48fb85f9d3", + "reference": "f3871b182c44997cf039f3b462af4a48fb85f9d3", "shasum": "" }, "require": { @@ -5973,7 +6054,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.2.3" + "source": "https://github.com/symfony/mailer/tree/v7.2.3" }, "funding": [ { @@ -5989,20 +6070,20 @@ "type": "tidelift" } ], - "time": "2025-01-27T11:08:17+00:00" + "time": "2025-01-27T11:08:17+00:00" }, { "name": "symfony/mime", - "version": "v7.2.3", + "version": "v7.2.3", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204" + "reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/2fc3b4bd67e4747e45195bc4c98bea4628476204", - "reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204", + "url": "https://api.github.com/repos/symfony/mime/zipball/2fc3b4bd67e4747e45195bc4c98bea4628476204", + "reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204", "shasum": "" }, "require": { @@ -6057,7 +6138,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.2.3" + "source": "https://github.com/symfony/mime/tree/v7.2.3" }, "funding": [ { @@ -6073,7 +6154,7 @@ "type": "tidelift" } ], - "time": "2025-01-27T11:08:17+00:00" + "time": "2025-01-27T11:08:17+00:00" }, { "name": "symfony/options-resolver", @@ -6924,16 +7005,16 @@ }, { "name": "symfony/routing", - "version": "v7.2.3", + "version": "v7.2.3", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "ee9a67edc6baa33e5fae662f94f91fd262930996" + "reference": "ee9a67edc6baa33e5fae662f94f91fd262930996" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/ee9a67edc6baa33e5fae662f94f91fd262930996", - "reference": "ee9a67edc6baa33e5fae662f94f91fd262930996", + "url": "https://api.github.com/repos/symfony/routing/zipball/ee9a67edc6baa33e5fae662f94f91fd262930996", + "reference": "ee9a67edc6baa33e5fae662f94f91fd262930996", "shasum": "" }, "require": { @@ -6985,7 +7066,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.2.3" + "source": "https://github.com/symfony/routing/tree/v7.2.3" }, "funding": [ { @@ -7001,7 +7082,7 @@ "type": "tidelift" } ], - "time": "2025-01-17T10:56:55+00:00" + "time": "2025-01-17T10:56:55+00:00" }, { "name": "symfony/service-contracts", @@ -7422,16 +7503,16 @@ }, { "name": "symfony/var-dumper", - "version": "v7.2.3", + "version": "v7.2.3", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "82b478c69745d8878eb60f9a049a4d584996f73a" + "reference": "82b478c69745d8878eb60f9a049a4d584996f73a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/82b478c69745d8878eb60f9a049a4d584996f73a", - "reference": "82b478c69745d8878eb60f9a049a4d584996f73a", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/82b478c69745d8878eb60f9a049a4d584996f73a", + "reference": "82b478c69745d8878eb60f9a049a4d584996f73a", "shasum": "" }, "require": { @@ -7485,7 +7566,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.2.3" + "source": "https://github.com/symfony/var-dumper/tree/v7.2.3" }, "funding": [ { @@ -7501,7 +7582,7 @@ "type": "tidelift" } ], - "time": "2025-01-17T11:39:41+00:00" + "time": "2025-01-17T11:39:41+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -8056,21 +8137,21 @@ }, { "name": "laravel/envoy", - "version": "v2.10.2", + "version": "v2.10.2", "source": { "type": "git", "url": "https://github.com/laravel/envoy.git", - "reference": "819a519e3d86b056c7aa3bd5d0801952a6fc14fd" + "reference": "819a519e3d86b056c7aa3bd5d0801952a6fc14fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/envoy/zipball/819a519e3d86b056c7aa3bd5d0801952a6fc14fd", - "reference": "819a519e3d86b056c7aa3bd5d0801952a6fc14fd", + "url": "https://api.github.com/repos/laravel/envoy/zipball/819a519e3d86b056c7aa3bd5d0801952a6fc14fd", + "reference": "819a519e3d86b056c7aa3bd5d0801952a6fc14fd", "shasum": "" }, "require": { "guzzlehttp/guzzle": "^6.0|^7.0", - "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", "php": "^7.2|^8.0", "symfony/console": "^4.3|^5.0|^6.0|^7.0", "symfony/process": "^4.3|^5.0|^6.0|^7.0" @@ -8113,41 +8194,41 @@ ], "support": { "issues": "https://github.com/laravel/envoy/issues", - "source": "https://github.com/laravel/envoy/tree/v2.10.2" + "source": "https://github.com/laravel/envoy/tree/v2.10.2" }, - "time": "2025-01-28T15:47:18+00:00" + "time": "2025-01-28T15:47:18+00:00" }, { "name": "laravel/pail", - "version": "v1.2.2", + "version": "v1.2.2", "source": { "type": "git", "url": "https://github.com/laravel/pail.git", - "reference": "f31f4980f52be17c4667f3eafe034e6826787db2" + "reference": "f31f4980f52be17c4667f3eafe034e6826787db2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pail/zipball/f31f4980f52be17c4667f3eafe034e6826787db2", - "reference": "f31f4980f52be17c4667f3eafe034e6826787db2", + "url": "https://api.github.com/repos/laravel/pail/zipball/f31f4980f52be17c4667f3eafe034e6826787db2", + "reference": "f31f4980f52be17c4667f3eafe034e6826787db2", "shasum": "" }, "require": { "ext-mbstring": "*", - "illuminate/console": "^10.24|^11.0|^12.0", - "illuminate/contracts": "^10.24|^11.0|^12.0", - "illuminate/log": "^10.24|^11.0|^12.0", - "illuminate/process": "^10.24|^11.0|^12.0", - "illuminate/support": "^10.24|^11.0|^12.0", + "illuminate/console": "^10.24|^11.0|^12.0", + "illuminate/contracts": "^10.24|^11.0|^12.0", + "illuminate/log": "^10.24|^11.0|^12.0", + "illuminate/process": "^10.24|^11.0|^12.0", + "illuminate/support": "^10.24|^11.0|^12.0", "nunomaduro/termwind": "^1.15|^2.0", "php": "^8.2", "symfony/console": "^6.0|^7.0" }, "require-dev": { - "laravel/framework": "^10.24|^11.0|^12.0", + "laravel/framework": "^10.24|^11.0|^12.0", "laravel/pint": "^1.13", - "orchestra/testbench-core": "^8.13|^9.0|^10.0", - "pestphp/pest": "^2.20|^3.0", - "pestphp/pest-plugin-type-coverage": "^2.3|^3.0", + "orchestra/testbench-core": "^8.13|^9.0|^10.0", + "pestphp/pest": "^2.20|^3.0", + "pestphp/pest-plugin-type-coverage": "^2.3|^3.0", "phpstan/phpstan": "^1.10", "symfony/var-dumper": "^6.3|^7.0" }, @@ -8193,20 +8274,20 @@ "issues": "https://github.com/laravel/pail/issues", "source": "https://github.com/laravel/pail" }, - "time": "2025-01-28T15:15:15+00:00" + "time": "2025-01-28T15:15:15+00:00" }, { "name": "laravel/pint", - "version": "v1.20.0", + "version": "v1.21.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "53072e8ea22213a7ed168a8a15b96fbb8b82d44b" + "reference": "531fa0871fbde719c51b12afa3a443b8f4e4b425" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/53072e8ea22213a7ed168a8a15b96fbb8b82d44b", - "reference": "53072e8ea22213a7ed168a8a15b96fbb8b82d44b", + "url": "https://api.github.com/repos/laravel/pint/zipball/531fa0871fbde719c51b12afa3a443b8f4e4b425", + "reference": "531fa0871fbde719c51b12afa3a443b8f4e4b425", "shasum": "" }, "require": { @@ -8214,15 +8295,15 @@ "ext-mbstring": "*", "ext-tokenizer": "*", "ext-xml": "*", - "php": "^8.1.0" + "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.66.0", - "illuminate/view": "^10.48.25", - "larastan/larastan": "^2.9.12", - "laravel-zero/framework": "^10.48.25", + "friendsofphp/php-cs-fixer": "^3.68.5", + "illuminate/view": "^11.42.0", + "larastan/larastan": "^3.0.4", + "laravel-zero/framework": "^11.36.1", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^1.17.0", + "nunomaduro/termwind": "^2.3", "pestphp/pest": "^2.36.0" }, "bin": [ @@ -8259,32 +8340,32 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-01-14T16:20:53+00:00" + "time": "2025-02-18T03:18:57+00:00" }, { "name": "laravel/sail", - "version": "v1.41.0", + "version": "v1.41.0", "source": { "type": "git", "url": "https://github.com/laravel/sail.git", - "reference": "fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec" + "reference": "fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sail/zipball/fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec", - "reference": "fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec", + "url": "https://api.github.com/repos/laravel/sail/zipball/fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec", + "reference": "fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec", "shasum": "" }, "require": { - "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0", - "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0", - "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0", + "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0", + "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0", + "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0", "php": "^8.0", "symfony/console": "^6.0|^7.0", "symfony/yaml": "^6.0|^7.0" }, "require-dev": { - "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", "phpstan/phpstan": "^1.10" }, "bin": [ @@ -8322,7 +8403,7 @@ "issues": "https://github.com/laravel/sail/issues", "source": "https://github.com/laravel/sail" }, - "time": "2025-01-24T15:45:36+00:00" + "time": "2025-01-24T15:45:36+00:00" }, { "name": "mockery/mockery", @@ -8409,16 +8490,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.0", + "version": "1.13.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414" + "reference": "024473a478be9df5fdaca2c793f2232fe788e414" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", + "reference": "024473a478be9df5fdaca2c793f2232fe788e414", "shasum": "" }, "require": { @@ -8457,7 +8538,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" }, "funding": [ { @@ -8465,7 +8546,7 @@ "type": "tidelift" } ], - "time": "2025-02-12T12:17:51+00:00" + "time": "2025-02-12T12:17:51+00:00" }, { "name": "nunomaduro/collision", @@ -8772,16 +8853,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.5", + "version": "2.1.6", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "451b17f9665481ee502adc39be987cb71067ece2" + "reference": "6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/451b17f9665481ee502adc39be987cb71067ece2", - "reference": "451b17f9665481ee502adc39be987cb71067ece2", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c", + "reference": "6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c", "shasum": "" }, "require": { @@ -8826,7 +8907,7 @@ "type": "github" } ], - "time": "2025-02-13T12:49:56+00:00" + "time": "2025-02-19T15:46:42+00:00" }, { "name": "phpunit/php-code-coverage", @@ -9153,16 +9234,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.7", + "version": "11.5.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "e1cb706f019e2547039ca2c839898cd5f557ee5d" + "reference": "c9bd61aab12f0fc5e82ecfe621ff518a1d1f1049" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e1cb706f019e2547039ca2c839898cd5f557ee5d", - "reference": "e1cb706f019e2547039ca2c839898cd5f557ee5d", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c9bd61aab12f0fc5e82ecfe621ff518a1d1f1049", + "reference": "c9bd61aab12f0fc5e82ecfe621ff518a1d1f1049", "shasum": "" }, "require": { @@ -9234,7 +9315,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.7" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.8" }, "funding": [ { @@ -9250,7 +9331,7 @@ "type": "tidelift" } ], - "time": "2025-02-06T16:10:05+00:00" + "time": "2025-02-18T06:26:59+00:00" }, { "name": "sebastian/cli-parser", @@ -10232,16 +10313,16 @@ }, { "name": "symfony/yaml", - "version": "v7.2.3", + "version": "v7.2.3", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "ac238f173df0c9c1120f862d0f599e17535a87ec" + "reference": "ac238f173df0c9c1120f862d0f599e17535a87ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/ac238f173df0c9c1120f862d0f599e17535a87ec", - "reference": "ac238f173df0c9c1120f862d0f599e17535a87ec", + "url": "https://api.github.com/repos/symfony/yaml/zipball/ac238f173df0c9c1120f862d0f599e17535a87ec", + "reference": "ac238f173df0c9c1120f862d0f599e17535a87ec", "shasum": "" }, "require": { @@ -10284,7 +10365,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.2.3" + "source": "https://github.com/symfony/yaml/tree/v7.2.3" }, "funding": [ { @@ -10300,7 +10381,7 @@ "type": "tidelift" } ], - "time": "2025-01-07T12:55:42+00:00" + "time": "2025-01-07T12:55:42+00:00" }, { "name": "theseer/tokenizer", @@ -10355,7 +10436,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -10364,6 +10445,6 @@ "ext-pdo": "*", "ext-zip": "*" }, - "platform-dev": [], - "plugin-api-version": "2.3.0" + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/config/ai-provider.php b/config/ai-provider.php new file mode 100644 index 0000000000000000000000000000000000000000..4fa3c3971b8664bc7ec43255805b6edf47d75fec --- /dev/null +++ b/config/ai-provider.php @@ -0,0 +1,13 @@ +<?php + +return [ + 'selected-provider' => env('AI_PROVIDER', 'openai'), + 'providers' => [ + 'openai' => [ + 'url' => env('OPENAI_URL', 'https://api.openai.com/v1/chat/completions'), + 'api-key' => env('OPENAI_API_KEY'), + 'model' => env('OPENAI_MODEL', 'gpt-4o-mini'), + 'max-tokens' => env('OPENAI_MAX_TOKENS', 256), + ], + ], +]; diff --git a/config/app.php b/config/app.php index b44d19f7b53e72e0c3756ca32a0d6d6540b17a67..9ea28a014eb03f08d0e32c6887094ee8780bf652 100644 --- a/config/app.php +++ b/config/app.php @@ -136,4 +136,6 @@ ], 'cdn_disk' => env('CDN_FILESYSTEM_DISK'), + + 'private_mode_secret' => env('APP_PRIVATE_MODE_SECRET'), ]; diff --git a/config/horizon.php b/config/horizon.php new file mode 100644 index 0000000000000000000000000000000000000000..56d79c80d32d9fcd40acf27625d3a97d9abb6df3 --- /dev/null +++ b/config/horizon.php @@ -0,0 +1,213 @@ +<?php + +use Illuminate\Support\Str; + +return [ + + /* + |-------------------------------------------------------------------------- + | Horizon Domain + |-------------------------------------------------------------------------- + | + | This is the subdomain where Horizon will be accessible from. If this + | setting is null, Horizon will reside under the same domain as the + | application. Otherwise, this value will serve as the subdomain. + | + */ + + 'domain' => env('HORIZON_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | Horizon Path + |-------------------------------------------------------------------------- + | + | This is the URI path where Horizon will be accessible from. Feel free + | to change this path to anything you like. Note that the URI will not + | affect the paths of its internal API that aren't exposed to users. + | + */ + + 'path' => env('HORIZON_PATH', 'horizon'), + + /* + |-------------------------------------------------------------------------- + | Horizon Redis Connection + |-------------------------------------------------------------------------- + | + | This is the name of the Redis connection where Horizon will store the + | meta information required for it to function. It includes the list + | of supervisors, failed jobs, job metrics, and other information. + | + */ + + 'use' => 'default', + + /* + |-------------------------------------------------------------------------- + | Horizon Redis Prefix + |-------------------------------------------------------------------------- + | + | This prefix will be used when storing all Horizon data in Redis. You + | may modify the prefix when you are running multiple installations + | of Horizon on the same server so that they don't have problems. + | + */ + + 'prefix' => env( + 'HORIZON_PREFIX', + Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:' + ), + + /* + |-------------------------------------------------------------------------- + | Horizon Route Middleware + |-------------------------------------------------------------------------- + | + | These middleware will get attached onto each Horizon route, giving you + | the chance to add your own middleware to this list or change any of + | the existing middleware. Or, you can simply stick with this list. + | + */ + + 'middleware' => ['web', 'auth'], + + /* + |-------------------------------------------------------------------------- + | Queue Wait Time Thresholds + |-------------------------------------------------------------------------- + | + | This option allows you to configure when the LongWaitDetected event + | will be fired. Every connection / queue combination may have its + | own, unique threshold (in seconds) before this event is fired. + | + */ + + 'waits' => [ + 'redis:default' => 60, + ], + + /* + |-------------------------------------------------------------------------- + | Job Trimming Times + |-------------------------------------------------------------------------- + | + | Here you can configure for how long (in minutes) you desire Horizon to + | persist the recent and failed jobs. Typically, recent jobs are kept + | for one hour while all failed jobs are stored for an entire week. + | + */ + + 'trim' => [ + 'recent' => 60, + 'pending' => 60, + 'completed' => 60, + 'recent_failed' => 10080, + 'failed' => 10080, + 'monitored' => 10080, + ], + + /* + |-------------------------------------------------------------------------- + | Silenced Jobs + |-------------------------------------------------------------------------- + | + | Silencing a job will instruct Horizon to not place the job in the list + | of completed jobs within the Horizon dashboard. This setting may be + | used to fully remove any noisy jobs from the completed jobs list. + | + */ + + 'silenced' => [ + // App\Jobs\ExampleJob::class, + ], + + /* + |-------------------------------------------------------------------------- + | Metrics + |-------------------------------------------------------------------------- + | + | Here you can configure how many snapshots should be kept to display in + | the metrics graph. This will get used in combination with Horizon's + | `horizon:snapshot` schedule to define how long to retain metrics. + | + */ + + 'metrics' => [ + 'trim_snapshots' => [ + 'job' => 24, + 'queue' => 24, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Fast Termination + |-------------------------------------------------------------------------- + | + | When this option is enabled, Horizon's "terminate" command will not + | wait on all of the workers to terminate unless the --wait option + | is provided. Fast termination can shorten deployment delay by + | allowing a new instance of Horizon to start while the last + | instance will continue to terminate each of its workers. + | + */ + + 'fast_termination' => false, + + /* + |-------------------------------------------------------------------------- + | Memory Limit (MB) + |-------------------------------------------------------------------------- + | + | This value describes the maximum amount of memory the Horizon master + | supervisor may consume before it is terminated and restarted. For + | configuring these limits on your workers, see the next section. + | + */ + + 'memory_limit' => 64, + + /* + |-------------------------------------------------------------------------- + | Queue Worker Configuration + |-------------------------------------------------------------------------- + | + | Here you may define the queue worker settings used by your application + | in all environments. These supervisors and settings handle all your + | queued jobs and will be provisioned by Horizon during deployment. + | + */ + + 'defaults' => [ + 'supervisor-1' => [ + 'connection' => 'redis', + 'queue' => ['default'], + 'balance' => 'auto', + 'autoScalingStrategy' => 'time', + 'maxProcesses' => 1, + 'maxTime' => 0, + 'maxJobs' => 0, + 'memory' => 128, + 'tries' => 1, + 'timeout' => 60, + 'nice' => 0, + ], + ], + + 'environments' => [ + 'production' => [ + 'supervisor-1' => [ + 'maxProcesses' => 10, + 'balanceMaxShift' => 1, + 'balanceCooldown' => 3, + ], + ], + + 'local' => [ + 'supervisor-1' => [ + 'maxProcesses' => 3, + ], + ], + ], +]; diff --git a/config/ip-address-resolver.php b/config/ip-address-resolver.php new file mode 100644 index 0000000000000000000000000000000000000000..7dc71a84fda14b4dd12a32ae5f08c7452edd2b95 --- /dev/null +++ b/config/ip-address-resolver.php @@ -0,0 +1,7 @@ +<?php + +return [ + 'url' => env('IP_ADDRESS_RESOLVER_URL', 'http://ip-api.com/batch'), + 'call_limit_per_minute' => env('IP_ADDRESS_RESOLVER_CALL_LIMIT_PER_MINUTE', 15), + 'max_ip_addresses_per_call' => env('IP_ADDRESS_RESOLVER_MAX_IP_ADDRESSES_PER_CALL', 100), +]; diff --git a/database/factories/IpAddressMetadataFactory.php b/database/factories/IpAddressMetadataFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..5581b8193b0a4a4f0f6a2edf952e61f5bcf10303 --- /dev/null +++ b/database/factories/IpAddressMetadataFactory.php @@ -0,0 +1,23 @@ +<?php + +namespace Database\Factories; + +use App\Models\IpAddressMetadata; +use Illuminate\Database\Eloquent\Factories\Factory; +use SlProjects\LaravelRequestLogger\app\Models\IpAddress; + +class IpAddressMetadataFactory extends Factory +{ + protected $model = IpAddressMetadata::class; + + public function definition(): array + { + return [ + 'country_code' => $this->faker->randomElement(IpAddressMetadata::COUNTRY_CODES), + 'lat' => $this->faker->latitude(), + 'lon' => $this->faker->longitude(), + + 'ip_address_id' => IpAddress::factory(), + ]; + } +} diff --git a/database/factories/UserAgentMetadataFactory.php b/database/factories/UserAgentMetadataFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..2ed13a1dce9e8f751277b7cf91668d1098aae5e0 --- /dev/null +++ b/database/factories/UserAgentMetadataFactory.php @@ -0,0 +1,21 @@ +<?php + +namespace Database\Factories; + +use App\Models\UserAgentMetadata; +use Illuminate\Database\Eloquent\Factories\Factory; +use SlProjects\LaravelRequestLogger\app\Models\UserAgent; + +class UserAgentMetadataFactory extends Factory +{ + protected $model = UserAgentMetadata::class; + + public function definition(): array + { + return [ + 'is_bot' => $this->faker->boolean(), + + 'user_agent_id' => UserAgent::factory(), + ]; + } +} diff --git a/database/migrations/2025_02_16_212109_create_user_agent_metadata_table.php b/database/migrations/2025_02_16_212109_create_user_agent_metadata_table.php new file mode 100644 index 0000000000000000000000000000000000000000..cf525b8d2feb2fb07ae19f6c860fcb6e818464e8 --- /dev/null +++ b/database/migrations/2025_02_16_212109_create_user_agent_metadata_table.php @@ -0,0 +1,23 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; +use SlProjects\LaravelRequestLogger\app\Models\UserAgent; + +return new class extends Migration +{ + public function up(): void + { + Schema::create('user_agent_metadata', function (Blueprint $table) { + $table->id(); + $table->foreignIdFor(UserAgent::class); + $table->boolean('is_bot'); + }); + } + + public function down(): void + { + Schema::dropIfExists('user_agent_metadata'); + } +}; diff --git a/database/migrations/2025_02_19_141925_create_ip_address_metadata_table.php b/database/migrations/2025_02_19_141925_create_ip_address_metadata_table.php new file mode 100644 index 0000000000000000000000000000000000000000..387e34bb37c327ce10231111387850c065c3f420 --- /dev/null +++ b/database/migrations/2025_02_19_141925_create_ip_address_metadata_table.php @@ -0,0 +1,24 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + public function up(): void + { + Schema::create('ip_address_metadata', function (Blueprint $table) { + $table->id(); + $table->foreignId('ip_address_id')->constrained('ip_addresses'); + $table->enum('country_code', ['AA', 'AB', 'AC', 'AD', 'AE', 'AF', 'AG', 'AH', 'AI', 'AJ', 'AK', 'AL', 'AM', 'AN', 'AO', 'AP', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AV', 'AW', 'AX', 'AY', 'AZ', 'BA', 'BB', 'BC', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BK', 'BL', 'BM', 'BN', 'BO', 'BP', 'BQ', 'BR', 'BS', 'BT', 'BU', 'BV', 'BW', 'BX', 'BY', 'BZ', 'CA', 'CB', 'CC', 'CD', 'CE', 'CF', 'CG', 'CH', 'CI', 'CJ', 'CK', 'CL', 'CM', 'CN', 'CO', 'CP', 'CQ', 'CR', 'CS', 'CT', 'CU', 'CV', 'CW', 'CX', 'CY', 'CZ', 'DA', 'DB', 'DC', 'DD', 'DE', 'DF', 'DG', 'DH', 'DI', 'DJ', 'DK', 'DL', 'DM', 'DN', 'DO', 'DP', 'DQ', 'DR', 'DS', 'DT', 'DU', 'DV', 'DW', 'DX', 'DY', 'DZ', 'EA', 'EB', 'EC', 'ED', 'EE', 'EF', 'EG', 'EH', 'EI', 'EJ', 'EK', 'EL', 'EM', 'EN', 'EO', 'EP', 'EQ', 'ER', 'ES', 'ET', 'EU', 'EV', 'EW', 'EX', 'EY', 'EZ', 'FA', 'FB', 'FC', 'FD', 'FE', 'FF', 'FG', 'FH', 'FI', 'FJ', 'FK', 'FL', 'FM', 'FN', 'FO', 'FP', 'FQ', 'FR', 'FS', 'FT', 'FU', 'FV', 'FW', 'FX', 'FY', 'FZ', 'GA', 'GB', 'GC', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI', 'GJ', 'GK', 'GL', 'GM', 'GN', 'GO', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GV', 'GW', 'GX', 'GY', 'GZ', 'HA', 'HB', 'HC', 'HD', 'HE', 'HF', 'HG', 'HH', 'HI', 'HJ', 'HK', 'HL', 'HM', 'HN', 'HO', 'HP', 'HQ', 'HR', 'HS', 'HT', 'HU', 'HV', 'HW', 'HX', 'HY', 'HZ', 'IA', 'IB', 'IC', 'ID', 'IE', 'IF', 'IG', 'IH', 'II', 'IJ', 'IK', 'IL', 'IM', 'IN', 'IO', 'IP', 'IQ', 'IR', 'IS', 'IT', 'IU', 'IV', 'IW', 'IX', 'IY', 'IZ', 'JA', 'JB', 'JC', 'JD', 'JE', 'JF', 'JG', 'JH', 'JI', 'JJ', 'JK', 'JL', 'JM', 'JN', 'JO', 'JP', 'JQ', 'JR', 'JS', 'JT', 'JU', 'JV', 'JW', 'JX', 'JY', 'JZ', 'KA', 'KB', 'KC', 'KD', 'KE', 'KF', 'KG', 'KH', 'KI', 'KJ', 'KK', 'KL', 'KM', 'KN', 'KO', 'KP', 'KQ', 'KR', 'KS', 'KT', 'KU', 'KV', 'KW', 'KX', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LD', 'LE', 'LF', 'LG', 'LH', 'LI', 'LJ', 'LK', 'LL', 'LM', 'LN', 'LO', 'LP', 'LQ', 'LR', 'LS', 'LT', 'LU', 'LV', 'LW', 'LX', 'LY', 'LZ', 'MA', 'MB', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MI', 'MJ', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NB', 'NC', 'ND', 'NE', 'NF', 'NG', 'NH', 'NI', 'NJ', 'NK', 'NL', 'NM', 'NN', 'NO', 'NP', 'NQ', 'NR', 'NS', 'NT', 'NU', 'NV', 'NW', 'NX', 'NY', 'NZ', 'OA', 'OB', 'OC', 'OD', 'OE', 'OF', 'OG', 'OH', 'OI', 'OJ', 'OK', 'OL', 'OM', 'ON', 'OO', 'OP', 'OQ', 'OR', 'OS', 'OT', 'OU', 'OV', 'OW', 'OX', 'OY', 'OZ', 'PA', 'PB', 'PC', 'PD', 'PE', 'PF', 'PG', 'PH', 'PI', 'PJ', 'PK', 'PL', 'PM', 'PN', 'PO', 'PP', 'PQ', 'PR', 'PS', 'PT', 'PU', 'PV', 'PW', 'PX', 'PY', 'PZ', 'QA', 'QB', 'QC', 'QD', 'QE', 'QF', 'QG', 'QH', 'QI', 'QJ', 'QK', 'QL', 'QM', 'QN', 'QO', 'QP', 'QQ', 'QR', 'QS', 'QT', 'QU', 'QV', 'QW', 'QX', 'QY', 'QZ', 'RA', 'RB', 'RC', 'RD', 'RE', 'RF', 'RG', 'RH', 'RI', 'RJ', 'RK', 'RL', 'RM', 'RN', 'RO', 'RP', 'RQ', 'RR', 'RS', 'RT', 'RU', 'RV', 'RW', 'RX', 'RY', 'RZ', 'SA', 'SB', 'SC', 'SD', 'SE', 'SF', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SP', 'SQ', 'SR', 'SS', 'ST', 'SU', 'SV', 'SW', 'SX', 'SY', 'SZ', 'TA', 'TB', 'TC', 'TD', 'TE', 'TF', 'TG', 'TH', 'TI', 'TJ', 'TK', 'TL', 'TM', 'TN', 'TO', 'TP', 'TQ', 'TR', 'TS', 'TT', 'TU', 'TV', 'TW', 'TX', 'TY', 'TZ', 'UA', 'UB', 'UC', 'UD', 'UE', 'UF', 'UG', 'UH', 'UI', 'UJ', 'UK', 'UL', 'UM', 'UN', 'UO', 'UP', 'UQ', 'UR', 'US', 'UT', 'UU', 'UV', 'UW', 'UX', 'UY', 'UZ', 'VA', 'VB', 'VC', 'VD', 'VE', 'VF', 'VG', 'VH', 'VI', 'VJ', 'VK', 'VL', 'VM', 'VN', 'VO', 'VP', 'VQ', 'VR', 'VS', 'VT', 'VU', 'VV', 'VW', 'VX', 'VY', 'VZ', 'WA', 'WB', 'WC', 'WD', 'WE', 'WF', 'WG', 'WH', 'WI', 'WJ', 'WK', 'WL', 'WM', 'WN', 'WO', 'WP', 'WQ', 'WR', 'WS', 'WT', 'WU', 'WV', 'WW', 'WX', 'WY', 'WZ', 'XA', 'XB', 'XC', 'XD', 'XE', 'XF', 'XG', 'XH', 'XI', 'XJ', 'XK', 'XL', 'XM', 'XN', 'XO', 'XP', 'XQ', 'XR', 'XS', 'XT', 'XU', 'XV', 'XW', 'XX', 'XY', 'XZ', 'YA', 'YB', 'YC', 'YD', 'YE', 'YF', 'YG', 'YH', 'YI', 'YJ', 'YK', 'YL', 'YM', 'YN', 'YO', 'YP', 'YQ', 'YR', 'YS', 'YT', 'YU', 'YV', 'YW', 'YX', 'YY', 'YZ', 'ZA', 'ZB', 'ZC', 'ZD', 'ZE', 'ZF', 'ZG', 'ZH', 'ZI', 'ZJ', 'ZK', 'ZL', 'ZM', 'ZN', 'ZO', 'ZP', 'ZQ', 'ZR', 'ZS', 'ZT', 'ZU', 'ZV', 'ZW', 'ZX', 'ZY', 'ZZ']); + $table->float('lat')->nullable(); + $table->float('lon')->nullable(); + }); + } + + public function down(): void + { + Schema::dropIfExists('ip_address_metadata'); + } +}; diff --git a/docker-compose.yml b/docker-compose.yml index 182619f84c0711bdc5892258207906240a1f97d8..1de12b7f98ba60bf0fb419f9245bb22dc297b694 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: context: . image: registry.gitlab.sl-projects.com/sofianelasri/rann-graphic-design-website:latest working_dir: /app - entrypoint: php artisan octane:frankenphp --workers=1 --max-requests=1 + entrypoint: ["docker-init/entrypoint.sh"] ports: - '80:8000' - '5173:5173' diff --git a/docker-init/000-default.conf b/docker-init/000-default.conf deleted file mode 100644 index c83da7ba3251309bcaf10653213b971ed60d59fe..0000000000000000000000000000000000000000 --- a/docker-init/000-default.conf +++ /dev/null @@ -1,13 +0,0 @@ -<VirtualHost *:80> - ServerAdmin webmaster@localhost - DocumentRoot /var/www/public - - <Directory /var/www/public> - Options Indexes FollowSymLinks MultiViews - AllowOverride All - Require all granted - </Directory> - - ErrorLog ${APACHE_LOG_DIR}/error.log - CustomLog ${APACHE_LOG_DIR}/access.log combined -</VirtualHost> diff --git a/docker-init/entrypoint.sh b/docker-init/entrypoint.sh index 20abb72bdd09e430be7b28adf27203c25f924260..c12663fd0e318205b8d48434248bfcc0850bce26 100644 --- a/docker-init/entrypoint.sh +++ b/docker-init/entrypoint.sh @@ -1,22 +1,15 @@ #!/bin/bash -# If var/wwww/composer.json exists... -if [ ! -f /var/www/composer.json ]; then - echo "Probably a Gitlab CI environment, skipping..." - exec "$@" -else - cd /var/www || exit +echo "Installing dependencies..." +composer install +php artisan key:generate - echo "Installing dependencies..." - composer install - php artisan key:generate +echo "Migrating database..." +php artisan migrate - npm install - npm run build +echo "Launching Supervisor..." +service supervisor start +supervisorctl start all - echo "Starting services..." - service apache2 start - - echo "All good!" - exec "$@" -fi +echo "Running Octane..." +php artisan octane:frankenphp --workers=1 --max-requests=1 \ No newline at end of file diff --git a/docker-init/php.ini b/docker-init/php.ini index dff9b622397b13220a49ba00dbe027fb9edfe47b..6a1056f71f7dc969935a8b86a9b0e46aa94518d7 100644 --- a/docker-init/php.ini +++ b/docker-init/php.ini @@ -1426,10 +1426,13 @@ session.use_trans_sid = 0 ; Shorter length than default is supported only for compatibility reason. ; Users should use 32 or more chars. ; https://php.net/session.sid-length + +; NOTE: Deprecated since PHP 8.4 + ; Default Value: 32 ; Development Value: 26 ; Production Value: 26 -session.sid_length = 26 +; session.sid_length = 32 ; The URL rewriter will look for URLs in a defined set of HTML tags. ; <form> is special; if you include them here, the rewriter will @@ -1466,7 +1469,10 @@ session.trans_sid_tags = "a=href,area=href,frame=src,form=" ; Development Value: 5 ; Production Value: 5 ; https://php.net/session.hash-bits-per-character -session.sid_bits_per_character = 5 + +; NOTE: Deprecated since PHP 8.4 + +; session.sid_bits_per_character = 5 ; Enable upload progress tracking in $_SESSION ; Default Value: On @@ -1882,13 +1888,13 @@ ldap.max_links = -1 ; List of headers files to preload, wildcard patterns allowed. ;ffi.preload= -[xdebug] -zend_extension=xdebug.so -xdebug.mode=develop,coverage,debug,profile -xdebug.start_with_request=trigger -xdebug.discover_client_host=true -xdebug.idekey=docker -xdebug.log=/dev/stdout -xdebug.log_level=0 -xdebug.client_port=9003 -xdebug.client_host=host.docker.internal \ No newline at end of file +;[xdebug] +;zend_extension=xdebug.so +;xdebug.mode=develop,coverage,debug,profile +;xdebug.start_with_request=trigger +;xdebug.discover_client_host=true +;xdebug.idekey=docker +;xdebug.log=/dev/stdout +;xdebug.log_level=0 +;xdebug.client_port=9003 +;xdebug.client_host=host.docker.internal \ No newline at end of file diff --git a/docker-init/supervisord.conf b/docker-init/supervisord.conf new file mode 100644 index 0000000000000000000000000000000000000000..c28a1573051ee3091e0fcc075e15f0d5d08d4fb0 --- /dev/null +++ b/docker-init/supervisord.conf @@ -0,0 +1,5 @@ +[program:laravel-worker] +process_name=%(program_name)s_%(process_num)02d +command=php /app/artisan queue:work --sleep=3 --tries=3 +autostart=true +autorestart=true \ No newline at end of file diff --git a/lang/en/contact.php b/lang/en/contact.php new file mode 100644 index 0000000000000000000000000000000000000000..87f2cd0a0374c6acae9a3c71e5e5fcb4dfb61cfe --- /dev/null +++ b/lang/en/contact.php @@ -0,0 +1,6 @@ +<?php + +return [ + 'desc' => 'Do you have a project in mind or want to discuss your graphic design needs? Feel free to contact me! Whether it’s for a collaboration, a quote request, or just to exchange creative ideas, I’m here to help.', + 'contact_me_on_one_of_my_social_media' => 'Contact me on one of my social media', +]; diff --git a/lang/en/footer.php b/lang/en/footer.php new file mode 100644 index 0000000000000000000000000000000000000000..8c46e83400e9f00b7abead2b34a3dd71c39d197e --- /dev/null +++ b/lang/en/footer.php @@ -0,0 +1,14 @@ +<?php + +return [ + 'contact' => [ + 'title' => 'Excited to work with you!', + 'desc' => 'Feel free to contact me if you need information, or if you just want to chat with me.', + ], + 'links' => [ + 'title' => 'Stay in touch', + ], + 'legals-infos' => [ + 'title' => 'Legal information', + ], +]; diff --git a/lang/en/generic.php b/lang/en/generic.php index 51de6fe9e666f20e6169dff894b319ce4b1416d9..a345a842c4e75dcc9747427837b12f5f10eb10f9 100644 --- a/lang/en/generic.php +++ b/lang/en/generic.php @@ -4,4 +4,6 @@ 'custom_needs' => 'A specific need?', 'custom_needs_desc' => 'I am open to any proposal. Don’t hesitate to contact me!', 'completed_in' => 'Completed in', + 'legal_mentions' => 'Legal notices', + 'terms' => 'Terms and conditions of sale', ]; diff --git a/lang/en/home.php b/lang/en/home.php index 16cf7a51a14278df222b272557969e29d607dc6e..93efc9e769e02189d61ea81d70d4486e327e8d6d 100644 --- a/lang/en/home.php +++ b/lang/en/home.php @@ -17,4 +17,5 @@ 'waving_hand' => 'Waving hand emoji', 'roxannas_photo' => 'Photo of Roxanna Valtre', ], + 'meta_description' => 'Roxanna Valtre, Graphic Designer. Discover my services and my portfolio.', ]; diff --git a/lang/fr/contact.php b/lang/fr/contact.php new file mode 100644 index 0000000000000000000000000000000000000000..67270fd422a5f39c002970a9ba9f6118848a3327 --- /dev/null +++ b/lang/fr/contact.php @@ -0,0 +1,6 @@ +<?php + +return [ + 'desc' => 'Vous avez un projet en tête ou souhaitez discuter de vos besoins en design graphique ? N\'hésitez pas à me contacter ! Que ce soit pour une collaboration, une demande de devis ou simplement pour échanger des idées créatives, je suis là pour vous aider.', + 'contact_me_on_one_of_my_social_media' => 'Contactez moi sur l\'un de mes réseaux', +]; diff --git a/lang/fr/footer.php b/lang/fr/footer.php new file mode 100644 index 0000000000000000000000000000000000000000..0e7acdedfc7c68d0ae44642d3a24acba707dda0b --- /dev/null +++ b/lang/fr/footer.php @@ -0,0 +1,14 @@ +<?php + +return [ + 'contact' => [ + 'title' => 'Hâte de travailler avec vous !', + 'desc' => 'N’hésitez pas à me contacter si vous avez besoin de renseignements, ou que vous souhaitez simplement discuter avec moi.', + ], + 'links' => [ + 'title' => 'Restons en contact', + ], + 'legals-infos' => [ + 'title' => 'Informations légales', + ], +]; diff --git a/lang/fr/generic.php b/lang/fr/generic.php index 4b27032c33a73fb3c47194d9a0b8197c2d786f5d..0c369358497101f7f0bfffd75983290fdc6b7610 100644 --- a/lang/fr/generic.php +++ b/lang/fr/generic.php @@ -4,4 +4,6 @@ 'custom_needs' => 'Un besoin particulier ?', 'custom_needs_desc' => 'Je suis ouverte à toute proposition. N’hésitez pas à me contacter !', 'completed_in' => 'Réalisé en', + 'legal_mentions' => 'Mentions légales', + 'terms' => 'Conditions générales de vente', ]; diff --git a/lang/fr/home.php b/lang/fr/home.php index c873aff786363813b37944ba5b8b5a907b465aef..b24c61701f0f400b4156847f875a2809446a9418 100644 --- a/lang/fr/home.php +++ b/lang/fr/home.php @@ -17,4 +17,5 @@ 'waving_hand' => 'Emoji de main qui salue', 'roxannas_photo' => 'Photo de Roxana Valtre', ], + 'meta_description' => 'Roxanna Valtre, Designer Graphique. Découvrez mes services et mon portfolio.', ]; diff --git a/package-lock.json b/package-lock.json index 35b434f274705fccc39e6b057cb001ca6ef04b3c..081927838d9555f2dce206f17261fab01edda1ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "rann-graphic-design-website", + "name": "app", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/resources/scss/public.scss b/resources/scss/public.scss index 2d88a1389a6b99742d97199479c04e37fe0dbbda..6a7d3c8e7010d4a3502bf9c1c9fee0a8bd89d6db 100644 --- a/resources/scss/public.scss +++ b/resources/scss/public.scss @@ -3,6 +3,10 @@ @tailwind utilities; @import "fonts"; +h1, h2, h3, h4, h5, h6 { + font-family: "Sniglet", serif; +} + .markdown { * { @apply mb-4; diff --git a/resources/views/admin/creations/index.blade.php b/resources/views/admin/creations/index.blade.php index e13f5b4f9fcb48b70d6605515bbc45addf9ca8a3..c5e81d17f85c53b0cce028eff3327f3618c4f197 100644 --- a/resources/views/admin/creations/index.blade.php +++ b/resources/views/admin/creations/index.blade.php @@ -28,6 +28,11 @@ size="sm" tag="a" variant="danger"> Supprimer </x-bs.button> + <x-bs.button size="sm" variant="secondary" id="translate-{{ $creation->id }}" + onclick="translateWithAi({{ $creation->id }})"> + {{ \Spatie\Emoji\Emoji::flagsForFlagUnitedKingdom() }} + Traduire + </x-bs.button> </x-slot:footer> </x-bs.card> @endforeach @@ -38,3 +43,24 @@ @endif </div> @endsection + +@pushonce('scripts') + <script type="text/javascript"> + function translateWithAi(creationId) { + const button = document.querySelector(`#translate-${creationId}`); + button.disabled = true; + fetch(`/admin/creations/${creationId}/translate-with-ai`) + .then(response => response.json()) + .then(data => { + if (data.success) { + button.classList.remove('btn-secondary'); + button.classList.add('btn-success'); + } else { + button.disabled = false; + button.classList.remove('btn-secondary'); + button.classList.add('btn-warning'); + } + }); + } + </script> +@endpushonce diff --git a/resources/views/admin/home.blade.php b/resources/views/admin/home.blade.php index 2ecf3e23d8592b7dd83e41a2ac5cabb1299f823e..46d75a745bb2a7d298febfa72b3cb2cecec24ebb 100644 --- a/resources/views/admin/home.blade.php +++ b/resources/views/admin/home.blade.php @@ -76,6 +76,90 @@ <div class="row g-4"> <!-- Pages les plus visitées --> + <div class="g-col-12 mb-4"> + <div class="card"> + <div class="card-body"> + <h5 class="card-title">Pages les plus visitées (24H)</h5> + <div class="table-responsive"> + <table class="table table-striped align-middle"> + <thead> + <tr> + <th scope="col">#</th> + <th scope="col">Page</th> + <th scope="col">Nombre de visites</th> + </tr> + </thead> + <tbody> + @foreach ($mostVisitedPagesForPastTwentyFourHours as $index => $page) + <tr> + <th scope="row">{{ $index + 1 }}</th> + <td>{{ $page['url'] }}</td> + <td>{{ $page['count'] }}</td> + </tr> + @endforeach + </tbody> + </table> + </div> + </div> + </div> + </div> + + <div class="g-col-12 mb-4"> + <div class="card"> + <div class="card-body"> + <h5 class="card-title">Pages les plus visitées (7 jours)</h5> + <div class="table-responsive"> + <table class="table table-striped align-middle"> + <thead> + <tr> + <th scope="col">#</th> + <th scope="col">Page</th> + <th scope="col">Nombre de visites</th> + </tr> + </thead> + <tbody> + @foreach ($mostVisitedPagesForPastSevenDays as $index => $page) + <tr> + <th scope="row">{{ $index + 1 }}</th> + <td>{{ $page['url'] }}</td> + <td>{{ $page['count'] }}</td> + </tr> + @endforeach + </tbody> + </table> + </div> + </div> + </div> + </div> + + <div class="g-col-12 mb-4"> + <div class="card"> + <div class="card-body"> + <h5 class="card-title">Pages les plus visitées (30 jours)</h5> + <div class="table-responsive"> + <table class="table table-striped align-middle"> + <thead> + <tr> + <th scope="col">#</th> + <th scope="col">Page</th> + <th scope="col">Nombre de visites</th> + </tr> + </thead> + <tbody> + @foreach ($mostVisitedPagesForPastThirtyDays as $index => $page) + <tr> + <th scope="row">{{ $index + 1 }}</th> + <td>{{ $page['url'] }}</td> + <td>{{ $page['count'] }}</td> + </tr> + @endforeach + </tbody> + </table> + </div> + </div> + </div> + </div> + <div class="g-col-12"> <div class="card"> <div class="card-body"> diff --git a/resources/views/components/public/footer.blade.php b/resources/views/components/public/footer.blade.php index 05142fdf514118c6824e2c1982921cb4f779788b..de488c30d597cdcf66ee759c5d8e1a0adce70b94 100644 --- a/resources/views/components/public/footer.blade.php +++ b/resources/views/components/public/footer.blade.php @@ -8,20 +8,20 @@ <x-public.rann-logo height="4rem" role="image"/> </div> <div class="self-stretch flex-col justify-start items-start gap-2 flex"> - <div class="self-stretch text-2xl font-bold">Hâte de travailler avec vous !</div> - <div class="self-stretch text-muted">N’hésitez pas à me contacter si vous avez besoin de - renseignements, ou que vous souhaitez simplement discuter avec moi. + <div class="self-stretch text-2xl font-bold">{{ __('footer.contact.title') }}</div> + <div class="self-stretch text-muted"> + {{ __('footer.contact.desc') }} </div> </div> - <x-public.button size="medium" tag="a" href="#"> - <div class="text-xl font-bold">Me contacter</div> + <x-public.button size="medium" tag="a" href="{{ route('contact') }}"> + <div class="text-xl font-bold">{{ __('navbar.contact_me') }}</div> </x-public.button> </div> </div> <div class="w-full lg:w-auto items-start flex flex-col lg:flex-row gap-8 lg:gap-24"> <div class="w-full lg:w-56 flex-col gap-8 flex"> - <div class="self-stretch text-2xl font-bold">Restons en contact</div> + <div class="self-stretch text-2xl font-bold">{{ __('footer.links.title') }}</div> <ul class="flex flex-col gap-2"> @foreach(\App\Models\SocialMediaLink::all() as $socialMediaLink) <li> @@ -33,16 +33,15 @@ </ul> </div> <div class="w-full lg:w-56 flex-col gap-8 flex"> - <div class="self-stretch text-2xl font-bold">Informations légales</div> + <div class="self-stretch text-2xl font-bold">{{ __('footer.legals-infos.title') }}</div> <ul class="flex flex-col gap-2"> <li> - <a href="#" class="text-muted hover:text-primary">Mentions légales</a> + <a href="{{ route('legal-mentions') }}" + class="text-muted hover:text-primary">{{ __('generic.legal_mentions') }}</a> </li> <li> - <a href="#" class="text-muted hover:text-primary">Conditions générales de vente</a> - </li> - <li> - <a href="#" class="text-muted hover:text-primary">Politique de confidentialité</a> + <a href="{{ route('terms') }}" + class="text-muted hover:text-primary">{{ __('generic.terms') }}</a> </li> </ul> </div> diff --git a/resources/views/components/public/generic-page-header.blade.php b/resources/views/components/public/generic-page-header.blade.php index 7883746c0253491d0b38ca139d379b74f96a19ac..defaac73b1c21652eacf74dd4be847ddcc368361 100644 --- a/resources/views/components/public/generic-page-header.blade.php +++ b/resources/views/components/public/generic-page-header.blade.php @@ -1,6 +1,6 @@ @props([ 'title' => 'Title', - 'description' => 'Description', + 'description' => '', ]) <div {{ $attributes->class(['relative flex flex-col max-w-2xl gap-3']) }}> diff --git a/resources/views/components/public/navbar.blade.php b/resources/views/components/public/navbar.blade.php index 0c0612b1bfd4171d88c7cc3d51e6daad738b95b4..f4d69177c983f15c502f55f274dc573c730ff346 100644 --- a/resources/views/components/public/navbar.blade.php +++ b/resources/views/components/public/navbar.blade.php @@ -17,7 +17,7 @@ <a href="{{ route('prestations') }}" class="text-xl font-bold">{{ __('navbar.services') }}</a> <a href="{{ route('portfolio') }}" class="text-xl font-bold">{{ __('navbar.portfolio') }}</a> <a href="{{ route('evenements') }}" class="text-xl font-bold">{{ __('navbar.events') }}</a> - <a href="#" class="text-xl font-bold">{{ __('navbar.contact_me') }}</a> + <a href="{{ route('contact') }}" class="text-xl font-bold">{{ __('navbar.contact_me') }}</a> <x-public.button tag="a" href="https://dalnarabyrann.sumupstore.com/"> {{ __('navbar.shop') }} </x-public.button> @@ -41,7 +41,7 @@ class="fixed z-50 inset-0 bg-white bg-opacity-90 flex-col items-center justify-c <a href="{{ route('prestations') }}" class="text-2xl font-bold">{{ __('navbar.services') }}</a> <a href="{{ route('portfolio') }}" class="text-2xl font-bold">{{ __('navbar.portfolio') }}</a> <a href="{{ route('evenements') }}" class="text-2xl font-bold">{{ __('navbar.events') }}</a> - <a href="#" class="text-2xl font-bold">{{ __('navbar.contact_me') }}</a> + <a href="{{ route('contact') }}" class="text-2xl font-bold">{{ __('navbar.contact_me') }}</a> <x-public.button tag="a" href="https://dalnarabyrann.sumupstore.com/" class="text-xl"> {{ __('navbar.shop') }} </x-public.button> diff --git a/resources/views/layouts/public.blade.php b/resources/views/layouts/public.blade.php index f2a8ee119fe0b91ff7bf298db03819fd2e8c710d..c465a550b7de938c2efd1668d78a98851c309d9a 100644 --- a/resources/views/layouts/public.blade.php +++ b/resources/views/layouts/public.blade.php @@ -5,8 +5,26 @@ <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{{ !empty($title) ? $title . ' - Rann Graphic Design' : 'Rann Graphic Design' }}</title> <link rel="icon" type="image/svg+xml" href="{{ asset('favicon.svg') }}"/> + + <meta property="og:locale" content="{{ config('app.locale') }}"> + <meta property="og:locale:alternate" content="{{ config('app.fallback_locale') }}"> + + <meta property="og:title" content="{{ !empty($title) ? $title : 'Rann Graphic Design' }}"> + <meta property="og:type" content="website"> + <meta property="og:url" content="{{ url()->current() }}"> + @if(!empty($description)) + <meta name="description" content="{{ $description }}"> + <meta property="og:description" content="{{ $description }}"> + @endif + @if(!empty($image) && is_a($image, \App\Models\UploadedPicture::class)) + <meta property="og:image" content="{{ $image->getFullsizeUrl() }}"> + <meta property="og:image:width" content="{{ $image->width }}"> + <meta property="og:image:height" content="{{ $image->height }}"> + @endif + <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> + <link href="https://fonts.googleapis.com/css2?family=Sniglet:wght@400;800&display=swap" rel="stylesheet"> @vite(['resources/scss/public.scss', 'resources/js/app.js']) </head> <body> diff --git a/resources/views/public/contact.blade.php b/resources/views/public/contact.blade.php new file mode 100644 index 0000000000000000000000000000000000000000..74e56ac3a417a67230e323a6e33024e1ae361cfe --- /dev/null +++ b/resources/views/public/contact.blade.php @@ -0,0 +1,30 @@ +@extends('layouts.public', ['title' => __('navbar.contact_me'), 'description' => __('contact.desc')]) + +@section('content') + <x-public.navbar class="container mx-auto px-4"/> + <div class="container mx-auto px-4 py-24 flex flex-col gap-16"> + <x-public.generic-page-header title="{{ __('navbar.contact_me') }}" :description="__('contact.desc')"/> + + <div> + <div class="text-center text-2xl"> + {{ __('contact.contact_me_on_one_of_my_social_media') }} + </div> + + <div class="flex flex-wrap align-center justify-center gap-4 mt-4"> + @foreach(\App\Models\SocialMediaLink::all() as $socialMediaLink) + <x-public.button tag="a" :href="$socialMediaLink->url" class="flex items-center gap-2" + size="medium"> + <div> + {{ $socialMediaLink->name }} + </div> + <div> + <x-font-awesome :icon="$socialMediaLink->icon_name" type="brands" class="w-6 h-6"/> + </div> + </x-public.button> + @endforeach + </div> + </div> + </div> + + <x-public.footer/> +@endsection \ No newline at end of file diff --git a/resources/views/public/index.blade.php b/resources/views/public/index.blade.php index 72ec9b60bf89d33f28586896b118e7ec2be50ec5..ed2c352e43576e2d6270065ef23cbba0c12874b3 100644 --- a/resources/views/public/index.blade.php +++ b/resources/views/public/index.blade.php @@ -1,4 +1,4 @@ -@extends('layouts.public') +@extends('layouts.public', ['description' => __('home.meta_description')]) @section('content') <section class="HeroSection position-relative w-100"> @@ -21,11 +21,12 @@ class="absolute hidden 2xl:block left-[-96px] top-[-26px] origin-top-left rotate </span> <x-public.arrow class="hidden xl:block w-28 h-9"/> </span> - <span class="text-5xl xl:text-6xl font-bold"> + <span class="text-5xl xl:text-6xl"> {{ __('home.hero.second_row') }} </span> </h1> - <x-public.button size="large" tag="a" href="#" class="flex items-center gap-8"> + <x-public.button size="large" tag="a" href="{{ route('contact') }}" + class="flex items-center gap-8"> <img class="w-10 h-10" src="{{Vite::asset("resources/images/public/waving-hand.avif")}}" alt="{{ __('home.img_alt.waving_hand') }}" loading="eager"/> <div class="flex flex-col justify-center items-start"> @@ -71,18 +72,8 @@ class="Creations relative hidden xl:grid grow w-full aspect-[885/622]"/> <div class="w-full h-24 bg-gradient-to-b from-neutral-200 to-light"></div> <div class="container mx-auto px-4 flex flex-col justify-start items-center gap-2.5"> <div class="flex flex-col justify-start items-start w-full gap-16"> - <div class="flex flex-col justify-start items-start max-w-6xl"> - <div class="text-2xl font-bold">{{ __('home.about_me') }}</div> - <h2 class="text-5xl xl:text-6xl font-normal"> - @if(!$aboutMeSection->isEmpty()) - {{ $aboutMeSection->first()->attractDescTransKey->getTranslation() }} - @else - Lorem ipsum dolor sit amet, - @endif - </h2> - </div> - <div class="flex flex-col xl:flex-row justify-start items-center w-full gap-4 xl:gap-0"> - <div class="flex shrink-0 basis-1/2 justify-center items-center p-4 xl:p-0 gap-2.5"> + <div class="flex flex-col xl:flex-row justify-start w-full gap-8 xl:gap-16"> + <div class="flex shrink-0 justify-center items-center p-4 xl:p-0 gap-2.5"> <div class="overflow-hidden w-full xl:w-[25rem] aspect-square rounded-2xl shadow-[-.5rem_.5rem_0px_0px_rgba(0,0,0,1.00)] shadow-pink-normal"> @if(!$aboutMeSection->isEmpty()) <img class="w-full h-full object-cover" @@ -95,8 +86,17 @@ class="Creations relative hidden xl:grid grow w-full aspect-[885/622]"/> @endif </div> </div> - <div class="flex flex-col shrink-0 basis-1/2 py-2.5 justify-center items-start gap-4"> - <x-markdown class="flex flex-col gap-3 text-muted text-xl xl:text-3xl font-normal"> + <div class="flex flex-col py-2.5 items-start gap-4"> + <div class="text-2xl font-bold">{{ __('home.about_me') }}</div> + <h2 class="text-5xl xl:text-6xl font-normal mb-8"> + @if(!$aboutMeSection->isEmpty()) + {{ $aboutMeSection->first()->attractDescTransKey->getTranslation() }} + @else + Lorem ipsum dolor sit amet, + @endif + </h2> + + <x-markdown class="flex flex-col gap-3 text-muted text-xl xl:text-2xl font-normal"> @if(!$aboutMeSection->isEmpty()) {{ $aboutMeSection->first()->contentTransKey->getTranslation() }} @else diff --git a/resources/views/public/legal-mentions.blade.php b/resources/views/public/legal-mentions.blade.php new file mode 100644 index 0000000000000000000000000000000000000000..65d3c5cc42d7c0a94a6e7782337eb2535d96c795 --- /dev/null +++ b/resources/views/public/legal-mentions.blade.php @@ -0,0 +1,16 @@ +@extends('layouts.public', ['title' => __('generic.legal_mentions')]) + +@section('content') + <x-public.navbar class="container mx-auto px-4"/> + + <div class="container mx-auto px-4 py-24 flex flex-col gap-16"> + <x-public.generic-page-header title="{{ __('generic.legal_mentions') }}"/> + <div> + <x-markdown class="markdown"> + {{ $activeLegalMentionsText }} + </x-markdown> + </div> + </div> + + <x-public.footer/> +@endsection \ No newline at end of file diff --git a/resources/views/public/maintenance.blade.php b/resources/views/public/maintenance.blade.php index 0df958413fd3073599f2ed770dc9887ff542ceb7..15da0114fd74c35f542237191938e6c3cf5a107c 100644 --- a/resources/views/public/maintenance.blade.php +++ b/resources/views/public/maintenance.blade.php @@ -1,4 +1,4 @@ -@extends('layouts.public', ['title' => 'Maintenance']) +@extends('layouts.public', ['title' => 'Maintenance', 'description' => 'Site en construction...']) @section('content') <div class="inline-flex w-full min-h-screen flex-col justify-center items-center gap-2.5" diff --git a/resources/views/public/portfolio-show.blade.php b/resources/views/public/portfolio-show.blade.php index 9a8127f9722df173ee58f04ba33b39f773de5bca..bd3173a5e49d5173275150093e37c4031cea937a 100644 --- a/resources/views/public/portfolio-show.blade.php +++ b/resources/views/public/portfolio-show.blade.php @@ -1,4 +1,8 @@ -@extends('layouts.public', ['title' => $creation->nameTranslationKey->getTranslation()]) +@extends('layouts.public', [ + 'title' => $creation->nameTranslationKey->getTranslation(), + 'description' => $creation->shortDescriptionTranslationKey->getTranslation(), + 'image' => $creation->coverUploadedPicture +]) @section('content') <x-public.navbar class="container mx-auto px-4"/> diff --git a/resources/views/public/portfolio.blade.php b/resources/views/public/portfolio.blade.php index 11eb8b9f18bfd49d4f1d7269f0d2be519117f94a..5f001c4f7e15b224c42256fb73f56c73634e2ea6 100644 --- a/resources/views/public/portfolio.blade.php +++ b/resources/views/public/portfolio.blade.php @@ -1,4 +1,4 @@ -@extends('layouts.public', ['title' => __('navbar.portfolio')]) +@extends('layouts.public', ['title' => __('navbar.portfolio'), 'description' => __('services.description')]) @section('content') <x-public.navbar class="container mx-auto px-4"/> diff --git a/resources/views/public/prestation.blade.php b/resources/views/public/prestation.blade.php index 7fd1dc6cba32231a337c539297609523825db2db..e998491d84ac3f4faee9d8a030233cde6eca272c 100644 --- a/resources/views/public/prestation.blade.php +++ b/resources/views/public/prestation.blade.php @@ -1,4 +1,8 @@ -@extends('layouts.public', ['title' => $prestation->nameTransKey->getTranslation()]) +@extends('layouts.public', [ + 'title' => $prestation->nameTransKey->getTranslation(), + 'description' => $prestation->attractDescTransKey->getTranslation(), + 'image' => $prestation->uploadedPicture +]) @section('content') <x-public.navbar class="container mx-auto px-4"/> @@ -45,7 +49,7 @@ class="w-full relative aspect-square"/> {{ __('generic.custom_needs_desc') }} </div> </div> - <x-public.button size="medium" tag="a" href="#"> + <x-public.button size="medium" tag="a" href="{{ route('contact') }}"> <div class="text-xl font-bold">{{ __('navbar.contact_me') }}</div> </x-public.button> </div> diff --git a/resources/views/public/prestations.blade.php b/resources/views/public/prestations.blade.php index 5d052fa588e10abe72c7c0f7580a09c50b906776..20b99d8a7fffe9978cb0085f02501ad9b0fc8614 100644 --- a/resources/views/public/prestations.blade.php +++ b/resources/views/public/prestations.blade.php @@ -1,4 +1,4 @@ -@extends('layouts.public', ['title' => __('navbar.services')]) +@extends('layouts.public', ['title' => __('navbar.services'), 'description' => __('services.attract_desc')]) @section('content') <x-public.navbar class="container mx-auto px-4"/> @@ -17,7 +17,7 @@ {{ __('generic.custom_needs_desc') }} </div> </div> - <x-public.button size="medium" tag="a" href="#"> + <x-public.button size="medium" tag="a" href="{{ route('contact') }}"> <div class="text-xl font-bold">{{ __('navbar.contact_me') }}</div> </x-public.button> </div> diff --git a/resources/views/public/terms.blade.php b/resources/views/public/terms.blade.php new file mode 100644 index 0000000000000000000000000000000000000000..c7d919ab651ef56b0fc097ccca6d36e41e0cdc9d --- /dev/null +++ b/resources/views/public/terms.blade.php @@ -0,0 +1,16 @@ +@extends('layouts.public', ['title' => __('generic.terms')]) + +@section('content') + <x-public.navbar class="container mx-auto px-4"/> + + <div class="container mx-auto px-4 py-24 flex flex-col gap-16"> + <x-public.generic-page-header title="{{ __('generic.terms') }}"/> + <div> + <x-markdown class="markdown"> + {{ $activeTermsText }} + </x-markdown> + </div> + </div> + + <x-public.footer/> +@endsection \ No newline at end of file diff --git a/routes/console.php b/routes/console.php index eb1048cbc16399ebdcf4476a2d3fc9310096d8e0..37195099b5989fd118ad261513cef434bea7a23b 100644 --- a/routes/console.php +++ b/routes/console.php @@ -8,6 +8,8 @@ })->purpose('Display an inspiring quote')->hourly(); Schedule::command('save:requests')->everyMinute()->sentryMonitor(); +Schedule::command('process:user-agents')->hourly()->sentryMonitor(); +Schedule::command('process:ip-adresses')->everyFiveMinutes()->sentryMonitor(); Schedule::command('flush:unused-uploaded-pictures')->daily()->sentryMonitor(); Schedule::command('backup:clean --disable-notifications')->daily()->at('01:00'); diff --git a/routes/web.php b/routes/web.php index aa637dc52583b1a3b5e1c9d22841990f31785ee2..d006a190a22dd7a0926f7202c57c29cd08fbcdd9 100644 --- a/routes/web.php +++ b/routes/web.php @@ -11,8 +11,10 @@ use App\Http\Controllers\Admin\SocialMediaLinksController; use App\Http\Controllers\Admin\TermsSectionController; use App\Http\Controllers\Public\IndexController; +use App\Http\Controllers\Public\LegalMentionsController; use App\Http\Controllers\Public\PortfolioController; use App\Http\Controllers\Public\PrestationController as PublicPrestationController; +use App\Http\Controllers\Public\TermsController; use App\Http\Middleware\CheckPrivateModeMiddleware; use App\Http\Middleware\RedirectIfUserExistsMiddleware; use Illuminate\Support\Facades\Route; @@ -29,6 +31,9 @@ Route::get('/portfolio/api', [PortfolioController::class, 'api'])->name('portfolio.api'); Route::get('/portfolio/{slug}', [PortfolioController::class, 'show'])->name('portfolio.show'); Route::get('/prestations/{slug}', [PublicPrestationController::class, 'index'])->name('prestations.show'); + Route::get('/mentions-legales', LegalMentionsController::class)->name('legal-mentions'); + Route::get('/conditions-generales-de-vente', TermsController::class)->name('terms'); + Route::view('/contact', 'public.contact')->name('contact'); Route::view('/maintenance', 'public.maintenance')->name('maintenance'); }); @@ -74,6 +79,8 @@ ->name('delete'); Route::delete('/{creation}/remove-addtionnal-image/{image}', [CreationController::class, 'removeAdditionalImage']) ->name('remove-additionnal-image'); + Route::get('/{creation}/translate-with-ai', [CreationController::class, 'translateWithAi']) + ->name('translate-with-ai'); }); Route::prefix('social-media-links')->name('social-media-links.')->group(function () { diff --git a/tests/Feature/Console/Command/FlushUnusedUploadedPicturesCommandTest.php b/tests/Feature/Console/Commands/FlushUnusedUploadedPicturesCommandTest.php similarity index 86% rename from tests/Feature/Console/Command/FlushUnusedUploadedPicturesCommandTest.php rename to tests/Feature/Console/Commands/FlushUnusedUploadedPicturesCommandTest.php index 0d30022ee60d508e8dbaf02a80e4fb216bda598c..c362a64e7c121bee5b6d32811e0b114a02823342 100644 --- a/tests/Feature/Console/Command/FlushUnusedUploadedPicturesCommandTest.php +++ b/tests/Feature/Console/Commands/FlushUnusedUploadedPicturesCommandTest.php @@ -1,6 +1,6 @@ <?php -namespace Tests\Feature\Console\Command; +namespace Tests\Feature\Console\Commands; use App\Console\Commands\FlushUnusedUploadedPicturesCommand; use App\Models\UploadedPicture; @@ -9,7 +9,8 @@ use PHPUnit\Framework\Attributes\CoversClass; use Tests\TestCase; -#[CoversClass(FlushUnusedUploadedPicturesCommand::class)] class FlushUnusedUploadedPicturesCommandTest extends TestCase +#[CoversClass(FlushUnusedUploadedPicturesCommand::class)] +class FlushUnusedUploadedPicturesCommandTest extends TestCase { use RefreshDatabase; diff --git a/tests/Feature/Console/Command/OptimizeUploadedPicturesCommandTest.php b/tests/Feature/Console/Commands/OptimizeUploadedPicturesCommandTest.php similarity index 89% rename from tests/Feature/Console/Command/OptimizeUploadedPicturesCommandTest.php rename to tests/Feature/Console/Commands/OptimizeUploadedPicturesCommandTest.php index f9f09eefc283cf17693a50f4373d9818e0c10848..8863523704bf1d11e5b1cb36f88ba5cbdf012954 100644 --- a/tests/Feature/Console/Command/OptimizeUploadedPicturesCommandTest.php +++ b/tests/Feature/Console/Commands/OptimizeUploadedPicturesCommandTest.php @@ -1,6 +1,6 @@ <?php -namespace Tests\Feature\Console\Command; +namespace Tests\Feature\Console\Commands; use App\Console\Commands\OptimizeUploadedPicturesCommand; use App\Models\UploadedPicture; @@ -12,7 +12,8 @@ /** * Tests de la commande d'optimisation des images uploadées */ -#[CoversClass(OptimizeUploadedPicturesCommand::class)] class OptimizeUploadedPicturesCommandTest extends TestCase +#[CoversClass(OptimizeUploadedPicturesCommand::class)] +class OptimizeUploadedPicturesCommandTest extends TestCase { use RefreshDatabase; diff --git a/tests/Feature/Console/Commands/ProcessIpAdressesCommandTest.php b/tests/Feature/Console/Commands/ProcessIpAdressesCommandTest.php new file mode 100644 index 0000000000000000000000000000000000000000..87153e4c3ca3e11f8d6d9d78003014968bbadf11 --- /dev/null +++ b/tests/Feature/Console/Commands/ProcessIpAdressesCommandTest.php @@ -0,0 +1,28 @@ +<?php + +namespace Tests\Feature\Console\Commands; + +use App\Console\Commands\ProcessIpAdressesCommand; +use App\Jobs\ProcessIpAddressesJob; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Facades\Queue; +use PHPUnit\Framework\Attributes\CoversClass; +use SlProjects\LaravelRequestLogger\app\Models\IpAddress; +use Tests\TestCase; + +#[CoversClass(ProcessIpAdressesCommand::class)] +class ProcessIpAdressesCommandTest extends TestCase +{ + use RefreshDatabase; + + public function test_it_dispatches_job() + { + Queue::fake(); + IpAddress::factory()->count(10)->create(); + + Artisan::call('process:ip-adresses'); + + Queue::assertPushed(ProcessIpAddressesJob::class); + } +} diff --git a/tests/Feature/Console/Commands/ProcessUserAgentsCommandTest.php b/tests/Feature/Console/Commands/ProcessUserAgentsCommandTest.php new file mode 100644 index 0000000000000000000000000000000000000000..c6390dc1a8ee4b9cd4845a767bf1514ef6dbb4ae --- /dev/null +++ b/tests/Feature/Console/Commands/ProcessUserAgentsCommandTest.php @@ -0,0 +1,27 @@ +<?php + +namespace Tests\Feature\Console\Commands; + +use App\Console\Commands\ProcessUserAgentsCommand; +use App\Jobs\ProcessUserAgentJob; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Queue; +use PHPUnit\Framework\Attributes\CoversClass; +use SlProjects\LaravelRequestLogger\app\Models\UserAgent; +use Tests\TestCase; + +#[CoversClass(ProcessUserAgentsCommand::class)] +class ProcessUserAgentsCommandTest extends TestCase +{ + use RefreshDatabase; + + public function test_it_dispatches_job() + { + Queue::fake(); + UserAgent::factory()->count(10)->create(); + + $this->artisan('process:user-agents'); + + Queue::assertPushed(ProcessUserAgentJob::class, 10); + } +} diff --git a/tests/Feature/Console/Command/UpdateImageDimensionsTest.php b/tests/Feature/Console/Commands/UpdateImageDimensionsCommandTest.php similarity index 92% rename from tests/Feature/Console/Command/UpdateImageDimensionsTest.php rename to tests/Feature/Console/Commands/UpdateImageDimensionsCommandTest.php index df50701450a1af1058cbe8f37272250ab2420b43..99b45783e993c20d5a5d156556b09330626dc42c 100644 --- a/tests/Feature/Console/Command/UpdateImageDimensionsTest.php +++ b/tests/Feature/Console/Commands/UpdateImageDimensionsCommandTest.php @@ -1,6 +1,6 @@ <?php -namespace Tests\Feature\Console\Command; +namespace Tests\Feature\Console\Commands; use App\Console\Commands\UpdateImageDimensionsCommand; use App\Models\UploadedPicture; @@ -15,7 +15,8 @@ /** * Tests de la commande de mise à jour des dimensions des images */ -#[CoversClass(UpdateImageDimensionsCommand::class)] class UpdateImageDimensionsTest extends TestCase +#[CoversClass(UpdateImageDimensionsCommand::class)] +class UpdateImageDimensionsCommandTest extends TestCase { use RefreshDatabase; diff --git a/tests/Feature/Jobs/ProcessIpAddressesJobTest.php b/tests/Feature/Jobs/ProcessIpAddressesJobTest.php new file mode 100644 index 0000000000000000000000000000000000000000..005713708d5713d40fe35eb84d601cc9b826f14c --- /dev/null +++ b/tests/Feature/Jobs/ProcessIpAddressesJobTest.php @@ -0,0 +1,59 @@ +<?php + +namespace Tests\Feature\Jobs; + +use App\Jobs\ProcessIpAddressesJob; +use App\Services\IpAddressMetadataResolverService; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Http; +use PHPUnit\Framework\Attributes\CoversClass; +use SlProjects\LaravelRequestLogger\app\Models\IpAddress; +use Tests\TestCase; + +#[CoversClass(ProcessIpAddressesJob::class)] +class ProcessIpAddressesJobTest extends TestCase +{ + use RefreshDatabase; + + public function test_processes_ip_addresses_successfully() + { + $ip1 = IpAddress::factory()->create(['ip' => '208.80.152.201']); + $ip2 = IpAddress::factory()->create(['ip' => '24.48.0.1']); + + Http::fake([ + '*' => Http::response([ + [ + 'status' => 'success', + 'countryCode' => 'US', + 'lat' => 37.7892, + 'lon' => -122.402, + 'query' => '208.80.152.201', + ], + [ + 'status' => 'success', + 'countryCode' => 'CA', + 'lat' => 45.6085, + 'lon' => -73.5493, + 'query' => '24.48.0.1', + ], + ]), + ]); + + $job = new ProcessIpAddressesJob(collect([$ip1, $ip2])); + $job->handle(app(IpAddressMetadataResolverService::class)); + + $this->assertDatabaseHas('ip_address_metadata', [ + 'ip_address_id' => $ip1->id, + 'country_code' => 'US', + 'lat' => 37.7892, + 'lon' => -122.402, + ]); + + $this->assertDatabaseHas('ip_address_metadata', [ + 'ip_address_id' => $ip2->id, + 'country_code' => 'CA', + 'lat' => 45.6085, + 'lon' => -73.5493, + ]); + } +} diff --git a/tests/Feature/Service/AiProviderServiceTest.php b/tests/Feature/Service/AiProviderServiceTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1de43af3942ac979ba9f366f06300529db396eb9 --- /dev/null +++ b/tests/Feature/Service/AiProviderServiceTest.php @@ -0,0 +1,163 @@ +<?php + +namespace Tests\Feature\Service; + +use App\Models\UploadedPicture; +use App\Services\AiProviderService; +use App\Services\ImageTranscodingService; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\App; +use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Storage; +use Mockery; +use PHPUnit\Framework\Attributes\CoversClass; +use RuntimeException; +use Tests\TestCase; + +#[CoversClass(AiProviderService::class)] +class AiProviderServiceTest extends TestCase +{ + use RefreshDatabase; + + private array $sampleResponse = [ + 'id' => 'chat_1', + 'object' => 'chat.completion', + 'created' => 1739718124, + 'model' => 'gpt-4o-mini', + 'choices' => [ + [ + 'index' => 0, + 'message' => [ + 'role' => 'assistant', + 'content' => "{\n \"message\": \"Why don't scientists trust atoms? Because they make up everything!\"\n}", + 'refusal' => null, + ], + 'logprobs' => null, + 'finish_reason' => 'stop', + ], + ], + 'usage' => [ + 'prompt_tokens' => 33, + 'completion_tokens' => 20, + 'total_tokens' => 53, + 'prompt_tokens_details' => [ + 'cached_tokens' => 0, + 'audio_tokens' => 0, + ], + 'completion_tokens_details' => [ + 'reasoning_tokens' => 0, + 'audio_tokens' => 0, + 'accepted_prediction_tokens' => 0, + 'rejected_prediction_tokens' => 0, + ], + ], + 'service_tier' => 'default', + 'system_fingerprint' => 'system-1', + ]; + + public function test_prompt_with_pictures_sends_correct_request() + { + Storage::fake('public'); + Http::fake([ + 'https://api.test-provider.com' => Http::response($this->sampleResponse), + ]); + + $mockTranscodingService = Mockery::mock(ImageTranscodingService::class); + $mockTranscodingService->shouldReceive('transcode') + ->andReturn('transcoded-image-content'); + App::instance(ImageTranscodingService::class, $mockTranscodingService); + + Config::set('ai-provider.selected-provider', 'test-provider'); + Config::set('ai-provider.providers.test-provider', [ + 'api-key' => 'test-api-key', + 'url' => 'https://api.test-provider.com', + 'model' => 'test-model', + 'max-tokens' => 100, + ]); + + $uploadedPicture = UploadedPicture::factory()->create(); + $service = new AiProviderService; + + $response = $service->promptWithPictures( + 'You are a helpful assistant.', + 'Describe this image.', + $uploadedPicture + ); + + $this->assertEquals(['message' => "Why don't scientists trust atoms? Because they make up everything!"], $response); + } + + public function test_prompt_sends_correct_request() + { + Http::fake([ + 'https://api.test-provider.com' => Http::response($this->sampleResponse), + ]); + + Config::set('ai-provider.selected-provider', 'test-provider'); + Config::set('ai-provider.providers.test-provider', [ + 'api-key' => 'test-api-key', + 'url' => 'https://api.test-provider.com', + 'model' => 'test-model', + 'max-tokens' => 100, + ]); + + $service = new AiProviderService; + + $response = $service->prompt( + 'You are a helpful assistant.', + 'Tell me a joke.' + ); + + $this->assertEquals(['message' => "Why don't scientists trust atoms? Because they make up everything!"], $response); + } + + public function test_prompt_with_pictures_handles_transcoding_failure() + { + Storage::fake('public'); + Http::fake([ + 'https://api.test-provider.com' => Http::response($this->sampleResponse), + ]); + + $mockTranscodingService = Mockery::mock(ImageTranscodingService::class); + $mockTranscodingService->shouldReceive('transcode') + ->andReturn(null); + App::instance(ImageTranscodingService::class, $mockTranscodingService); + + $uploadedPicture = UploadedPicture::factory()->create(); + $service = new AiProviderService; + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Failed to transcode picture'); + + $service->promptWithPictures( + 'You are a helpful assistant.', + 'Describe this image.', + $uploadedPicture + ); + } + + public function test_prompt_handles_api_failure() + { + Http::fake([ + 'https://api.test-provider.com' => Http::response('Error', 500), + ]); + + Config::set('ai-provider.selected-provider', 'test-provider'); + Config::set('ai-provider.providers.test-provider', [ + 'api-key' => 'test-api-key', + 'url' => 'https://api.test-provider.com', + 'model' => 'test-model', + 'max-tokens' => 100, + ]); + + $service = new AiProviderService; + + $this->expectException(RuntimeException::class); + + $service->prompt( + 'You are a helpful assistant.', + 'Tell me a joke.' + ); + } +} diff --git a/tests/Feature/Service/IpAddressMetadataResolverServiceTest.php b/tests/Feature/Service/IpAddressMetadataResolverServiceTest.php new file mode 100644 index 0000000000000000000000000000000000000000..dcb6c86b358cbf6c93a836a1711927efbfc5e395 --- /dev/null +++ b/tests/Feature/Service/IpAddressMetadataResolverServiceTest.php @@ -0,0 +1,51 @@ +<?php + +namespace Tests\Feature\Service; + +use App\Services\IpAddressMetadataResolverService; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Exceptions; +use Illuminate\Support\Facades\Http; +use PHPUnit\Framework\Attributes\CoversClass; +use SlProjects\LaravelRequestLogger\app\Models\IpAddress; +use Tests\TestCase; + +#[CoversClass(IpAddressMetadataResolverService::class)] +class IpAddressMetadataResolverServiceTest extends TestCase +{ + use RefreshDatabase; + + public function test_resolves_ip_addresses_successfully() + { + Exceptions::fake(); + + Http::fake([ + '*' => Http::response([ + [ + 'status' => 'success', + 'countryCode' => 'US', + 'lat' => 37.7892, + 'lon' => -122.402, + 'query' => '208.80.152.201', + ], + [ + 'status' => 'success', + 'countryCode' => 'CA', + 'lat' => 45.6085, + 'lon' => -73.5493, + 'query' => '24.48.0.1', + ], + ]), + ]); + + $service = new IpAddressMetadataResolverService; + + $ipAddresses = IpAddress::factory()->count(2)->create(); + $result = $service->resolve($ipAddresses); + + Exceptions::assertNothingReported(); + $this->assertCount(2, $result); + $this->assertEquals('US', $result[0]['countryCode']); + $this->assertEquals('CA', $result[1]['countryCode']); + } +}